556 lines
39 KiB
TypeScript
556 lines
39 KiB
TypeScript
import { AlertCircle, Building2, Download, Eye, FileText, Info, Loader2, ShieldAlert, ShieldCheck, Upload } from 'lucide-react';
|
|
import { toast } from 'sonner';
|
|
import { onboardingService } from '@/services/onboarding.service';
|
|
import { cn, formatDateTime } from '@/components/ui/utils';
|
|
import { DocumentPreviewModal } from '@/components/ui/DocumentPreviewModal';
|
|
import { Badge } from '@/components/ui/badge';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
|
import { Input } from '@/components/ui/input';
|
|
import { Label } from '@/components/ui/label';
|
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
|
import { Separator } from '@/components/ui/separator';
|
|
import {
|
|
Table,
|
|
TableBody,
|
|
TableCell,
|
|
TableHead,
|
|
TableHeader,
|
|
TableRow,
|
|
} from '@/components/ui/table';
|
|
import { Textarea } from '@/components/ui/textarea';
|
|
|
|
interface ApplicationDetailsExtendedModalsProps {
|
|
[key: string]: any;
|
|
}
|
|
|
|
export function ApplicationDetailsExtendedModals(props: ApplicationDetailsExtendedModalsProps) {
|
|
const {
|
|
application,
|
|
ktCriteria,
|
|
l2Fields,
|
|
l3Fields,
|
|
showKTMatrixModal,
|
|
setShowKTMatrixModal,
|
|
ktMatrixSelectedValues,
|
|
handleKTMatrixChange,
|
|
ktMatrixRemarks,
|
|
setKtMatrixRemarks,
|
|
calculateKTScore,
|
|
handleSubmitKTMatrix,
|
|
isSubmittingKT,
|
|
showLevel2FeedbackModal,
|
|
setShowLevel2FeedbackModal,
|
|
level2Feedback,
|
|
handleLevel2Change,
|
|
handleSubmitLevel2Feedback,
|
|
isSubmittingLevel2,
|
|
showFeedbackDetailsModal,
|
|
setShowFeedbackDetailsModal,
|
|
selectedEvaluationForView,
|
|
showLevel3FeedbackModal,
|
|
setShowLevel3FeedbackModal,
|
|
level3Feedback,
|
|
handleLevel3Change,
|
|
handleSubmitLevel3Feedback,
|
|
isSubmittingLevel3,
|
|
showDocumentsModal,
|
|
setShowDocumentsModal,
|
|
showUploadForm,
|
|
setShowUploadForm,
|
|
selectedStage,
|
|
getDocumentsForStage,
|
|
setPreviewDoc,
|
|
setShowPreviewModal,
|
|
flattenedStages,
|
|
setSelectedStage,
|
|
uploadDocType,
|
|
setUploadDocType,
|
|
setUploadFile,
|
|
isUploading,
|
|
handleUpload,
|
|
uploadFile,
|
|
documentConfigs,
|
|
showPreviewModal,
|
|
previewDoc,
|
|
showFddFinalizeModal,
|
|
setShowFddFinalizeModal,
|
|
currentUser,
|
|
fddAuditRecommendation,
|
|
setFddAuditRecommendation,
|
|
fddAuditFindings,
|
|
setFddAuditFindings,
|
|
isFinalizingFdd,
|
|
setIsFinalizingFdd,
|
|
fetchApplication,
|
|
showFddFlagModal,
|
|
setShowFddFlagModal,
|
|
isFddFlagging,
|
|
setIsFddFlagging,
|
|
showFirmTypeModal,
|
|
setShowFirmTypeModal,
|
|
tempFirmType,
|
|
setTempFirmType,
|
|
updatingFirmType,
|
|
handleUpdateFirmType,
|
|
} = props;
|
|
|
|
return (
|
|
<>
|
|
<Dialog open={showKTMatrixModal} onOpenChange={setShowKTMatrixModal}>
|
|
<DialogContent
|
|
className="flex min-h-0 max-h-[90vh] w-[calc(100%-2rem)] max-w-lg flex-col gap-0 overflow-hidden p-0 sm:max-w-lg"
|
|
data-testid="onboarding-kt-matrix-modal"
|
|
>
|
|
<DialogHeader className="shrink-0 space-y-2 border-b px-5 py-4 text-left">
|
|
<DialogTitle className="text-base">KT matrix</DialogTitle>
|
|
<DialogDescription className="text-sm leading-relaxed">
|
|
Level 1 interview · {application.name}
|
|
<span className="mt-1 block text-xs text-muted-foreground">
|
|
{Object.keys(ktMatrixSelectedValues).length} of {ktCriteria.length} criteria answered
|
|
</span>
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
<div className="custom-scrollbar-slim min-h-0 flex-1 overflow-y-auto px-5 py-5">
|
|
<div className="space-y-6">
|
|
{ktCriteria.map((criterion: any, idx: number) => (
|
|
<div key={criterion.name} className="space-y-2">
|
|
<Label htmlFor={`kt-matrix-${idx}`} className="block text-sm font-medium leading-relaxed text-foreground">
|
|
<span className="text-muted-foreground">{idx + 1}.</span> {criterion.name} <span className="text-red-500">*</span>{' '}
|
|
<span className="font-normal text-muted-foreground">({criterion.weight}%)</span>
|
|
</Label>
|
|
<Select
|
|
value={ktMatrixSelectedValues[criterion.name] ?? undefined}
|
|
onValueChange={(value) => {
|
|
const option = criterion.options.find((o: any) => o.value === value);
|
|
if (option) handleKTMatrixChange(criterion.name, option.value, option.score);
|
|
}}
|
|
>
|
|
<SelectTrigger id={`kt-matrix-${idx}`} className="h-10 w-full text-left text-sm font-normal" data-testid={`onboarding-kt-matrix-select-${idx}`}>
|
|
<SelectValue placeholder="Choose an option…" />
|
|
</SelectTrigger>
|
|
<SelectContent position="popper" className="max-h-72 w-[var(--radix-select-trigger-width)]">
|
|
{criterion.options.map((option: any) => (
|
|
<SelectItem key={option.value} value={option.value} className="py-2.5 text-sm leading-snug" data-testid={`onboarding-kt-matrix-option-${idx}-${option.value}`}>
|
|
{option.label} <span className="text-muted-foreground">({option.score})</span>
|
|
</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
))}
|
|
<div className="space-y-2 border-t border-border pt-6">
|
|
<Label htmlFor="kt-matrix-remarks" className="text-sm font-medium">Notes <span className="font-normal text-muted-foreground">(optional)</span></Label>
|
|
<Textarea
|
|
id="kt-matrix-remarks"
|
|
placeholder="Optional remarks…"
|
|
className="min-h-[96px] resize-y text-sm leading-relaxed"
|
|
value={ktMatrixRemarks}
|
|
onChange={(e) => setKtMatrixRemarks(e.target.value)}
|
|
data-testid="onboarding-kt-matrix-remarks-textarea"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div className="flex shrink-0 flex-col gap-4 border-t px-5 py-4 sm:flex-row sm:items-center sm:justify-between">
|
|
<p className="text-sm text-muted-foreground">Weighted total <span className="font-semibold tabular-nums text-foreground" data-testid="onboarding-kt-matrix-total-score">{calculateKTScore()}</span><span className="text-muted-foreground"> / 100</span></p>
|
|
<div className="flex gap-2 sm:shrink-0">
|
|
<Button variant="outline" onClick={() => setShowKTMatrixModal(false)} data-testid="onboarding-kt-matrix-cancel">Cancel</Button>
|
|
<Button onClick={handleSubmitKTMatrix} disabled={isSubmittingKT || Object.keys(ktMatrixSelectedValues).length < ktCriteria.length} data-testid="onboarding-kt-matrix-submit">{isSubmittingKT ? 'Saving…' : 'Submit'}</Button>
|
|
</div>
|
|
</div>
|
|
</DialogContent>
|
|
</Dialog>
|
|
|
|
<Dialog open={showLevel2FeedbackModal} onOpenChange={setShowLevel2FeedbackModal}>
|
|
<DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto" data-testid="onboarding-level2-feedback-modal">
|
|
<DialogHeader>
|
|
<DialogTitle>Level 2 Interview Feedback</DialogTitle>
|
|
<DialogDescription>Provide detailed feedback from the Level 2 interview (DD Lead + ZBH evaluation).</DialogDescription>
|
|
</DialogHeader>
|
|
<div className="space-y-4">
|
|
<div><Label>Interview Date <span className="text-red-500">*</span></Label><Input type="date" className="mt-2" value={level2Feedback.interviewDate} disabled /></div>
|
|
<div><Label>Interviewer Name <span className="text-red-500">*</span></Label><Input placeholder="Enter your name" className="mt-2" value={level2Feedback.interviewerName} disabled /></div>
|
|
<div>
|
|
<Label>Overall Performance Score <span className="text-red-500">*</span></Label>
|
|
<Select value={level2Feedback.overallScore} onValueChange={(value) => handleLevel2Change('overallScore', value)}>
|
|
<SelectTrigger className="mt-2" data-testid="onboarding-level2-overall-score-select"><SelectValue placeholder="Select score" /></SelectTrigger>
|
|
<SelectContent><SelectItem value="10">Outstanding (9-10)</SelectItem><SelectItem value="8">Excellent (7-8)</SelectItem><SelectItem value="6">Good (5-6)</SelectItem><SelectItem value="4">Average (3-4)</SelectItem><SelectItem value="2">Below Average (1-2)</SelectItem></SelectContent>
|
|
</Select>
|
|
</div>
|
|
<Separator />
|
|
{(l2Fields || []).map((field: any, idx: number) => (
|
|
<div key={field.itemKey || idx}>
|
|
<Label>
|
|
{field.label}
|
|
{field.isRequired && <span className="text-red-500">*</span>}
|
|
</Label>
|
|
{field.type === 'select' ? (
|
|
<Select value={level2Feedback[field.itemKey] || ''} onValueChange={(value) => handleLevel2Change(field.itemKey, value)}>
|
|
<SelectTrigger className="mt-2"><SelectValue placeholder={`Select ${field.label}...`} /></SelectTrigger>
|
|
<SelectContent>
|
|
{(field.options || []).map((opt: any, oIdx: number) => (
|
|
<SelectItem key={oIdx} value={opt.optionValue || opt.value}>{opt.optionLabel || opt.label}</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
) : field.type === 'number' ? (
|
|
<Input type="number" className="mt-2" value={level2Feedback[field.itemKey] || ''} onChange={(e) => handleLevel2Change(field.itemKey, e.target.value)} />
|
|
) : (
|
|
<Textarea
|
|
placeholder={`Enter ${field.label.toLowerCase()}...`}
|
|
className="mt-2"
|
|
rows={3}
|
|
value={level2Feedback[field.itemKey] || ''}
|
|
onChange={(e) => handleLevel2Change(field.itemKey, e.target.value)}
|
|
/>
|
|
)}
|
|
</div>
|
|
))}
|
|
<div className="flex gap-3">
|
|
<Button variant="outline" className="flex-1" onClick={() => setShowLevel2FeedbackModal(false)} data-testid="onboarding-level2-feedback-cancel">Cancel</Button>
|
|
<Button className="flex-1 bg-black hover:bg-zinc-800 text-white" onClick={handleSubmitLevel2Feedback} disabled={isSubmittingLevel2} data-testid="onboarding-level2-feedback-submit">{isSubmittingLevel2 ? 'Submitting...' : 'Submit Feedback'}</Button>
|
|
</div>
|
|
</div>
|
|
</DialogContent>
|
|
</Dialog>
|
|
|
|
<Dialog open={showFeedbackDetailsModal} onOpenChange={setShowFeedbackDetailsModal}>
|
|
<DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto" data-testid="onboarding-feedback-details-modal">
|
|
<DialogHeader><DialogTitle>Interview Feedback Details</DialogTitle></DialogHeader>
|
|
{selectedEvaluationForView && (
|
|
<div className="space-y-4">
|
|
<div className="grid grid-cols-2 gap-4 bg-slate-50 p-4 rounded-lg">
|
|
<div><p className="text-sm font-medium text-slate-500">Interviewer</p><p className="font-semibold" data-testid="onboarding-feedback-details-interviewer">{selectedEvaluationForView.evaluator?.fullName}</p></div>
|
|
<div><p className="text-sm font-medium text-slate-500">Role</p><p data-testid="onboarding-feedback-details-role">{selectedEvaluationForView.evaluator?.role?.roleName || 'N/A'}</p></div>
|
|
<div><p className="text-sm font-medium text-slate-500">{selectedEvaluationForView.interview?.level === 1 ? 'Score (KT Matrix)' : 'Overall Score'}</p><p className="font-bold text-lg" data-testid="onboarding-feedback-details-score">{selectedEvaluationForView.ktMatrixScore ? `${selectedEvaluationForView.ktMatrixScore}/${selectedEvaluationForView.interview?.level === 1 ? '100' : '10'}` : 'N/A'}</p></div>
|
|
<div><p className="text-sm font-medium text-slate-500">Recommendation</p><Badge variant={selectedEvaluationForView.recommendation?.toLowerCase().includes('reject') ? 'destructive' : selectedEvaluationForView.recommendation?.toLowerCase().includes('hold') ? 'secondary' : 'default'} data-testid="onboarding-feedback-details-recommendation">{selectedEvaluationForView.recommendation || 'N/A'}</Badge></div>
|
|
</div>
|
|
<Separator />
|
|
<div>
|
|
<h4 className="font-semibold mb-3">Detailed Feedback</h4>
|
|
{selectedEvaluationForView.feedbackDetails?.length > 0 ? (
|
|
<div className="space-y-4">
|
|
{selectedEvaluationForView.feedbackDetails.map((detail: any, index: number) => (
|
|
<div key={index} className="border-b last:border-0 pb-3 last:pb-0" data-testid={`onboarding-feedback-detail-item-${index}`}>
|
|
<p className="font-medium text-slate-900">{detail.feedbackType}</p>
|
|
<p className="text-slate-700 mt-1 whitespace-pre-wrap text-sm">{detail.comments}</p>
|
|
</div>
|
|
))}
|
|
</div>
|
|
) : (
|
|
<p className="text-slate-500 italic">No detailed feedback available.</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
)}
|
|
</DialogContent>
|
|
</Dialog>
|
|
|
|
<Dialog open={showLevel3FeedbackModal} onOpenChange={setShowLevel3FeedbackModal}>
|
|
<DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto" data-testid="onboarding-level3-feedback-modal">
|
|
<DialogHeader>
|
|
<DialogTitle>Level 3 Interview Feedback</DialogTitle>
|
|
<DialogDescription>Provide detailed feedback from the Level 3 interview (NBH + DD-Head evaluation).</DialogDescription>
|
|
</DialogHeader>
|
|
<div className="space-y-4">
|
|
<div><Label>Interview Date <span className="text-red-500">*</span></Label><Input type="date" className="mt-2" value={level3Feedback.interviewDate} disabled /></div>
|
|
<div><Label>Interviewer Name <span className="text-red-500">*</span></Label><Input placeholder="Enter your name" className="mt-2" value={level3Feedback.interviewerName} disabled /></div>
|
|
<div>
|
|
<Label>Overall Performance Score <span className="text-red-500">*</span></Label>
|
|
<Select value={level3Feedback.overallScore} onValueChange={(value) => handleLevel3Change('overallScore', value)}>
|
|
<SelectTrigger className="mt-2" data-testid="onboarding-level3-overall-score-select"><SelectValue placeholder="Select score" /></SelectTrigger>
|
|
<SelectContent><SelectItem value="10">Outstanding (9-10)</SelectItem><SelectItem value="8">Excellent (7-8)</SelectItem><SelectItem value="6">Good (5-6)</SelectItem><SelectItem value="4">Average (3-4)</SelectItem><SelectItem value="2">Below Average (1-2)</SelectItem></SelectContent>
|
|
</Select>
|
|
</div>
|
|
<Separator />
|
|
{(l3Fields || []).map((field: any, idx: number) => (
|
|
<div key={field.itemKey || idx}>
|
|
<Label>
|
|
{field.label}
|
|
{field.isRequired && <span className="text-red-500">*</span>}
|
|
</Label>
|
|
{field.type === 'select' ? (
|
|
<Select value={level3Feedback[field.itemKey] || ''} onValueChange={(value) => handleLevel3Change(field.itemKey, value)}>
|
|
<SelectTrigger className="mt-2"><SelectValue placeholder={`Select ${field.label}...`} /></SelectTrigger>
|
|
<SelectContent>
|
|
{(field.options || []).map((opt: any, oIdx: number) => (
|
|
<SelectItem key={oIdx} value={opt.optionValue || opt.value}>{opt.optionLabel || opt.label}</SelectItem>
|
|
))}
|
|
</SelectContent>
|
|
</Select>
|
|
) : field.type === 'number' ? (
|
|
<Input type="number" className="mt-2" value={level3Feedback[field.itemKey] || ''} onChange={(e) => handleLevel3Change(field.itemKey, e.target.value)} />
|
|
) : (
|
|
<Textarea
|
|
placeholder={`Enter ${field.label.toLowerCase()}...`}
|
|
className="mt-2"
|
|
rows={3}
|
|
value={level3Feedback[field.itemKey] || ''}
|
|
onChange={(e) => handleLevel3Change(field.itemKey, e.target.value)}
|
|
/>
|
|
)}
|
|
</div>
|
|
))}
|
|
<div className="flex gap-3">
|
|
<Button variant="outline" className="flex-1" onClick={() => setShowLevel3FeedbackModal(false)} data-testid="onboarding-level3-feedback-cancel">Cancel</Button>
|
|
<Button className="flex-1 bg-black hover:bg-zinc-800 text-white" onClick={handleSubmitLevel3Feedback} disabled={isSubmittingLevel3} data-testid="onboarding-level3-feedback-submit">{isSubmittingLevel3 ? 'Submitting...' : 'Submit Feedback'}</Button>
|
|
</div>
|
|
</div>
|
|
</DialogContent>
|
|
</Dialog>
|
|
|
|
<Dialog open={showDocumentsModal} onOpenChange={(open) => { setShowDocumentsModal(open); if (!open) setShowUploadForm(false); }}>
|
|
<DialogContent className="max-w-[95vw] sm:max-w-2xl md:max-w-3xl lg:max-w-4xl max-h-[90vh] overflow-hidden flex flex-col p-4 sm:p-6" data-testid="onboarding-documents-modal">
|
|
<DialogHeader className="pb-4">
|
|
<DialogTitle className="text-xl font-bold flex items-center gap-2"><FileText className="w-5 h-5 text-amber-600" />Documents - {selectedStage || 'General'}</DialogTitle>
|
|
<DialogDescription className="text-slate-500">View and manage documents uploaded for this stage.</DialogDescription>
|
|
</DialogHeader>
|
|
{!showUploadForm ? (
|
|
<div className="flex-1 flex flex-col min-h-0 space-y-4">
|
|
{getDocumentsForStage(selectedStage || '').length > 0 ? (
|
|
<div className="flex-1 overflow-auto border rounded-lg border-slate-200" data-testid="onboarding-documents-table-container">
|
|
<Table className="w-full table-auto">
|
|
<TableHeader className="bg-slate-50/80 sticky top-0 z-10">
|
|
<TableRow className="hover:bg-transparent border-b">
|
|
<TableHead className="w-[45%] min-w-[150px] font-semibold text-slate-900 py-3">Document Name</TableHead>
|
|
<TableHead className="w-[15%] min-w-[100px] font-semibold text-slate-900 py-3">Type</TableHead>
|
|
<TableHead className="w-[15%] min-w-[100px] font-semibold text-slate-900 py-3">Upload Date</TableHead>
|
|
<TableHead className="w-[15%] min-w-[140px] font-semibold text-slate-900 py-3">Uploaded By</TableHead>
|
|
<TableHead className="text-right w-[10%] min-w-[80px] font-semibold text-slate-900 py-3">Actions</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{getDocumentsForStage(selectedStage || '').map((doc: any, index: number) => (
|
|
<TableRow key={doc.id} className="hover:bg-slate-50/50 transition-colors" data-testid={`onboarding-document-row-${index}`}>
|
|
<TableCell className="py-3"><div className="flex items-center gap-2 min-w-0"><FileText className="w-4 h-4 text-slate-400 shrink-0" /><span className="truncate font-medium text-slate-700" title={doc.fileName} data-testid={`onboarding-document-name-${index}`}>{doc.fileName}</span></div></TableCell>
|
|
<TableCell className="py-3"><Badge variant="outline" className="capitalize whitespace-nowrap font-normal border-slate-200 bg-white" data-testid={`onboarding-document-type-${index}`}>{doc.documentType?.toLowerCase() || 'Other'}</Badge></TableCell>
|
|
<TableCell className="py-3 whitespace-nowrap text-slate-600">{formatDateTime(doc.createdAt)}</TableCell>
|
|
<TableCell className="py-3 text-slate-600">{doc.uploader?.fullName || (doc.uploadedBy ? 'System User' : 'Applicant')}</TableCell>
|
|
<TableCell className="text-right py-3">
|
|
<div className="flex gap-1 justify-end">
|
|
<Button variant="ghost" size="icon" className="h-8 w-8 text-slate-400 hover:text-indigo-600 hover:bg-indigo-50 rounded-full" onClick={() => { setPreviewDoc(doc); setShowPreviewModal(true); }} data-testid={`onboarding-document-preview-${index}`}><Eye className="w-4 h-4" /></Button>
|
|
<Button variant="ghost" size="icon" className="h-8 w-8 text-slate-400 hover:text-amber-600 hover:bg-amber-50 rounded-full" onClick={() => { const baseUrl = (import.meta as any).env?.VITE_API_URL || 'http://localhost:5000'; window.open(`${baseUrl}/${doc.filePath}`, '_blank'); }} data-testid={`onboarding-document-download-${index}`}><Download className="w-4 h-4" /></Button>
|
|
</div>
|
|
</TableCell>
|
|
</TableRow>
|
|
))}
|
|
</TableBody>
|
|
</Table>
|
|
</div>
|
|
) : (
|
|
<div className="flex-1 flex flex-col items-center justify-center py-12 text-center border rounded-lg bg-slate-50/30" data-testid="onboarding-documents-empty"><div className="w-16 h-16 rounded-full bg-slate-100 flex items-center justify-center mb-4"><FileText className="w-8 h-8 text-slate-300" /></div><h3 className="text-slate-900 font-semibold mb-2">No Documents Found</h3><p className="text-slate-600 text-sm max-w-[250px]">No documents have been uploaded for this stage yet.</p></div>
|
|
)}
|
|
<div className="flex flex-col sm:flex-row gap-3 pt-2 mt-auto">
|
|
<Button className="flex-1 bg-amber-600 hover:bg-amber-700 text-white font-bold py-3 sm:py-5 rounded-xl shadow-lg shadow-amber-600/15 transition-all hover:scale-[1.01] active:scale-[0.99]" onClick={() => setShowUploadForm(true)} data-testid="onboarding-documents-upload-button"><Upload className="w-5 h-5 mr-3" />Upload Document</Button>
|
|
<Button variant="outline" className="flex-1 sm:flex-none py-3 sm:py-5 px-8 rounded-xl border-slate-200 font-semibold text-slate-600 hover:bg-slate-50" onClick={() => setShowDocumentsModal(false)} data-testid="onboarding-documents-close-button">Close</Button>
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<div className="space-y-6 py-4" data-testid="onboarding-documents-upload-form">
|
|
<div className="grid gap-6 bg-slate-50/50 p-4 sm:p-6 rounded-2xl border border-slate-200">
|
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-6">
|
|
<div className="space-y-2">
|
|
<Label className="text-slate-700 font-semibold px-1">Stage context <span className="text-red-500">*</span></Label>
|
|
<Select value={selectedStage || 'null'} onValueChange={(val) => setSelectedStage(val === 'null' ? null : val)}>
|
|
<SelectTrigger className="bg-white border-slate-200 h-11 rounded-xl focus:ring-amber-500 shadow-sm" data-testid="onboarding-documents-stage-select"><SelectValue placeholder="Select stage" /></SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="null">General / No Stage</SelectItem>
|
|
{flattenedStages.map((s: any, idx: number) => <SelectItem key={`${s.name}-${idx}`} value={s.name}>{s.parentBranch ? `${s.parentBranch}: ${s.name}` : s.name}</SelectItem>)}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label className="text-slate-700 font-semibold px-1">Document Type <span className="text-red-500">*</span></Label>
|
|
<Select value={uploadDocType} onValueChange={setUploadDocType}>
|
|
<SelectTrigger className="bg-white border-slate-200 h-11 rounded-xl focus:ring-amber-500 shadow-sm" data-testid="onboarding-documents-type-select"><SelectValue placeholder="Select type" /></SelectTrigger>
|
|
<SelectContent>
|
|
{(() => {
|
|
const baseDocs = ['Other'];
|
|
const stageConfigs = documentConfigs.filter((c: any) => {
|
|
const cfgStage = c.stageCode?.trim();
|
|
const selStage = (selectedStage || 'General').trim();
|
|
if (cfgStage === selStage) return true;
|
|
if (selStage.startsWith('EOR:') && cfgStage === 'EOR') return true;
|
|
if (!selectedStage && cfgStage === 'General') return true;
|
|
return false;
|
|
});
|
|
let filteredDocs: string[] = [];
|
|
if (stageConfigs.length > 0) filteredDocs = stageConfigs.map((c: any) => c.documentType);
|
|
else if (!selectedStage || selectedStage === 'General') {
|
|
filteredDocs = ['PAN Card', 'GST Certificate', 'Aadhaar Card', 'Passport Size Photograph', 'Partnership Deed', 'LLP Agreement', 'Certificate of Incorporation', 'Board Resolution', 'Firm Registration Certificate', 'Cancelled Check', 'Bank Statement', 'Other'];
|
|
} else if (selectedStage?.toLowerCase().includes('architecture')) {
|
|
filteredDocs = ['Architecture Blueprint', 'Site Plan', 'Proposed Site City Map', 'Site Readiness Report', 'Architecture Completion Certificate', 'Other'];
|
|
} else if (selectedStage?.toLowerCase().includes('fdd')) {
|
|
filteredDocs = ['FDD Final Audit Report', 'Bank Statement', 'Income Tax Returns (ITR)', 'CIBIL Report', 'Other'];
|
|
} else filteredDocs = baseDocs;
|
|
if (selectedStage?.startsWith('EOR: ')) {
|
|
const eorItem = selectedStage.replace('EOR: ', '');
|
|
if (!filteredDocs.includes(eorItem)) filteredDocs = [eorItem, ...filteredDocs];
|
|
}
|
|
return Array.from(new Set(filteredDocs)).map((doc, idx) => <SelectItem key={`${doc}-${idx}`} value={doc} data-testid={`onboarding-documents-type-option-${idx}`}>{doc}</SelectItem>);
|
|
})()}
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label className="text-slate-700 font-semibold px-1">Select File <span className="text-red-500">*</span></Label>
|
|
<Input type="file" className="bg-white border-slate-200 h-12 rounded-xl focus:ring-amber-500 shadow-sm file:mr-4 file:py-2 file:px-4 file:rounded-full file:border-0 file:text-sm file:font-semibold file:bg-amber-50 file:text-amber-700 hover:file:bg-amber-100 cursor-pointer" onChange={(e) => setUploadFile(e.target.files ? e.target.files[0] : null)} data-testid="onboarding-documents-file-input" />
|
|
</div>
|
|
</div>
|
|
<div className="flex flex-col sm:flex-row gap-3 pt-4">
|
|
<Button className="flex-1 order-2 sm:order-1 py-3 sm:py-5 rounded-xl border-slate-200 font-semibold text-slate-600 hover:bg-slate-50" variant="outline" onClick={() => setShowUploadForm(false)} disabled={isUploading} data-testid="onboarding-documents-upload-cancel">Cancel</Button>
|
|
<Button className="flex-1 order-1 sm:order-2 bg-amber-600 hover:bg-amber-700 text-white font-bold py-3 sm:py-5 rounded-xl shadow-lg shadow-amber-600/15 transition-all hover:scale-[1.01] active:scale-[0.99]" onClick={async () => { await handleUpload(); setShowUploadForm(false); }} disabled={!uploadFile || !uploadDocType || isUploading} data-testid="onboarding-documents-upload-submit">
|
|
{isUploading ? <span className="flex items-center gap-2"><div className="w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin" />Uploading...</span> : <span className="flex items-center gap-2"><Upload className="w-5 h-5" />Confirm Upload</span>}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</DialogContent>
|
|
</Dialog>
|
|
<DocumentPreviewModal isOpen={showPreviewModal} onClose={() => setShowPreviewModal(false)} document={previewDoc} />
|
|
|
|
<Dialog open={showFddFinalizeModal} onOpenChange={setShowFddFinalizeModal}>
|
|
<DialogContent className="max-w-md p-0 overflow-hidden border-none shadow-2xl rounded-3xl" data-testid="onboarding-fdd-finalize-modal">
|
|
<div className="bg-slate-950 p-8 flex items-center justify-center relative overflow-hidden"><div className="absolute inset-0 bg-gradient-to-br from-amber-600/20 to-transparent" /><div className="w-20 h-20 bg-amber-600/20 rounded-full flex items-center justify-center animate-pulse relative z-10 shadow-[0_0_40px_rgba(245,158,11,0.2)]"><ShieldCheck className="w-10 h-10 text-amber-500" /></div></div>
|
|
<div className="p-8 space-y-6 bg-white">
|
|
<DialogHeader>
|
|
<DialogTitle className="text-2xl font-black text-slate-900 text-center tracking-tight">Finalize FDD Audit</DialogTitle>
|
|
<DialogDescription className="text-slate-500 text-center pt-2 leading-relaxed text-sm font-medium">You are about to submit your final findings. This action will <span className="font-bold text-slate-900 underline decoration-amber-500 decoration-2">lock the audit session</span> and trigger the LOI approval workflow.</DialogDescription>
|
|
</DialogHeader>
|
|
<div className="space-y-4">
|
|
{(currentUser?.role !== 'FDD' && currentUser?.roleCode !== 'FDD') && (
|
|
<div className="space-y-2">
|
|
<Label className="text-[10px] font-black uppercase tracking-widest text-slate-400">Auditor Recommendation <span className="text-red-500">*</span></Label>
|
|
<div className="flex gap-2">
|
|
{['Recommended', 'Qualified with Observations', 'Not Recommended'].map((rec) => (
|
|
<Button key={rec} variant={fddAuditRecommendation === rec ? 'default' : 'outline'} className={cn("flex-1 h-10 font-bold text-[9px] uppercase tracking-wider rounded-xl transition-all", fddAuditRecommendation === rec && rec === 'Recommended' && "bg-emerald-600 hover:bg-emerald-700", fddAuditRecommendation === rec && rec === 'Qualified with Observations' && "bg-amber-500 hover:bg-amber-600", fddAuditRecommendation === rec && rec === 'Not Recommended' && "bg-red-600 hover:bg-red-700")} onClick={() => setFddAuditRecommendation(rec)} data-testid={`onboarding-fdd-recommendation-${rec.replace(/\s+/g, '-').toLowerCase()}`}>{rec}</Button>
|
|
))}
|
|
</div>
|
|
</div>
|
|
)}
|
|
<div className="space-y-2">
|
|
<Label className="text-[10px] font-black uppercase tracking-widest text-slate-400">Findings Summary</Label>
|
|
<Textarea
|
|
placeholder="Summarize key financial findings or discrepancies..."
|
|
className="min-h-[100px] rounded-xl border-slate-200 focus:ring-amber-500 text-sm"
|
|
value={fddAuditFindings}
|
|
onChange={(e) => setFddAuditFindings(e.target.value)}
|
|
data-testid="onboarding-fdd-findings-textarea"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div className="bg-amber-50 p-4 rounded-2xl flex gap-3 border border-amber-100"><Info className="w-5 h-5 text-amber-600 shrink-0 mt-0.5" /><p className="text-[11px] text-amber-800 font-medium italic">Ensure the final PDF report is uploaded first. This satisfies the FDD statutory requirement.</p></div>
|
|
<div className="flex flex-col sm:flex-row gap-3 pt-2">
|
|
<Button variant="outline" className="w-full sm:flex-1 h-12 rounded-2xl font-bold text-slate-600 hover:bg-slate-50 border-slate-200" onClick={() => setShowFddFinalizeModal(false)} disabled={isFinalizingFdd} data-testid="onboarding-fdd-finalize-cancel">Cancel</Button>
|
|
<Button
|
|
className="w-full sm:flex-1 h-12 rounded-2xl font-bold bg-slate-950 hover:bg-slate-900 text-white shadow-lg shadow-slate-200 transition-all active:scale-95 border-b-4 border-amber-500"
|
|
disabled={isFinalizingFdd || !fddAuditFindings}
|
|
data-testid="onboarding-fdd-finalize-submit"
|
|
onClick={async () => {
|
|
try {
|
|
setIsFinalizingFdd(true);
|
|
await onboardingService.submitStageDecision({
|
|
applicationId: application!.id,
|
|
stageCode: 'FDD_VERIFICATION',
|
|
decision: 'Approved',
|
|
remarks: (currentUser?.role === 'FDD' || currentUser?.roleCode === 'FDD')
|
|
? `Findings: ${fddAuditFindings}`
|
|
: `[RECOMMENDATION: ${fddAuditRecommendation}] \nFindings: ${fddAuditFindings}`,
|
|
nextStatus: 'LOI In Progress',
|
|
nextProgress: 65
|
|
});
|
|
toast.success('FDD Audit finalized and submitted.');
|
|
setShowFddFinalizeModal(false);
|
|
fetchApplication();
|
|
} catch {
|
|
toast.error('Submission failed');
|
|
} finally {
|
|
setIsFinalizingFdd(false);
|
|
}
|
|
}}
|
|
>
|
|
{isFinalizingFdd ? <Loader2 className="w-5 h-5 animate-spin" /> : 'Confirm & Submit'}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</DialogContent>
|
|
</Dialog>
|
|
|
|
<Dialog open={showFddFlagModal} onOpenChange={setShowFddFlagModal}>
|
|
<DialogContent className="max-w-md p-0 overflow-hidden border-none shadow-2xl rounded-3xl" data-testid="onboarding-fdd-flag-modal">
|
|
<div className="bg-slate-950 p-8 flex items-center justify-center relative overflow-hidden"><div className="absolute inset-0 bg-gradient-to-br from-red-600/20 to-transparent" /><div className="w-20 h-20 bg-red-600/20 rounded-full flex items-center justify-center relative z-10 shadow-[0_0_40px_rgba(220,38,38,0.2)]"><ShieldAlert className="w-10 h-10 text-red-500" /></div></div>
|
|
<div className="p-8 space-y-6 bg-white text-center">
|
|
<DialogHeader>
|
|
<DialogTitle className="text-2xl font-black text-slate-900 tracking-tight">Flag Non-Responsive</DialogTitle>
|
|
<DialogDescription className="text-slate-500 pt-2 leading-relaxed text-sm font-medium">Are you sure you want to flag this applicant? This will notify the DD Admin that the audit cannot proceed due to applicant's non-cooperation.</DialogDescription>
|
|
</DialogHeader>
|
|
<div className="bg-red-50 p-4 rounded-2xl flex gap-3 border border-red-100"><AlertCircle className="w-5 h-5 text-red-600 shrink-0 mt-0.5" /><p className="text-[11px] text-red-800 text-left font-medium">"Applicant is unresponsive to multiple queries and financial document requests."</p></div>
|
|
<div className="flex flex-col sm:flex-row gap-3 pt-2">
|
|
<Button variant="outline" className="w-full sm:flex-1 h-12 rounded-2xl font-bold text-slate-600 hover:bg-slate-50 border-slate-200" onClick={() => setShowFddFlagModal(false)} disabled={isFddFlagging} data-testid="onboarding-fdd-flag-cancel">Go Back</Button>
|
|
<Button
|
|
className="w-full sm:flex-1 h-12 rounded-2xl font-bold bg-slate-950 hover:bg-slate-900 text-white shadow-lg shadow-slate-200 transition-all active:scale-95 border-b-4 border-red-600"
|
|
disabled={isFddFlagging}
|
|
data-testid="onboarding-fdd-flag-submit"
|
|
onClick={async () => {
|
|
try {
|
|
setIsFddFlagging(true);
|
|
await onboardingService.submitStageDecision({
|
|
applicationId: application!.id,
|
|
stageCode: 'FDD_VERIFICATION',
|
|
decision: 'Rejected',
|
|
remarks: 'Applicant is non-responsive to FDD queries.'
|
|
});
|
|
toast.error('Applicant flagged as non-responsive.');
|
|
setShowFddFlagModal(false);
|
|
fetchApplication();
|
|
} catch {
|
|
toast.error('Action failed');
|
|
} finally {
|
|
setIsFddFlagging(false);
|
|
}
|
|
}}
|
|
>
|
|
{isFddFlagging ? <Loader2 className="w-5 h-5 animate-spin" /> : 'Confirm Flag'}
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</DialogContent>
|
|
</Dialog>
|
|
|
|
<Dialog open={showFirmTypeModal} onOpenChange={setShowFirmTypeModal}>
|
|
<DialogContent className="max-w-md p-0 overflow-hidden rounded-3xl border-none shadow-2xl" data-testid="onboarding-firm-type-modal">
|
|
<div className="bg-amber-600 p-8 text-white">
|
|
<div className="w-16 h-16 rounded-2xl bg-white/20 flex items-center justify-center mb-6 backdrop-blur-sm border border-white/30 shadow-inner"><Building2 className="w-8 h-8 text-white" /></div>
|
|
<h3 className="text-2xl font-black tracking-tight mb-2">Update Firm Type</h3>
|
|
<p className="text-amber-100/80 text-sm font-medium leading-relaxed">Select the proposed legal constitution for this dealership application.</p>
|
|
</div>
|
|
<div className="p-8 space-y-6 bg-white">
|
|
<div className="space-y-2">
|
|
<Label className="text-[10px] text-slate-400 uppercase tracking-widest font-black">Proposed Legal Constitution <span className="text-red-500">*</span></Label>
|
|
<Select value={tempFirmType} onValueChange={setTempFirmType}>
|
|
<SelectTrigger className="h-12 rounded-xl border-slate-200 focus:ring-amber-500" data-testid="onboarding-firm-type-select"><SelectValue placeholder="Select Firm Type" /></SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="Proprietorship" data-testid="onboarding-firm-type-proprietorship">Proprietorship</SelectItem>
|
|
<SelectItem value="Partnership" data-testid="onboarding-firm-type-partnership">Partnership</SelectItem>
|
|
<SelectItem value="Limited Liability partnership" data-testid="onboarding-firm-type-llp">LLP (Limited Liability partnership)</SelectItem>
|
|
<SelectItem value="Private Limited Company" data-testid="onboarding-firm-type-pvt-ltd">Private Limited Company</SelectItem>
|
|
<SelectItem value="Public Limited Company" data-testid="onboarding-firm-type-pub-ltd">Public Limited Company</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
</div>
|
|
<div className="flex gap-3 pt-2">
|
|
<Button variant="outline" className="flex-1 h-12 rounded-xl font-bold text-slate-600 border-slate-200" onClick={() => setShowFirmTypeModal(false)} disabled={updatingFirmType} data-testid="onboarding-firm-type-cancel">Cancel</Button>
|
|
<Button className="flex-1 h-12 rounded-xl font-bold bg-amber-600 hover:bg-amber-700 text-white shadow-lg shadow-amber-200 transition-all active:scale-95" disabled={updatingFirmType || !tempFirmType} onClick={handleUpdateFirmType} data-testid="onboarding-firm-type-submit">{updatingFirmType ? <Loader2 className="w-5 h-5 animate-spin" /> : 'Update Type'}</Button>
|
|
</div>
|
|
</div>
|
|
</DialogContent>
|
|
</Dialog>
|
|
|
|
</>
|
|
);
|
|
}
|
|
|
|
|