diff --git a/src/App.tsx b/src/App.tsx index 47c8e8e..b18e72e 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -40,6 +40,7 @@ import { ConstitutionalChangeDetails } from './components/applications/Constitut import { RelocationRequestPage } from './components/applications/RelocationRequestPage'; import { RelocationRequestDetails } from './components/applications/RelocationRequestDetails'; import { DealerResignationPage } from './components/dealer/DealerResignationPage'; +import { DealerResignationDetailsPage } from './components/dealer/DealerResignationDetailsPage'; import { DealerConstitutionalChangePage } from './components/dealer/DealerConstitutionalChangePage'; import { DealerRelocationPage } from './components/dealer/DealerRelocationPage'; import QuestionnaireBuilder from './components/admin/QuestionnaireBuilder'; @@ -72,9 +73,11 @@ export default function App() { const [showAdminLogin, setShowAdminLogin] = useState(false); const navigate = useNavigate(); const location = useLocation(); - const currentRole = currentUser?.role || ''; - const resignationRoles = ['DD Admin', 'ASM', 'DD Lead', 'ZBH', 'NBH', 'Legal', 'Super Admin']; - const terminationRoles = ['ASM', 'DD Lead', 'DD Admin', 'Super Admin']; + const currentRole = currentUser?.role || currentUser?.roleCode || ''; + const normalizedRole = String(currentRole).trim().toLowerCase(); + const hasRole = (roles: string[]) => roles.map((r) => r.toLowerCase()).includes(normalizedRole); + const resignationRoles = ['DD Admin', 'ASM', 'RBM', 'DD Lead', 'ZBH', 'NBH', 'Legal', 'Legal Admin', 'Super Admin']; + const terminationRoles = ['ASM', 'RBM', 'ZBH', 'DD Lead', 'DD Head', 'NBH', 'Legal Admin', 'Legal', 'DD Admin', 'CCO', 'CEO', 'Super Admin']; const fnfRoles = ['DD Admin', 'DD Lead', 'NBH', 'Finance', 'Finance Admin', 'Super Admin']; const financeRoles = ['Finance', 'Finance Admin']; @@ -210,9 +213,9 @@ export default function App() { {/* Dashboards */} navigate(`/${path}`)} onViewPaymentDetails={(id) => navigate(`/finance-onboarding/${id}`)} onViewAuditDetails={(id) => navigate(`/finance-audit/${id}`)} onViewFnFDetails={(id) => navigate(`/finance-fnf/${id}`)} /> : - currentRole === 'Dealer' ? + hasRole(['Dealer']) ? navigate(`/${path}`)} /> : navigate(`/${path}`)} /> } /> @@ -229,7 +232,7 @@ export default function App() { } /> navigate(`/applications/${id}`)} initialFilter="all" /> : + hasRole(['DD']) ? navigate(`/applications/${id}`)} initialFilter="all" /> : } /> {/* FDD Routes - Integrated into Layout */} @@ -243,7 +246,7 @@ export default function App() { {/* Other Modules */} } /> : } /> @@ -255,57 +258,57 @@ export default function App() { {/* HR/Finance Modules (Simplified for brevity, following pattern) */} navigate(`/resignation/${id}`)} /> : } /> navigate('/resignation')} currentUser={currentUser} /> : } /> navigate(`/termination/${id}`)} /> : } /> navigate('/termination')} currentUser={currentUser} /> : } /> navigate(`/fnf/${id}`)} /> : } /> navigate('/fnf')} currentUser={currentUser} /> : } /> navigate(`/finance-onboarding/${id}`)} onViewAuditDetails={(id) => navigate(`/finance-audit/${id}`)} /> : } /> navigate('/finance-onboarding')} /> : } /> } /> navigate(`/finance-fnf/${id}`)} /> : } /> navigate('/finance-fnf')} /> : } /> @@ -317,7 +320,12 @@ export default function App() { navigate('/relocation-requests')} currentUser={currentUser} />} /> {/* Dealer Routes */} - navigate(`/resignation/${id}`)} />} /> + navigate(`/dealer-resignation/${id}`)} />} /> + navigate('/dealer-resignation')} /> + : + } /> navigate(`/constitutional-change/${id}`)} />} /> navigate(`/relocation-requests/${id}`)} />} /> diff --git a/src/api/API.ts b/src/api/API.ts index bfc4ac5..9620e66 100644 --- a/src/api/API.ts +++ b/src/api/API.ts @@ -98,7 +98,7 @@ export const API = { deleteUser: (id: string) => client.delete(`/admin/users/${id}`), // Dealer & Outlets - getDealers: (params?: { onboarded?: string }) => client.get('/dealer', { params }), + getDealers: (params?: { onboarded?: string; activeOnly?: string }) => client.get('/dealer', { params }), createDealer: (data: any) => client.post('/dealer', data), getDealerById: (id: string) => client.get(`/dealer/${id}`), updateDealer: (id: string, data: any) => client.put(`/dealer/${id}`, data), diff --git a/src/components/applications/ConstitutionalChangeDetails.tsx b/src/components/applications/ConstitutionalChangeDetails.tsx index 1a1ec9a..9e00978 100644 --- a/src/components/applications/ConstitutionalChangeDetails.tsx +++ b/src/components/applications/ConstitutionalChangeDetails.tsx @@ -34,6 +34,12 @@ const workflowStages = [ { id: 9, name: 'Completed', key: 'completed', role: 'System' } ]; +const formatStageLabel = (label: string) => + label === 'ZM/RBM Review' ? 'ZM+RBM Review' : label; + +const formatStageRole = (role: string) => + role === 'ZM/RBM' ? 'ZM+RBM' : role; + // Document requirements mapping (same as in ConstitutionalChangePage) const documentRequirements: Record = { 'Partnership': [1, 2, 3, 4, 8, 9, 10, 16], @@ -238,6 +244,7 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }: 'Submitted': 1, 'ASM Review': 2, 'ZM/RBM Review': 3, + 'ZM+RBM Review': 3, 'ZBH Review': 4, 'DD Lead Review': 5, 'DD Head Review': 6, @@ -263,7 +270,7 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }: const aliases: Record = { 'Submitted': ['Submitted', 'Draft'], 'ASM Review': ['ASM Review'], - 'ZM/RBM Review': ['ZM/RBM Review', 'ZM Review', 'RBM Review'], + 'ZM/RBM Review': ['ZM/RBM Review', 'ZM+RBM Review', 'ZM Review', 'RBM Review'], 'ZBH Review': ['ZBH Review'], 'DD Lead Review': ['DD Lead Review', 'Lead Review'], 'DD Head Review': ['DD Head Review', 'Head Review'], @@ -283,12 +290,13 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }: // Centralized Permissions Utility (Fixes security gap where buttons showed for everyone) const getConstitutionalPermissions = () => { if (!request || !currentUser) { - return { canApprove: false, canReject: false, canSendBack: false, canRevoke: false, isFinalState: false }; + return { canApprove: false, canReject: false, canSendBack: false, canRevoke: false, isFinalState: false, hasCurrentUserApprovedZmRbm: false }; } const currentStage = request.currentStage; const status = request.status; - const userRole = currentUser.role; + const userRole = currentUser.role || currentUser.roleCode; + const userRoleCode = String(currentUser.roleCode || '').toUpperCase(); const isFinalState = ['Completed', 'Rejected', 'Revoked'].includes(String(status || '')) || ['Rejected', 'Revoked', 'Completed'].includes(String(currentStage || '')); @@ -306,7 +314,7 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }: (!atSubmittedDbStage && !!( (stageDef?.role === 'ASM' && userRole === 'ASM') || - (stageDef?.role === 'ZM/RBM' && (userRole === 'DD-ZM' || userRole === 'RBM')) || + ((stageDef?.role === 'ZM/RBM' || stageDef?.role === 'ZM+RBM') && (userRole === 'DD-ZM' || userRole === 'RBM')) || (stageDef?.role === 'ZBH' && userRole === 'ZBH') || (stageDef?.role === 'DD Lead' && userRole === 'DD Lead') || (stageDef?.role === 'DD Head' && userRole === 'DD Head') || @@ -323,12 +331,33 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }: currentStage !== 'Legal Review' && currentStage !== 'Submitted'; + const jointZmRbmMeta = (request.metadata as any)?.jointApprovals?.zmRbm || {}; + const isZmRbmStage = currentStage === 'ZM/RBM Review' || currentStage === 'ZM+RBM Review'; + const actorKey = userRoleCode === 'RBM' ? 'RBM' : (userRoleCode === 'DD-ZM' ? 'DD-ZM' : null); + const approvedFromMetadata = actorKey ? Boolean(jointZmRbmMeta?.[actorKey]?.approvedByUserId) : false; + const approvedFromTimeline = isZmRbmStage && Boolean( + (request.timeline || []).some((entry: any) => { + const stage = String(entry?.stage || '').trim(); + const action = String(entry?.action || '').toLowerCase(); + const actor = String(entry?.user || '').trim().toLowerCase(); + const me = String(currentUser?.name || '').trim().toLowerCase(); + return ( + (stage === 'ZM/RBM Review' || stage === 'ZM+RBM Review' || stage === 'RBM Review' || stage === 'ZM Review') && + action.includes('approved') && + me.length > 0 && + actor === me + ); + }) + ); + const hasCurrentUserApprovedZmRbm = isZmRbmStage && (approvedFromMetadata || approvedFromTimeline); + return { - canApprove: isCurrentlyAssigned && !isFinalState, - canReject: isCurrentlyAssigned && !isFinalState, + canApprove: isCurrentlyAssigned && !isFinalState && !hasCurrentUserApprovedZmRbm, + canReject: isCurrentlyAssigned && !isFinalState && !hasCurrentUserApprovedZmRbm, canSendBack: canSendBackOrRevoke, canRevoke: canSendBackOrRevoke, - isFinalState + isFinalState, + hasCurrentUserApprovedZmRbm }; }; @@ -384,7 +413,8 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }: } } catch (error) { console.error('Submit action error:', error); - toast.error('Failed to submit action'); + const message = (error as any)?.response?.data?.message || 'Failed to submit action'; + toast.error(message); } finally { setIsActionLoading(false); } @@ -406,7 +436,7 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }: fileName: uploadFile.name, status: 'Pending Verification', uploadedOn: new Date().toISOString(), - uploadedBy: currentUser?.fullName || 'Dealer' + uploadedBy: currentUser?.name || 'Dealer' }; if (existingIndex >= 0) existingDocs[existingIndex] = { ...existingDocs[existingIndex], ...payloadDoc }; @@ -437,7 +467,7 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }: const isTargetByIndex = index === targetIndex; const isTargetByDocNumber = targetDoc.docNumber && doc.docNumber === targetDoc.docNumber; if (!(isTargetByIndex || isTargetByDocNumber)) return doc; - return { ...doc, status: 'Verified', verifiedOn: new Date().toISOString(), verifiedBy: currentUser?.fullName || 'System' }; + return { ...doc, status: 'Verified', verifiedOn: new Date().toISOString(), verifiedBy: currentUser?.name || 'System' }; }); const response = await API.uploadConstitutionalDocuments(requestId, updatedDocs) as any; @@ -636,7 +666,7 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }:
    {workflowStages.map((stage) => (
  • - {stage.name} — {stage.role} + {formatStageLabel(stage.name)} — {formatStageRole(stage.role)}
  • ))}
