From 830f66b5f79072017e518d1e3f947b4607bcadcc Mon Sep 17 00:00:00 2001 From: laxman h Date: Thu, 2 Apr 2026 19:31:58 +0530 Subject: [PATCH] onboarding flow made stable end to end also fincance team verification implementation done relocation stated --- src/api/API.ts | 6 + .../applications/ApplicationDetails.tsx | 497 ++++++---- .../applications/FinanceOnboardingPage.tsx | 642 ++++--------- .../FinancePaymentDetailsPage.tsx | 575 +++++++----- src/components/applications/MasterPage.tsx | 12 +- .../MasterPage/SecurityDepositMaster.tsx | 156 ++++ .../applications/RelocationRequestDetails.tsx | 73 +- src/components/dashboard/FinanceDashboard.tsx | 855 +++++++----------- .../dashboard/ProspectiveDashboardPage.tsx | 12 +- .../dealer/DealerRelocationPage.tsx | 129 ++- src/components/ui/DocumentPreviewModal.tsx | 203 +++++ src/hooks/useMasterData.ts | 8 +- src/services/master.service.ts | 9 + src/services/onboarding.service.ts | 15 + 14 files changed, 1740 insertions(+), 1452 deletions(-) create mode 100644 src/components/applications/MasterPage/SecurityDepositMaster.tsx create mode 100644 src/components/ui/DocumentPreviewModal.tsx diff --git a/src/api/API.ts b/src/api/API.ts index 5d8be3d..ad176e6 100644 --- a/src/api/API.ts +++ b/src/api/API.ts @@ -48,6 +48,8 @@ export const API = { generateDealerCodes: (applicationId: string) => client.post(`/onboarding/applications/${applicationId}/generate-codes`), updateApplicationStatus: (id: string, data: any) => client.put(`/onboarding/applications/${id}/status`, data), retriggerEvaluators: (id: string) => client.post(`/onboarding/applications/${id}/retrigger-evaluators`), + getSecurityDeposit: (applicationId: string) => client.get(`/loa/security-deposit/${applicationId}`), + updateSecurityDeposit: (data: any) => client.post('/loa/security-deposit', data), // Documents uploadDocument: (id: string, data: any) => client.post(`/onboarding/applications/${id}/documents`, data, { @@ -158,6 +160,10 @@ export const API = { // SLA getSlaConfigs: () => client.get('/sla/configs'), + + // System Configs + getSystemConfigs: (params?: any) => client.get('/master/system-configs', params), + saveSystemConfig: (data: any) => client.post('/master/system-configs', data), }; export default API; diff --git a/src/components/applications/ApplicationDetails.tsx b/src/components/applications/ApplicationDetails.tsx index 11036aa..6124361 100644 --- a/src/components/applications/ApplicationDetails.tsx +++ b/src/components/applications/ApplicationDetails.tsx @@ -9,6 +9,7 @@ import QuestionnaireResponseView from './QuestionnaireResponseView'; import { useSelector } from 'react-redux'; import { RootState } from '../../store'; import { cn } from '@/components/ui/utils'; +import { DocumentPreviewModal } from '../ui/DocumentPreviewModal'; import { Button } from '../ui/button'; import { Badge } from '../ui/badge'; @@ -39,6 +40,7 @@ import { Zap, ShieldCheck, Eye, + Lock, } from 'lucide-react'; import { Progress } from '../ui/progress'; import { Textarea } from '../ui/textarea'; @@ -83,6 +85,8 @@ interface ProcessStage { evaluators?: string[]; documentsUploaded?: number; isParallel?: boolean; + isLocked?: boolean; + lockMessage?: string; branches?: { name: string; color: string; @@ -438,6 +442,29 @@ export function ApplicationDetails() { const [isSubmittingKT, setIsSubmittingKT] = useState(false); const [selectedInterviewForFeedback, setSelectedInterviewForFeedback] = useState(null); + // Payment Details State + const [deposits, setDeposits] = useState([]); + const [paymentConfigs, setPaymentConfigs] = useState({}); + + useEffect(() => { + if (applicationId) { + const fetchPaymentData = async () => { + try { + const [depositData, configData] = await Promise.all([ + onboardingService.getSecurityDeposit(applicationId), + onboardingService.getSystemConfigs({ category: 'SECURITY_DEPOSIT', format: 'map' }) + ]); + setDeposits(Array.isArray(depositData) ? depositData : [depositData].filter(Boolean)); + setPaymentConfigs(configData || {}); + } catch (error) { + console.error('Failed to fetch payment data', error); + } + }; + fetchPaymentData(); + } + }, [applicationId]); + + const getDeposit = (type: string) => deposits.find(d => d.depositType === type); const handleKTMatrixChange = (criterionName: string, score: number) => { setKtMatrixScores(prev => ({ @@ -749,8 +776,8 @@ export function ApplicationDetails() { // Auto-fill participants based on pre-assigned evaluators for this level const levelNum = parseInt(interviewType.replace('level', '')) || 1; const preAssigned = (application?.participants || []) - .filter((p: any) => - p.metadata?.interviewLevel === levelNum || + .filter((p: any) => + p.metadata?.interviewLevel === levelNum || p.metadata?.interviewLevel === String(levelNum) || p.metadata?.allAssignments?.includes(levelNum) || p.metadata?.allAssignments?.includes(String(levelNum)) @@ -909,9 +936,9 @@ export function ApplicationDetails() { date: application.level1InterviewDate, description: 'DD-ZM + RBM evaluation', evaluators: Array.from(new Set((application.participants || []) - .filter((p: any) => - p.metadata?.interviewLevel === 1 || - p.metadata?.interviewLevel === '1' || + .filter((p: any) => + p.metadata?.interviewLevel === 1 || + p.metadata?.interviewLevel === '1' || p.metadata?.allAssignments?.includes(1) || p.metadata?.allAssignments?.includes('1') ) @@ -926,9 +953,9 @@ export function ApplicationDetails() { date: application.level2InterviewDate, description: 'DD Lead + ZBH evaluation', evaluators: Array.from(new Set((application.participants || []) - .filter((p: any) => - p.metadata?.interviewLevel === 2 || - p.metadata?.interviewLevel === '2' || + .filter((p: any) => + p.metadata?.interviewLevel === 2 || + p.metadata?.interviewLevel === '2' || p.metadata?.allAssignments?.includes(2) || p.metadata?.allAssignments?.includes('2') ) @@ -943,9 +970,9 @@ export function ApplicationDetails() { date: application.level3InterviewDate, description: 'NBH + DD Head evaluation', evaluators: Array.from(new Set((application.participants || []) - .filter((p: any) => - p.metadata?.interviewLevel === 3 || - p.metadata?.interviewLevel === '3' || + .filter((p: any) => + p.metadata?.interviewLevel === 3 || + p.metadata?.interviewLevel === '3' || p.metadata?.allAssignments?.includes(3) || p.metadata?.allAssignments?.includes('3') ) @@ -968,8 +995,8 @@ export function ApplicationDetails() { date: application.loiApprovalDate, description: 'Letter of Intent approval', evaluators: Array.from(new Set((application.participants || []) - .filter((p: any) => - p.metadata?.stageCode === 'LOI_APPROVAL' || + .filter((p: any) => + p.metadata?.stageCode === 'LOI_APPROVAL' || p.metadata?.allAssignments?.includes('LOI_APPROVAL') ) .map((p: any) => `${p.user?.name} (${p.user?.role})`) @@ -1120,11 +1147,15 @@ export function ApplicationDetails() { id: 12, name: 'LOA', status: getStageStatus('LOA', () => ['EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Onboarded'].includes(application.status) ? 'completed' : application.status === 'LOA Pending' ? 'active' : 'pending'), + isLocked: application.status === 'LOA Pending' && + getDeposit('FINAL')?.status !== 'Verified' && + !documents.some(d => (d.documentType?.toLowerCase().includes('final') && d.documentType?.toLowerCase().includes('deposit')) && d.status === 'Approved'), + lockMessage: 'Final Security Deposit (₹15L) must be verified by Finance before LOA Approval.', date: application.loaDate, description: 'Letter of Authorization', evaluators: Array.from(new Set((application.participants || []) - .filter((p: any) => - p.metadata?.stageCode === 'LOA_APPROVAL' || + .filter((p: any) => + p.metadata?.stageCode === 'LOA_APPROVAL' || p.metadata?.allAssignments?.includes('LOA_APPROVAL') ) .map((p: any) => `${p.user?.name} (${p.user?.role})`) @@ -1581,15 +1612,15 @@ export function ApplicationDetails() { const currentStageCode = policyManagedStages[application.status]; const currentUserStageAction = application.stageApprovals?.find( - (a: any) => - a.stageCode === currentStageCode && + (a: any) => + a.stageCode === currentStageCode && String(a.actorUserId) === String(currentUser?.id) ); const hasMadeStageDecision = !!currentUserStageAction; - const hasMadeDecisionForUser = !!currentUserStageAction || - currentUserEvaluation?.decision === 'Approved' || + const hasMadeDecisionForUser = !!currentUserStageAction || + currentUserEvaluation?.decision === 'Approved' || currentUserEvaluation?.decision === 'Rejected' || ['Approved', 'Rejected', 'Selected'].includes(currentUserEvaluation?.recommendation || ''); @@ -1819,89 +1850,102 @@ export function ApplicationDetails() { -
- {(() => { - const getApproverStatus = (stageCode: string | number) => { - const stageParticipants = (application.participants || []).filter((p: any) => - p.metadata?.stageCode === stageCode || - p.metadata?.allAssignments?.includes(stageCode) || - (typeof stageCode === 'number' && (p.metadata?.interviewLevel === stageCode || p.metadata?.allAssignments?.includes(stageCode))) || - (typeof stageCode === 'string' && !isNaN(Number(stageCode)) && (p.metadata?.interviewLevel === Number(stageCode) || p.metadata?.allAssignments?.includes(Number(stageCode)))) +
+ {(() => { + const getApproverStatus = (stageCode: string | number) => { + const stageParticipants = (application.participants || []).filter((p: any) => + p.metadata?.stageCode === stageCode || + p.metadata?.allAssignments?.includes(stageCode) || + (typeof stageCode === 'number' && (p.metadata?.interviewLevel === stageCode || p.metadata?.allAssignments?.includes(stageCode))) || + (typeof stageCode === 'string' && !isNaN(Number(stageCode)) && (p.metadata?.interviewLevel === Number(stageCode) || p.metadata?.allAssignments?.includes(Number(stageCode)))) + ); + + return stageParticipants.map((p: any) => { + const saCode = typeof stageCode === 'number' ? `INTERVIEW_LEVEL_${stageCode}` : stageCode; + const approval = (application.stageApprovals || []).find((sa: any) => + sa.stageCode === saCode && + String(sa.actorUserId) === String(p.userId) ); - return stageParticipants.map((p: any) => { - const saCode = typeof stageCode === 'number' ? `INTERVIEW_LEVEL_${stageCode}` : stageCode; - const approval = (application.stageApprovals || []).find((sa: any) => - sa.stageCode === saCode && - String(sa.actorUserId) === String(p.userId) - ); - - return { - name: p.user?.name || 'Unknown', - role: p.user?.role || 'Reviewer', - status: approval ? (approval.decision === 'Approved' ? 'approved' : 'rejected') : 'pending' - }; - }); - }; - - const renderApprovers = (stageName: string) => { - const stageMapping: Record = { - '1st Level Interview': 1, - '2nd Level Interview': 2, - '3rd Level Interview': 3, - 'LOI Approval': 'LOI_APPROVAL', - 'LOA': 'LOA_APPROVAL' + return { + name: p.user?.name || 'Unknown', + role: p.user?.role || 'Reviewer', + status: approval ? (approval.decision === 'Approved' ? 'approved' : 'rejected') : 'pending' }; + }); + }; - const stageCode = stageMapping[stageName]; - if (!stageCode) return null; - - const approvers = getApproverStatus(stageCode); - if (approvers.length === 0) return null; - - return ( -
- {approvers.map((approver, i) => ( -
-
- {approver.name.split(' ').map(n => n[0]).join('').substring(0, 2).toUpperCase()} -
-
- {approver.name} - {approver.role} -
- - {/* Status Dot Overlay */} -
- - {/* Tooltip */} -
- {approver.role}: {approver.status.toUpperCase()} -
-
- ))} -
- ); + const renderApprovers = (stageName: string) => { + const stageMapping: Record = { + '1st Level Interview': 1, + '2nd Level Interview': 2, + '3rd Level Interview': 3, + 'LOI Approval': 'LOI_APPROVAL', + 'LOA': 'LOA_APPROVAL' }; - return processStages.map((stage, index) => ( + const stageCode = stageMapping[stageName]; + if (!stageCode) return null; + + const approvers = getApproverStatus(stageCode); + if (approvers.length === 0) return null; + + return ( +
+ {approvers.map((approver, i) => ( +
+
+ {approver.name.split(' ').map(n => n[0]).join('').substring(0, 2).toUpperCase()} +
+
+ {approver.name} + {approver.role} +
+ + {/* Status Dot Overlay */} +
+ + {/* Tooltip */} +
+ {approver.role}: {approver.status.toUpperCase()} +
+
+ ))} +
+ ); + }; + + return processStages.map((stage, index) => (
{stage.isParallel ? ( + ) : stage.isLocked ? ( +
+ +
+
+ + Stage Locked + + {stage.lockMessage} +
+
+
+
) : ( <> {stage.status === 'completed' ? ( @@ -1976,7 +2020,7 @@ export function ApplicationDetails() { } return null; })()} - + {(() => { const stageDocsCount = documents.filter(doc => doc.stage === stage.name || @@ -2478,33 +2522,173 @@ export function ApplicationDetails() { {/* Payments Tab */} - -

Payment Information

- -
-
-
- Advance Payment - Paid -
-

₹5,00,000

-

Receipt: RCP-2025-001.pdf

-
- -
-
- Final Payment - Pending -
-

₹15,00,000

-
- - {application.status === 'EOR In Progress' && ( - - )} + +
+

Security Deposits

+ + {deposits.length} Payment Record(s) +
+ +
+ {/* Initial Security Deposit */} + {(() => { + const deposit = getDeposit('INITIAL'); + const config = paymentConfigs.INITIAL_SECURITY_DEPOSIT; + const expectedAmount = config?.amount || 500000; + + return ( + + +
+
+
+ +
+ Advance Payment +
+ + {deposit?.status || 'Awaiting'} + +
+ +
+
+ Amount Received + ₹{Number(deposit?.amount || 0).toLocaleString()} +
+
+ Expected Total + ₹{expectedAmount.toLocaleString()} +
+ + {deposit?.paymentReference && ( +
+ Ref: {deposit.paymentReference} + {deposit.verifiedAt && {new Date(deposit.verifiedAt).toLocaleDateString()}} +
+ )} + + {deposit?.remarks && ( +
+ "{deposit.remarks}" +
+ )} +
+
+
+ ); + })()} + + {/* Final Security Deposit */} + {(() => { + const deposit = getDeposit('FINAL'); + const config = paymentConfigs.FINAL_SECURITY_DEPOSIT; + const expectedAmount = config?.amount || 1500000; + + return ( + + +
+
+
+ +
+ Final Security Deposit +
+ + {deposit?.status || 'Awaiting'} + +
+ +
+
+ Amount Received + ₹{Number(deposit?.amount || 0).toLocaleString()} +
+
+ Expected Total + ₹{expectedAmount.toLocaleString()} +
+ + {deposit?.paymentReference && ( +
+ Ref: {deposit.paymentReference} + {deposit.verifiedAt && {new Date(deposit.verifiedAt).toLocaleDateString()}} +
+ )} + + {deposit?.remarks && ( +
+ "{deposit.remarks}" +
+ )} +
+
+
+ ); + })()} +
+ + {/* Payment Proof Documents */} + + + + + Verification Documents + + + + {documents.filter((d: any) => d.documentType?.toLowerCase().includes('deposit')).length > 0 ? ( +
+ {documents.filter((d: any) => d.documentType?.toLowerCase().includes('deposit')).map((doc: any, index: number) => ( +
+
+
+ +
+
+

{doc.fileName || doc.name}

+

{doc.documentType}

+
+
+ +
+ ))} +
+ ) : ( +
+

No payment proofs uploaded yet.

+
+ )} +
+
{/* Audit Trail Tab */} @@ -3925,27 +4109,29 @@ export function ApplicationDetails() { 'Site Plan': ['Site Plan'], 'FDD': ['FDD Final Audit Report', 'FDD Agency Assignment Letter', 'Statutory Approval Certificate'], 'FDD Verification': ['FDD Final Audit Report', 'FDD Agency Assignment Letter', 'Statutory Approval Certificate'], - 'LOA': ['LOA Acceptance Copy'], - 'LOI Approval': ['LOI Agreement', 'LOI Acknowledgement'], + 'LOA': ['LOA Acceptance Copy', 'Final Security Deposit Receipt'], + 'LOI Approval': ['Initial Security Deposit Receipt'], + 'LOA Approval': ['Final Security Deposit Receipt'], + 'LOA Acknowledgement': ['Final Security Deposit Receipt'], 'Inauguration': ['Inauguration Photos', 'Inauguration Report'], '3rd Level Interview': ['AI Recommendation Summary', 'Interview Evaluation Sheet'], '2nd Level Interview': ['Interview Evaluation Sheet'], '1st Level Interview': ['Interview Evaluation Sheet'], - 'Shortlist': ['CIBIL Report', 'Proposed Site City Map'] + 'Shortlist': ['CIBIL Report', 'Proposed Site City Map', 'PAN Card', 'GST Certificate', 'Aadhaar'] }; const baseDocs = ['Other']; let filteredDocs: string[] = []; if (!selectedStage) { - // Show standard core docs if no stage select context - filteredDocs = ['PAN Card', 'GST Certificate', 'Aadhaar', 'Partnership Deed', 'LLP Agreement', 'Certificate of Incorporation', 'MOA', 'AOA', 'Bank Statement', 'Other']; - } else if (selectedStage.startsWith('EOR: ')) { - // Map EOR specific item directly - filteredDocs = [selectedStage.replace('EOR: ', ''), 'Other']; + filteredDocs = ['PAN Card', 'GST Certificate', 'Aadhaar', 'Partnership Deed', 'LLP Agreement', 'Certificate of Incorporation', 'MOA', 'AOA', 'Board Resolution', 'Initial Security Deposit Receipt (₹2L)', 'Final Security Deposit Receipt (₹15L)', 'Rental Agreement', 'Property Documents', 'Bank Statement', 'Cancelled Check', 'Other']; } else { - // Use mapping or fallback to current stage's specific docs - filteredDocs = [...(STAGE_DOCUMENT_MAP[selectedStage] || []), ...baseDocs]; + const stageName = selectedStage as string; + if (stageName.startsWith('EOR: ')) { + filteredDocs = [stageName.replace('EOR: ', ''), 'Other']; + } else { + filteredDocs = [...(STAGE_DOCUMENT_MAP[stageName] || []), ...baseDocs]; + } } return Array.from(new Set(filteredDocs)).map((doc, idx) => ( @@ -4002,66 +4188,11 @@ export function ApplicationDetails() { )} - {/* Preview Modal */} - - -
-
-
- -
-
- - {previewDoc?.fileName} - - - {previewDoc?.documentType || 'Document'} • {new Date(previewDoc?.createdAt).toLocaleDateString()} - -
-
-
- -
- {previewDoc && ( -
- {previewDoc.fileName?.match(/\.(jpg|jpeg|png|gif|webp)$/i) ? ( - {previewDoc.fileName} - ) : previewDoc.fileName?.match(/\.pdf$/i) ? ( -