started removing document type dependecy and f&F screeen bug changes fixed

This commit is contained in:
Laxman 2026-05-19 21:26:47 +05:30
parent 61deac775c
commit faa29a7511
9 changed files with 405 additions and 276 deletions

View File

@ -151,6 +151,9 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }:
const [isUploadDialogOpen, setIsUploadDialogOpen] = useState(false); const [isUploadDialogOpen, setIsUploadDialogOpen] = useState(false);
const [selectedDocType, setSelectedDocType] = useState<number | null>(null); const [selectedDocType, setSelectedDocType] = useState<number | null>(null);
const [uploadFile, setUploadFile] = useState<File | null>(null); const [uploadFile, setUploadFile] = useState<File | null>(null);
// True when the dialog was opened from a checklist row -> doc type is implicit,
// so we hide the dropdown and show the doc name as a read-only badge instead.
const [docTypeLocked, setDocTypeLocked] = useState(false);
const [activeMainTab, setActiveMainTab] = useState('workflow'); const [activeMainTab, setActiveMainTab] = useState('workflow');
const [activeDocumentTab, setActiveDocumentTab] = useState('required'); const [activeDocumentTab, setActiveDocumentTab] = useState('required');
const [request, setRequest] = useState<any>(null); const [request, setRequest] = useState<any>(null);
@ -890,9 +893,26 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }:
<div className="space-y-4"> <div className="space-y-4">
<div className="flex items-center justify-between gap-4"> <div className="flex items-center justify-between gap-4">
<h4 className="text-slate-900">Document Checklist</h4> <h4 className="text-slate-900">Document Checklist</h4>
<Dialog open={isUploadDialogOpen} onOpenChange={setIsUploadDialogOpen}> <Dialog
open={isUploadDialogOpen}
onOpenChange={(open) => {
setIsUploadDialogOpen(open);
if (!open) {
setDocTypeLocked(false);
setUploadFile(null);
}
}}
>
<DialogTrigger asChild> <DialogTrigger asChild>
<Button size="sm" className="bg-amber-600 hover:bg-amber-700"> <Button
size="sm"
className="bg-amber-600 hover:bg-amber-700"
onClick={() => {
setDocTypeLocked(false);
setSelectedDocType(null);
setUploadFile(null);
}}
>
<Upload className="w-4 h-4 mr-2" /> <Upload className="w-4 h-4 mr-2" />
Upload Document Upload Document
</Button> </Button>
@ -901,29 +921,42 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }:
<DialogHeader> <DialogHeader>
<DialogTitle>Upload Document</DialogTitle> <DialogTitle>Upload Document</DialogTitle>
<DialogDescription> <DialogDescription>
Select the document type and upload the file {docTypeLocked
? 'Pick a file for the selected document.'
: 'Select the document type and upload the file.'}
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<div className="space-y-4"> <div className="space-y-4">
<div> {docTypeLocked && selectedDocType != null ? (
<Label>Document Type</Label> <div>
<select <Label>Document</Label>
className="mt-1 flex h-10 w-full rounded-md border border-slate-200 bg-white px-3 py-2 text-sm ring-offset-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-amber-500 focus-visible:ring-offset-2" <div className="mt-1 flex items-center gap-2 bg-amber-50 border border-amber-200 rounded-md px-3 h-10">
value={selectedDocType != null ? String(selectedDocType) : ''} <Badge className="bg-amber-600 text-white border-transparent">
onChange={(e) => { {documentNames[selectedDocType] || `Document ${selectedDocType}`}
const v = e.target.value; </Badge>
setSelectedDocType(v ? Number(v) : null); </div>
}} </div>
> ) : (
<option value="">Select document type</option> <div>
{uploadDocumentTypeOptions.map((docNum) => ( <Label>Document Type</Label>
<option key={docNum} value={String(docNum)}> <select
{docNum !== OTHER_DOCUMENT_DOC_NUMBER && isDocTypeUploaded(docNum) ? '✓ ' : ''} className="mt-1 flex h-10 w-full rounded-md border border-slate-200 bg-white px-3 py-2 text-sm ring-offset-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-amber-500 focus-visible:ring-offset-2"
{documentNames[docNum] || `Document ${docNum}`} value={selectedDocType != null ? String(selectedDocType) : ''}
</option> onChange={(e) => {
))} const v = e.target.value;
</select> setSelectedDocType(v ? Number(v) : null);
</div> }}
>
<option value="">Select document type</option>
{uploadDocumentTypeOptions.map((docNum) => (
<option key={docNum} value={String(docNum)}>
{docNum !== OTHER_DOCUMENT_DOC_NUMBER && isDocTypeUploaded(docNum) ? '✓ ' : ''}
{documentNames[docNum] || `Document ${docNum}`}
</option>
))}
</select>
</div>
)}
<div> <div>
<Label>Upload File</Label> <Label>Upload File</Label>
<Input type="file" className="mt-1" onChange={(e) => setUploadFile(e.target.files?.[0] || null)} /> <Input type="file" className="mt-1" onChange={(e) => setUploadFile(e.target.files?.[0] || null)} />
@ -976,15 +1009,33 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }:
)} )}
</div> </div>
</div> </div>
{uploaded ? ( <div className="flex items-center gap-2">
<Badge className={getStatusColor(uploaded.status)}> {uploaded ? (
{uploaded.status} <Badge className={getStatusColor(uploaded.status)}>
</Badge> {uploaded.status}
) : ( </Badge>
<Badge className="bg-slate-100 text-slate-600 border-slate-300"> ) : (
Not Uploaded <Badge className="bg-slate-100 text-slate-600 border-slate-300">
</Badge> Not Uploaded
)} </Badge>
)}
{!ok && (
<Button
size="sm"
variant="ghost"
className="h-8 px-2 text-amber-700 hover:bg-amber-50"
onClick={() => {
setSelectedDocType(docNum);
setUploadFile(null);
setDocTypeLocked(true);
setIsUploadDialogOpen(true);
}}
>
<Upload className="w-3.5 h-3.5 mr-1" />
{isRejected ? 'Re-upload' : 'Upload'}
</Button>
)}
</div>
</div> </div>
); );
})} })}

