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 + + + +
+ +