diff --git a/src/features/constitutional/pages/ConstitutionalChangeDetails.tsx b/src/features/constitutional/pages/ConstitutionalChangeDetails.tsx index 451082e..ae69dd5 100644 --- a/src/features/constitutional/pages/ConstitutionalChangeDetails.tsx +++ b/src/features/constitutional/pages/ConstitutionalChangeDetails.tsx @@ -151,6 +151,9 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }: const [isUploadDialogOpen, setIsUploadDialogOpen] = useState(false); const [selectedDocType, setSelectedDocType] = useState(null); const [uploadFile, setUploadFile] = useState(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(null); @@ -890,9 +893,26 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }:

Document Checklist

- + { + setIsUploadDialogOpen(open); + if (!open) { + setDocTypeLocked(false); + setUploadFile(null); + } + }} + > - @@ -901,29 +921,42 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }: Upload Document - Select the document type and upload the file + {docTypeLocked + ? 'Pick a file for the selected document.' + : 'Select the document type and upload the file.'}
-
- - -
+ {docTypeLocked && selectedDocType != null ? ( +
+ +
+ + {documentNames[selectedDocType] || `Document ${selectedDocType}`} + +
+
+ ) : ( +
+ + +
+ )}
setUploadFile(e.target.files?.[0] || null)} /> @@ -976,15 +1009,33 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }: )}
- {uploaded ? ( - - {uploaded.status} - - ) : ( - - Not Uploaded - - )} +
+ {uploaded ? ( + + {uploaded.status} + + ) : ( + + Not Uploaded + + )} + {!ok && ( + + )} +
); })} diff --git a/src/features/fnf/pages/FnFDetails.tsx b/src/features/fnf/pages/FnFDetails.tsx index b2792c1..0802a2e 100644 --- a/src/features/fnf/pages/FnFDetails.tsx +++ b/src/features/fnf/pages/FnFDetails.tsx @@ -691,9 +691,9 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) { Progress Case Details Department Responses - Financial Summary Documents - Bank Details + {/* Bank Details tab hidden temporarily */} + {/* Bank Details */} Audit Trail @@ -1451,145 +1451,143 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) { - - {/* Financial Summary Tab */} - -
- - - Department Claim vs Finance Validation - - Final settlement totals are based on finance validated values. - - - - - - - Department - Department Claim - Finance Validated - Variance + {/* Department Claim vs Finance Validation */} + + + Department Claim vs Finance Validation + + Final settlement totals are based on finance validated values. + + + +
+ + + Department + Department Claim + Finance Validated + Variance + + + + {departmentReconciliation.map((row) => ( + + {row.department} + {row.claimAmount > 0 ? `${row.claimType} ₹${row.claimAmount.toLocaleString()}` : '-'} + {row.validatedAmount > 0 ? `${row.validatedType} ₹${row.validatedAmount.toLocaleString()}` : '-'} + 0 ? 'text-red-600' : 'text-green-600'}> + {row.claimAmount === 0 && row.validatedAmount === 0 ? '-' : `₹${row.variance.toLocaleString()}`} + - - - {departmentReconciliation.map((row) => ( - - {row.department} - {row.claimAmount > 0 ? `${row.claimType} ₹${row.claimAmount.toLocaleString()}` : '-'} - {row.validatedAmount > 0 ? `${row.validatedType} ₹${row.validatedAmount.toLocaleString()}` : '-'} - 0 ? 'text-red-600' : 'text-green-600'}> - {row.claimAmount === 0 && row.validatedAmount === 0 ? '-' : `₹${row.variance.toLocaleString()}`} - - - ))} - -
-
-
+ ))} + + + + - - - Financial Summary - - Consolidated view of all payable and receivable amounts - - - -
-
-

- Total Payable Amount -

-

- ₹{fnfCase.totalPayableAmount?.toLocaleString() || "0"} -

-

- Amount to be paid to dealer -

-
-
-

- Total receivable amount -

-

- ₹{fnfCase.totalRecoveryAmount?.toLocaleString() || "0"} -

-

- Amount receivable from dealer -

-
-
-

- Total Deductions -

-

- ₹{fnfCase.totalDeductions?.toLocaleString() || "0"} -

-

- Warranty holdbacks / Policy penalties -

-
-
-

Net Settlement Amount

-

- ₹{Math.abs(fnfCase.netAmount || 0).toLocaleString()} -

-

- {(fnfCase.netAmount || 0) < 0 - ? "Receivable from dealer" - : "Payment to dealer"} -

-
+ {/* Financial Summary */} + + + Financial Summary + + Consolidated view of all payable and receivable amounts + + + +
+
+

+ Total Payable Amount +

+

+ ₹{fnfCase.totalPayableAmount?.toLocaleString() || "0"} +

+

+ Amount to be paid to dealer +

- - - - - - Finance Report Status - - -
- +

+ Total receivable amount +

+

+ ₹{fnfCase.totalRecoveryAmount?.toLocaleString() || "0"} +

+

+ Amount receivable from dealer +

+
+
+

+ Total Deductions +

+

+ ₹{fnfCase.totalDeductions?.toLocaleString() || "0"} +

+

+ Warranty holdbacks / Policy penalties +

+
+
+

Net Settlement Amount

+

- {fnfCase.financeReportStatus} - - {fnfCase.financeReportStatus === "Pending" && ( -

- Waiting for all department responses before finance can - prepare final report -

- )} - {fnfCase.financeReportStatus === "In Progress" && ( -

- Finance team is reviewing department responses and - preparing final settlement report -

- )} + ₹{Math.abs(fnfCase.netAmount || 0).toLocaleString()} +

+

