480 lines
23 KiB
TypeScript
480 lines
23 KiB
TypeScript
import {
|
||
AlertCircle,
|
||
Calendar,
|
||
CheckCircle,
|
||
ChevronDown,
|
||
ClipboardCheck,
|
||
Clock,
|
||
GitBranch,
|
||
Info,
|
||
Lock,
|
||
Mail,
|
||
MessageSquare,
|
||
Star,
|
||
User,
|
||
XCircle,
|
||
Zap,
|
||
} from 'lucide-react';
|
||
import { cn, formatDateTime } from '@/components/ui/utils';
|
||
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
|
||
import { Badge } from '@/components/ui/badge';
|
||
import { Button } from '@/components/ui/button';
|
||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
|
||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog';
|
||
import {
|
||
DropdownMenu,
|
||
DropdownMenuContent,
|
||
DropdownMenuItem,
|
||
DropdownMenuTrigger,
|
||
} from '@/components/ui/dropdown-menu';
|
||
import { Label } from '@/components/ui/label';
|
||
import { Progress } from '@/components/ui/progress';
|
||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||
import { Separator } from '@/components/ui/separator';
|
||
|
||
interface ApplicationDetailsSidebarProps {
|
||
application: any;
|
||
permissions: any;
|
||
getDeposit: (type: string) => any;
|
||
isNonResponsive: boolean;
|
||
isAdmin: boolean;
|
||
currentUserStageAction: any;
|
||
currentUserEvaluation: any;
|
||
onOpenApproveModal: () => void;
|
||
onOpenRejectModal: () => void;
|
||
onOpenWorknote: () => void;
|
||
onOpenScheduleModal: () => void;
|
||
currentUser: any;
|
||
handleGenerateDealerCodes: () => void;
|
||
onOpenAssignArchitectureModal: () => void;
|
||
onOpenAssignFdd: () => void;
|
||
showAssignFddModal: boolean;
|
||
setShowAssignFddModal: (value: boolean) => void;
|
||
fddAgencies: any[];
|
||
selectedAgencyId: string;
|
||
setSelectedAgencyId: (value: string) => void;
|
||
isAssigningAgency: boolean;
|
||
handleAssignAgency: () => void;
|
||
activeInterviewForUser: any;
|
||
hasSubmittedFeedback: boolean;
|
||
setSelectedInterviewForFeedback: (value: any) => void;
|
||
setShowKTMatrixModal: (value: boolean) => void;
|
||
setShowLevel2FeedbackModal: (value: boolean) => void;
|
||
setShowLevel3FeedbackModal: (value: boolean) => void;
|
||
onGoToDashboard: () => void;
|
||
showAssignModal: boolean;
|
||
setShowAssignModal: (value: boolean) => void;
|
||
selectedUser: string;
|
||
setSelectedUser: (value: string) => void;
|
||
users: any[];
|
||
participantType: string;
|
||
setParticipantType: (value: string) => void;
|
||
handleAddParticipant: () => void;
|
||
isAssigningParticipant: boolean;
|
||
}
|
||
|
||
export function ApplicationDetailsSidebar(props: ApplicationDetailsSidebarProps) {
|
||
const {
|
||
application,
|
||
permissions,
|
||
getDeposit,
|
||
isNonResponsive,
|
||
isAdmin,
|
||
currentUserStageAction,
|
||
currentUserEvaluation,
|
||
onOpenApproveModal,
|
||
onOpenRejectModal,
|
||
onOpenWorknote,
|
||
onOpenScheduleModal,
|
||
currentUser,
|
||
handleGenerateDealerCodes,
|
||
onOpenAssignArchitectureModal,
|
||
onOpenAssignFdd,
|
||
showAssignFddModal,
|
||
setShowAssignFddModal,
|
||
fddAgencies,
|
||
selectedAgencyId,
|
||
setSelectedAgencyId,
|
||
isAssigningAgency,
|
||
handleAssignAgency,
|
||
activeInterviewForUser,
|
||
hasSubmittedFeedback,
|
||
setSelectedInterviewForFeedback,
|
||
setShowKTMatrixModal,
|
||
setShowLevel2FeedbackModal,
|
||
setShowLevel3FeedbackModal,
|
||
onGoToDashboard,
|
||
showAssignModal,
|
||
setShowAssignModal,
|
||
selectedUser,
|
||
setSelectedUser,
|
||
users,
|
||
participantType,
|
||
setParticipantType,
|
||
handleAddParticipant,
|
||
isAssigningParticipant,
|
||
} = props;
|
||
|
||
return (
|
||
<div className="space-y-6">
|
||
<Card data-testid="onboarding-details-summary-card">
|
||
<CardHeader>
|
||
<CardTitle>Summary</CardTitle>
|
||
</CardHeader>
|
||
<CardContent className="space-y-4">
|
||
<div>
|
||
<p className="text-slate-600">Registration ID</p>
|
||
<p className="text-slate-900" data-testid="onboarding-details-summary-reg-id">{application.registrationNumber}</p>
|
||
</div>
|
||
<div>
|
||
<p className="text-slate-600">Current Status</p>
|
||
<Badge
|
||
className={cn(
|
||
"mt-1",
|
||
application.status === 'Onboarded' ? "bg-green-600 hover:bg-green-700 text-white" :
|
||
application.status === 'Rejected' ? "bg-red-600" :
|
||
"bg-amber-600"
|
||
)}
|
||
data-testid="onboarding-details-summary-status"
|
||
>
|
||
{application.status}
|
||
</Badge>
|
||
</div>
|
||
{application.rank && (
|
||
<div>
|
||
<p className="text-slate-600">Rank</p>
|
||
<p className="text-slate-900" data-testid="onboarding-details-summary-rank">
|
||
{application.rank} of {application.totalApplicantsAtLocation}
|
||
<span className="text-slate-500"> in {application.preferredLocation}</span>
|
||
</p>
|
||
</div>
|
||
)}
|
||
<div>
|
||
<p className="text-slate-600">Progress</p>
|
||
<div className="flex items-center gap-2 mt-2">
|
||
<Progress value={application.progress} className="flex-1" data-testid="onboarding-details-summary-progress-bar" />
|
||
<span className="text-slate-900" data-testid="onboarding-details-summary-progress-text">{application.progress}%</span>
|
||
</div>
|
||
</div>
|
||
{application.deadline && (
|
||
<div>
|
||
<p className="text-slate-600">Questionnaire Deadline</p>
|
||
<p className="text-slate-900" data-testid="onboarding-details-summary-deadline">{formatDateTime(application.deadline)}</p>
|
||
</div>
|
||
)}
|
||
</CardContent>
|
||
</Card>
|
||
|
||
{(application.isShortlisted !== false || application.status === 'Submitted') && (
|
||
<Card data-testid="onboarding-details-actions-card">
|
||
<CardHeader>
|
||
<CardTitle>Actions</CardTitle>
|
||
</CardHeader>
|
||
<CardContent className="space-y-3">
|
||
{permissions.isLoaLocked && (
|
||
<Alert variant="destructive" className="mb-4 bg-amber-50 border-amber-200 text-amber-800" data-testid="onboarding-details-loa-locked-alert">
|
||
<Lock className="w-4 h-4 text-amber-600" />
|
||
<AlertTitle className="text-amber-900 font-semibold">LOA approval locked</AlertTitle>
|
||
<AlertDescription className="text-amber-800">
|
||
<span className="font-medium">First Fill</span> (later-stage payment) must be verified by Finance
|
||
before LOA approval can proceed. This is separate from the initial security deposit before LOI Issued.
|
||
</AlertDescription>
|
||
</Alert>
|
||
)}
|
||
|
||
{getDeposit('FIRST_FILL')?.status === 'Verified' &&
|
||
application.status !== 'LOA Pending' &&
|
||
!['LOA Issued', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Onboarded', 'Rejected'].includes(
|
||
application.status,
|
||
) && (
|
||
<Alert className="mb-4 border-violet-200 bg-violet-50/90 text-violet-950" data-testid="onboarding-details-first-fill-verified-alert">
|
||
<Info className="h-4 w-4 text-violet-700" />
|
||
<AlertTitle className="font-semibold">First Fill verified on file</AlertTitle>
|
||
<AlertDescription className="text-sm text-violet-900/90 leading-relaxed">
|
||
Finance has verified the <span className="font-medium">First Fill</span> payment. The application
|
||
status was <span className="font-medium">not</span> changed until you reach{' '}
|
||
<span className="font-medium">LOA Pending</span>. When you get there, LOA approval will not be
|
||
blocked by payment (same pattern as recording the initial security deposit before the LOI
|
||
security step).
|
||
</AlertDescription>
|
||
</Alert>
|
||
)}
|
||
|
||
{permissions.isSecurityDetailsLocked && (
|
||
<Alert variant="destructive" className="mb-4 bg-amber-50 border-amber-200 text-amber-800" data-testid="onboarding-details-security-locked-alert">
|
||
<Lock className="w-4 h-4 text-amber-600" />
|
||
<AlertTitle className="text-amber-900 font-semibold">Security Details approval locked</AlertTitle>
|
||
<AlertDescription className="text-amber-800">
|
||
Finance must verify the <span className="font-medium">Security Deposit</span> before this stage can be approved.
|
||
You can still use <span className="font-medium">Reject</span> if needed.
|
||
</AlertDescription>
|
||
</Alert>
|
||
)}
|
||
|
||
{['Security Details', 'Payment Pending'].includes(application.status) && (
|
||
<Alert className="mb-4 border-sky-200 bg-sky-50/90 text-sky-900" data-testid="onboarding-details-security-review-alert">
|
||
<Info className="h-4 w-4 text-sky-700" />
|
||
<AlertTitle className="text-sky-950 font-semibold">Security Details review</AlertTitle>
|
||
<AlertDescription className="text-sm text-sky-900/90 leading-relaxed">
|
||
Check the initial security deposit on the <span className="font-medium">Payments</span> tab (Finance
|
||
may have already marked it verified). When satisfied, use <span className="font-medium">Approve</span>{' '}
|
||
to move to <span className="font-medium">LOI Issued</span>.
|
||
</AlertDescription>
|
||
</Alert>
|
||
)}
|
||
|
||
{isNonResponsive && isAdmin && (
|
||
<Alert variant="destructive" className="mb-4 bg-red-50 border-red-200 text-red-800" data-testid="onboarding-details-non-responsive-alert">
|
||
<AlertCircle className="w-4 h-4 text-red-600" />
|
||
<AlertTitle className="text-red-900 font-black uppercase tracking-tighter">⚠️ Non-Responsive Flag</AlertTitle>
|
||
<AlertDescription className="text-red-800 text-xs font-bold leading-tight">
|
||
FDD Audit has flagged this applicant. Review audit logs before approval.
|
||
</AlertDescription>
|
||
</Alert>
|
||
)}
|
||
|
||
{isAdmin && (application.status === 'Level 3 Approved' || application.status === 'FDD Verification') && (!application.fddAssignments || application.fddAssignments.length === 0) && (
|
||
<Alert className="mb-4 bg-amber-50 border-amber-200 text-amber-800" data-testid="onboarding-details-fdd-assignment-alert">
|
||
<AlertCircle className="w-4 h-4 text-amber-600" />
|
||
<AlertTitle className="text-amber-900 font-bold">FDD Assignment Required</AlertTitle>
|
||
<AlertDescription className="text-amber-800 font-medium">
|
||
This application is pending financial due diligence. Please assign an FDD Agency to proceed with the audit.
|
||
</AlertDescription>
|
||
</Alert>
|
||
)}
|
||
|
||
{permissions.canApprove && (
|
||
<Button className="w-full bg-green-600 hover:bg-green-700 font-bold" onClick={onOpenApproveModal} data-testid="onboarding-details-approve-button">
|
||
<CheckCircle className="w-4 h-4 mr-2" />
|
||
{['Inauguration', 'Approved'].includes(application.status) ? 'Onboard Dealer' : 'Approve'}
|
||
</Button>
|
||
)}
|
||
|
||
{permissions.canReject && (
|
||
<Button variant="destructive" className="w-full font-bold" onClick={onOpenRejectModal} data-testid="onboarding-details-reject-button">
|
||
<XCircle className="w-4 h-4 mr-2" />
|
||
Reject
|
||
</Button>
|
||
)}
|
||
|
||
{permissions.showDecisionMessage && (
|
||
<div
|
||
className={`w-full p-2 text-center rounded border ${(currentUserStageAction?.decision === 'Approved' || currentUserEvaluation?.decision === 'Approved' || currentUserEvaluation?.recommendation === 'Approved' || currentUserEvaluation?.decision === 'Selected') ? 'bg-green-50 border-green-200 text-green-700' : 'bg-red-50 border-red-200 text-red-700'}`}
|
||
data-testid="onboarding-details-decision-message"
|
||
>
|
||
You have {(currentUserStageAction?.decision === 'Approved' || currentUserEvaluation?.decision === 'Approved' || currentUserEvaluation?.recommendation === 'Approved' || currentUserEvaluation?.decision === 'Selected') ? 'Approved' : 'Rejected'}
|
||
</div>
|
||
)}
|
||
|
||
<Separator />
|
||
|
||
<Button variant="outline" className="w-full" onClick={onOpenWorknote} data-testid="onboarding-details-worknote-button">
|
||
<MessageSquare className="w-4 h-4 mr-2" />
|
||
Work Note
|
||
</Button>
|
||
|
||
{permissions.canSchedule && (
|
||
<Button variant="outline" className="w-full" onClick={onOpenScheduleModal} data-testid="onboarding-details-schedule-button">
|
||
<Calendar className="w-4 h-4 mr-2" />
|
||
Schedule Interview
|
||
</Button>
|
||
)}
|
||
|
||
{currentUser && ['DD Admin', 'Super Admin'].includes(currentUser.role) &&
|
||
['Dealer Code Generation', 'LOA Pending', 'Architecture Team Assigned', 'Architecture Document Upload', 'Architecture Team Completion'].includes(application.status) && (
|
||
<>
|
||
{!application.dealerCode && (
|
||
<Button className="w-full bg-blue-600 hover:bg-blue-700" onClick={handleGenerateDealerCodes} data-testid="onboarding-details-generate-dealer-codes">
|
||
<Zap className="w-4 h-4 mr-2" />
|
||
Generate Dealer Codes
|
||
</Button>
|
||
)}
|
||
|
||
{application.dealerCode && !application.architectureAssignedTo && (
|
||
<Button
|
||
variant="outline"
|
||
className="w-full border-blue-200 hover:bg-blue-50 text-blue-700"
|
||
onClick={onOpenAssignArchitectureModal}
|
||
data-testid="onboarding-details-assign-architecture"
|
||
>
|
||
<GitBranch className="w-4 h-4 mr-2" />
|
||
Assign Architecture Team
|
||
</Button>
|
||
)}
|
||
</>
|
||
)}
|
||
|
||
{isAdmin &&
|
||
['Level 3 Approved', 'Level 3 Recommended', 'FDD Verification', 'FDD In Progress'].includes(application.status) &&
|
||
(!application.fddAssignments || application.fddAssignments.length === 0) && (
|
||
<Dialog open={showAssignFddModal} onOpenChange={setShowAssignFddModal}>
|
||
<Button
|
||
variant="outline"
|
||
className="w-full border-purple-200 hover:bg-purple-50 text-purple-700"
|
||
onClick={onOpenAssignFdd}
|
||
data-testid="onboarding-details-assign-fdd"
|
||
>
|
||
<ClipboardCheck className="w-4 h-4 mr-2" />
|
||
Assign FDD
|
||
</Button>
|
||
<DialogContent data-testid="onboarding-details-assign-fdd-modal">
|
||
<DialogHeader>
|
||
<DialogTitle>Assign FDD Agency</DialogTitle>
|
||
<DialogDescription>
|
||
Select an FDD partner agency to perform the financial due diligence audit for this application.
|
||
</DialogDescription>
|
||
</DialogHeader>
|
||
<div className="space-y-4">
|
||
<div>
|
||
<Label>FDD Agency</Label>
|
||
<Select value={selectedAgencyId} onValueChange={setSelectedAgencyId}>
|
||
<SelectTrigger className="mt-2" data-testid="onboarding-details-assign-fdd-select">
|
||
<SelectValue placeholder={fddAgencies?.length ? 'Choose partner agency...' : 'No agencies available'} />
|
||
</SelectTrigger>
|
||
<SelectContent>
|
||
{(fddAgencies || []).map((agency: any) => (
|
||
<SelectItem key={agency.id} value={agency.id}>
|
||
{agency.fullName || agency.name} ({agency.email})
|
||
</SelectItem>
|
||
))}
|
||
</SelectContent>
|
||
</Select>
|
||
</div>
|
||
<Button
|
||
className="w-full bg-amber-600 hover:bg-amber-700 font-bold h-11"
|
||
onClick={handleAssignAgency}
|
||
disabled={isAssigningAgency || !selectedAgencyId}
|
||
data-testid="onboarding-details-assign-fdd-submit"
|
||
>
|
||
{isAssigningAgency ? 'Assigning...' : 'Assign Agency'}
|
||
</Button>
|
||
</div>
|
||
</DialogContent>
|
||
</Dialog>
|
||
)}
|
||
|
||
{activeInterviewForUser && !hasSubmittedFeedback && (
|
||
<DropdownMenu>
|
||
<DropdownMenuTrigger asChild>
|
||
<Button variant="outline" className="w-full" data-testid="onboarding-details-feedback-dropdown-trigger">
|
||
<Star className="w-4 h-4 mr-2" />
|
||
Interview Feedback
|
||
<ChevronDown className="w-4 h-4 ml-auto" />
|
||
</Button>
|
||
</DropdownMenuTrigger>
|
||
<DropdownMenuContent className="w-56" data-testid="onboarding-details-feedback-dropdown-content">
|
||
<DropdownMenuItem
|
||
key={activeInterviewForUser.id}
|
||
data-testid={`onboarding-details-feedback-item-${activeInterviewForUser.id}`}
|
||
onClick={() => {
|
||
setSelectedInterviewForFeedback(activeInterviewForUser);
|
||
if (activeInterviewForUser.level === 1) setShowKTMatrixModal(true);
|
||
else if (activeInterviewForUser.level === 2) setShowLevel2FeedbackModal(true);
|
||
else setShowLevel3FeedbackModal(true);
|
||
}}
|
||
>
|
||
Level {activeInterviewForUser.level} - {activeInterviewForUser.interviewType}
|
||
</DropdownMenuItem>
|
||
</DropdownMenuContent>
|
||
</DropdownMenu>
|
||
)}
|
||
|
||
{application.status === 'Questionnaire Pending' && (
|
||
<>
|
||
<Button variant="outline" className="w-full" data-testid="onboarding-details-send-reminder">
|
||
<Mail className="w-4 h-4 mr-2" />
|
||
Send Reminder
|
||
</Button>
|
||
|
||
<Button variant="outline" className="w-full" data-testid="onboarding-details-extend-deadline">
|
||
<Clock className="w-4 h-4 mr-2" />
|
||
Extend Deadline
|
||
</Button>
|
||
</>
|
||
)}
|
||
|
||
{application.dealer && (
|
||
<div className="p-4 bg-green-50 border border-green-200 rounded-lg space-y-3" data-testid="onboarding-details-dealer-active-banner">
|
||
<div className="flex items-center gap-2 text-green-800 font-semibold">
|
||
<CheckCircle className="w-5 h-5 text-green-600" />
|
||
Dealer Profile Active
|
||
</div>
|
||
<div className="text-sm text-green-700">
|
||
This application has been successfully onboarded as a dealer. A user account has been created for the dealer.
|
||
</div>
|
||
{application.dealerCode && (
|
||
<div className="flex items-center justify-between text-xs font-mono bg-white p-2 rounded border border-green-100" data-testid="onboarding-details-active-dealer-code">
|
||
<span className="text-slate-500">Dealer Code:</span>
|
||
<span className="font-bold text-slate-900">{application.dealerCode.code}</span>
|
||
</div>
|
||
)}
|
||
<Button className="w-full bg-green-600 hover:bg-green-700 text-white" onClick={onGoToDashboard} data-testid="onboarding-details-goto-dashboard">
|
||
<Zap className="w-4 h-4 mr-2" />
|
||
Go to Dealer Dashboard
|
||
</Button>
|
||
</div>
|
||
)}
|
||
|
||
{currentUser && ['DD Admin', 'Super Admin'].includes(currentUser.role) && (
|
||
<Dialog open={showAssignModal} onOpenChange={setShowAssignModal}>
|
||
<DialogTrigger asChild>
|
||
<Button variant="outline" className="w-full" data-testid="onboarding-details-assign-user-trigger">
|
||
<User className="w-4 h-4 mr-2" />
|
||
Assign User
|
||
</Button>
|
||
</DialogTrigger>
|
||
<DialogContent data-testid="onboarding-details-assign-user-modal">
|
||
<DialogHeader>
|
||
<DialogTitle>Assign User to Application</DialogTitle>
|
||
<DialogDescription>
|
||
Select a user and their role for this application.
|
||
</DialogDescription>
|
||
</DialogHeader>
|
||
<div className="space-y-4">
|
||
<div>
|
||
<Label>Select User</Label>
|
||
<Select value={selectedUser} onValueChange={setSelectedUser}>
|
||
<SelectTrigger className="mt-2" data-testid="onboarding-details-assign-user-select">
|
||
<SelectValue placeholder="Search users..." />
|
||
</SelectTrigger>
|
||
<SelectContent>
|
||
{users.map((u) => (
|
||
<SelectItem key={u.id} value={u.id}>
|
||
{u.fullName} ({u.email})
|
||
</SelectItem>
|
||
))}
|
||
</SelectContent>
|
||
</Select>
|
||
</div>
|
||
<div>
|
||
<Label>Assignment Role</Label>
|
||
<Select value={participantType} onValueChange={setParticipantType}>
|
||
<SelectTrigger className="mt-2" data-testid="onboarding-details-assign-role-select">
|
||
<SelectValue placeholder="Select role" />
|
||
</SelectTrigger>
|
||
<SelectContent>
|
||
<SelectItem value="owner">Owner</SelectItem>
|
||
<SelectItem value="contributor">Contributor</SelectItem>
|
||
<SelectItem value="reviewer">Reviewer</SelectItem>
|
||
</SelectContent>
|
||
</Select>
|
||
</div>
|
||
<Button
|
||
className="w-full bg-amber-600 hover:bg-amber-700 font-bold h-11"
|
||
onClick={handleAddParticipant}
|
||
disabled={isAssigningParticipant}
|
||
data-testid="onboarding-details-assign-user-submit"
|
||
>
|
||
{isAssigningParticipant ? 'Assigning...' : 'Assign User'}
|
||
</Button>
|
||
</div>
|
||
</DialogContent>
|
||
</Dialog>
|
||
)}
|
||
</CardContent>
|
||
</Card>
|
||
)}
|
||
</div>
|
||
);
|
||
}
|