From c37ca50d4cf29be1d953ae80c520373816cff219 Mon Sep 17 00:00:00 2001 From: laxman h Date: Mon, 6 Apr 2026 19:12:27 +0530 Subject: [PATCH] delaer onboarding end to end fleo checked and also made some new changes in the progress trck now after the LOI approvl finance and securaty details kept separate for approval also LOI issue is differnt approval --- src/App.tsx | 6 +- .../applications/AllApplicationsPage.tsx | 19 +- .../applications/ApplicationCard.tsx | 5 +- .../applications/ApplicationDetails.tsx | 433 +++++++++--- .../applications/ApplicationsPage.tsx | 13 +- .../applications/FDDApplicationDetails.tsx | 398 +++++++---- .../applications/FinanceFddDetailPage.tsx | 636 ++++++++++++++++++ .../applications/FinanceOnboardingPage.tsx | 365 +++++----- .../FinancePaymentDetailsPage.tsx | 5 +- .../ProspectiveApplicationDetails.tsx | 3 +- src/components/dashboard/FinanceDashboard.tsx | 114 ++-- src/lib/mock-data.ts | 1 + 12 files changed, 1517 insertions(+), 481 deletions(-) create mode 100644 src/components/applications/FinanceFddDetailPage.tsx diff --git a/src/App.tsx b/src/App.tsx index b45e4d0..bbe6486 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -30,6 +30,7 @@ import { FnFDetails } from './components/applications/FnFDetails'; import { FinanceOnboardingPage } from './components/applications/FinanceOnboardingPage'; import { FinanceFnFPage } from './components/applications/FinanceFnFPage'; import { FinancePaymentDetailsPage } from './components/applications/FinancePaymentDetailsPage'; +import { FinanceFddDetailPage } from './components/applications/FinanceFddDetailPage'; import { FinanceFnFDetailsPage } from './components/applications/FinanceFnFDetailsPage'; import { MasterPage } from './components/applications/MasterPage'; import { UserManagementPage } from './components/admin/UserManagementPage'; @@ -205,7 +206,7 @@ export default function App() { {/* Dashboards */} navigate(`/${path}`)} onViewPaymentDetails={(id) => navigate(`/finance-onboarding/${id}`)} onViewFnFDetails={(id) => navigate(`/finance-fnf/${id}`)} /> : + navigate(`/${path}`)} onViewPaymentDetails={(id) => navigate(`/finance-onboarding/${id}`)} onViewAuditDetails={(id) => navigate(`/finance-audit/${id}`)} onViewFnFDetails={(id) => navigate(`/finance-fnf/${id}`)} /> : currentUser?.role === 'Dealer' ? navigate(`/${path}`)} /> : navigate(`/${path}`)} /> @@ -257,8 +258,9 @@ export default function App() { navigate(`/fnf/${id}`)} />} /> navigate('/fnf')} currentUser={currentUser} />} /> - navigate(`/finance-onboarding/${id}`)} />} /> + navigate(`/finance-onboarding/${id}`)} onViewAuditDetails={(id) => navigate(`/finance-audit/${id}`)} />} /> navigate('/finance-onboarding')} />} /> + navigate('/finance-onboarding')} />} /> navigate(`/finance-fnf/${id}`)} />} /> navigate('/finance-fnf')} />} /> diff --git a/src/components/applications/AllApplicationsPage.tsx b/src/components/applications/AllApplicationsPage.tsx index b1f28bb..695a147 100644 --- a/src/components/applications/AllApplicationsPage.tsx +++ b/src/components/applications/AllApplicationsPage.tsx @@ -12,7 +12,6 @@ import { } from '../ui/select'; import { Search, - Filter, Download, Grid3x3, List, @@ -35,6 +34,7 @@ import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } f import { Label } from '../ui/label'; import { Textarea } from '../ui/textarea'; import { toast } from 'sonner'; +import { formatDateTime } from '../ui/utils'; interface AllApplicationsPageProps { onViewDetails: (id: string) => void; @@ -130,14 +130,17 @@ export function AllApplicationsPage({ onViewDetails, initialFilter = 'all' }: Al 'Questionnaire Pending': 'bg-yellow-100 text-yellow-800', 'Questionnaire Completed': 'bg-cyan-100 text-cyan-800', 'Shortlisted': 'bg-purple-100 text-purple-800', - 'Level 1 Pending': 'bg-orange-100 text-orange-800', + 'Level 1 Interview Pending': 'bg-orange-100 text-orange-800', 'Level 1 Approved': 'bg-green-100 text-green-800', - 'Level 2 Pending': 'bg-orange-100 text-orange-800', + 'Level 2 Interview Pending': 'bg-orange-100 text-orange-800', 'Level 2 Approved': 'bg-green-100 text-green-800', 'Level 2 Recommended': 'bg-teal-100 text-teal-800', - 'Level 3 Pending': 'bg-orange-100 text-orange-800', + 'Level 3 Interview Pending': 'bg-orange-100 text-orange-800', + 'In Review': 'bg-slate-100 text-slate-800', + 'Level 3 Approved': 'bg-green-100 text-green-800', 'FDD Verification': 'bg-indigo-100 text-indigo-800', 'Payment Pending': 'bg-amber-100 text-amber-800', + 'LOI In Progress': 'bg-sky-100 text-sky-800', 'LOI Issued': 'bg-sky-100 text-sky-800', 'Dealer Code Generation': 'bg-purple-100 text-purple-800', 'Architecture Team Assigned': 'bg-blue-100 text-blue-800', @@ -149,15 +152,19 @@ export function AllApplicationsPage({ onViewDetails, initialFilter = 'all' }: Al 'Statutory Check': 'bg-emerald-100 text-emerald-800', 'Statutory Partnership': 'bg-emerald-100 text-emerald-800', 'Statutory Firm Reg': 'bg-emerald-100 text-emerald-800', + 'Statutory Rental': 'bg-emerald-100 text-emerald-800', 'Statutory Virtual Code': 'bg-emerald-100 text-emerald-800', 'Statutory Domain': 'bg-emerald-100 text-emerald-800', 'Statutory MSD': 'bg-emerald-100 text-emerald-800', 'Statutory LOI Ack': 'bg-emerald-100 text-emerald-800', 'EOR In Progress': 'bg-violet-100 text-violet-800', + 'EOR Complete': 'bg-violet-100 text-violet-800', 'LOA Pending': 'bg-pink-100 text-pink-800', + 'Inauguration': 'bg-amber-100 text-amber-800', 'Approved': 'bg-green-100 text-green-800', 'Rejected': 'bg-red-100 text-red-800', - 'Disqualified': 'bg-gray-100 text-gray-800' + 'Disqualified': 'bg-gray-100 text-gray-800', + 'Onboarded': 'bg-emerald-100 text-emerald-800', }; return colors[status] || 'bg-gray-100 text-gray-800'; }; @@ -375,7 +382,7 @@ export function AllApplicationsPage({ onViewDetails, initialFilter = 'all' }: Al - {app.submissionDate} + {formatDateTime(app.submissionDate)} ))} diff --git a/src/components/applications/ApplicationCard.tsx b/src/components/applications/ApplicationCard.tsx index 410167b..29b1f92 100644 --- a/src/components/applications/ApplicationCard.tsx +++ b/src/components/applications/ApplicationCard.tsx @@ -3,6 +3,7 @@ import { Button } from '../ui/button'; import { Progress } from '../ui/progress'; import { Application } from '../../lib/mock-data'; import { MapPin, Phone, Mail, Award, Calendar, Building } from 'lucide-react'; +import { formatDateTime } from '../ui/utils'; interface ApplicationCardProps { application: Application; @@ -106,7 +107,7 @@ export function ApplicationCard({ application, onViewDetails }: ApplicationCardP
- Submitted: {new Date(application.submissionDate).toLocaleDateString()} + Submitted: {formatDateTime(application.submissionDate)}
@@ -123,7 +124,7 @@ export function ApplicationCard({ application, onViewDetails }: ApplicationCardP {application.deadline && application.status === 'Questionnaire Pending' && (

- Deadline: {new Date(application.deadline).toLocaleDateString()} + Deadline: {formatDateTime(application.deadline)}

)} diff --git a/src/components/applications/ApplicationDetails.tsx b/src/components/applications/ApplicationDetails.tsx index b26aff3..3da4ceb 100644 --- a/src/components/applications/ApplicationDetails.tsx +++ b/src/components/applications/ApplicationDetails.tsx @@ -1,5 +1,5 @@ import { useState, useEffect } from 'react'; -import { useParams, useNavigate } from 'react-router-dom'; +import { useParams, useNavigate, useLocation } from 'react-router-dom'; import { toast } from 'sonner'; import { Application, ApplicationStatus } from '../../lib/mock-data'; import { onboardingService } from '../../services/onboarding.service'; @@ -8,7 +8,7 @@ import { eorService } from '../../services/eor.service'; import QuestionnaireResponseView from './QuestionnaireResponseView'; import { useSelector } from 'react-redux'; import { RootState } from '../../store'; -import { cn } from '@/components/ui/utils'; +import { cn, formatDateTime } from '@/components/ui/utils'; import { DocumentPreviewModal } from '../ui/DocumentPreviewModal'; import { Button } from '../ui/button'; @@ -263,8 +263,8 @@ export function ApplicationDetails() { // Helper to find stage date const getStageDate = (stageName: string) => { const stage = data.progressTracking?.find((p: any) => p.stageName === stageName); - return stage?.stageCompletedAt ? new Date(stage.stageCompletedAt).toISOString().split('T')[0] : - stage?.stageStartedAt ? new Date(stage.stageStartedAt).toISOString().split('T')[0] : undefined; + return stage?.stageCompletedAt ? new Date(stage.stageCompletedAt).toISOString() : + stage?.stageStartedAt ? new Date(stage.stageStartedAt).toISOString() : undefined; }; // Map backend data to frontend Application interface @@ -301,7 +301,7 @@ export function ApplicationDetails() { ownRoyalEnfield: data.ownRoyalEnfield, address: data.address, // Map timeline dates from progressTracking - submissionDate: data.createdAt ? new Date(data.createdAt).toISOString().split('T')[0] : '', + submissionDate: data.createdAt ? new Date(data.createdAt).toISOString() : '', questionnaireDate: getStageDate('Questionnaire'), shortlistDate: getStageDate('Shortlist'), level1InterviewDate: getStageDate('1st Level Interview'), @@ -318,7 +318,7 @@ export function ApplicationDetails() { loaDate: getStageDate('LOA'), eorCompleteDate: getStageDate('EOR Complete'), inaugurationDate: getStageDate('Inauguration'), - onboardedDate: data.overallStatus === 'Onboarded' ? (data.updatedAt ? new Date(data.updatedAt).toISOString().split('T')[0] : new Date().toISOString().split('T')[0]) : undefined, + onboardedDate: data.overallStatus === 'Onboarded' ? (data.updatedAt ? new Date(data.updatedAt).toISOString() : new Date().toISOString()) : undefined, progressTracking: data.progressTracking || [], participants: data.participants || [], dealerCode: data.dealerCode, @@ -327,6 +327,7 @@ export function ApplicationDetails() { areaId: data.areaId, districtId: data.districtId, stageApprovals: data.stageApprovals || [], + fddAssignments: data.fddAssignments || [], }; setApplication(mappedApp); } catch (error) { @@ -387,7 +388,8 @@ export function ApplicationDetails() { } }, [applicationId]); - const [activeTab, setActiveTab] = useState('questionnaire'); + const routerLocation = useLocation(); + const [activeTab, setActiveTab] = useState(routerLocation.state?.activeTab || 'questionnaire'); const [showApproveModal, setShowApproveModal] = useState(false); const [showOnboardModal, setShowOnboardModal] = useState(false); const [isOnboarding, setIsOnboarding] = useState(false); @@ -433,6 +435,9 @@ export function ApplicationDetails() { const [architectureStatus, setArchitectureStatus] = useState('COMPLETED'); const [architectureRemarks, setArchitectureRemarks] = useState(''); const [isUpdatingArchitecture, setIsUpdatingArchitecture] = useState(false); + const [isAssigningParticipant, setIsAssigningParticipant] = useState(false); + const [isApproving, setIsApproving] = useState(false); + const [isRejecting, setIsRejecting] = useState(false); // KT Matrix State const [ktMatrixScores, setKtMatrixScores] = useState>({}); @@ -1229,73 +1234,74 @@ export function ApplicationDetails() { }; const handleApprove = async () => { - // Check if user has an active interview to approve - const activeInterview = interviews.find(i => - i.status !== 'Completed' && i.status !== 'Cancelled' && - i.participants?.some((p: any) => p.userId === currentUser?.id) - ); - - // Handle File Upload if exists - if (approvalFile && applicationId) { - try { - const formData = new FormData(); - formData.append('file', approvalFile); - formData.append('documentType', 'Approval Attachment'); - - // Determine stage based on active interview - let stageName = null; - if (activeInterview) { - if (activeInterview.level === 1 || activeInterview.level === '1') stageName = '1st Level Interview'; - else if (activeInterview.level === 2 || activeInterview.level === '2') stageName = '2nd Level Interview'; - else if (activeInterview.level === 3 || activeInterview.level === '3') stageName = '3rd Level Interview'; - } - - // Fallback for document stage if it's a general approval - if (!stageName) { - if (application.status === 'Shortlisted' || application.status === 'Level 1 Interview Pending') stageName = '1st Level Interview'; - else if (application.status === 'Level 1 Approved' || application.status === 'Level 2 Interview Pending') stageName = '2nd Level Interview'; - else if (application.status === 'Level 2 Approved' || application.status === 'Level 3 Interview Pending') stageName = '3rd Level Interview'; - } - - if (stageName) { - formData.append('stage', stageName); - } - - await onboardingService.uploadDocument(applicationId, formData); - toast.success('Document uploaded with approval'); - } catch (error) { - console.error('Failed to upload approval document', error); - toast.error('Failed to upload document'); - } - } - - if (activeInterview) { - try { - await onboardingService.updateInterviewDecision({ - interviewId: activeInterview.id, - decision: 'Approved', - remarks: approvalRemark - }); - toast.success('Interview approved successfully'); - setShowApproveModal(false); - setApprovalRemark(''); - setApprovalFile(null); // Reset file - fetchInterviews(); - // Refresh application to check if status updated - fetchApplication(); - return; - } catch (error) { - toast.error('Failed to approve interview'); - return; - } - } - - if (!approvalRemark.trim()) { - toast.warning('Please enter a remark'); - return; - } - try { + setIsApproving(true); + // Check if user has an active interview to approve + const activeInterview = interviews.find(i => + i.status !== 'Completed' && i.status !== 'Cancelled' && + i.participants?.some((p: any) => p.userId === currentUser?.id) + ); + + // Handle File Upload if exists + if (approvalFile && applicationId) { + try { + const formData = new FormData(); + formData.append('file', approvalFile); + formData.append('documentType', 'Approval Attachment'); + + // Determine stage based on active interview + let stageName = null; + if (activeInterview) { + if (activeInterview.level === 1 || activeInterview.level === '1') stageName = '1st Level Interview'; + else if (activeInterview.level === 2 || activeInterview.level === '2') stageName = '2nd Level Interview'; + else if (activeInterview.level === 3 || activeInterview.level === '3') stageName = '3rd Level Interview'; + } + + // Fallback for document stage if it's a general approval + if (!stageName) { + if (application.status === 'Shortlisted' || application.status === 'Level 1 Interview Pending') stageName = '1st Level Interview'; + else if (application.status === 'Level 1 Approved' || application.status === 'Level 2 Interview Pending') stageName = '2nd Level Interview'; + else if (application.status === 'Level 2 Approved' || application.status === 'Level 3 Interview Pending') stageName = '3rd Level Interview'; + } + + if (stageName) { + formData.append('stage', stageName); + } + + await onboardingService.uploadDocument(applicationId, formData); + toast.success('Document uploaded with approval'); + } catch (error) { + console.error('Failed to upload approval document', error); + toast.error('Failed to upload document'); + } + } + + if (activeInterview) { + try { + await onboardingService.updateInterviewDecision({ + interviewId: activeInterview.id, + decision: 'Approved', + remarks: approvalRemark + }); + toast.success('Interview approved successfully'); + setShowApproveModal(false); + setApprovalRemark(''); + setApprovalFile(null); // Reset file + fetchInterviews(); + // Refresh application to check if status updated + fetchApplication(); + return; + } catch (error) { + toast.error('Failed to approve interview'); + return; + } + } + + if (!approvalRemark.trim()) { + toast.warning('Please enter a remark'); + return; + } + // Application level approval - Robust State Machine let newStatus = application.status; @@ -1398,41 +1404,44 @@ export function ApplicationDetails() { } catch (error) { console.error('Approval error:', error); toast.error('Failed to process approval'); + } finally { + setIsApproving(false); } }; const handleReject = async () => { - // Check if user has an active interview to reject - const activeInterview = interviews.find(i => - i.status !== 'Completed' && i.status !== 'Cancelled' && - i.participants?.some((p: any) => p.userId === currentUser?.id) - ); + try { + setIsRejecting(true); + // Check if user has an active interview to reject + const activeInterview = interviews.find(i => + i.status !== 'Completed' && i.status !== 'Cancelled' && + i.participants?.some((p: any) => p.userId === currentUser?.id) + ); - if (activeInterview) { - try { - await onboardingService.updateInterviewDecision({ - interviewId: activeInterview.id, - decision: 'Rejected', - remarks: rejectionReason - }); - toast.success('Interview rejected'); - setShowRejectModal(false); - setRejectionReason(''); - fetchInterviews(); - fetchApplication(); - return; - } catch (error) { - toast.error('Failed to reject interview'); + if (activeInterview) { + try { + await onboardingService.updateInterviewDecision({ + interviewId: activeInterview.id, + decision: 'Rejected', + remarks: rejectionReason + }); + toast.success('Interview rejected'); + setShowRejectModal(false); + setRejectionReason(''); + fetchInterviews(); + fetchApplication(); + return; + } catch (error) { + toast.error('Failed to reject interview'); + return; + } + } + + if (!rejectionReason.trim()) { + toast.warning('Please enter a reason for rejection'); return; } - } - if (!rejectionReason.trim()) { - toast.warning('Please enter a reason for rejection'); - return; - } - - try { const policyManagedStages: { [key: string]: string } = { 'Level 1 Interview Pending': 'INTERVIEW_LEVEL_1', 'Level 2 Interview Pending': 'INTERVIEW_LEVEL_2', @@ -1466,6 +1475,8 @@ export function ApplicationDetails() { } catch (error) { console.error('Rejection error:', error); toast.error('Failed to process rejection'); + } finally { + setIsRejecting(false); } }; @@ -1518,11 +1529,12 @@ export function ApplicationDetails() { return; } try { + setIsAssigningParticipant(true); await onboardingService.addParticipant({ requestId: applicationId, requestType: 'application', userId: selectedUser, - participantType: 'contributor' + participantType: participantType || 'contributor' }); toast.success('User assigned successfully!'); // Refresh application data @@ -1531,6 +1543,8 @@ export function ApplicationDetails() { setShowAssignModal(false); } catch (error) { toast.error('Failed to assign user'); + } finally { + setIsAssigningParticipant(false); } }; @@ -1624,18 +1638,175 @@ export function ApplicationDetails() { 'LOA Pending', 'EOR Complete', 'Inauguration' ].includes(application.status); + const finalDepositVerified = getDeposit('FINAL')?.status === 'Verified'; + const isLoaLocked = application.status === 'LOA Pending' && !finalDepositVerified; + // Show Approve/Reject if: // 1. It's an interview and feedback is submitted AND no decision made yet // 2. OR it's an administrative stage and user is Admin AND hasn't made a decision yet const shouldShowApproveReject = - (!hasMadeDecisionForUser && hasSubmittedFeedbackForActive) || - (isAdmin && isAdministrativeStage && !hasMadeStageDecision); + !isLoaLocked && ( + (!hasMadeDecisionForUser && hasSubmittedFeedbackForActive) || + (isAdmin && isAdministrativeStage && !hasMadeStageDecision) + ); const shouldShowDecisionMessage = hasMadeDecisionForUser && (!isAdministrativeStage || hasMadeStageDecision); + const renderFddAuditContent = () => { + const assignments = application?.fddAssignments || []; + + if (assignments.length === 0) { + return ( +
+ +

No FDD Assignment

+

+ The Financial Due Diligence process has not been initiated for this application yet. +

+
+ ); + } + + return ( +
+
+

Financial Due Diligence Reports

+ + {assignments.length} Assignment(s) + +
+ + {assignments.map((assignment: any) => ( + + +
+
+
+ +
+
+

FDD Agency Audit

+

+ Agency ID: {assignment.assignedToAgency || 'Assigned'} • Status: {assignment.status} +

+
+
+ + {assignment.status} + +
+
+ + {(!assignment.reports || assignment.reports.length === 0) ? ( +
+ +

Waiting for internal or external agency to submit the final audit report...

+
+ ) : ( +
+ {assignment.reports.map((report: any) => ( +
+
+
+
+ +
+
+ {report.recommendation?.toUpperCase()} +
+
+ +
+ +
+ "{report.findings || 'No detail findings provided by the auditor.'}" +
+
+
+ +
+ + {report.reportDocument ? ( +
+
+
+ +
+
+

{report.reportDocument.fileName}

+

SUBMITTED {new Date(report.createdAt).toLocaleDateString()}

+
+
+
+ + +
+
+ ) : ( +
+ No audit report file attached +
+ )} + +
+ {report.verifiedAt ? ( +
+ + Audit Verified by Finance +
+ ) : ( +
+ + Pending Finance Review +
+ )} +
+
+
+
+ ))} +
+ )} + + + ))} +
+ ); + }; return (
@@ -1828,6 +1999,7 @@ export function ApplicationDetails() { Progress Documents Interviews + FDD Audit EOR Checklist Payments Audit Trail @@ -2045,7 +2217,7 @@ export function ApplicationDetails() { })()}

- {stage.status === 'completed' && stage.date && `Completed: ${new Date(stage.date).toLocaleDateString()}`} + {stage.status === 'completed' && stage.date && `Completed: ${formatDateTime(stage.date)}`} {stage.status === 'active' && 'In Progress'} {stage.status === 'pending' && 'Pending'}

@@ -2139,7 +2311,7 @@ export function ApplicationDetails() { ); })()}