+ {(fnfCase.netAmount || 0) < 0 + ? "Receivable from dealer" + : "Payment to dealer"} +

- {fnfCase.financeRemarks && ( -
- -

{fnfCase.financeRemarks}

-
+
+
+
+ + {/* Finance Report Status */} + + + Finance Report Status + + +
+ + {fnfCase.financeReportStatus} + + {fnfCase.financeReportStatus === "Pending" && ( +

+ Waiting for all department responses before finance can + prepare final report +

)} - - -
+ {fnfCase.financeReportStatus === "In Progress" && ( +

+ Finance team is reviewing department responses and + preparing final settlement report +

+ )} +
+ {fnfCase.financeRemarks && ( +
+ +

{fnfCase.financeRemarks}

+
+ )} +
+
{/* Documents Tab */} diff --git a/src/features/onboarding/components/application-details/ApplicationDetailsExtendedModals.tsx b/src/features/onboarding/components/application-details/ApplicationDetailsExtendedModals.tsx index 1eb584d..fe606ad 100644 --- a/src/features/onboarding/components/application-details/ApplicationDetailsExtendedModals.tsx +++ b/src/features/onboarding/components/application-details/ApplicationDetailsExtendedModals.tsx @@ -416,56 +416,36 @@ export function ApplicationDetailsExtendedModals(props: ApplicationDetailsExtend
) : (
-
-
+
+
- - + + 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" + />
- - + + { + 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" + />
-
- - setUploadFile(e.target.files ? e.target.files[0] : null)} data-testid="onboarding-documents-file-input" /> -
diff --git a/src/features/onboarding/components/application-details/ApplicationDetailsTabs.tsx b/src/features/onboarding/components/application-details/ApplicationDetailsTabs.tsx index c8519e3..d462d85 100644 --- a/src/features/onboarding/components/application-details/ApplicationDetailsTabs.tsx +++ b/src/features/onboarding/components/application-details/ApplicationDetailsTabs.tsx @@ -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 (
-
+ {(() => { + // Upload is allowed only on the currently active branch stage. + const canUploadHere = branchStage.status === 'active'; + if (stageDocs.length === 0 && !canUploadHere) { + return null; + } + return ( +
+ +
+ ); + })()}

{isDone && branchStage.date ? `Done: ${formatDateTime(branchStage.date)}` : isDone && stageDocs.length > 0 ? `Uploaded: ${formatDateTime(stageDocs[0].updatedAt || stageDocs[0].createdAt)}` : 'Pending'}

diff --git a/src/features/onboarding/hooks/useApplicationDetailsAdminActions.ts b/src/features/onboarding/hooks/useApplicationDetailsAdminActions.ts index 889d005..2db657b 100644 --- a/src/features/onboarding/hooks/useApplicationDetailsAdminActions.ts +++ b/src/features/onboarding/hooks/useApplicationDetailsAdminActions.ts @@ -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'); diff --git a/src/features/relocation/pages/RelocationRequestDetails.tsx b/src/features/relocation/pages/RelocationRequestDetails.tsx index 9124d45..5d3f243 100644 --- a/src/features/relocation/pages/RelocationRequestDetails.tsx +++ b/src/features/relocation/pages/RelocationRequestDetails.tsx @@ -217,6 +217,9 @@ export function RelocationRequestDetails({ requestId, onBack, currentUser }: Rel const [isSubmittingEor, setIsSubmittingEor] = useState(false); const [selectedFile, setSelectedFile] = useState(null); const [selectedDocType, setSelectedDocType] = useState(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 */}

Required Documents

- + { + setIsUploadDialogOpen(open); + if (!open) { + setDocTypeLocked(false); + setSelectedFile(null); + } + }} + > - @@ -888,29 +908,42 @@ export function RelocationRequestDetails({ requestId, onBack, currentUser }: Rel Upload Document - Select the document type and upload the file + {docTypeLocked + ? 'Pick a file for the selected document.' + : 'Select the document type and upload the file.'}
-
- - -
+ {docTypeLocked ? ( +
+ +
+ + {selectedDocType} + +
+
+ ) : ( +
+ + +
+ )}
- {uploaded ? ( - - ) : ( - +
+ {uploaded ? ( + + ) : ( + + )} + + {doc} + +
+ {!uploaded && ( + )} - - {doc} -
); })} diff --git a/src/features/resignation/pages/ResignationDetails.tsx b/src/features/resignation/pages/ResignationDetails.tsx index 9e84e52..4534a07 100644 --- a/src/features/resignation/pages/ResignationDetails.tsx +++ b/src/features/resignation/pages/ResignationDetails.tsx @@ -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 }; }; diff --git a/src/features/termination/pages/TerminationDetails.tsx b/src/features/termination/pages/TerminationDetails.tsx index 1142288..1499f46 100644 --- a/src/features/termination/pages/TerminationDetails.tsx +++ b/src/features/termination/pages/TerminationDetails.tsx @@ -595,13 +595,6 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi return (
{/* Warning Alert */} - - - Sensitive Information - - This is a termination case. All actions are logged and audited. Proceed with caution. - - {/* Header */}
diff --git a/src/features/termination/pages/TerminationPage.tsx b/src/features/termination/pages/TerminationPage.tsx index 96448cb..46605d0 100644 --- a/src/features/termination/pages/TerminationPage.tsx +++ b/src/features/termination/pages/TerminationPage.tsx @@ -291,13 +291,13 @@ export function TerminationPage({ currentUser, onViewDetails }: TerminationPageP return (
{/* Warning Alert */} - + {/* Restricted Access This section contains sensitive information. All termination actions are logged and require proper authorization. - + */} {/* Header Stats */}