@@ -656,6 +686,17 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }: (atSubmittedGate ? index === 1 : index === currentStageIndex - 1); const timelineEntry = getLatestStageTimelineEntry(stage.name); const explicitFeedback = timelineEntry?.comments || timelineEntry?.feedback || timelineEntry?.remarks; + const isJointZmRbmStage = stage.name === 'ZM/RBM Review'; + const jointZmRbmMeta = (request.metadata as any)?.jointApprovals?.zmRbm || {}; + const rbmApproval = jointZmRbmMeta?.RBM; + const ddZmApproval = jointZmRbmMeta?.['DD-ZM']; + const currentRoleNormalized = String(currentUser?.roleCode || currentUser?.role || '').toUpperCase(); + const currentRoleApproval = + currentRoleNormalized === 'RBM' + ? rbmApproval + : (currentRoleNormalized === 'DD-ZM' || currentRoleNormalized === 'DD ZM' || currentRoleNormalized === 'ZM') + ? ddZmApproval + : null; return (
@@ -686,12 +727,12 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }:

- {stage.name} + {formatStageLabel(stage.name)}

{atSubmittedGate && index === 0 ? 'Dealer action: filing complete (no further step here).' - : `Responsible: ${stage.role}`} + : `Responsible: ${formatStageRole(stage.role)}`} {atSubmittedGate && index === 1 ? ( ASM approves to advance the request (first workflow action after submission). @@ -722,6 +763,36 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }: )}

)} + {isJointZmRbmStage && ( +
+

Joint approval status

+
+ + RBM: {rbmApproval?.approvedByUserId ? 'Approved' : 'Pending'} + + + DD-ZM: {ddZmApproval?.approvedByUserId ? 'Approved' : 'Pending'} + + {currentRoleApproval?.approvedByUserId && ( + + Approved by you + + )} +
+ {(rbmApproval?.remarks || ddZmApproval?.remarks) && ( +
+
+

RBM Comment

+

{rbmApproval?.remarks || '-'}

+
+
+

DD-ZM Comment

+

{ddZmApproval?.remarks || '-'}

+
+
+ )} +
+ )}
); @@ -771,7 +842,7 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }: {requiredDocs.map((docNum) => ( ))} @@ -1017,6 +1088,17 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }: Approve Request )} + + {!permissions.canApprove && permissions.hasCurrentUserApprovedZmRbm && ( + + )} {permissions.canReject && ( @@ -1018,7 +1180,13 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr size="icon" variant="ghost" className="h-8 w-8" - onClick={() => setEditingPayableId(item.id)} + onClick={() => { + setEditingPayableId(item.id); + setEditingPayableDrafts((prev) => ({ + ...prev, + [item.id]: { ...item } + })); + }} > @@ -1116,7 +1284,7 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr {editingReceivableId === item.id ? ( handleUpdateReceivable(item.id, 'description', e.target.value)} className="h-8" /> @@ -1147,7 +1315,7 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr {editingReceivableId === item.id ? ( handleUpdateReceivable(item.id, 'amount', e.target.value)} className="h-8 text-right" /> @@ -1162,10 +1330,7 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr size="icon" variant="ghost" className="h-8 w-8" - onClick={() => { - setEditingReceivableId(null); - toast.success('Changes saved'); - }} + onClick={() => handleSaveReceivableEdit(item.id)} > @@ -1174,7 +1339,13 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr size="icon" variant="ghost" className="h-8 w-8" - onClick={() => setEditingReceivableId(item.id)} + onClick={() => { + setEditingReceivableId(item.id); + setEditingReceivableDrafts((prev) => ({ + ...prev, + [item.id]: { ...item } + })); + }} > @@ -1272,7 +1443,7 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr {editingDeductionId === item.id ? ( handleUpdateDeduction(item.id, 'description', e.target.value)} className="h-8" /> @@ -1303,7 +1474,7 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr {editingDeductionId === item.id ? ( handleUpdateDeduction(item.id, 'amount', e.target.value)} className="h-8 text-right" /> @@ -1318,10 +1489,7 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr size="icon" variant="ghost" className="h-8 w-8" - onClick={() => { - setEditingDeductionId(null); - toast.success('Changes saved'); - }} + onClick={() => handleSaveDeductionEdit(item.id)} > @@ -1330,7 +1498,13 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr size="icon" variant="ghost" className="h-8 w-8" - onClick={() => setEditingDeductionId(item.id)} + onClick={() => { + setEditingDeductionId(item.id); + setEditingDeductionDrafts((prev) => ({ + ...prev, + [item.id]: { ...item } + })); + }} > diff --git a/src/components/applications/FinanceOnboardingPage.tsx b/src/components/applications/FinanceOnboardingPage.tsx index 0409151..6960eb4 100644 --- a/src/components/applications/FinanceOnboardingPage.tsx +++ b/src/components/applications/FinanceOnboardingPage.tsx @@ -46,28 +46,70 @@ export function FinanceOnboardingPage({ onViewPaymentDetails }: FinanceOnboardin } }; - const getRelevantPaymentStatus = (app: any) => { - if (!app.securityDeposits || app.securityDeposits.length === 0) return 'Awaiting Payment'; - const s = app.overallStatus || app.status; - const relevantType = (s.includes('LOI') || s === 'PAYMENT_VERIFICATION' || s === 'Security Details' || s === 'Payment Pending') ? 'SECURITY_DEPOSIT' : 'FIRST_FILL'; - const deposit = app.securityDeposits.find((d: any) => d.depositType === relevantType); - return deposit ? deposit.status : 'Awaiting Payment'; + const normalizeStatus = (status: any) => String(status || '').trim().toLowerCase(); + const isVerifiedLikeStatus = (status: any) => { + const normalized = normalizeStatus(status); + return normalized === 'verified' || normalized === 'paid'; }; - // Filter for Payment Mode - const paymentApps = applications.filter((app: any) => { + const paymentRows = applications.flatMap((app: any) => { const s = app.overallStatus || app.status; - return [ - 'LOI In Progress', 'Security Details', 'LOI Issued', 'LOA Pending', 'Dealer Code Generation', - 'LOI_APPROVAL', 'LOA_APPROVAL', 'PAYMENT_VERIFICATION', 'SECURITY_DEPOSIT', 'Payment Pending' + const isPaymentStage = [ + 'Payment Pending', 'Security Details', 'LOI In Progress', 'LOI Issued', + 'LOA Pending', 'Dealer Code Generation', 'LOA_APPROVAL' ].includes(s); + const deposits = app.securityDeposits || []; + + // Always include actual recorded deposits (including already verified rows) + if (deposits.length > 0) { + return deposits.map((d: any) => ({ + id: d.id, + applicationId: app.applicationId || app.id, + application: app, + paymentStatus: d.status, + paymentType: d.depositType, + amount: d.amount, + createdAt: d.createdAt, + verificationDate: d.verifiedAt, + isVirtual: false + })); + } + + // Keep virtual pending rows for in-flight cases with no deposit record yet + if (isPaymentStage) { + if (['Payment Pending', 'Security Details', 'LOI In Progress'].includes(s)) { + return [{ + id: `virtual-${app.id}-sd`, + applicationId: app.applicationId || app.id, + application: app, + paymentStatus: 'Pending', + paymentType: 'SECURITY_DEPOSIT', + amount: 500000, + createdAt: app.updatedAt, + verificationDate: null, + isVirtual: true + }]; + } + return [{ + id: `virtual-${app.id}-ff`, + applicationId: app.applicationId || app.id, + application: app, + paymentStatus: 'Pending', + paymentType: 'FIRST_FILL', + amount: 1500000, + createdAt: app.updatedAt, + verificationDate: null, + isVirtual: true + }]; + } + + return []; }); - const displayApps = paymentApps.filter(app => { - const status = getRelevantPaymentStatus(app); + const displayRows = paymentRows.filter((row: any) => { if (filterStatus === 'all') return true; - if (filterStatus === 'pending') return status !== 'Verified'; - if (filterStatus === 'verified') return status === 'Verified'; + if (filterStatus === 'pending') return !isVerifiedLikeStatus(row.paymentStatus); + if (filterStatus === 'verified') return isVerifiedLikeStatus(row.paymentStatus); return true; }); @@ -107,7 +149,7 @@ export function FinanceOnboardingPage({ onViewPaymentDetails }: FinanceOnboardin
- Pending Payments ({paymentApps.filter(a => getRelevantPaymentStatus(a) !== 'Verified').length}) + Pending Payments ({paymentRows.filter((row: any) => !isVerifiedLikeStatus(row.paymentStatus)).length})
@@ -126,7 +168,7 @@ export function FinanceOnboardingPage({ onViewPaymentDetails }: FinanceOnboardin onClick={() => setFilterStatus('verified')} className={filterStatus === 'verified' ? 'bg-slate-200 text-slate-900' : 'text-slate-500'} > - Completed + Verified
@@ -217,7 +260,13 @@ export function FinanceOnboardingPage({ onViewPaymentDetails }: FinanceOnboardin
-

No applications pending in the queue

+

+ {filterStatus === 'verified' + ? 'No verified payments found' + : filterStatus === 'pending' + ? 'No pending payments in the queue' + : 'No onboarding payments found'} +

diff --git a/src/components/applications/FnFDetails.tsx b/src/components/applications/FnFDetails.tsx index f4f361c..209c0b4 100644 --- a/src/components/applications/FnFDetails.tsx +++ b/src/components/applications/FnFDetails.tsx @@ -62,10 +62,14 @@ const ALL_DEPARTMENTS = [ 'Legal Department', 'Quality Department', 'Logistics Department', 'Customer Relations Department' ]; +const DEPARTMENT_CLAIM_PREFIX = "[DEPARTMENT_CLAIM]"; +const FINANCE_VALIDATED_PREFIX = "[FINANCE_VALIDATED]"; + export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) { const navigate = useNavigate(); const [fnfCase, setFnfCase] = useState(null); const [loading, setLoading] = useState(true); + const [activeTab, setActiveTab] = useState('details'); const [sendStakeholdersDialog, setSendStakeholdersDialog] = useState(false); const [previewDocument, setPreviewDocument] = useState(null); const [auditLogs, setAuditLogs] = useState([]); @@ -128,6 +132,15 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) { return name; }; + const isDepartmentClaimLine = (description?: string, sourceType?: string) => + sourceType === "DepartmentClaim" || + (typeof description === "string" && + (description.startsWith(DEPARTMENT_CLAIM_PREFIX) || description.includes('Clearance:'))); + + const isFinanceValidatedLine = (description?: string, sourceType?: string) => + sourceType === "FinanceValidated" || + (typeof description === "string" && description.startsWith(FINANCE_VALIDATED_PREFIX)); + const getFriendlyActionName = (action: string) => { if (!action) return 'Action'; const mapping: Record = { @@ -141,14 +154,20 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) { return mapping[action] || action.split('_').map(w => w.charAt(0).toUpperCase() + w.slice(1).toLowerCase()).join(' '); }; - const fetchFnFDetails = async () => { + const fetchFnFDetails = async (showLoader: boolean = true) => { try { - setLoading(true); + if (showLoader) setLoading(true); const response = await API.getFnFSettlementById(fnfId); const data = response.data as any; if (data.success) { const s = data.fnf; // Map backend data to UI format + const allLineItems = (s.lineItems || []).filter((li: any) => li.isActive !== false); + const hasFinanceValidatedLines = allLineItems.some((li: any) => isFinanceValidatedLine(li.description, li.sourceType)); + const calculationLineItems = hasFinanceValidatedLines + ? allLineItems.filter((li: any) => isFinanceValidatedLine(li.description, li.sourceType)) + : allLineItems.filter((li: any) => !isDepartmentClaimLine(li.description, li.sourceType)); + const mappedCase: any = { id: s.id, caseNumber: s.settlementId || s.resignation?.resignationId || s.terminationRequest?.requestId || s.id.substring(0, 8), @@ -180,9 +199,10 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) { : s.status === "Completed" ? "Completed" : "Pending", - totalPayableAmount: (s.lineItems || []).filter((li: any) => li.itemType === 'Payable').reduce((sum: number, li: any) => sum + (parseFloat(li.amount) || 0), 0), - totalRecoveryAmount: (s.lineItems || []).filter((li: any) => li.itemType === 'Receivable' || li.itemType === 'Recovery').reduce((sum: number, li: any) => sum + (parseFloat(li.amount) || 0), 0), - totalDeductions: (s.lineItems || []).filter((li: any) => li.itemType === 'Deduction').reduce((sum: number, li: any) => sum + (parseFloat(li.amount) || 0), 0), + totalPayableAmount: calculationLineItems.filter((li: any) => li.itemType === 'Payable').reduce((sum: number, li: any) => sum + (parseFloat(li.amount) || 0), 0), + totalRecoveryAmount: calculationLineItems.filter((li: any) => li.itemType === 'Receivable' || li.itemType === 'Recovery').reduce((sum: number, li: any) => sum + (parseFloat(li.amount) || 0), 0), + totalDeductions: calculationLineItems.filter((li: any) => li.itemType === 'Deduction').reduce((sum: number, li: any) => sum + (parseFloat(li.amount) || 0), 0), + allLineItems, netAmount: 0, departmentResponses: [] as any[] }; @@ -198,8 +218,10 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) { const c = (s.clearances || []).find( (clearance: any) => normalizeDepartment(clearance.department) === deptName, ); - const relatedItems = (s.lineItems || []).filter( - (li: any) => normalizeDepartment(li.department) === deptName, + const relatedItems = allLineItems.filter( + (li: any) => + normalizeDepartment(li.department) === deptName && + isDepartmentClaimLine(li.description, li.sourceType), ); // Calculate departmental net @@ -212,12 +234,17 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) { }); const netAmount = deptPayables - deptRecoveries; + const hasDuesAmount = Math.abs(netAmount) > 0; + const rawStatus = c?.status || "Pending"; + const normalizedStatus = hasDuesAmount + ? "Dues Pending" + : (rawStatus === "Cleared" ? "NOC Submitted" : rawStatus); return { id: c?.id || `dept-${deptName}`, clearanceId: c?.id || null, departmentName: deptName, - status: c?.status || "Pending", + status: normalizedStatus, amountType: netAmount > 0 ? "Payable" : netAmount < 0 ? "Recovery" : null, amount: Math.abs(netAmount), submittedDate: c?.clearedAt ? formatDateTime(c.clearedAt) : "-", @@ -272,7 +299,7 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) { console.error("Fetch F&F details error:", error); toast.error("Failed to fetch settlement details"); } finally { - setLoading(false); + if (showLoader) setLoading(false); } }; @@ -423,7 +450,8 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) { toast.success(`Clearance updated for ${selectedDept.departmentName}`); setShowClearanceDialog(false); setClearanceFile(null); - fetchFnFDetails(); + setActiveTab('departments'); + fetchFnFDetails(false); } catch (error) { console.error("Update clearance error:", error); toast.error("Failed to update department clearance"); @@ -470,6 +498,32 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) { ).length; const totalDepartments = fnfCase.departmentResponses.length; const progressPercentage = (responsesReceived / totalDepartments) * 100; + const departmentReconciliation = ALL_DEPARTMENTS.map((deptName) => { + const claim = (fnfCase.departmentResponses || []).find((d: any) => d.departmentName === deptName); + const claimAmount = Number(claim?.amount) || 0; + const claimType = claim?.amountType || '-'; + const deptLines = (fnfCase.allLineItems || []).filter((li: any) => normalizeDepartment(li.department) === deptName); + const payable = deptLines + .filter((li: any) => li.sourceType === 'FinanceValidated' && li.itemType === 'Payable') + .reduce((sum: number, li: any) => sum + Math.abs(parseFloat(li.amount) || 0), 0); + const receivable = deptLines + .filter((li: any) => li.sourceType === 'FinanceValidated' && (li.itemType === 'Receivable' || li.itemType === 'Recovery')) + .reduce((sum: number, li: any) => sum + Math.abs(parseFloat(li.amount) || 0), 0); + const deduction = deptLines + .filter((li: any) => li.sourceType === 'FinanceValidated' && li.itemType === 'Deduction') + .reduce((sum: number, li: any) => sum + Math.abs(parseFloat(li.amount) || 0), 0); + const validatedNet = payable - receivable - deduction; + const validatedAmount = Math.abs(validatedNet); + const validatedType = validatedNet > 0 ? 'Payable' : validatedNet < 0 ? 'Recovery' : '-'; + return { + department: deptName, + claimAmount, + claimType, + validatedAmount, + validatedType, + variance: validatedAmount - claimAmount + }; + }); return (
@@ -607,7 +661,7 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) { {/* Tabs */} - + Progress Case Details @@ -1383,6 +1437,39 @@ 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 + + + + {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 diff --git a/src/components/applications/ResignationDetails.tsx b/src/components/applications/ResignationDetails.tsx index 73eb620..61a796d 100644 --- a/src/components/applications/ResignationDetails.tsx +++ b/src/components/applications/ResignationDetails.tsx @@ -21,52 +21,6 @@ import { formatDateTime } from '../ui/utils'; import { RESIGNATION_DOCUMENT_TYPES, RESIGNATION_STAGE_OPTIONS } from '../../lib/offboardingDocumentOptions'; import { WIDE_DIALOG_CLASS } from '../../lib/dialogStyles'; -const ALL_DEPARTMENTS = [ - 'Warranty Department', 'Accessories Department', 'Sales Department', 'RTO Department', - 'Service Department', 'Parts Department', 'Finance Department', 'Insurance Department', - 'Inventory Department', 'Marketing Department', 'HR Department', 'IT Department', - 'Legal Department', 'Quality Department', 'Logistics Department', 'Customer Relations Department' -]; - -const normalizeDepartment = (name: string) => { - if (!name) return name; - let inputName = name.trim(); - - // Exact match first - const exactMatch = ALL_DEPARTMENTS.find(d => d.toLowerCase() === inputName.toLowerCase()); - if (exactMatch) return exactMatch; - - // Smart mapping for shorthands - const mapping: Record = { - 'sales': 'Sales Department', - 'service': 'Service Department', - 'spares': 'Parts Department', - 'parts': 'Parts Department', - 'spares / parts': 'Parts Department', - 'finance': 'Finance Department', - 'accounts': 'Finance Department', - 'warranty': 'Warranty Department', - 'marketing': 'Marketing Department', - 'hr': 'HR Department', - 'it': 'IT Department', - 'legal': 'Legal Department', - 'logistics': 'Logistics Department', - 'quality': 'Quality Department', - 'fdd': 'Finance Department', - 'apparel': 'Accessories Department', - 'accessories': 'Accessories Department', - 'dms': 'IT Department', - 'rto': 'Admin Department', - 'admin': 'Admin Department', - 'admin / dd-admin': 'Admin Department' - }; - - const mapped = mapping[inputName.toLowerCase().replace(' department', '')]; - if (mapped) return mapped; - - return name; -}; - interface ResignationDetailsProps { resignationId: string; @@ -545,7 +499,6 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig Details Progress - {currentUser?.role !== 'Dealer' && Clearances} Documents Audit Trail @@ -566,6 +519,10 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig

{resignationData?.dealer?.dealerProfile?.gstNumber || resignationData?.outlet?.gstNumber || 'N/A'}

+
+ +

{resignationData?.dealer?.email || 'N/A'}

+

{resignationData?.dealer?.dealerProfile?.dealerCode?.salesCode || 'N/A'}

@@ -756,90 +713,6 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig - {/* Clearances Tab */} - {currentUser?.role !== 'Dealer' && ( - - - -
- Departmental Clearances - Status of clearances from various departments -
-
- - - - - Department - Status - Amount Type - Amount - Remarks - - - - {ALL_DEPARTMENTS.map((dept) => { - const settlement = resignationData?.settlement; - const fffClearance = (settlement?.clearances || []).find((c: any) => normalizeDepartment(c.department) === dept); - const relatedLineItems = (settlement?.lineItems || []).filter((li: any) => normalizeDepartment(li.department) === dept); - - let deptPayables = 0; - let deptRecoveries = 0; - relatedLineItems.forEach((li: any) => { - const amt = Math.abs(parseFloat(li.amount) || 0); - if (li.itemType === 'Payable') deptPayables += amt; - else deptRecoveries += amt; - }); - - const netAmount = deptPayables - deptRecoveries; - const jsonClearance = (resignationData?.departmentalClearances || {})[dept] || { status: 'Pending', remarks: '', amount: 0, type: 'Recovery' }; - const displayStatus = fffClearance - ? (fffClearance.status === 'NOC Submitted' || fffClearance.status === 'Cleared' ? (netAmount < 0 ? 'Dues' : 'Cleared') : fffClearance.status) - : jsonClearance.status; - const displayRemarks = fffClearance ? fffClearance.remarks : jsonClearance.remarks; - const displayAmount = Math.abs(netAmount) || jsonClearance.amount || 0; - const displayType = netAmount > 0 ? 'Payable' : 'Recovery'; - - return ( - - {dept} - - - {displayStatus || 'Pending'} - - - - - {displayType} - - - - - ₹{displayAmount.toLocaleString()} - - - - {displayRemarks || 'Awaiting departmental verification.'} - - - ); - })} - -
-
-
-
- )} - {/* Documents Tab */} diff --git a/src/components/applications/TerminationDetails.tsx b/src/components/applications/TerminationDetails.tsx index ddd0703..69acb74 100644 --- a/src/components/applications/TerminationDetails.tsx +++ b/src/components/applications/TerminationDetails.tsx @@ -153,7 +153,7 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi }; // Check if user can push to F&F (DD Lead and above) - const canPushToFnF = currentUser && ['DD Lead', 'DD Head', 'NBH', 'DD Admin', 'Super Admin'].includes(currentUser.role); + const canPushToFnF = currentUser && ['DD Lead', 'DD Head', 'NBH', 'DD Admin', 'Super Admin'].includes(currentUser.role || currentUser.roleCode); // Centralized Permissions Utility for Termination logic (Robust Validation) const getTerminationPermissions = () => { @@ -163,7 +163,8 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi const currentStage = terminationData.currentStage; const status = terminationData.status; - const userRole = currentUser.role; + const userRole = currentUser.role || currentUser.roleCode; + const isScnStage = ['Show Cause Notice', 'Show Cause Notice (SCN)', 'SCN'].includes(currentStage); const isFinalState = ['Completed', 'Rejected', 'Withdrawn', 'Terminated'].includes(status) || currentStage === 'Terminated'; const isSettlementPhase = status === 'F&F Initiated' || currentStage === 'F&F Initiated' || status === 'Settled' || status === 'FNF_INITIATED'; @@ -173,6 +174,7 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi (currentStage === 'ZBH Review' && userRole === 'ZBH') || (currentStage === 'DD Lead Review' && userRole === 'DD Lead') || (currentStage === 'Legal Verification' && userRole === 'Legal Admin') || + (currentStage === 'DD Head Review' && userRole === 'DD Head') || (currentStage === 'NBH Evaluation' && userRole === 'NBH') || (currentStage === 'NBH Final Approval' && userRole === 'NBH') || (currentStage === 'CCO Approval' && userRole === 'CCO') || @@ -181,10 +183,15 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi ); return { - canApprove: isCurrentlyAssigned && !isFinalState && !isSettlementPhase && !['Show Cause Notice', 'NBH Final Approval', 'CCO Approval', 'CEO Final Approval'].includes(currentStage), + canApprove: isCurrentlyAssigned && !isFinalState && !isSettlementPhase && ![...['NBH Final Approval', 'CCO Approval', 'CEO Final Approval'], 'Show Cause Notice', 'Show Cause Notice (SCN)', 'SCN'].includes(currentStage), canIssueSCN: currentStage === 'NBH Evaluation' && (userRole === 'NBH' || userRole === 'Super Admin') && !isFinalState, - canUploadSCNResponse: currentStage === 'Show Cause Notice' && (['Legal Admin', 'DD Admin', 'Super Admin'].includes(userRole)) && !isFinalState, - canFinalize: ['NBH Final Approval', 'CCO Approval', 'CEO Final Approval'].includes(currentStage) && (userRole === currentStage.replace(' Approval', '') || userRole === 'Super Admin') && !isFinalState, + canUploadSCNResponse: isScnStage && (['Legal Admin', 'DD Admin', 'Super Admin'].includes(userRole)) && !isFinalState, + canFinalize: ( + (currentStage === 'NBH Final Approval' && userRole === 'NBH') || + (currentStage === 'CCO Approval' && userRole === 'CCO') || + (currentStage === 'CEO Final Approval' && userRole === 'CEO') || + userRole === 'Super Admin' + ) && ['NBH Final Approval', 'CCO Approval', 'CEO Final Approval'].includes(currentStage) && !isFinalState, canPushToFnF: canPushToFnF && !isSettlementPhase && !isFinalState, canWithdraw: userRole === 'ASM' && currentStage === 'Request Initiated' && !isFinalState, isFinalState, @@ -196,6 +203,7 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi // Use actual data from backend const request = terminationData || {}; + const isScnStage = ['Show Cause Notice', 'Show Cause Notice (SCN)', 'SCN'].includes(request.currentStage); const stageAliases: Record = { 'Submitted': ['Submitted', 'Request Initiated'], @@ -203,6 +211,7 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi 'ZBH Review': ['ZBH Review'], 'DD Lead Review': ['DD Lead Review'], 'Legal Verification': ['Legal Verification'], + 'DD Head Review': ['DD Head Review'], 'NBH Evaluation': ['NBH Evaluation'], 'Show Cause Notice (SCN)': ['Show Cause Notice', 'Show Cause Notice (SCN)', 'SCN'], 'Personal Hearing': ['Personal Hearing'], @@ -213,6 +222,45 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi 'Dealer Terminated': ['Terminated', 'Dealer Terminated'] }; + const stageSequence = [ + 'Submitted', + 'RBM Review', + 'ZBH Review', + 'DD Lead Review', + 'Legal Verification', + 'DD Head Review', + 'NBH Evaluation', + 'Show Cause Notice (SCN)', + 'Personal Hearing', + 'NBH Final Approval', + 'CCO Approval', + 'CEO Final Approval', + 'Legal - Termination Letter', + 'Dealer Terminated' + ]; + + const resolveCanonicalStage = (currentStage?: string) => { + if (!currentStage) return ''; + const normalized = String(currentStage).trim(); + const matched = stageSequence.find((stageName) => + (stageAliases[stageName] || [stageName]).includes(normalized) + ); + return matched || normalized; + }; + + const getProgressStatus = (stageName: string) => { + const currentCanonical = resolveCanonicalStage(request.currentStage || request.status); + const currentIndex = stageSequence.indexOf(currentCanonical); + const stageIndex = stageSequence.indexOf(stageName); + + if (stageIndex === -1) return 'pending'; + if (currentIndex === -1) return stageName === 'Submitted' ? 'completed' : 'pending'; + if (stageName === 'Dealer Terminated' && currentIndex >= stageIndex) return 'completed'; + if (stageIndex < currentIndex) return 'completed'; + if (stageIndex === currentIndex) return 'active'; + return 'pending'; + }; + const allUploadedDocs = [ ...(request.documents || []), ...(request.uploadedDocuments || []) @@ -236,17 +284,26 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi const getLatestStageTimelineEntry = (stageName: string) => { const aliases = stageAliases[stageName] || [stageName]; - const entries = (request.timeline || []).filter((entry: any) => - aliases.includes(entry.stage) || aliases.includes(entry.targetStage) - ); - return entries.length > 0 ? entries[entries.length - 1] : null; + const entries = (request.timeline || []).filter((entry: any) => aliases.includes(entry.stage)); + + if (entries.length === 0) return null; + + // Keep submitted row anchored to initiation details, not later stage-transition remarks. + if (stageName === 'Submitted') { + const initiatedEntry = entries.find((entry: any) => + String(entry?.action || '').toLowerCase().includes('initiated') + ); + return initiatedEntry || entries[0]; + } + + return entries[entries.length - 1]; }; const progressStages = [ { id: 1, name: 'Submitted', - status: 'completed', + status: getProgressStatus('Submitted'), description: 'Termination request initiated', date: '', actionType: '', @@ -257,73 +314,79 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi { id: 2, name: 'RBM Review', - status: request.currentStage === 'RBM Review' ? 'active' : ['ZBH Review', 'DD Lead Review', 'Legal Verification', 'NBH Evaluation', 'Show Cause Notice', 'Personal Hearing', 'NBH Final Approval', 'CCO Approval', 'CEO Final Approval', 'Legal - Termination Letter', 'Terminated'].includes(request.currentStage) ? 'completed' : 'pending', + status: getProgressStatus('RBM Review'), description: 'Regional Business Manager review' }, { id: 3, name: 'ZBH Review', - status: request.currentStage === 'ZBH Review' ? 'active' : ['DD Lead Review', 'Legal Verification', 'NBH Evaluation', 'Show Cause Notice', 'Personal Hearing', 'NBH Final Approval', 'CCO Approval', 'CEO Final Approval', 'Legal - Termination Letter', 'Terminated'].includes(request.currentStage) ? 'completed' : 'pending', + status: getProgressStatus('ZBH Review'), description: 'Zonal Business Head evaluation' }, { id: 4, name: 'DD Lead Review', - status: request.currentStage === 'DD Lead Review' ? 'active' : ['Legal Verification', 'NBH Evaluation', 'Show Cause Notice', 'Personal Hearing', 'NBH Final Approval', 'CCO Approval', 'CEO Final Approval', 'Legal - Termination Letter', 'Terminated'].includes(request.currentStage) ? 'completed' : 'pending', + status: getProgressStatus('DD Lead Review'), description: 'DD Lead validation' }, { id: 5, name: 'Legal Verification', - status: request.currentStage === 'Legal Verification' ? 'active' : ['NBH Evaluation', 'Show Cause Notice', 'Personal Hearing', 'NBH Final Approval', 'CCO Approval', 'CEO Final Approval', 'Legal - Termination Letter', 'Terminated'].includes(request.currentStage) ? 'completed' : 'pending', + status: getProgressStatus('Legal Verification'), description: 'Legal team validates termination grounds' }, { id: 6, - name: 'NBH Evaluation', - status: request.currentStage === 'NBH Evaluation' ? 'active' : ['Show Cause Notice', 'Personal Hearing', 'NBH Final Approval', 'CCO Approval', 'CEO Final Approval', 'Legal - Termination Letter', 'Terminated'].includes(request.currentStage) ? 'completed' : 'pending', - description: 'National Business Head decision' + name: 'DD Head Review', + status: getProgressStatus('DD Head Review'), + description: 'DD Head strategic review' }, { id: 7, - name: 'Show Cause Notice (SCN)', - status: request.currentStage === 'Show Cause Notice' ? 'active' : ['Personal Hearing', 'NBH Final Approval', 'CCO Approval', 'CEO Final Approval', 'Legal - Termination Letter', 'Terminated'].includes(request.currentStage) ? 'completed' : 'pending', - description: 'SCN sent to dealer, awaiting response' + name: 'NBH Evaluation', + status: getProgressStatus('NBH Evaluation'), + description: 'National Business Head decision' }, { id: 8, - name: 'Personal Hearing', - status: request.currentStage === 'Personal Hearing' ? 'active' : ['NBH Final Approval', 'CCO Approval', 'CEO Final Approval', 'Legal - Termination Letter', 'Terminated'].includes(request.currentStage) ? 'completed' : 'pending', - description: 'Evaluation of SCN response & Hearing' + name: 'Show Cause Notice (SCN)', + status: getProgressStatus('Show Cause Notice (SCN)'), + description: 'SCN sent to dealer, awaiting response' }, { id: 9, - name: 'NBH Final Approval', - status: request.currentStage === 'NBH Final Approval' ? 'active' : ['CCO Approval', 'CEO Final Approval', 'Legal - Termination Letter', 'Terminated'].includes(request.currentStage) ? 'completed' : 'pending', - description: 'NBH final termination decision' + name: 'Personal Hearing', + status: getProgressStatus('Personal Hearing'), + description: 'Evaluation of SCN response & Hearing' }, { id: 10, - name: 'CCO Approval', - status: request.currentStage === 'CCO Approval' ? 'active' : ['CEO Final Approval', 'Legal - Termination Letter', 'Terminated'].includes(request.currentStage) ? 'completed' : 'pending', - description: 'Chief Commercial Officer approval' + name: 'NBH Final Approval', + status: getProgressStatus('NBH Final Approval'), + description: 'NBH final termination decision' }, { id: 11, - name: 'CEO Final Approval', - status: request.currentStage === 'CEO Final Approval' ? 'active' : ['Legal - Termination Letter', 'Terminated'].includes(request.currentStage) ? 'completed' : 'pending', - description: 'CEO final authorization' + name: 'CCO Approval', + status: getProgressStatus('CCO Approval'), + description: 'Chief Commercial Officer approval' }, { id: 12, - name: 'Legal - Termination Letter', - status: request.currentStage === 'Legal - Termination Letter' ? 'active' : request.currentStage === 'Terminated' ? 'completed' : 'pending', - description: 'Legal team issues final termination letter' + name: 'CEO Final Approval', + status: getProgressStatus('CEO Final Approval'), + description: 'CEO final authorization' }, { id: 13, + name: 'Legal - Termination Letter', + status: getProgressStatus('Legal - Termination Letter'), + description: 'Legal team issues final termination letter' + }, + { + id: 14, name: 'Dealer Terminated', - status: request.currentStage === 'Terminated' ? 'completed' : 'pending', + status: getProgressStatus('Dealer Terminated'), description: 'Dealership termination effective', date: '', actionType: '', @@ -590,6 +653,10 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi

{request.dealer?.gstNumber || 'N/A'}

+
+ +

{request.dealer?.user?.email || 'N/A'}

+

{request.dealer?.registeredAddress || request.dealer?.application?.address || 'N/A'}

@@ -1101,16 +1168,16 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi - {request.currentStage === 'SCN' ? 'Upload SCN Response' : 'Issue Show Cause Notice (SCN)'} + {isScnStage ? 'Upload SCN Response' : 'Issue Show Cause Notice (SCN)'} - {request.currentStage === 'SCN' + {isScnStage ? 'Upload the response received from the dealer regarding the SCN.' : 'Confirm the issuance of a formal Show Cause Notice to the dealer.'}
- {request.currentStage === 'SCN' && ( + {isScnStage && (
@@ -1144,11 +1211,11 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi Cancel
diff --git a/src/components/applications/TerminationPage.tsx b/src/components/applications/TerminationPage.tsx index c47d74f..a567688 100644 --- a/src/components/applications/TerminationPage.tsx +++ b/src/components/applications/TerminationPage.tsx @@ -86,10 +86,15 @@ export function TerminationPage({ currentUser, onViewDetails }: TerminationPageP (async () => { try { setDialogDataLoading(true); - const response = await API.getDealers({ onboarded: 'true' }); + const response = await API.getDealers({ onboarded: 'true', activeOnly: 'true' }); const data = response.data as any; if (!cancelled && data?.success) { - setDealers(Array.isArray(data.data) ? data.data : []); + const activeDealers = (Array.isArray(data.data) ? data.data : []).filter((dealer: any) => { + const dealerStatus = String(dealer?.status || '').toLowerCase(); + const userStatus = String(dealer?.user?.status || '').toLowerCase(); + return dealerStatus === 'active' && dealer?.user?.isActive && userStatus === 'active'; + }); + setDealers(activeDealers); } } catch (error) { if (!cancelled) { @@ -211,21 +216,23 @@ export function TerminationPage({ currentUser, onViewDetails }: TerminationPageP // Helper function to check if request is at current user's level const isRequestAtMyLevel = (request: any) => { if (!currentUser) return false; + const userRole = currentUser.role || currentUser.roleCode; const roleToStageMapping: Record = { - 'DD Lead': ['DD Lead Review'], 'RBM': ['RBM Review'], 'ZBH': ['ZBH Review'], + 'DD Lead': ['DD Lead Review'], + 'DD Head': ['DD Head Review'], 'NBH': ['NBH Evaluation', 'NBH Final Approval'], 'Legal Admin': ['Legal Verification', 'Legal - Termination Letter'], 'Legal': ['Legal Verification'], 'DD Admin': ['Show Cause Notice', 'Terminated'], 'CCO': ['CCO Approval'], 'CEO': ['CEO Final Approval'], - 'Super Admin': ['DD Lead Review', 'RBM Review', 'ZBH Review', 'NBH Evaluation', 'Legal Verification', 'Show Cause Notice', 'NBH Final Approval', 'CCO Approval', 'CEO Final Approval', 'Legal - Termination Letter', 'Terminated'] + 'Super Admin': ['RBM Review', 'ZBH Review', 'DD Lead Review', 'DD Head Review', 'NBH Evaluation', 'Legal Verification', 'Show Cause Notice', 'NBH Final Approval', 'CCO Approval', 'CEO Final Approval', 'Legal - Termination Letter', 'Terminated'] }; - const userStages = roleToStageMapping[currentUser.role] || []; + const userStages = roleToStageMapping[userRole] || []; return userStages.some(stage => (request.currentStage && request.currentStage.includes(stage)) || (request.status && request.status.includes(stage)) diff --git a/src/components/auth/RoleGuard.tsx b/src/components/auth/RoleGuard.tsx index c83dc1f..a8d844d 100644 --- a/src/components/auth/RoleGuard.tsx +++ b/src/components/auth/RoleGuard.tsx @@ -17,6 +17,9 @@ export const RoleGuard: React.FC = ({ }) => { const { user, isAuthenticated, loading } = useSelector((state: RootState) => state.auth); const location = useLocation(); + const normalizedRole = String((user as any)?.role || (user as any)?.roleCode || '').trim().toLowerCase(); + const normalizedAllowedRoles = (allowedRoles || []).map((r) => String(r).trim().toLowerCase()); + const normalizedExcludedRoles = (excludeRoles || []).map((r) => String(r).trim().toLowerCase()); if (loading) { return
Loading...
; @@ -27,16 +30,16 @@ export const RoleGuard: React.FC = ({ } // Check excluded roles first (e.g. Block Prospective Dealer from main dashboard) - if (excludeRoles && user && excludeRoles.includes(user.role)) { + if (excludeRoles && user && normalizedExcludedRoles.includes(normalizedRole)) { // If prospective dealer is excluded, redirect to their dashboard - if (user.role === 'Prospective Dealer') { + if (normalizedRole === 'prospective dealer') { return ; } return ; } // Check allowed roles (e.g. Only Prospective Dealer can see their dashboard) - if (allowedRoles && user && !allowedRoles.includes(user.role)) { + if (allowedRoles && user && !normalizedAllowedRoles.includes(normalizedRole)) { // If regular dealer tries to access prospective dashboard return ; } diff --git a/src/components/dashboard/FinanceDashboard.tsx b/src/components/dashboard/FinanceDashboard.tsx index d68f8c9..85fdd4d 100644 --- a/src/components/dashboard/FinanceDashboard.tsx +++ b/src/components/dashboard/FinanceDashboard.tsx @@ -61,24 +61,25 @@ export function FinanceDashboard({ onNavigate, onViewPaymentDetails, onViewAudit 'Payment Pending', 'Security Details', 'LOI In Progress', 'LOI Issued', 'LOA Pending', 'Dealer Code Generation', 'LOA_APPROVAL' ].includes(s); + const deposits = app.securityDeposits || []; - if (isPaymentStage) { - const deposits = app.securityDeposits || []; - if (deposits.length > 0) { - deposits.forEach((d: any) => { - consolidatedPayments.push({ - ...d, - application: app, - paymentStatus: d.status, - paymentType: d.depositType, - amount: d.amount, - id: d.id, - applicationId: app.applicationId || app.id, - createdAt: d.createdAt, - verificationDate: d.verifiedAt - }); + // Always include real payment rows, even if the app has moved beyond payment stages. + if (deposits.length > 0) { + deposits.forEach((d: any) => { + consolidatedPayments.push({ + ...d, + application: app, + paymentStatus: d.status, + paymentType: d.depositType, + amount: d.amount, + id: d.id, + applicationId: app.applicationId || app.id, + createdAt: d.createdAt, + verificationDate: d.verifiedAt }); - } else if (['Payment Pending', 'Security Details', 'LOI In Progress'].includes(s)) { + }); + } else if (isPaymentStage) { + if (['Payment Pending', 'Security Details', 'LOI In Progress'].includes(s)) { // Virtual pending record for Security Deposit (5L) consolidatedPayments.push({ id: `virtual-${app.id}-sd`, @@ -175,8 +176,12 @@ export function FinanceDashboard({ onNavigate, onViewPaymentDetails, onViewAudit } }; - const pendingOnboarding = onboardingPayments.filter(p => p.paymentStatus !== 'Paid' && p.paymentStatus !== 'Verified'); - const verifiedOnboarding = onboardingPayments.filter(p => p.paymentStatus === 'Paid' || p.paymentStatus === 'Verified'); + const isVerifiedLikeStatus = (status: any) => { + const normalized = String(status || '').trim().toLowerCase(); + return normalized === 'paid' || normalized === 'verified'; + }; + const pendingOnboarding = onboardingPayments.filter(p => !isVerifiedLikeStatus(p.paymentStatus)); + const verifiedOnboarding = onboardingPayments.filter(p => isVerifiedLikeStatus(p.paymentStatus)); const pendingFnF = fnfSettlements.filter(f => f.status === 'Initiated' || f.status === 'Calculated'); const completedFnF = fnfSettlements.filter(f => f.status === 'Completed' || f.status === 'Cleared'); diff --git a/src/components/dealer/DealerResignationDetailsPage.tsx b/src/components/dealer/DealerResignationDetailsPage.tsx new file mode 100644 index 0000000..c99c749 --- /dev/null +++ b/src/components/dealer/DealerResignationDetailsPage.tsx @@ -0,0 +1,384 @@ +import { ArrowLeft, Calendar, CheckCircle2, Clock, FileText, MapPin, MessageSquare, Upload, User } from 'lucide-react'; +import { useEffect, useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { toast } from 'sonner'; +import { Button } from '../ui/button'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../ui/card'; +import { Badge } from '../ui/badge'; +import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '../ui/table'; +import { Label } from '../ui/label'; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../ui/select'; +import { resignationService } from '../../services/resignation.service'; +import { formatDateTime } from '../ui/utils'; +import { RESIGNATION_STAGE_OPTIONS, RESIGNATION_DOCUMENT_TYPES } from '../../lib/offboardingDocumentOptions'; + +interface DealerResignationDetailsPageProps { + resignationId: string; + onBack: () => void; +} + +const getStatusColor = (status: string) => { + if (status === 'Completed') return 'bg-green-100 text-green-700 border-green-300'; + if (status === 'Rejected') return 'bg-red-100 text-red-700 border-red-300'; + if (status.includes('Review') || status.includes('Pending')) return 'bg-yellow-100 text-yellow-700 border-yellow-300'; + return 'bg-slate-100 text-slate-700 border-slate-300'; +}; + +export function DealerResignationDetailsPage({ resignationId, onBack }: DealerResignationDetailsPageProps) { + const navigate = useNavigate(); + const [loading, setLoading] = useState(true); + const [details, setDetails] = useState(null); + const [uploading, setUploading] = useState(false); + const [uploadFile, setUploadFile] = useState(null); + const [uploadDocType, setUploadDocType] = useState(RESIGNATION_DOCUMENT_TYPES[0]); + const [uploadStage, setUploadStage] = useState(''); + const [auditLogs, setAuditLogs] = useState([]); + + useEffect(() => { + const fetchDetails = async () => { + try { + setLoading(true); + const [data, audits] = await Promise.all([ + resignationService.getResignationById(resignationId), + fetchAuditLogs(resignationId) + ]); + setDetails(data); + setAuditLogs(audits); + } catch (error) { + console.error('Failed to fetch resignation details:', error); + toast.error('Unable to load resignation details'); + } finally { + setLoading(false); + } + }; + + if (resignationId) { + fetchDetails(); + } + }, [resignationId]); + + const fetchAuditLogs = async (id: string) => { + try { + // Lazy import through existing API helper shape used in other modules. + const { API } = await import('../../api/API'); + const response = await API.getAuditLogs('resignation', id) as any; + if (response?.data?.success) return response.data.data || []; + return []; + } catch (error) { + return []; + } + }; + + const refreshDetails = async () => { + try { + const [data, audits] = await Promise.all([ + resignationService.getResignationById(resignationId), + fetchAuditLogs(resignationId) + ]); + setDetails(data); + setAuditLogs(audits); + } catch (error) { + toast.error('Unable to refresh resignation details'); + } + }; + + const handleUpload = async () => { + if (!uploadFile) { + toast.error('Please choose a file'); + return; + } + try { + setUploading(true); + const formData = new FormData(); + formData.append('file', uploadFile); + formData.append('documentType', uploadDocType); + if (uploadStage) formData.append('stage', uploadStage); + await resignationService.uploadDocument(resignationId, formData); + toast.success('Document uploaded successfully'); + setUploadFile(null); + await refreshDetails(); + } catch (error: any) { + toast.error(error?.response?.data?.message || 'Document upload failed'); + } finally { + setUploading(false); + } + }; + + if (loading) { + return ( +
+ +
+ ); + } + + if (!details) { + return ( +
+ + + + Resignation details not found. + + +
+ ); + } + + const docs = details.uploadedDocuments || []; + const timeline = Array.isArray(details.timeline) ? details.timeline : []; + + return ( +
+
+ +
+

Resignation Request Details

+

+ Track your request progress and uploaded documents +

+
+
+ + + + + + Request Summary + + Current request status and key metadata + + +
+

Request ID

+

{details.resignationId || details.id}

+
+
+

Status

+ + {details.status || 'Pending'} + +
+
+

Current Stage

+

{details.currentStage || 'Submitted'}

+
+
+

Submitted On

+

{formatDateTime(details.submittedOn || details.createdAt)}

+
+
+

Resignation Type

+

{details.resignationType || 'N/A'}

+
+
+

Progress

+

{details.progressPercentage || 0}%

+
+
+
+ + + + + + Outlet and Dates + + + +
+

Outlet

+

{details.outlet?.name || 'N/A'}

+
+
+

Outlet Code

+

{details.outlet?.code || 'N/A'}

+
+
+

Last Operational Date (Sales)

+

{details.lastOperationalDateSales ? formatDateTime(details.lastOperationalDateSales) : 'N/A'}

+
+
+

Last Operational Date (Services)

+

{details.lastOperationalDateServices ? formatDateTime(details.lastOperationalDateServices) : 'N/A'}

+
+
+

Reason

+

{details.reason || '-'}

+
+
+

+ + Outlet Address +

+

{details.outlet?.address || '-'}

+
+
+
+ + + + + + Uploaded Documents + + Dealer can upload resignation-related documents for review + + +
+
+ + +
+
+ + +
+
+ + setUploadFile(e.target.files?.[0] || null)} + /> +
+
+ +
+
+ + + + + Document Type + File + Uploaded By + Uploaded On + + + + {docs.length === 0 ? ( + + + No documents uploaded yet. + + + ) : ( + docs.map((doc: any) => ( + + {doc.documentType || '-'} + {doc.fileName || '-'} + {doc.uploader?.fullName || '-'} + {formatDateTime(doc.createdAt)} + + )) + )} + +
+
+
+ + + + + + Work Notes Communication + + Official channel for internal-dealer clarifications + + + + + + + + + + + Progress Timeline + + + + {timeline.length === 0 ? ( +

No timeline events available yet.

+ ) : ( +
+ {timeline.slice().reverse().map((entry: any, idx: number) => ( +
+

{entry.action || entry.stage || 'Stage Update'}

+

{formatDateTime(entry.timestamp || entry.createdAt)}

+

{entry.comments || entry.remarks || 'No remarks'}

+
+ ))} +
+ )} +
+
+ + + + Audit Trail + Traceability of status/actions on this request + + + {auditLogs.length === 0 ? ( +

No audit records found.

+ ) : ( +
+ {auditLogs.map((log: any) => ( +
+
+

{log.action || 'Action'}

+

{formatDateTime(log.createdAt || log.timestamp)}

+
+

{log.remarks || log.description || 'No remarks'}

+
+ ))} +
+ )} +
+
+
+ ); +} diff --git a/src/components/layout/Sidebar.tsx b/src/components/layout/Sidebar.tsx index 661c95f..223aa44 100644 --- a/src/components/layout/Sidebar.tsx +++ b/src/components/layout/Sidebar.tsx @@ -36,14 +36,16 @@ export function Sidebar({ onLogout }: SidebarProps) { const [searchQuery, setSearchQuery] = useState(''); const [offboardingExpanded, setOffboardingExpanded] = useState(false); const [allRequestsExpanded, setAllRequestsExpanded] = useState(false); - const currentRole = currentUser?.role || ''; + const currentRole = currentUser?.role || currentUser?.roleCode || ''; + const normalizedRole = String(currentRole).trim().toLowerCase(); + const hasRole = (roles: string[]) => roles.map((r) => r.toLowerCase()).includes(normalizedRole); - const resignationRoles = ['DD Admin', 'ASM', 'DD Lead', 'ZBH', 'NBH', 'Legal', 'Super Admin']; - const terminationRoles = ['ASM', 'DD Lead', 'DD Admin', 'Super Admin']; + const resignationRoles = ['DD Admin', 'ASM', 'RBM', 'DD Lead', 'ZBH', 'NBH', 'Legal', 'Legal Admin', 'Super Admin']; + const terminationRoles = ['ASM', 'RBM', 'ZBH', 'DD Lead', 'DD Head', 'NBH', 'Legal Admin', 'Legal', 'DD Admin', 'CCO', 'CEO', 'Super Admin']; const fnfRoles = ['DD Admin', 'DD Lead', 'NBH', 'Finance', 'Finance Admin', 'Super Admin']; - const canSeeResignation = resignationRoles.includes(currentRole); - const canSeeTermination = terminationRoles.includes(currentRole); - const canSeeFnF = fnfRoles.includes(currentRole); + const canSeeResignation = hasRole(resignationRoles); + const canSeeTermination = hasRole(terminationRoles); + const canSeeFnF = hasRole(fnfRoles); const offboardingSubmenu = [ canSeeResignation ? { id: 'resignation', label: 'Resignation' } : null, canSeeTermination ? { id: 'termination', label: 'Termination' } : null, @@ -51,16 +53,16 @@ export function Sidebar({ onLogout }: SidebarProps) { ].filter(Boolean) as { id: string; label: string }[]; // Finance role has only specific menu items - const menuItems = currentRole === 'Finance' || currentRole === 'Finance Admin' ? [ + const menuItems = hasRole(['Finance', 'Finance Admin']) ? [ { id: 'dashboard', label: 'Dashboard', icon: LayoutDashboard }, { id: 'finance-onboarding', label: 'Onboarding', icon: FileText }, { id: 'finance-fnf', label: 'F&F', icon: UserMinus }, - ] : currentRole === 'Dealer' ? [ + ] : hasRole(['Dealer']) ? [ { id: 'dashboard', label: 'Dashboard', icon: LayoutDashboard }, { id: 'dealer-resignation', label: 'My Resignations', icon: UserMinus }, { id: 'dealer-constitutional', label: 'Constitutional Change', icon: RefreshCcw }, { id: 'dealer-relocation', label: 'Relocation Requests', icon: MapPin }, - ] : currentRole === 'FDD' ? [ + ] : hasRole(['FDD']) ? [ { id: 'fdd-dashboard', label: 'FDD Dashboard', icon: LayoutDashboard }, ] : [ { id: 'dashboard', label: 'Dashboard', icon: LayoutDashboard }, @@ -78,12 +80,12 @@ export function Sidebar({ onLogout }: SidebarProps) { ]; // Add All Applications for DD role (before Dealership Requests) - if (currentRole === 'DD') { + if (hasRole(['DD'])) { menuItems.splice(1, 0, { id: 'all-applications', label: 'All Applications', icon: Inbox }); } // Add All Requests for DD Lead role (before Dealership Requests) - if (currentRole === 'DD Lead' || currentRole === 'Super Admin') { + if (hasRole(['DD Lead', 'Super Admin'])) { menuItems.splice(1, 0, { id: 'all-requests', label: 'All Requests', @@ -98,11 +100,11 @@ export function Sidebar({ onLogout }: SidebarProps) { } // Add Master for Super Admin, DD Admin, and DD Lead - if (currentRole === 'Super Admin' || currentRole === 'DD Admin' || currentRole === 'DD Lead') { + if (hasRole(['Super Admin', 'DD Admin', 'DD Lead'])) { menuItems.push({ id: 'master', label: 'Master', icon: Settings }); } - if (currentRole === 'Super Admin') { + if (hasRole(['Super Admin'])) { menuItems.push({ id: 'users', label: 'User Management', icon: Users }); menuItems.push({ id: 'questionnaires', label: 'Questionnaire Templates', icon: ClipboardList }); } diff --git a/src/lib/mock-data.ts b/src/lib/mock-data.ts index 6b67012..c8e5d6a 100644 --- a/src/lib/mock-data.ts +++ b/src/lib/mock-data.ts @@ -220,55 +220,106 @@ export interface QuestionnaireResponse { // Mock test users for different roles export const mockUsers: User[] = [ - { - id: '5', - name: 'Meera Iyer', - email: 'ddlead@royalenfield.com', + { + id: '15', + name: 'Super Admin', + email: 'admin@royalenfield.com', password: 'Admin@123', - role: 'DD Lead', + role: 'Super Admin', }, { id: '13', - name: 'Rahul Verma', + name: 'piyush', + email: 'piyush@royalenfield.com', + password: 'Admin@123', + role: 'DD-ZM', + }, + { + id: '14', + name: 'manish', + email: 'manish@royalenfield.com', + password: 'Admin@123', + role: 'RBM', + }, + { + id: '14', + name: 'manav', + email: 'manav@royalenfield.com', + password: 'Admin@123', + role: 'ZBH', + }, + + { + id: '5', + name: 'Jaya', + email: 'jaya@royalenfield.com', + password: 'Admin@123', + role: 'DD Lead', + }, + + { + id: '14', + name: 'ganesh', + email: 'ganesh@royalenfield.com', + password: 'Admin@123', + role: 'DD Head', + }, + { + id: '16', + name: 'Yashwin', + email: 'yashwin@royalenfield.com', + password: 'Admin@123', + role: 'NBH', + }, + { + id: '15', + name: 'FDD Team', + email: 'fdd@royalenfield.com', + password: 'Admin@123', + role: 'FDD', + }, + { + id: '13', + name: 'Finance Admin', email: 'finance@royalenfield.com', password: 'Admin@123', role: 'Finance', }, { - id: '14', - name: 'Amit Sharma', - email: 'dealer@royalenfield.com', + id: '13', + name: 'abhishek', + email: 'abhishek@royalenfield.com', password: 'Admin@123', - role: 'Dealer', - }, - { - id: '15', - name: 'Laxman H', - email: 'admin@royalenfield.com', - password: 'Admin@123', - role: 'DD Lead', - }, - { - id: '16', - name: 'Yashwin', - email: 'yashwin@gmail.com', - password: 'Admin@123', - role: 'ZBH', - }, - { - id: '17', - name: 'Kenil', - email: 'kenil@gmail.com', - password: 'Admin@123', - role: 'DD Lead', + role: 'ASM', }, { id: '18', name: 'Lince', - email: 'lince@gmail.com', + email: 'lince@royalenfield.com', password: 'Admin@123', role: 'DD Admin', }, + { + id: '18', + name: 'Legal Admin', + email: 'legal@royalenfield.com', + password: 'Admin@123', + role: 'Legal Admin', + }, + { + id: '18', + name: 'CEO', + email: 'ceo@royalenfield.com', + password: 'Admin@123', + role: 'CEO', + }, + { + id: '18', + name: 'CCO', + email: 'cco@royalenfield.com', + password: 'Admin@123', + role: 'CCO', + }, ]; // Mock current user (default)