- {branchStage.status === 'completed' && branchStage.date && `Done: ${new Date(branchStage.date).toLocaleDateString()}`} + {branchStage.status === 'completed' && branchStage.date && `Done: ${formatDateTime(branchStage.date)}`} {branchStage.status === 'active' && 'Evaluating'} {branchStage.status === 'pending' && 'Pending'}

@@ -2381,6 +2553,10 @@ export function ApplicationDetails() { )} + + {renderFddAuditContent()} + + {/* EOR Checklist Tab */}
@@ -2795,6 +2971,16 @@ export function ApplicationDetails() { {/* Show Approve/Reject block */} + {isLoaLocked && ( + + + Stage Locked + + Final Security Deposit (₹15L) must be verified by Finance before LOA Approval can proceed. + + + )} + {shouldShowApproveReject && ( <>
-
@@ -3065,14 +3262,23 @@ export function ApplicationDetails() { variant="outline" className="flex-1" onClick={() => setShowApproveModal(false)} + disabled={isApproving} > Cancel
@@ -3171,6 +3377,7 @@ export function ApplicationDetails() { variant="outline" className="flex-1" onClick={() => setShowRejectModal(false)} + disabled={isRejecting} > Cancel @@ -3178,8 +3385,16 @@ export function ApplicationDetails() { variant="destructive" className="flex-1" onClick={handleReject} + disabled={isRejecting} > - Confirm Rejection + {isRejecting ? ( + <> + + Rejecting... + + ) : ( + 'Confirm Rejection' + )} diff --git a/src/components/applications/ApplicationsPage.tsx b/src/components/applications/ApplicationsPage.tsx index 8848b14..1295e49 100644 --- a/src/components/applications/ApplicationsPage.tsx +++ b/src/components/applications/ApplicationsPage.tsx @@ -1,5 +1,6 @@ import { useState, useEffect } from 'react'; -import { mockApplications, locations, ApplicationStatus, Application } from '../../lib/mock-data'; +import { locations, ApplicationStatus, Application } from '../../lib/mock-data'; +import { formatDateTime } from '../ui/utils'; import { onboardingService } from '../../services/onboarding.service'; import { Button } from '../ui/button'; import { Input } from '../ui/input'; @@ -12,10 +13,8 @@ import { } from '../ui/select'; import { Search, - Filter, Download, - Mail, - Plus + Mail } from 'lucide-react'; import { Badge } from '../ui/badge'; import { Checkbox } from '../ui/checkbox'; @@ -51,12 +50,10 @@ export function ApplicationsPage({ onViewDetails, initialFilter }: ApplicationsP // Real Data Integration const [applications, setApplications] = useState([]); - const [loading, setLoading] = useState(true); useEffect(() => { const fetchApplications = async () => { try { - setLoading(true); const response = await onboardingService.getApplications(); // Check if response is array or wrapped in data property const applicationsData = response.data || (Array.isArray(response) ? response : []); @@ -100,7 +97,7 @@ export function ApplicationsPage({ onViewDetails, initialFilter }: ApplicationsP } catch (error) { console.error('Failed to fetch applications', error); } finally { - setLoading(false); + // setLoading(false); } }; fetchApplications(); @@ -346,7 +343,7 @@ export function ApplicationsPage({ onViewDetails, initialFilter }: ApplicationsP - {new Date(app.submissionDate).toLocaleDateString()} + {formatDateTime(app.submissionDate)}
- - - + {isNotReachedYet ? ( +
+ + Awaiting Previous Stages +
+ ) : !isCompleted ? ( + <> + + + + + ) : ( +
+ + Final Audit Report Submitted +
+ )}
@@ -261,55 +267,80 @@ export function FDDApplicationDetails() { - Financial Report Submission + {isCompleted ? 'Finalized Financial Reports' : isNotReachedYet ? 'Audit Workspace' : 'Financial Report Submission'} -
-
- -
-

Select and upload the due diligence report

-

PDF or JPG formats accepted (Max 10MB)

- -
- - -
- {uploading ? ( -
- - Uploading... -
- ) : ( - <> - -
- Browse & Upload -
- - )} + {isNotReachedYet && ( +
+
+ +
+

Stage Not Yet Active

+

This application is still being processed in previous documentation or interview stages. The FDD workspace will activate once the previous stages are approved.

+
+ Status: {application.status || 'Pending Review'}
-
+ )} + {isCompleted && ( +
+
+ +
+
+

Verification Stage Completed

+

The FDD report has been submitted and the case is now locked for further audits.

+
+
+ )} + {!isCompleted && !isNotReachedYet && ( +
+
+ +
+

Select and upload the due diligence report

+

PDF or JPG formats accepted (Max 10MB)

+ +
+ + +
+ {uploading ? ( +
+ + Uploading... +
+ ) : ( + <> + +
+ Browse & Upload +
+ + )} +
+
+
+ )} {/* List of Uploaded Documents */}
@@ -473,6 +504,133 @@ export function FDDApplicationDetails() { onClose={() => setIsPreviewOpen(false)} document={selectedPreviewDoc} /> + + {/* Finalize Confirmation Modal */} + + +
+
+
+ +
+
+
+ + Finalize Audit Report + + You are about to submit your final findings. This action will lock the report and move the application to the next stage. + + + +
+ +

+ Ensure all required financial documents are uploaded and verified before proceeding. +

+
+ + + + + +
+ +
+ + {/* Flag Non-Responsive Confirmation Modal */} + + +
+
+
+ +
+
+
+ + Flag Applicant + + Are you sure you want to flag this applicant as Non-Responsive? + + + +
+

+ "Applicant is non-responsive to FDD queries." +

+
+ + + + + +
+ +
); } diff --git a/src/components/applications/FinanceFddDetailPage.tsx b/src/components/applications/FinanceFddDetailPage.tsx new file mode 100644 index 0000000..341f721 --- /dev/null +++ b/src/components/applications/FinanceFddDetailPage.tsx @@ -0,0 +1,636 @@ +import { useState, useEffect } from 'react'; +import { Card, CardContent, CardHeader, CardTitle } from '../ui/card'; +import { Badge } from '../ui/badge'; +import { Button } from '../ui/button'; +import { Label } from '../ui/label'; +import { Textarea } from '../ui/textarea'; +import { useSelector } from 'react-redux'; +import { RootState } from '../../store'; +import { + Tabs, + TabsContent, + TabsList, + TabsTrigger +} from '../ui/tabs'; +import { Avatar, AvatarFallback } from '../ui/avatar'; +import { + ArrowLeft, + ShieldCheck, + CheckCircle, + XCircle, + FileText, + User, + Clock, + Download, + Eye, + AlertCircle, + MessageSquare, + FileCheck, + RotateCcw, + History, + Send +} from 'lucide-react'; +import { toast } from 'sonner'; +import { onboardingService } from '../../services/onboarding.service'; +import { worknoteService } from '../../services/worknote.service'; +import { DocumentPreviewModal } from '../ui/DocumentPreviewModal'; + +// Simple helper for class merging +const cn = (...classes: any[]) => classes.filter(Boolean).join(' '); + +interface FinanceFddDetailPageProps { + applicationId: string; + onBack: () => void; +} + +export function FinanceFddDetailPage({ applicationId, onBack }: FinanceFddDetailPageProps) { + const { user: currentUser } = useSelector((state: RootState) => state.auth); + const [application, setApplication] = useState(null); + const [loading, setLoading] = useState(true); + const [isSubmitting, setIsSubmitting] = useState(false); + const [approvalRemark, setApprovalRemark] = useState(''); + const [newNote, setNewNote] = useState(''); + const [isNoteSubmitting, setIsNoteSubmitting] = useState(false); + const [showPreviewModal, setShowPreviewModal] = useState(false); + const [previewDoc, setPreviewDoc] = useState(null); + + useEffect(() => { + fetchData(); + }, [applicationId]); + + const fetchData = async () => { + try { + setLoading(true); + const appData = await onboardingService.getApplicationById(applicationId); + setApplication(appData); + } catch (error) { + console.error('Fetch error:', error); + toast.error('Failed to load application data'); + } finally { + setLoading(false); + } + }; + + const handleDecision = async (decision: 'Approved' | 'Rejected') => { + if (!approvalRemark.trim()) { + toast.warning('Please enter a remark or justification'); + return; + } + + try { + setIsSubmitting(true); + + // Map current status to next status for LOI stage + let nextStatus = 'LOI Issued'; // Default + if (application.status === 'LOI In Progress') { + nextStatus = 'LOI Issued'; + } + + const response = await onboardingService.submitStageDecision({ + applicationId: application.id, + stageCode: 'LOI_APPROVAL', + decision, + remarks: approvalRemark, + nextStatus + }); + + if (response.data?.statusUpdated) { + toast.success(response.message || `Application ${decision.toLowerCase()} successfully`); + } else { + toast.info(response.message || 'Decision recorded. Waiting for other mandatory approvers.'); + } + + setApprovalRemark(''); + await fetchData(); + } catch (error) { + console.error('Decision error:', error); + toast.error('Failed to process decision'); + } finally { + setIsSubmitting(false); + } + }; + + const handlePostNote = async () => { + if (!newNote.trim()) return; + + try { + setIsNoteSubmitting(true); + await worknoteService.addWorknote({ + requestId: application.id, + requestType: 'application', + noteText: newNote, + noteType: 'fdd_query' + }); + + setNewNote(''); + toast.success('Work note posted successfully'); + await fetchData(); + } catch (error) { + console.error('Add note error:', error); + toast.error('Failed to post work note'); + } finally { + setIsNoteSubmitting(false); + } + }; + + if (loading) { + return ( +
+
+
+ ); + } + + if (!application) { + return
Application not found
; + } + + const isFinance = currentUser?.role === 'Finance' || currentUser?.role === 'Finance Admin'; + const isReadOnly = !isFinance; + + const assignments = application.fddAssignments || []; + const workNotes = application.workNotes || []; + const hasMadeDecision = application.stageApprovals?.some( + (a: any) => a.stageCode === 'LOI_APPROVAL' && String(a.actorUserId) === String(currentUser?.id) + ); + + const MANDATORY_FINANCIAL_DOCS = [ + { type: 'Bank Statement', label: 'Bank Statements' }, + { type: 'Income Tax Returns (ITR)', label: 'ITR (Last 3 Years)' }, + { type: 'Credit Reports', label: 'CIBIL / Credit Reports' }, + { type: 'Property Documents', label: 'Property Documents' }, + { type: 'Business Valuation Report', label: 'Valuation Reports' } + ]; + + const getDocByTypeName = (typeName: string) => { + return application.uploadedDocuments?.find((d: any) => d.documentType === typeName); + }; + + return ( +
+ {/* Header */} +
+
+ +
+

FDD Audit Detail

+

Review findings and provide finance sign-off for LOI stage

+
+
+
+ + APP ID: {application.applicationId || application.id} + + + {application.status} + +
+
+ + + + + Audit Review + + + Work Notes + {workNotes.length > 0 && {workNotes.length}} + + + Audit Trail + + + + +
+ {/* Main Content Area */} +
+ + {/* Applicant Summary Card */} + + + + Applicant Summary + + + +
+
+ +

{application.name || application.applicantName}

+
+
+ +

{application.city || application.preferredLocation}, {application.state}

+
+
+ +
+ {application.email} + {application.phone} +
+
+
+ +

{application.constitutionType || 'N/A'}

+
+
+
+
+ + {/* Financial Document Checklist Card */} + + + +
Financial Artefacts Checklist
+ Mandatory for FDD Sign-off +
+
+ +
+ {MANDATORY_FINANCIAL_DOCS.map((docType) => { + const doc = getDocByTypeName(docType.type); + return ( +
+
+
+ {doc ? : } +
+
+

{docType.label}

+

+ {doc ? `Uploaded: ${new Date(doc.createdAt).toLocaleDateString()}` : 'Missing in Documentation'} +

+
+
+ {doc && ( +
+ +
+ )} +
+ ); + })} +
+
+
+ + {/* Audit Reports Section */} +
+
+

+ Audit Findings & Reports +

+ + {assignments.length} Reports Found + +
+ + {assignments.length === 0 ? ( +
+ +

No Audit Reports Available

+

The FDD team has not yet uploaded the audit reports for this application.

+
+ ) : ( +
+ {assignments.map((assignment: any) => ( + +
+
+
+
+ +
+
+

FDD Audit Assignment

+

Status: {assignment.status}

+
+
+ + {assignment.status} + +
+ + {!assignment.reports || assignment.reports.length === 0 ? ( +
+ +

Waiting for agency report submission...

+
+ ) : ( +
+ {assignment.reports.map((report: any) => ( +
+
+
+ +
+
+ {report.recommendation?.toUpperCase()} SIGNAL +
+
+ +
+ +

+ "{report.findings || 'No detail findings provided.'}" +

+
+
+ +
+ + {report.reportDocument ? ( +
+
+
+ +
+
+

{report.reportDocument.fileName}

+

SUBMITTED {new Date(report.createdAt).toLocaleDateString()}

+
+
+
+ + +
+
+ ) : ( +
+ No report file attached +
+ )} + +
+ {report.verifiedAt ? ( +
+ + Audited & Verified +
+ ) : ( +
+ + Pending Verification +
+ )} +
+
+
+ ))} +
+ )} +
+ + ))} +
+ )} +
+
+ + {/* Action Sidebar */} +
+ + + + Finance Action + + + +
+ +