View File

@ -691,9 +691,9 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
<TabsTrigger value="progress">Progress</TabsTrigger> <TabsTrigger value="progress">Progress</TabsTrigger>
<TabsTrigger value="details">Case Details</TabsTrigger> <TabsTrigger value="details">Case Details</TabsTrigger>
<TabsTrigger value="departments">Department Responses</TabsTrigger> <TabsTrigger value="departments">Department Responses</TabsTrigger>
<TabsTrigger value="financial">Financial Summary</TabsTrigger>
<TabsTrigger value="documents">Documents</TabsTrigger> <TabsTrigger value="documents">Documents</TabsTrigger>
<TabsTrigger value="bank">Bank Details</TabsTrigger> {/* Bank Details tab hidden temporarily */}
{/* <TabsTrigger value="bank">Bank Details</TabsTrigger> */}
<TabsTrigger value="audit">Audit Trail</TabsTrigger> <TabsTrigger value="audit">Audit Trail</TabsTrigger>
</TabsList> </TabsList>
@ -1451,145 +1451,143 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
</Table> </Table>
</CardContent> </CardContent>
</Card> </Card>
</TabsContent>
{/* Financial Summary Tab */} {/* Department Claim vs Finance Validation */}
<TabsContent value="financial"> <Card className="border-blue-200 bg-blue-50 mt-6">
<div className="space-y-6"> <CardHeader>
<Card className="border-blue-200 bg-blue-50"> <CardTitle>Department Claim vs Finance Validation</CardTitle>
<CardHeader> <CardDescription>
<CardTitle>Department Claim vs Finance Validation</CardTitle> Final settlement totals are based on finance validated values.
<CardDescription> </CardDescription>
Final settlement totals are based on finance validated values. </CardHeader>
</CardDescription> <CardContent>
</CardHeader> <Table>
<CardContent> <TableHeader>
<Table> <TableRow>
<TableHeader> <TableHead>Department</TableHead>
<TableRow> <TableHead>Department Claim</TableHead>
<TableHead>Department</TableHead> <TableHead>Finance Validated</TableHead>
<TableHead>Department Claim</TableHead> <TableHead>Variance</TableHead>
<TableHead>Finance Validated</TableHead> </TableRow>
<TableHead>Variance</TableHead> </TableHeader>
<TableBody>
{departmentReconciliation.map((row) => (
<TableRow key={row.department}>
<TableCell>{row.department}</TableCell>
<TableCell>{row.claimAmount > 0 ? `${row.claimType}${row.claimAmount.toLocaleString()}` : '-'}</TableCell>
<TableCell>{row.validatedAmount > 0 ? `${row.validatedType}${row.validatedAmount.toLocaleString()}` : '-'}</TableCell>
<TableCell className={row.variance === 0 ? 'text-slate-600' : row.variance > 0 ? 'text-red-600' : 'text-green-600'}>
{row.claimAmount === 0 && row.validatedAmount === 0 ? '-' : `${row.variance.toLocaleString()}`}
</TableCell>
</TableRow> </TableRow>
</TableHeader> ))}
<TableBody> </TableBody>
{departmentReconciliation.map((row) => ( </Table>
<TableRow key={row.department}> </CardContent>
<TableCell>{row.department}</TableCell> </Card>
<TableCell>{row.claimAmount > 0 ? `${row.claimType}${row.claimAmount.toLocaleString()}` : '-'}</TableCell>
<TableCell>{row.validatedAmount > 0 ? `${row.validatedType}${row.validatedAmount.toLocaleString()}` : '-'}</TableCell>
<TableCell className={row.variance === 0 ? 'text-slate-600' : row.variance > 0 ? 'text-red-600' : 'text-green-600'}>
{row.claimAmount === 0 && row.validatedAmount === 0 ? '-' : `${row.variance.toLocaleString()}`}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
</Card>
<Card> {/* Financial Summary */}
<CardHeader> <Card className="mt-6">
<CardTitle>Financial Summary</CardTitle> <CardHeader>
<CardDescription> <CardTitle>Financial Summary</CardTitle>
Consolidated view of all payable and receivable amounts <CardDescription>
</CardDescription> Consolidated view of all payable and receivable amounts
</CardHeader> </CardDescription>
<CardContent> </CardHeader>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6"> <CardContent>
<div className="p-6 bg-green-50 rounded-lg border border-green-200"> <div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<p className="text-sm text-green-700 mb-2"> <div className="p-6 bg-green-50 rounded-lg border border-green-200">
Total Payable Amount <p className="text-sm text-green-700 mb-2">
</p> Total Payable Amount
<p className="text-3xl text-green-600"> </p>
{fnfCase.totalPayableAmount?.toLocaleString() || "0"} <p className="text-3xl text-green-600">
</p> {fnfCase.totalPayableAmount?.toLocaleString() || "0"}
<p className="text-xs text-green-600 mt-1"> </p>
Amount to be paid to dealer <p className="text-xs text-green-600 mt-1">
</p> Amount to be paid to dealer
</div> </p>
<div className="p-6 bg-red-50 rounded-lg border border-red-200">
<p className="text-sm text-red-700 mb-2">
Total receivable amount
</p>
<p className="text-3xl text-red-600">
{fnfCase.totalRecoveryAmount?.toLocaleString() || "0"}
</p>
<p className="text-xs text-red-600 mt-1">
Amount receivable from dealer
</p>
</div>
<div className="p-6 bg-amber-50 rounded-lg border border-amber-200">
<p className="text-sm text-amber-700 mb-2">
Total Deductions
</p>
<p className="text-3xl text-amber-600 font-bold">
{fnfCase.totalDeductions?.toLocaleString() || "0"}
</p>
<p className="text-xs text-amber-600 mt-1">
Warranty holdbacks / Policy penalties
</p>
</div>
<div className="p-6 bg-blue-50 rounded-lg border border-blue-200">
<p className="text-sm text-blue-700 mb-2">Net Settlement Amount</p>
<p
className={`text-3xl font-extrabold ${(fnfCase.netAmount || 0) < 0
? "text-red-600"
: "text-green-600"
}`}
>
{Math.abs(fnfCase.netAmount || 0).toLocaleString()}
</p>
<p className="text-xs text-blue-600 mt-1">
{(fnfCase.netAmount || 0) < 0
? "Receivable from dealer"
: "Payment to dealer"}
</p>
</div>
</div> </div>
</CardContent> <div className="p-6 bg-red-50 rounded-lg border border-red-200">
</Card> <p className="text-sm text-red-700 mb-2">
Total receivable amount
<Card> </p>
<CardHeader> <p className="text-3xl text-red-600">
<CardTitle>Finance Report Status</CardTitle> {fnfCase.totalRecoveryAmount?.toLocaleString() || "0"}
</CardHeader> </p>
<CardContent> <p className="text-xs text-red-600 mt-1">
<div className="flex items-center gap-4"> Amount receivable from dealer
<Badge </p>
className={ </div>
fnfCase.financeReportStatus === "Completed" <div className="p-6 bg-amber-50 rounded-lg border border-amber-200">
? "bg-green-100 text-green-700 border-green-300" <p className="text-sm text-amber-700 mb-2">
: fnfCase.financeReportStatus === "In Progress" Total Deductions
? "bg-yellow-100 text-yellow-700 border-yellow-300" </p>
: "bg-slate-100 text-slate-700 border-slate-300" <p className="text-3xl text-amber-600 font-bold">
} {fnfCase.totalDeductions?.toLocaleString() || "0"}
</p>
<p className="text-xs text-amber-600 mt-1">
Warranty holdbacks / Policy penalties
</p>
</div>
<div className="p-6 bg-blue-50 rounded-lg border border-blue-200">
<p className="text-sm text-blue-700 mb-2">Net Settlement Amount</p>
<p
className={`text-3xl font-extrabold ${(fnfCase.netAmount || 0) < 0
? "text-red-600"
: "text-green-600"
}`}
> >
{fnfCase.financeReportStatus} {Math.abs(fnfCase.netAmount || 0).toLocaleString()}
</Badge> </p>
{fnfCase.financeReportStatus === "Pending" && ( <p className="text-xs text-blue-600 mt-1">
<p className="text-slate-600 text-sm"> {(fnfCase.netAmount || 0) < 0
Waiting for all department responses before finance can ? "Receivable from dealer"
prepare final report : "Payment to dealer"}
</p> </p>
)}
{fnfCase.financeReportStatus === "In Progress" && (
<p className="text-slate-600 text-sm">
Finance team is reviewing department responses and
preparing final settlement report
</p>
)}
</div> </div>
{fnfCase.financeRemarks && ( </div>
<div className="mt-4 p-4 bg-slate-50 rounded-lg"> </CardContent>
<Label className="text-slate-600">Finance Remarks</Label> </Card>
<p className="mt-1">{fnfCase.financeRemarks}</p>
</div> {/* Finance Report Status */}
<Card className="mt-6">
<CardHeader>
<CardTitle>Finance Report Status</CardTitle>
</CardHeader>
<CardContent>
<div className="flex items-center gap-4">
<Badge
className={
fnfCase.financeReportStatus === "Completed"
? "bg-green-100 text-green-700 border-green-300"
: fnfCase.financeReportStatus === "In Progress"
? "bg-yellow-100 text-yellow-700 border-yellow-300"
: "bg-slate-100 text-slate-700 border-slate-300"
}
>
{fnfCase.financeReportStatus}
</Badge>
{fnfCase.financeReportStatus === "Pending" && (
<p className="text-slate-600 text-sm">
Waiting for all department responses before finance can
prepare final report
</p>
)} )}
</CardContent> {fnfCase.financeReportStatus === "In Progress" && (
</Card> <p className="text-slate-600 text-sm">
</div> Finance team is reviewing department responses and
preparing final settlement report
</p>
)}
</div>
{fnfCase.financeRemarks && (
<div className="mt-4 p-4 bg-slate-50 rounded-lg">
<Label className="text-slate-600">Finance Remarks</Label>
<p className="mt-1">{fnfCase.financeRemarks}</p>
</div>
)}
</CardContent>
</Card>
</TabsContent> </TabsContent>
{/* Documents Tab */} {/* Documents Tab */}

View File

@ -416,56 +416,36 @@ export function ApplicationDetailsExtendedModals(props: ApplicationDetailsExtend
</div> </div>
) : ( ) : (
<div className="space-y-6 py-4" data-testid="onboarding-documents-upload-form"> <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="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="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div className="space-y-2"> <div className="space-y-2">
<Label className="text-slate-700 font-semibold px-1">Stage context <span className="text-red-500">*</span></Label> <Label className="text-slate-700 font-semibold px-1">Document Name <span className="text-red-500">*</span></Label>
<Select value={selectedStage || 'null'} onValueChange={(val) => setSelectedStage(val === 'null' ? null : val)}> <Input
<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> type="text"
<SelectContent> placeholder="Enter document name"
<SelectItem value="null">General / No Stage</SelectItem> value={uploadDocType}
{flattenedStages.map((s: any, idx: number) => <SelectItem key={`${s.name}-${idx}`} value={s.name}>{s.parentBranch ? `${s.parentBranch}: ${s.name}` : s.name}</SelectItem>)} onChange={(e) => setUploadDocType(e.target.value)}
</SelectContent> className="bg-white border-slate-200 h-12 rounded-xl focus:ring-amber-500 shadow-sm"
</Select> data-testid="onboarding-documents-name-input"
/>
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label className="text-slate-700 font-semibold px-1">Document Type <span className="text-red-500">*</span></Label> <Label className="text-slate-700 font-semibold px-1">Select File <span className="text-red-500">*</span></Label>
<Select value={uploadDocType} onValueChange={setUploadDocType}> <Input
<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> type="file"
<SelectContent> 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) => {
const baseDocs = ['Other']; const file = e.target.files ? e.target.files[0] : null;
const stageConfigs = documentConfigs.filter((c: any) => { setUploadFile(file);
const cfgStage = c.stageCode?.trim(); if (file) {
const selStage = (selectedStage || 'General').trim(); const baseName = file.name.replace(/\.[^/.]+$/, '');
if (cfgStage === selStage) return true; setUploadDocType(baseName);
if (selStage.startsWith('EOR:') && cfgStage === 'EOR') return true; }
if (!selectedStage && cfgStage === 'General') return true; }}
return false; data-testid="onboarding-documents-file-input"
}); />
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> </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>
<div className="flex flex-col sm:flex-row gap-3 pt-4"> <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-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>

View File

@ -368,13 +368,20 @@ export function ApplicationDetailsTabs(props: ApplicationDetailsTabsProps) {
(!doc.stage && doc.documentType?.toLowerCase().includes(stage.name.toLowerCase().split(' ')[0])) (!doc.stage && doc.documentType?.toLowerCase().includes(stage.name.toLowerCase().split(' ')[0]))
).length; ).length;
// Upload is allowed only on the currently active (and unlocked) stage.
const canUploadHere = stage.status === 'active' && !stage.isLocked;
if (stageDocsCount === 0 && !canUploadHere) {
return null;
}
return ( return (
<div className="flex items-center gap-2 mt-1"> <div className="flex items-center gap-2 mt-1">
<button <button
onClick={() => { onClick={() => {
setSelectedStage(stage.name); setSelectedStage(stage.name);
setShowDocumentsModal(true); setShowDocumentsModal(true);
if (stageDocsCount === 0) setShowUploadForm(true); if (stageDocsCount === 0 && canUploadHere) setShowUploadForm(true);
}} }}
className="text-xs font-semibold text-blue-600 hover:text-blue-800 flex items-center gap-1.5 px-3 py-1 rounded-full bg-blue-50 border border-blue-100 hover:bg-blue-100 transition-all shadow-sm" className="text-xs font-semibold text-blue-600 hover:text-blue-800 flex items-center gap-1.5 px-3 py-1 rounded-full bg-blue-50 border border-blue-100 hover:bg-blue-100 transition-all shadow-sm"
data-testid={`onboarding-progress-stage-docs-${index}`} data-testid={`onboarding-progress-stage-docs-${index}`}
@ -471,23 +478,32 @@ export function ApplicationDetailsTabs(props: ApplicationDetailsTabsProps) {
<p className="text-slate-500 text-xs mt-0.5">{branchStage.description}</p> <p className="text-slate-500 text-xs mt-0.5">{branchStage.description}</p>
)} )}
<div className="flex items-center gap-2 mt-1"> {(() => {
<button // Upload is allowed only on the currently active branch stage.
onClick={() => { const canUploadHere = branchStage.status === 'active';
setSelectedStage(branchStage.name); if (stageDocs.length === 0 && !canUploadHere) {
setShowDocumentsModal(true); return null;
if (stageDocs.length === 0) setShowUploadForm(true); }
}} return (
className={cn( <div className="flex items-center gap-2 mt-1">
"text-[10px] font-medium flex items-center gap-1 transition-colors", <button
branchColor === 'blue' ? "text-blue-600 hover:text-blue-800" : "text-green-600 hover:text-green-800" onClick={() => {
)} setSelectedStage(branchStage.name);
data-testid={`onboarding-progress-branch-stage-docs-${branchKey}-${bsIdx}`} setShowDocumentsModal(true);
> if (stageDocs.length === 0 && canUploadHere) setShowUploadForm(true);
<FileText className="w-2.5 h-2.5" /> }}
{stageDocs.length > 0 ? `${stageDocs.length} Docs` : 'Upload'} className={cn(
</button> "text-[10px] font-medium flex items-center gap-1 transition-colors",
</div> branchColor === 'blue' ? "text-blue-600 hover:text-blue-800" : "text-green-600 hover:text-green-800"
)}
data-testid={`onboarding-progress-branch-stage-docs-${branchKey}-${bsIdx}`}
>
<FileText className="w-2.5 h-2.5" />
{stageDocs.length > 0 ? `${stageDocs.length} Docs` : 'Upload'}
</button>
</div>
);
})()}
<p className="text-slate-400 text-[10px] mt-1" data-testid={`onboarding-progress-branch-stage-status-${branchKey}-${bsIdx}`}> <p className="text-slate-400 text-[10px] mt-1" data-testid={`onboarding-progress-branch-stage-status-${branchKey}-${bsIdx}`}>
{isDone && branchStage.date ? `Done: ${formatDateTime(branchStage.date)}` : isDone && stageDocs.length > 0 ? `Uploaded: ${formatDateTime(stageDocs[0].updatedAt || stageDocs[0].createdAt)}` : 'Pending'} {isDone && branchStage.date ? `Done: ${formatDateTime(branchStage.date)}` : isDone && stageDocs.length > 0 ? `Uploaded: ${formatDateTime(stageDocs[0].updatedAt || stageDocs[0].createdAt)}` : 'Pending'}
</p> </p>

