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 [selectedDocType, setSelectedDocType] = useState<number | 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 [activeDocumentTab, setActiveDocumentTab] = useState('required');
const [request, setRequest] = useState<any>(null);
@ -890,9 +893,26 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }:
<div className="space-y-4">
<div className="flex items-center justify-between gap-4">
<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>
<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 Document
</Button>
@ -901,29 +921,42 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }:
<DialogHeader>
<DialogTitle>Upload Document</DialogTitle>
<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>
</DialogHeader>
<div className="space-y-4">
<div>
<Label>Document Type</Label>
<select
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"
value={selectedDocType != null ? String(selectedDocType) : ''}
onChange={(e) => {
const v = e.target.value;
setSelectedDocType(v ? Number(v) : null);
}}
>
<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>
{docTypeLocked && selectedDocType != null ? (
<div>
<Label>Document</Label>
<div className="mt-1 flex items-center gap-2 bg-amber-50 border border-amber-200 rounded-md px-3 h-10">
<Badge className="bg-amber-600 text-white border-transparent">
{documentNames[selectedDocType] || `Document ${selectedDocType}`}
</Badge>
</div>
</div>
) : (
<div>
<Label>Document Type</Label>
<select
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"
value={selectedDocType != null ? String(selectedDocType) : ''}
onChange={(e) => {
const v = e.target.value;
setSelectedDocType(v ? Number(v) : null);
}}
>
<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>
<Label>Upload File</Label>
<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>
{uploaded ? (
<Badge className={getStatusColor(uploaded.status)}>
{uploaded.status}
</Badge>
) : (
<Badge className="bg-slate-100 text-slate-600 border-slate-300">
Not Uploaded
</Badge>
)}
<div className="flex items-center gap-2">
{uploaded ? (
<Badge className={getStatusColor(uploaded.status)}>
{uploaded.status}
</Badge>
) : (
<Badge className="bg-slate-100 text-slate-600 border-slate-300">
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>
);
})}

View File

@ -691,9 +691,9 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
<TabsTrigger value="progress">Progress</TabsTrigger>
<TabsTrigger value="details">Case Details</TabsTrigger>
<TabsTrigger value="departments">Department Responses</TabsTrigger>
<TabsTrigger value="financial">Financial Summary</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>
</TabsList>
@ -1451,145 +1451,143 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
</Table>
</CardContent>
</Card>
</TabsContent>
{/* Financial Summary Tab */}
<TabsContent value="financial">
<div className="space-y-6">
<Card className="border-blue-200 bg-blue-50">
<CardHeader>
<CardTitle>Department Claim vs Finance Validation</CardTitle>
<CardDescription>
Final settlement totals are based on finance validated values.
</CardDescription>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead>Department</TableHead>
<TableHead>Department Claim</TableHead>
<TableHead>Finance Validated</TableHead>
<TableHead>Variance</TableHead>
{/* Department Claim vs Finance Validation */}
<Card className="border-blue-200 bg-blue-50 mt-6">
<CardHeader>
<CardTitle>Department Claim vs Finance Validation</CardTitle>
<CardDescription>
Final settlement totals are based on finance validated values.
</CardDescription>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead>Department</TableHead>
<TableHead>Department Claim</TableHead>
<TableHead>Finance Validated</TableHead>
<TableHead>Variance</TableHead>
</TableRow>
</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>
</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>
))}
</TableBody>
</Table>
</CardContent>
</Card>
))}
</TableBody>
</Table>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Financial Summary</CardTitle>
<CardDescription>
Consolidated view of all payable and receivable amounts
</CardDescription>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div className="p-6 bg-green-50 rounded-lg border border-green-200">
<p className="text-sm text-green-700 mb-2">
Total Payable Amount
</p>
<p className="text-3xl text-green-600">
{fnfCase.totalPayableAmount?.toLocaleString() || "0"}
</p>
<p className="text-xs text-green-600 mt-1">
Amount to be paid to dealer
</p>
</div>
<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>
{/* Financial Summary */}
<Card className="mt-6">
<CardHeader>
<CardTitle>Financial Summary</CardTitle>
<CardDescription>
Consolidated view of all payable and receivable amounts
</CardDescription>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div className="p-6 bg-green-50 rounded-lg border border-green-200">
<p className="text-sm text-green-700 mb-2">
Total Payable Amount
</p>
<p className="text-3xl text-green-600">
{fnfCase.totalPayableAmount?.toLocaleString() || "0"}
</p>
<p className="text-xs text-green-600 mt-1">
Amount to be paid to dealer
</p>
</div>
</CardContent>
</Card>
<Card>
<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"
}
<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"
}`}
>
{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>
)}
{fnfCase.financeReportStatus === "In Progress" && (
<p className="text-slate-600 text-sm">
Finance team is reviewing department responses and
preparing final settlement report
</p>
)}
{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>
{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>
</div>
</CardContent>
</Card>
{/* 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>
</Card>
</div>
{fnfCase.financeReportStatus === "In Progress" && (
<p className="text-slate-600 text-sm">
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>
{/* Documents Tab */}

View File

@ -416,56 +416,36 @@ export function ApplicationDetailsExtendedModals(props: ApplicationDetailsExtend
</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="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-4">
<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>
<Label className="text-slate-700 font-semibold px-1">Document Name <span className="text-red-500">*</span></Label>
<Input
type="text"
placeholder="Enter document name"
value={uploadDocType}
onChange={(e) => setUploadDocType(e.target.value)}
className="bg-white border-slate-200 h-12 rounded-xl focus:ring-amber-500 shadow-sm"
data-testid="onboarding-documents-name-input"
/>
</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>
<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) => {
const file = e.target.files ? e.target.files[0] : null;
setUploadFile(file);
if (file) {
const baseName = file.name.replace(/\.[^/.]+$/, '');
setUploadDocType(baseName);
}
}}
data-testid="onboarding-documents-file-input"
/>
</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>

View File

@ -368,13 +368,20 @@ export function ApplicationDetailsTabs(props: ApplicationDetailsTabsProps) {
(!doc.stage && doc.documentType?.toLowerCase().includes(stage.name.toLowerCase().split(' ')[0]))
).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 (
<div className="flex items-center gap-2 mt-1">
<button
onClick={() => {
setSelectedStage(stage.name);
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"
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>
)}
<div className="flex items-center gap-2 mt-1">
<button
onClick={() => {
setSelectedStage(branchStage.name);
setShowDocumentsModal(true);
if (stageDocs.length === 0) setShowUploadForm(true);
}}
className={cn(
"text-[10px] font-medium flex items-center gap-1 transition-colors",
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>
{(() => {
// Upload is allowed only on the currently active branch stage.
const canUploadHere = branchStage.status === 'active';
if (stageDocs.length === 0 && !canUploadHere) {
return null;
}
return (
<div className="flex items-center gap-2 mt-1">
<button
onClick={() => {
setSelectedStage(branchStage.name);
setShowDocumentsModal(true);
if (stageDocs.length === 0 && canUploadHere) setShowUploadForm(true);
}}
className={cn(
"text-[10px] font-medium flex items-center gap-1 transition-colors",
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}`}>
{isDone && branchStage.date ? `Done: ${formatDateTime(branchStage.date)}` : isDone && stageDocs.length > 0 ? `Uploaded: ${formatDateTime(stageDocs[0].updatedAt || stageDocs[0].createdAt)}` : 'Pending'}
</p>

View File