View File

@ -328,14 +328,26 @@ export function useApplicationDetailsAdminActions(params: UseApplicationDetailsA
const handleUpload = async () => { const handleUpload = async () => {
if (!uploadFile || !uploadDocType) { if (!uploadFile || !uploadDocType) {
toast.warning('Please select a file and document type'); toast.warning('Please enter a document name and select a file');
return; return;
} }
try { try {
setIsUploading(true); setIsUploading(true);
const formData = new FormData(); const formData = new FormData();
formData.append('file', uploadFile); const originalExt = uploadFile.name.match(/\.[^/.]+$/)?.[0] || '';
formData.append('documentType', uploadDocType); const typedName = uploadDocType.trim();
const customFileName = typedName.toLowerCase().endsWith(originalExt.toLowerCase())
? typedName
: `${typedName}${originalExt}`;
formData.append('file', uploadFile, customFileName);
// Document type is owned by the entry point, not a user-facing dropdown.
// For checklist-driven entry points (e.g. EOR items), use the checklist item's name
// so backend auto-linking (EOR compliance, architecture date, etc.) still works.
// Everything else uploads as a generic 'Other' document.
const checklistDocType = selectedStage?.startsWith('EOR: ')
? selectedStage.replace(/^EOR:\s*/, '')
: null;
formData.append('documentType', checklistDocType || 'Other');
if (selectedStage) formData.append('stage', selectedStage); if (selectedStage) formData.append('stage', selectedStage);
await onboardingService.uploadDocument(applicationId, formData); await onboardingService.uploadDocument(applicationId, formData);
toast.success('Document uploaded successfully'); toast.success('Document uploaded successfully');

View File

@ -217,6 +217,9 @@ export function RelocationRequestDetails({ requestId, onBack, currentUser }: Rel
const [isSubmittingEor, setIsSubmittingEor] = useState(false); const [isSubmittingEor, setIsSubmittingEor] = useState(false);
const [selectedFile, setSelectedFile] = useState<File | null>(null); const [selectedFile, setSelectedFile] = useState<File | null>(null);
const [selectedDocType, setSelectedDocType] = useState<string>(requiredDocuments[0]); const [selectedDocType, setSelectedDocType] = useState<string>(requiredDocuments[0]);
// True when the dialog was opened from a checklist row -> doc type is implicit,
// so we hide the dropdown and show the doc name as a read-only badge instead.
const [docTypeLocked, setDocTypeLocked] = useState(false);
const [isUploading, setIsUploading] = useState(false); const [isUploading, setIsUploading] = useState(false);
const [activeTab, setActiveTab] = useState('workflow'); const [activeTab, setActiveTab] = useState('workflow');
const [isPreviewOpen, setIsPreviewOpen] = useState(false); const [isPreviewOpen, setIsPreviewOpen] = useState(false);
@ -877,9 +880,26 @@ export function RelocationRequestDetails({ requestId, onBack, currentUser }: Rel
{/* Upload Button */} {/* Upload Button */}
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<h4 className="text-slate-900">Required Documents</h4> <h4 className="text-slate-900">Required Documents</h4>
<Dialog open={isUploadDialogOpen} onOpenChange={setIsUploadDialogOpen}> <Dialog
open={isUploadDialogOpen}
onOpenChange={(open) => {
setIsUploadDialogOpen(open);
if (!open) {
setDocTypeLocked(false);
setSelectedFile(null);
}
}}
>
<DialogTrigger asChild> <DialogTrigger asChild>
<Button size="sm" className="bg-amber-600 hover:bg-amber-700"> <Button
size="sm"
className="bg-amber-600 hover:bg-amber-700"
onClick={() => {
setDocTypeLocked(false);
setSelectedDocType(requiredDocuments[0]);
setSelectedFile(null);
}}
>
<Upload className="w-4 h-4 mr-2" /> <Upload className="w-4 h-4 mr-2" />
Upload Document Upload Document
</Button> </Button>
@ -888,29 +908,42 @@ export function RelocationRequestDetails({ requestId, onBack, currentUser }: Rel
<DialogHeader> <DialogHeader>
<DialogTitle>Upload Document</DialogTitle> <DialogTitle>Upload Document</DialogTitle>
<DialogDescription> <DialogDescription>
Select the document type and upload the file {docTypeLocked
? 'Pick a file for the selected document.'
: 'Select the document type and upload the file.'}
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<div className="space-y-4"> <div className="space-y-4">
<div> {docTypeLocked ? (
<Label>Document Type</Label> <div>
<select <Label>Document</Label>
className="w-full mt-1 px-3 py-2 border border-slate-300 rounded-md" <div className="mt-1 flex items-center gap-2 bg-amber-50 border border-amber-200 rounded-md px-3 h-10">
value={selectedDocType} <Badge className="bg-amber-600 text-white border-transparent">
onChange={(e) => setSelectedDocType(e.target.value)} {selectedDocType}
> </Badge>
{requiredDocuments.map((doc, index) => { </div>
const isAlreadyUploaded = request.documents?.some((d: any) => </div>
d.type === doc || d.name.toLowerCase().includes(doc.toLowerCase().split(' ')[0]) ) : (
); <div>
return ( <Label>Document Type</Label>
<option key={index} value={doc}> <select
{isAlreadyUploaded ? `${doc}` : doc} className="w-full mt-1 px-3 py-2 border border-slate-300 rounded-md"
</option> value={selectedDocType}
); onChange={(e) => setSelectedDocType(e.target.value)}
})} >
</select> {requiredDocuments.map((doc, index) => {
</div> const isAlreadyUploaded = request.documents?.some((d: any) =>
d.type === doc || d.name.toLowerCase().includes(doc.toLowerCase().split(' ')[0])
);
return (
<option key={index} value={doc}>
{isAlreadyUploaded ? `${doc}` : doc}
</option>
);
})}
</select>
</div>
)}
<div> <div>
<Label>Upload File</Label> <Label>Upload File</Label>
<Input <Input
@ -952,17 +985,35 @@ export function RelocationRequestDetails({ requestId, onBack, currentUser }: Rel
return ( return (
<div <div
key={index} key={index}
className={`flex items-center gap-2 p-2 rounded border text-sm ${uploaded ? 'bg-green-50 border-green-200' : 'bg-slate-50 border-slate-200' className={`flex items-center justify-between gap-2 p-2 rounded border text-sm ${uploaded ? 'bg-green-50 border-green-200' : 'bg-slate-50 border-slate-200'
}`} }`}
> >
{uploaded ? ( <div className="flex items-center gap-2 min-w-0">
<CheckCircle2 className="w-4 h-4 text-green-600 flex-shrink-0" /> {uploaded ? (
) : ( <CheckCircle2 className="w-4 h-4 text-green-600 flex-shrink-0" />
<AlertCircle className="w-4 h-4 text-slate-400 flex-shrink-0" /> ) : (
<AlertCircle className="w-4 h-4 text-slate-400 flex-shrink-0" />
)}
<span className={`truncate ${uploaded ? 'text-green-900' : 'text-slate-700'}`}>
{doc}
</span>
</div>
{!uploaded && (
<Button
size="sm"
variant="ghost"
className="h-7 px-2 text-amber-700 hover:bg-amber-50 flex-shrink-0"
onClick={() => {
setSelectedDocType(doc);
setSelectedFile(null);
setDocTypeLocked(true);
setIsUploadDialogOpen(true);
}}
>
<Upload className="w-3.5 h-3.5 mr-1" />
Upload
</Button>
)} )}
<span className={uploaded ? 'text-green-900' : 'text-slate-700'}>
{doc}
</span>
</div> </div>
); );
})} })}

View File

@ -239,6 +239,22 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
const isAwaitingFnfGate = currentStage === 'Awaiting F&F'; const isAwaitingFnfGate = currentStage === 'Awaiting F&F';
const resolvedStageKey = (() => {
const normalized = String(currentStage || '').trim();
const matched = stagesOrdered.find(
(key) => key === normalized || (RESIGNATION_STAGE_ALIASES[key] || []).includes(normalized)
);
return matched || normalized;
})();
const isNbHApprovalStep = resolvedStageKey === 'NBH';
const isAwaitingFnfStep = resolvedStageKey === 'Awaiting F&F';
/** Legacy rows only: acceptance letter at Legal before DD Admin completed Awaiting F&F gate */
const isLegalLegacyFnfStep = resolvedStageKey === 'Legal';
const fnfPushRoles = ['DD Lead', 'DD Head', 'DD Admin', 'Super Admin'];
const fnfPushLegacyRoles = ['DD Lead', 'DD Head', 'DD Admin', 'Super Admin'];
const canApprove = isCurrentlyAssigned && const canApprove = isCurrentlyAssigned &&
!isFinalState && !isFinalState &&
!isSettlementPhase && !isSettlementPhase &&
@ -250,14 +266,26 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
return { return {
canApprove, canApprove,
canSendBack: isCurrentlyAssigned && !isFinalState && !isSettlementPhase && stageIndex > 0, // SRS §7.3.2: Send Back returns to DD Admin for correction. Legal Admin only drafts/uploads
// the Resignation Acceptance Letter and cannot send the case back to earlier reviewers.
canSendBack:
isCurrentlyAssigned &&
!isFinalState &&
!isSettlementPhase &&
stageIndex > 0 &&
userRole !== 'Legal Admin' &&
userRoleCode !== 'LEGAL_ADMIN',
canWithdraw: userRole === 'Dealer' && !isPastNBH && !isFinalState, canWithdraw: userRole === 'Dealer' && !isPastNBH && !isFinalState,
canRevoke: (userRoleCode === 'SUPER_ADMIN' || userRole === 'DD Admin') && !isFinalState && !isSettlementPhase, canRevoke: (userRoleCode === 'SUPER_ADMIN' || userRole === 'DD Admin') && !isFinalState && !isSettlementPhase,
canPushToFnF: ['DD Lead', 'DD Head', 'NBH', 'DD Admin', 'Super Admin'].includes(userRole) && // Push to F&F: after DD Admin gate only — not during NBH (or any earlier) approval.
!isSettlementPhase && // Roles: DD Lead / DD Head / DD Admin / Super Admin (not NBH).
!isFinalState && canPushToFnF:
(currentStage === 'Awaiting F&F' || currentStage === 'Legal') && fnfPushRoles.includes(userRole) &&
isLwdReached, !isSettlementPhase &&
!isFinalState &&
!isNbHApprovalStep &&
isLwdReached &&
(isAwaitingFnfStep || (isLegalLegacyFnfStep && fnfPushLegacyRoles.includes(userRole))),
canAssign: userRole !== 'Dealer' && !isFinalState canAssign: userRole !== 'Dealer' && !isFinalState
}; };
}; };

View File

@ -595,13 +595,6 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
return ( return (
<div className="space-y-6"> <div className="space-y-6">
{/* Warning Alert */} {/* Warning Alert */}
<Alert className="border-amber-200 bg-amber-50">
<AlertTriangle className="h-4 w-4 text-amber-600" />
<AlertTitle className="text-amber-900">Sensitive Information</AlertTitle>
<AlertDescription className="text-amber-700">
This is a termination case. All actions are logged and audited. Proceed with caution.
</AlertDescription>
</Alert>
{/* Header */} {/* Header */}
<div className="flex items-start justify-between"> <div className="flex items-start justify-between">

View File

@ -291,13 +291,13 @@ export function TerminationPage({ currentUser, onViewDetails }: TerminationPageP
return ( return (
<div className="space-y-6"> <div className="space-y-6">
{/* Warning Alert */} {/* Warning Alert */}
<Alert className="border-red-200 bg-red-50"> {/* <Alert className="border-red-200 bg-red-50">
<AlertTriangle className="h-4 w-4 text-red-600" /> <AlertTriangle className="h-4 w-4 text-red-600" />
<AlertTitle className="text-red-900">Restricted Access</AlertTitle> <AlertTitle className="text-red-900">Restricted Access</AlertTitle>
<AlertDescription className="text-red-700"> <AlertDescription className="text-red-700">
This section contains sensitive information. All termination actions are logged and require proper authorization. This section contains sensitive information. All termination actions are logged and require proper authorization.
</AlertDescription> </AlertDescription>
</Alert> </Alert> */}
{/* Header Stats */} {/* Header Stats */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4"> <div className="grid grid-cols-1 md:grid-cols-3 gap-4">