@ -328,14 +328,26 @@ export function useApplicationDetailsAdminActions(params: UseApplicationDetailsA
const handleUpload = async () => {
if (!uploadFile || !uploadDocType) {
toast.warning('Please select a file and document type');
toast.warning('Please enter a document name and select a file');
return;
}
try {
setIsUploading(true);
const formData = new FormData();
formData.append('file', uploadFile);
formData.append('documentType', uploadDocType);
const originalExt = uploadFile.name.match(/\.[^/.]+$/)?.[0] || '';
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);
await onboardingService.uploadDocument(applicationId, formData);
toast.success('Document uploaded successfully');

View File

@ -217,6 +217,9 @@ export function RelocationRequestDetails({ requestId, onBack, currentUser }: Rel
const [isSubmittingEor, setIsSubmittingEor] = useState(false);
const [selectedFile, setSelectedFile] = useState<File | null>(null);
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 [activeTab, setActiveTab] = useState('workflow');
const [isPreviewOpen, setIsPreviewOpen] = useState(false);
@ -877,9 +880,26 @@ export function RelocationRequestDetails({ requestId, onBack, currentUser }: Rel
{/* Upload Button */}
<div className="flex items-center justify-between">
<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>
<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 Document
</Button>
@ -888,29 +908,42 @@ export function RelocationRequestDetails({ requestId, onBack, currentUser }: Rel
<DialogHeader>
<DialogTitle>Upload Document</DialogTitle>
<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>
</DialogHeader>
<div className="space-y-4">
<div>
<Label>Document Type</Label>
<select
className="w-full mt-1 px-3 py-2 border border-slate-300 rounded-md"
value={selectedDocType}
onChange={(e) => setSelectedDocType(e.target.value)}
>
{requiredDocuments.map((doc, index) => {
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>
{docTypeLocked ? (
<div>
<Label>Document</Label>
<div className="mt-1 flex items-center gap-2 bg-amber-50 border border-amber-200 rounded-md px-3 h-10">
<Badge className="bg-amber-600 text-white border-transparent">
{selectedDocType}
</Badge>
</div>
</div>
) : (
<div>
<Label>Document Type</Label>
<select
className="w-full mt-1 px-3 py-2 border border-slate-300 rounded-md"
value={selectedDocType}
onChange={(e) => setSelectedDocType(e.target.value)}
>
{requiredDocuments.map((doc, index) => {
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>
<Label>Upload File</Label>
<Input
@ -952,17 +985,35 @@ export function RelocationRequestDetails({ requestId, onBack, currentUser }: Rel
return (
<div
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 ? (
<CheckCircle2 className="w-4 h-4 text-green-600 flex-shrink-0" />
) : (
<AlertCircle className="w-4 h-4 text-slate-400 flex-shrink-0" />
<div className="flex items-center gap-2 min-w-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" />
)}
<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>
);
})}

View File

@ -239,6 +239,22 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
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 &&
!isFinalState &&
!isSettlementPhase &&
@ -250,14 +266,26 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
return {
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,
canRevoke: (userRoleCode === 'SUPER_ADMIN' || userRole === 'DD Admin') && !isFinalState && !isSettlementPhase,
canPushToFnF: ['DD Lead', 'DD Head', 'NBH', 'DD Admin', 'Super Admin'].includes(userRole) &&
!isSettlementPhase &&
!isFinalState &&
(currentStage === 'Awaiting F&F' || currentStage === 'Legal') &&
isLwdReached,
// Push to F&F: after DD Admin gate only — not during NBH (or any earlier) approval.
// Roles: DD Lead / DD Head / DD Admin / Super Admin (not NBH).
canPushToFnF:
fnfPushRoles.includes(userRole) &&
!isSettlementPhase &&
!isFinalState &&
!isNbHApprovalStep &&
isLwdReached &&
(isAwaitingFnfStep || (isLegalLegacyFnfStep && fnfPushLegacyRoles.includes(userRole))),
canAssign: userRole !== 'Dealer' && !isFinalState
};
};

View File

@ -595,13 +595,6 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
return (
<div className="space-y-6">
{/* 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 */}
<div className="flex items-start justify-between">

View File

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