diff --git a/src/App.tsx b/src/App.tsx index bbe6486..0578268 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -15,7 +15,6 @@ import { FinanceDashboard } from './components/dashboard/FinanceDashboard'; import { DealerDashboard } from './components/dashboard/DealerDashboard'; import { ProspectiveDashboardPage } from './components/dashboard/ProspectiveDashboardPage'; import { FDDDashboardPage } from './components/dashboard/FDDDashboardPage'; -import { FDDApplicationDetails } from './components/applications/FDDApplicationDetails'; import { ApplicationsPage } from './components/applications/ApplicationsPage'; import { AllApplicationsPage } from './components/applications/AllApplicationsPage'; import { OpportunityRequestsPage } from './components/applications/OpportunityRequestsPage'; @@ -30,7 +29,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'; @@ -229,7 +228,6 @@ export default function App() { {/* FDD Routes - Integrated into Layout */} } /> - } /> {/* Admin/Lead Routes */} navigate(`/applications/${id}`)} />} /> @@ -260,7 +258,7 @@ export default function App() { 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/api/API.ts b/src/api/API.ts index 7c6539e..5c24a21 100644 --- a/src/api/API.ts +++ b/src/api/API.ts @@ -38,6 +38,7 @@ export const API = { getApplications: () => client.get('/onboarding/applications'), shortlistApplications: (data: any) => client.post('/onboarding/applications/shortlist', data), getApplicationById: (id: string) => client.get(`/onboarding/applications/${id}`), + updateApplication: (id: string, data: any) => client.put(`/onboarding/applications/${id}`, data), getLatestQuestionnaire: () => client.get('/questionnaire/latest'), createQuestionnaireVersion: (data: any) => client.post('/questionnaire/version', data), submitQuestionnaireResponse: (data: any) => client.post('/questionnaire/response', data), @@ -56,6 +57,11 @@ export const API = { headers: { 'Content-Type': 'multipart/form-data' } }), getDocuments: (id: string) => client.get(`/onboarding/applications/${id}/documents`), + getDocumentConfigMetadata: () => client.get('/onboarding/document-configs/metadata'), + getDocumentConfigs: (params?: any) => client.get('/onboarding/document-configs', params), + createDocumentConfig: (data: any) => client.post('/onboarding/document-configs', data), + updateDocumentConfig: (id: string, data: any) => client.put(`/onboarding/document-configs/${id}`, data), + deleteDocumentConfig: (id: string) => client.delete(`/onboarding/document-configs/${id}`), // Public Questionnaire getPublicQuestionnaire: (appId: string) => axios.get(`http://localhost:5000/api/questionnaire/public/${appId}`), // Direct axios to bypass interceptors if client has auth diff --git a/src/components/applications/ApplicationDetails.tsx b/src/components/applications/ApplicationDetails.tsx index 3da4ceb..b726601 100644 --- a/src/components/applications/ApplicationDetails.tsx +++ b/src/components/applications/ApplicationDetails.tsx @@ -41,13 +41,20 @@ import { ShieldCheck, Eye, Lock, + AlertCircle, + RefreshCw, + Building2, + Pencil, + Check, + Loader2, + Info, + ShieldAlert, } from 'lucide-react'; import { Progress } from '../ui/progress'; import { Textarea } from '../ui/textarea'; import { Input } from '../ui/input'; import { Label } from '../ui/label'; import { Alert, AlertDescription, AlertTitle } from '../ui/alert'; -import { AlertCircle, RefreshCw, Check, Loader2 } from 'lucide-react'; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogTrigger } from '../ui/dialog'; import { ScrollArea } from '../ui/scroll-area'; import { @@ -245,7 +252,7 @@ const KT_MATRIX_CRITERIA = [ } ]; -export function ApplicationDetails() { +export const ApplicationDetails = () => { const { id } = useParams<{ id: string }>(); const navigate = useNavigate(); const { user: currentUser } = useSelector((state: RootState) => state.auth); @@ -261,10 +268,17 @@ export function ApplicationDetails() { const data = await onboardingService.getApplicationById(applicationId!); // Helper to find stage date - const getStageDate = (stageName: string) => { + const getStageDate = (stageName: string, fallbackStatus?: string) => { const stage = data.progressTracking?.find((p: any) => p.stageName === stageName); - return stage?.stageCompletedAt ? new Date(stage.stageCompletedAt).toISOString() : - stage?.stageStartedAt ? new Date(stage.stageStartedAt).toISOString() : undefined; + if (stage?.stageCompletedAt) return new Date(stage.stageCompletedAt).toISOString(); + if (stage?.stageStartedAt) return new Date(stage.stageStartedAt).toISOString(); + + // Fallback to Status History if progress track is missing + if (fallbackStatus) { + const history = (data.statusHistory || []).find((h: any) => h.newStatus === fallbackStatus); + if (history) return new Date(history.createdAt).toISOString(); + } + return undefined; }; // Map backend data to frontend Application interface @@ -302,22 +316,22 @@ export function ApplicationDetails() { address: data.address, // Map timeline dates from progressTracking submissionDate: data.createdAt ? new Date(data.createdAt).toISOString() : '', - questionnaireDate: getStageDate('Questionnaire'), - shortlistDate: getStageDate('Shortlist'), - level1InterviewDate: getStageDate('1st Level Interview'), - level2InterviewDate: getStageDate('2nd Level Interview'), - level3InterviewDate: getStageDate('3rd Level Interview'), - fddDate: getStageDate('FDD'), - loiApprovalDate: getStageDate('LOI Approval'), - securityDetailsDate: getStageDate('Security Details'), - loiIssueDate: getStageDate('LOI Issue'), - dealerCodeDate: getStageDate('Dealer Code Generation'), - architectureAssignedDate: getStageDate('Architecture Team Assigned'), - architectureDocumentDate: getStageDate('Architecture Document Upload'), - architectureCompletionDate: getStageDate('Architecture Team Completion'), - loaDate: getStageDate('LOA'), - eorCompleteDate: getStageDate('EOR Complete'), - inaugurationDate: getStageDate('Inauguration'), + questionnaireDate: getStageDate('Questionnaire', 'Questionnaire Completed') || getStageDate('Questionnaire', 'Questionnaire Pending'), + shortlistDate: getStageDate('Shortlist', 'Shortlisted'), + level1InterviewDate: getStageDate('1st Level Interview', 'Level 1 Approved'), + level2InterviewDate: getStageDate('2nd Level Interview', 'Level 2 Approved'), + level3InterviewDate: getStageDate('3rd Level Interview', 'Level 3 Approved'), + fddDate: getStageDate('FDD', 'FDD Verification'), + loiApprovalDate: getStageDate('LOI Approval', 'LOI In Progress'), + securityDetailsDate: getStageDate('Security Details', 'Security Details'), + loiIssueDate: getStageDate('LOI Issue', 'LOI Issued'), + dealerCodeDate: getStageDate('Dealer Code Generation', 'Dealer Code Generation'), + architectureAssignedDate: getStageDate('Architecture Team Assigned', 'Architecture Team Assigned'), + architectureDocumentDate: getStageDate('Architecture Document Upload', 'Architecture Document Upload'), + architectureCompletionDate: getStageDate('Architecture Team Completion', 'Architecture Team Completion'), + loaDate: getStageDate('LOA', 'LOA Pending'), + eorCompleteDate: getStageDate('EOR Complete', 'EOR Complete'), + inaugurationDate: getStageDate('Inauguration', 'Inauguration'), onboardedDate: data.overallStatus === 'Onboarded' ? (data.updatedAt ? new Date(data.updatedAt).toISOString() : new Date().toISOString()) : undefined, progressTracking: data.progressTracking || [], participants: data.participants || [], @@ -328,6 +342,7 @@ export function ApplicationDetails() { districtId: data.districtId, stageApprovals: data.stageApprovals || [], fddAssignments: data.fddAssignments || [], + constitutionType: data.constitutionType, }; setApplication(mappedApp); } catch (error) { @@ -369,6 +384,24 @@ export function ApplicationDetails() { const [auditLogs, setAuditLogs] = useState([]); const [auditLoading, setAuditLoading] = useState(false); + const [showFirmTypeModal, setShowFirmTypeModal] = useState(false); + const [updatingFirmType, setUpdatingFirmType] = useState(false); + const [tempFirmType, setTempFirmType] = useState(''); + + const handleUpdateFirmType = async () => { + try { + setUpdatingFirmType(true); + await onboardingService.updateApplication(applicationId!, { constitutionType: tempFirmType }); + toast.success('Firm type updated successfully'); + setShowFirmTypeModal(false); + fetchApplication(); + } catch (error) { + toast.error('Failed to update firm type'); + } finally { + setUpdatingFirmType(false); + } + }; + // Fetch audit logs when application loads useEffect(() => { if (applicationId) { @@ -436,1294 +469,1508 @@ export function ApplicationDetails() { const [architectureRemarks, setArchitectureRemarks] = useState(''); const [isUpdatingArchitecture, setIsUpdatingArchitecture] = useState(false); const [isAssigningParticipant, setIsAssigningParticipant] = useState(false); - const [isApproving, setIsApproving] = useState(false); - const [isRejecting, setIsRejecting] = useState(false); + const [documentConfigs, setDocumentConfigs] = useState([]); - // KT Matrix State - const [ktMatrixScores, setKtMatrixScores] = useState>({}); - const [ktMatrixRemarks, setKtMatrixRemarks] = useState(''); - const [isSubmittingKT, setIsSubmittingKT] = useState(false); - const [selectedInterviewForFeedback, setSelectedInterviewForFeedback] = useState(null); - - // Payment Details State - const [deposits, setDeposits] = useState([]); - const [paymentConfigs, setPaymentConfigs] = useState({}); + // Fetch document configurations + // Fetch document configurations 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 => ({ - ...prev, - [criterionName]: score - })); - }; - - const calculateKTScore = () => { - let totalWeightedScore = 0; - KT_MATRIX_CRITERIA.forEach(criterion => { - const score = ktMatrixScores[criterion.name] || 0; - const weightedScore = (score / criterion.maxScore) * criterion.weight; - totalWeightedScore += weightedScore; - }); - return totalWeightedScore.toFixed(2); - }; - - const handleSubmitKTMatrix = async () => { - if (Object.keys(ktMatrixScores).length < KT_MATRIX_CRITERIA.length) { - toast.warning('Please fill all fields in the KT Matrix'); - return; - } - - // Use the selected interview ID or fallback (though UI now forces selection) - const interviewId = selectedInterviewForFeedback?.id || interviews.find(i => i.status !== 'Completed')?.id || interviews[0]?.id; - - if (!interviewId) { - toast.error('No active interview found to link this KT Matrix to.'); - return; - } - - try { - setIsSubmittingKT(true); - - const criteriaScores = KT_MATRIX_CRITERIA.map(c => ({ - criterionName: c.name, - score: ktMatrixScores[c.name] || 0, - maxScore: c.maxScore, - weightage: c.weight - })); - - await onboardingService.submitKTMatrix({ - interviewId, - criteriaScores, - feedback: ktMatrixRemarks, - recommendation: null // No auto-decision - }); - - toast.success('KT Matrix submitted successfully'); - setShowKTMatrixModal(false); - - // Reset form - setKtMatrixScores({}); - setKtMatrixRemarks(''); - await fetchInterviews(); - await fetchApplication(); // Refresh application status and progress - } catch (error) { - toast.error('Failed to submit KT Matrix'); - } finally { - setIsSubmittingKT(false); - } - }; - - // Level 2 Feedback State - const [level2Feedback, setLevel2Feedback] = useState({ - strategicVision: '', - managementCapabilities: '', - operationalUnderstanding: '', - keyStrengths: '', - areasOfConcern: '', - additionalComments: '', - overallScore: '', - interviewerName: currentUser?.name || '', - interviewDate: new Date().toISOString().split('T')[0] - }); - const [isSubmittingLevel2, setIsSubmittingLevel2] = useState(false); - - const handleLevel2Change = (field: string, value: string) => { - setLevel2Feedback(prev => ({ ...prev, [field]: value })); - }; - - const handleSubmitLevel2Feedback = async () => { - if (!level2Feedback.overallScore) { - toast.warning('Please provide an overall score.'); - return; - } - - const interviewId = selectedInterviewForFeedback?.id || interviews.find(i => i.status !== 'Completed' && i.level === 2)?.id; - - if (!interviewId) { - toast.error('No active Level 2 interview found to link this feedback to.'); - return; - } - - try { - setIsSubmittingLevel2(true); - - const feedbackItems = [ - { type: 'Strategic Vision', comments: level2Feedback.strategicVision }, - { type: 'Management Capabilities', comments: level2Feedback.managementCapabilities }, - { type: 'Operational Understanding', comments: level2Feedback.operationalUnderstanding }, - { type: 'Key Strengths', comments: level2Feedback.keyStrengths }, - { type: 'Areas of Concern', comments: level2Feedback.areasOfConcern }, - { type: 'Additional Comments', comments: level2Feedback.additionalComments } - ].filter(item => item.comments.trim() !== ''); - - await onboardingService.submitLevel2Feedback({ - interviewId, - overallScore: Number(level2Feedback.overallScore), - feedbackItems - }); - - toast.success('Level 2 Feedback submitted successfully'); - setShowLevel2FeedbackModal(false); - - // Reset form - setLevel2Feedback({ - strategicVision: '', - managementCapabilities: '', - operationalUnderstanding: '', - keyStrengths: '', - areasOfConcern: '', - additionalComments: '', - overallScore: '', - interviewerName: currentUser?.name || '', - interviewDate: new Date().toISOString().split('T')[0] - }); - fetchInterviews(); // Refresh to show feedback - fetchApplication(); // Refresh application status - } catch (error) { - toast.error('Failed to submit Level 2 Feedback'); - } finally { - setIsSubmittingLevel2(false); - } - }; - - // Level 3 Feedback State - const [level3Feedback, setLevel3Feedback] = useState({ - strategicVision: '', - managementCapabilities: '', - operationalUnderstanding: '', - brandAlignment: '', - executiveSummary: '', - keyStrengths: '', - areasOfConcern: '', - additionalComments: '', - overallScore: '', - interviewerName: currentUser?.name || '', - interviewDate: new Date().toISOString().split('T')[0] - }); - const [isSubmittingLevel3, setIsSubmittingLevel3] = useState(false); - - const handleLevel3Change = (field: string, value: string) => { - setLevel3Feedback(prev => ({ ...prev, [field]: value })); - }; - - const handleSubmitLevel3Feedback = async () => { - if (!level3Feedback.overallScore) { - toast.warning('Please provide an overall score.'); - return; - } - - const interviewId = selectedInterviewForFeedback?.id || interviews.find(i => i.status !== 'Completed' && i.level === 3)?.id; - - if (!interviewId) { - toast.error('No active Level 3 interview found to link this feedback to.'); - return; - } - - try { - setIsSubmittingLevel3(true); - - // Level 3 might have slightly different fields or same structure. Assuming same for now. - const feedbackItems = [ - { type: 'Business Vision & Strategy', comments: level3Feedback.strategicVision }, - { type: 'Leadership & Decision Making', comments: level3Feedback.managementCapabilities }, - { type: 'Operational & Financial Readiness', comments: level3Feedback.operationalUnderstanding }, - { type: 'Brand Alignment', comments: level3Feedback.brandAlignment }, - { type: 'Key Strengths', comments: level3Feedback.keyStrengths }, - { type: 'Areas of Concern', comments: level3Feedback.areasOfConcern }, - { type: 'Executive Summary', comments: level3Feedback.executiveSummary }, - { type: 'Additional Comments', comments: level3Feedback.additionalComments } - ].filter(item => item.comments.trim() !== ''); - - // Reusing submitLevel2Feedback endpoint as it maps to InterviewFeedback table generic enough for Level 3 too - // Or we can create specific one if needed, but logic is identical so reusing service method - await onboardingService.submitLevel2Feedback({ - interviewId, - overallScore: Number(level3Feedback.overallScore), - feedbackItems - }); - - toast.success('Level 3 Feedback submitted successfully'); - setShowLevel3FeedbackModal(false); - - // Reset form - setLevel3Feedback({ - strategicVision: '', - managementCapabilities: '', - operationalUnderstanding: '', - brandAlignment: '', - executiveSummary: '', - keyStrengths: '', - areasOfConcern: '', - additionalComments: '', - overallScore: '', - interviewerName: currentUser?.name || '', - interviewDate: new Date().toISOString().split('T')[0] - }); - fetchInterviews(); - fetchApplication(); - } catch (error) { - toast.error('Failed to submit Level 3 Feedback'); - } finally { - setIsSubmittingLevel3(false); - } - }; - - // Feedback Details Modal State - const [selectedEvaluationForView, setSelectedEvaluationForView] = useState(null); - const [showFeedbackDetailsModal, setShowFeedbackDetailsModal] = useState(false); - - const fetchInterviews = async () => { - if (applicationId) { + const fetchConfigs = async () => { try { - const data = await onboardingService.getInterviews(applicationId); - setInterviews(data || []); + const res = await onboardingService.getDocumentConfigs({ limit: 1000 }); // Fetch all for lookup + const configs = res.data || (Array.isArray(res) ? res : []); + setDocumentConfigs(configs); } catch (error) { - console.error('Failed to fetch interviews', error); + console.error('Failed to fetch document configs:', error); } - } - }; - - useEffect(() => { - fetchInterviews(); - }, [applicationId]); - - const handleAddInterviewer = () => { - if (!selectedInterviewerId) return; - const usersList = Array.isArray(users) ? users : []; - const userToAdd = usersList.find(u => u.id === selectedInterviewerId); - if (userToAdd && !scheduledInterviewParticipants.find(p => p.id === userToAdd.id)) { - setScheduledInterviewParticipants([...scheduledInterviewParticipants, userToAdd]); - setSelectedInterviewerId(''); - } - }; - - const handleRemoveInterviewer = (userId: string) => { - setScheduledInterviewParticipants(scheduledInterviewParticipants.filter(p => p.id !== userId)); - }; - - useEffect(() => { - if ((activeTab === 'documents' || activeTab === 'progress') && applicationId) { - const fetchDocuments = async () => { - try { - const docs = await onboardingService.getDocuments(applicationId); - setDocuments(docs || []); - } catch (error) { - console.error('Failed to fetch documents', error); - } - }; - fetchDocuments(); - } - }, [activeTab, applicationId]); - - const fetchUsers = async (type?: string) => { - // Only fetch users if user has admin/DD/NBH roles to avoid 403s - if (!currentUser || !['DD Admin', 'Super Admin', 'DD Lead', 'DD Head', 'NBH'].includes(currentUser.role)) { - return; - } - try { - const params: any = {}; - if (type) { - const roleMapping: any = { - 'level1': ['DD-ZM', 'RBM'], - 'level2': ['DD Lead', 'ZBH'], - 'level3': ['NBH', 'DD Head'] - }; - params.roleCode = roleMapping[type]; - - // Include location from the application - if (application) { - params.locationId = application.districtId || application.areaId || application.regionId || application.zoneId; - } - } - - const response = await onboardingService.getUsers(params); - if (Array.isArray(response)) { - setUsers(response); - } else if (response && Array.isArray(response.data)) { - setUsers(response.data); - } else if (response && Array.isArray(response.users)) { - setUsers(response.users); - } else { - console.warn('Unexpected users response:', response); - setUsers([]); - } - } catch (error) { - console.error('Failed to fetch users', error); - setUsers([]); - } - }; - - useEffect(() => { - if (showScheduleModal && application) { - fetchUsers(interviewType); - - // 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 || - p.metadata?.interviewLevel === String(levelNum) || - p.metadata?.allAssignments?.includes(levelNum) || - p.metadata?.allAssignments?.includes(String(levelNum)) - ) - .map((p: any) => p.user) - .filter(Boolean); - - if (preAssigned.length > 0) { - // Ensure uniqueness by user ID - const uniquePreassigned: any[] = []; - const seenIds = new Set(); - preAssigned.forEach((u: any) => { - if (u.id && !seenIds.has(u.id)) { - seenIds.add(u.id); - uniquePreassigned.push(u); - } - }); - setScheduledInterviewParticipants(uniquePreassigned); - } else { - setScheduledInterviewParticipants([]); - } - } else if ((showAssignArchitectureModal || showAssignModal) && application) { - fetchUsers(); // Default fetch for other modals like Assign - } - }, [showScheduleModal, showAssignArchitectureModal, showAssignModal, interviewType, application?.participants]); - - const handleScheduleInterview = async () => { - if (!interviewDate) { - toast.warning('Please select date and time'); - return; - } - try { - setIsScheduling(true); - - const payload = { - applicationId: application?.id, - level: interviewType, - scheduledAt: interviewDate, - type: interviewMode === 'virtual' ? 'Virtual Interview' : 'Physical Interview', - location: interviewMode === 'virtual' ? meetingLink : location, - participants: scheduledInterviewParticipants.map(p => p.id) - }; - - await onboardingService.scheduleInterview(payload); - toast.success('Interview scheduled successfully'); - setShowScheduleModal(false); - // Refresh interviews - await fetchInterviews(); - await fetchApplication(); // Refresh application status - - } catch (error) { - toast.error('Failed to schedule interview'); - console.error(error); - } finally { - setIsScheduling(false); - } - }; - - const handleCancelInterview = async (interviewId: string) => { - if (!window.confirm('Are you sure you want to cancel this interview?')) return; - try { - await onboardingService.updateInterview(interviewId, { status: 'Cancelled' }); - toast.success('Interview cancelled successfully'); - fetchInterviews(); - } catch (error) { - toast.error('Failed to cancel interview'); - console.error(error); - } - }; - - const handleUpload = async () => { - if (!uploadFile || !uploadDocType) { - toast.warning('Please select a file and document type'); - return; - } - - try { - setIsUploading(true); - const formData = new FormData(); - formData.append('file', uploadFile); - formData.append('documentType', uploadDocType); - if (selectedStage) { - formData.append('stage', selectedStage); - } - - await onboardingService.uploadDocument(applicationId!, formData); - - toast.success('Document uploaded successfully'); - setShowUploadForm(false); - setUploadFile(null); - setUploadDocType(''); - - // Refresh documents - const docs = await onboardingService.getDocuments(applicationId); - setDocuments(docs || []); - - // Refresh EOR Data in case an EOR document was uploaded - fetchEorData(); - } catch (error) { - console.error('Upload failed', error); - toast.error('Failed to upload document'); - } finally { - setIsUploading(false); - } - }; - - if (loading) { - return
Loading application details...
; - } - - if (!application) { - return
Application not found
; - } - - const isDocumentUploaded = (docType: string) => { - return (documents || []).some(d => d.documentType === docType); - }; - - const getStageStatus = (stageName: string, fallbackLogic: () => ProcessStage['status']): ProcessStage['status'] => { - const backendStage = (application.progressTracking || []).find((ps: any) => ps.stageName === stageName); - if (backendStage && (backendStage.status === 'completed' || backendStage.status === 'active')) { - return backendStage.status as any; - } - return fallbackLogic(); - }; - - const processStages: ProcessStage[] = [ - { - id: 1, - name: 'Submitted', - status: 'completed', - date: application.submissionDate, - description: 'Application submitted', - documentsUploaded: 3 - }, - { - id: 2, - name: 'Questionnaire', - status: getStageStatus('Questionnaire', () => application.questionnaireMarks ? 'completed' : 'pending'), - date: application.questionnaireDate, - description: 'Questionnaire completed', - documentsUploaded: 0 - }, - { - id: 3, - name: 'Shortlist', - status: getStageStatus('Shortlist', () => ['Shortlisted', 'Level 1 Interview Pending', 'Level 1 Approved', 'Level 2 Interview Pending', 'Level 2 Approved', 'Level 2 Recommended', 'Level 3 Interview Pending', 'Level 3 Approved', 'FDD Verification', 'LOI In Progress', 'Payment Pending', 'LOI Issued', 'Statutory LOI Ack', 'Dealer Code Generation', 'Architecture Team Assigned', 'Architecture Document Upload', 'Architecture Team Completion', 'LOA Pending', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Rejected', 'Onboarded'].includes(application.status) ? 'completed' : 'pending'), - date: application.shortlistDate, - description: 'Application shortlisted by DD', - documentsUploaded: 2 - }, - { - id: 4, - name: '1st Level Interview', - status: getStageStatus('1st Level Interview', () => ['Level 1 Approved', 'Level 2 Interview Pending', 'Level 2 Approved', 'Level 2 Recommended', 'Level 3 Interview Pending', 'Level 3 Approved', 'FDD Verification', 'LOI In Progress', 'Payment Pending', 'LOI Issued', 'Statutory LOI Ack', 'Dealer Code Generation', 'Architecture Team Assigned', 'Architecture Document Upload', 'Architecture Team Completion', 'LOA Pending', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Onboarded'].includes(application.status) ? 'completed' : application.status === 'Level 1 Interview Pending' ? 'active' : application.status === 'Rejected' && application.progress >= 40 ? 'completed' : 'pending'), - 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' || - p.metadata?.allAssignments?.includes(1) || - p.metadata?.allAssignments?.includes('1') - ) - .map((p: any) => `${p.user?.name} (${p.user?.role})`) - )), - documentsUploaded: 1 - }, - { - id: 5, - name: '2nd Level Interview', - status: getStageStatus('2nd Level Interview', () => ['Level 2 Approved', 'Level 2 Recommended', 'Level 3 Interview Pending', 'Level 3 Approved', 'FDD Verification', 'LOI In Progress', 'Payment Pending', 'LOI Issued', 'Statutory LOI Ack', 'Dealer Code Generation', 'Architecture Team Assigned', 'Architecture Document Upload', 'Architecture Team Completion', 'LOA Pending', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Onboarded'].includes(application.status) ? 'completed' : application.status === 'Level 2 Interview Pending' ? 'active' : application.status === 'Rejected' && application.progress >= 55 ? 'completed' : 'pending'), - 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' || - p.metadata?.allAssignments?.includes(2) || - p.metadata?.allAssignments?.includes('2') - ) - .map((p: any) => `${p.user?.name} (${p.user?.role})`) - )), - documentsUploaded: 1 - }, - { - id: 6, - name: '3rd Level Interview', - status: getStageStatus('3rd Level Interview', () => ['Level 3 Approved', 'FDD Verification', 'LOI In Progress', 'Payment Pending', 'LOI Issued', 'Statutory LOI Ack', 'Dealer Code Generation', 'Architecture Team Assigned', 'Architecture Document Upload', 'Architecture Team Completion', 'LOA Pending', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Onboarded'].includes(application.status) ? 'completed' : application.status === 'Level 3 Interview Pending' ? 'active' : application.status === 'Rejected' && application.progress >= 70 ? 'completed' : 'pending'), - 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' || - p.metadata?.allAssignments?.includes(3) || - p.metadata?.allAssignments?.includes('3') - ) - .map((p: any) => `${p.user?.name} (${p.user?.role})`) - )), - documentsUploaded: 2 - }, - { - id: 7, - name: 'FDD', - status: getStageStatus('FDD', () => ['LOI In Progress', 'Payment Pending', 'LOI Issued', 'Statutory LOI Ack', 'Dealer Code Generation', 'Architecture Team Assigned', 'Architecture Document Upload', 'Architecture Team Completion', 'LOA Pending', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Onboarded'].includes(application.status) ? 'completed' : application.status === 'FDD Verification' ? 'active' : 'pending'), - date: application.fddDate, - description: 'Financial Due Diligence', - documentsUploaded: 5 - }, - { - id: 8, - name: 'LOI Approval', - status: getStageStatus('LOI Approval', () => ['Payment Pending', 'LOI Issued', 'Statutory LOI Ack', 'Dealer Code Generation', 'Architecture Team Assigned', 'Architecture Document Upload', 'Architecture Team Completion', 'LOA Pending', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Onboarded'].includes(application.status) ? 'completed' : application.status === 'LOI In Progress' ? 'active' : 'pending'), - date: application.loiApprovalDate, - description: 'Letter of Intent approval', - evaluators: Array.from(new Set((application.participants || []) - .filter((p: any) => - p.metadata?.stageCode === 'LOI_APPROVAL' || - p.metadata?.allAssignments?.includes('LOI_APPROVAL') - ) - .map((p: any) => `${p.user?.name} (${p.user?.role})`) - )), - documentsUploaded: 1 - }, - { - id: 9, - name: 'Security Details', - status: getStageStatus('Security Details', () => ['LOI Issued', 'Statutory LOI Ack', 'Dealer Code Generation', 'Architecture Team Assigned', 'Architecture Document Upload', 'Architecture Team Completion', 'LOA Pending', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Onboarded'].includes(application.status) ? 'completed' : application.status === 'Payment Pending' ? 'active' : 'pending'), - date: application.securityDetailsDate, - description: 'Security verification', - documentsUploaded: 3 - }, - { - id: 10, - name: 'LOI Issue', - status: getStageStatus('LOI Issue', () => ['Statutory LOI Ack', 'Dealer Code Generation', 'Architecture Team Assigned', 'Architecture Document Upload', 'Architecture Team Completion', 'LOA Pending', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Onboarded'].includes(application.status) ? 'completed' : application.status === 'LOI Issued' ? 'active' : 'pending'), - date: application.loiIssueDate, - description: 'Letter of Intent issued', - documentsUploaded: 1 - }, - { - id: 11, - name: 'Dealer Code Generation', - status: getStageStatus('Dealer Code Generation', () => (application.dealerCode || ['Architecture Team Assigned', 'Architecture Document Upload', 'Architecture Team Completion', 'Statutory GST', 'Statutory PAN', 'Statutory NODAL', 'Statutory NODAL', 'Statutory Check', 'Statutory Partnership', 'Statutory Firm Reg', 'Statutory Rental', 'Statutory Virtual Code', 'Statutory Domain', 'Statutory MSD', 'Statutory LOI Ack', 'LOA Pending', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Onboarded'].includes(application.status)) ? 'completed' : application.status === 'Dealer Code Generation' ? 'active' : 'pending'), - date: application.dealerCodeDate, - description: 'Dealer code generated and assigned', - documentsUploaded: 0, - isParallel: true, - branches: [ - { - name: 'Architectural Work', - color: 'blue', - stages: [ - { - id: '11a-1', - name: 'Assigned to Architecture Team', - status: application.architectureAssignedTo || ['Architecture Document Upload', 'Architecture Team Completion', 'Statutory GST', 'Statutory PAN', 'Statutory Nodal', 'Statutory Check', 'Statutory Partnership', 'Statutory Firm Reg', 'Statutory Rental', 'Statutory Virtual Code', 'Statutory Domain', 'Statutory MSD', 'Statutory LOI Ack', 'LOA Pending', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Onboarded'].includes(application.status) || application.architectureStatus === 'COMPLETED' || isDocumentUploaded('Architecture Assignment Document') ? 'completed' : application.status === 'Architecture Team Assigned' ? 'active' : 'pending', - date: application.architectureAssignedDate, - description: 'Assigned to architecture team for site planning', - documentsUploaded: 0 - }, - { - id: '11a-2', - name: 'Architectural Document Upload', - status: isDocumentUploaded('Architecture Blueprint') || isDocumentUploaded('Site Plan') || ['Architecture Team Completion', 'LOA Pending', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Onboarded'].includes(application.status) || application.architectureStatus === 'COMPLETED' ? 'completed' : (application.architectureAssignedTo || application.status === 'Architecture Document Upload' || application.architectureStatus === 'IN_PROGRESS') ? 'active' : 'pending', - date: application.architectureDocumentDate, - description: 'Architectural documents and blueprints uploaded', - documentsUploaded: (documents || []).filter(d => ['Architecture Blueprint', 'Site Plan'].includes(d.documentType)).length - }, - { - id: '11a-3', - name: 'Architecture Team Completion', - status: ['LOA Pending', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Onboarded'].includes(application.status) || application.architectureStatus === 'COMPLETED' || isDocumentUploaded('Architecture Completion Certificate') ? 'completed' : application.status === 'Architecture Team Completion' ? 'active' : 'pending', - date: application.architectureCompletionDate, - description: 'Architecture team work completed', - documentsUploaded: 0 - } - ] - }, - { - name: 'Statutory Documents', - color: 'green', - stages: [ - { - id: '11b-1', - name: 'GST', - status: isDocumentUploaded('GST Certificate') || ['Statutory PAN', 'Statutory Nodal', 'Statutory Check', 'Statutory Partnership', 'Statutory Firm Reg', 'Statutory Rental', 'Statutory Virtual Code', 'Statutory Domain', 'Statutory MSD', 'Statutory LOI Ack', 'LOA Pending', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Onboarded'].includes(application.status) ? 'completed' : application.status === 'Statutory GST' ? 'active' : 'pending', - description: 'GST certificate', - documentsUploaded: (documents || []).filter(d => d.documentType === 'GST Certificate').length - }, - { - id: '11b-2', - name: 'PAN', - status: isDocumentUploaded('PAN Card') || ['Statutory Nodal', 'Statutory Check', 'Statutory Partnership', 'Statutory Firm Reg', 'Statutory Rental', 'Statutory Virtual Code', 'Statutory Domain', 'Statutory MSD', 'Statutory LOI Ack', 'LOA Pending', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Onboarded'].includes(application.status) ? 'completed' : application.status === 'Statutory PAN' ? 'active' : 'pending', - description: 'PAN card', - documentsUploaded: (documents || []).filter(d => d.documentType === 'PAN Card').length - }, - { - id: '11b-3', - name: 'Nodal Agreement', - status: isDocumentUploaded('Nodal Agreement') || ['Statutory Check', 'Statutory Partnership', 'Statutory Firm Reg', 'Statutory Rental', 'Statutory Virtual Code', 'Statutory Domain', 'Statutory MSD', 'Statutory LOI Ack', 'LOA Pending', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Onboarded'].includes(application.status) ? 'completed' : application.status === 'Statutory Nodal' ? 'active' : 'pending', - description: 'Nodal agreement document', - documentsUploaded: (documents || []).filter(d => d.documentType === 'Nodal Agreement').length - }, - { - id: '11b-4', - name: 'Cancelled Check', - status: isDocumentUploaded('Cancelled Check') || ['Statutory Partnership', 'Statutory Firm Reg', 'Statutory Rental', 'Statutory Virtual Code', 'Statutory Domain', 'Statutory MSD', 'Statutory LOI Ack', 'LOA Pending', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Onboarded'].includes(application.status) ? 'completed' : application.status === 'Statutory Check' ? 'active' : 'pending', - description: 'Cancelled check copy', - documentsUploaded: (documents || []).filter(d => d.documentType === 'Cancelled Check').length - }, - { - id: '11b-5', - name: 'Partnership Deed/LLP/MOA/AOA/COI', - status: isDocumentUploaded('Partnership Deed') || isDocumentUploaded('LLP Agreement') || isDocumentUploaded('Certificate of Incorporation') || isDocumentUploaded('MOA') || isDocumentUploaded('AOA') || ['Statutory Firm Reg', 'Statutory Rental', 'Statutory Virtual Code', 'Statutory Domain', 'Statutory MSD', 'Statutory LOI Ack', 'LOA Pending', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Onboarded'].includes(application.status) ? 'completed' : application.status === 'Statutory Partnership' ? 'active' : 'pending', - description: 'Business entity documents', - documentsUploaded: (documents || []).filter(d => ['Partnership Deed', 'LLP Agreement', 'Certificate of Incorporation', 'MOA', 'AOA'].includes(d.documentType)).length - }, - { - id: '11b-6', - name: 'Firm Registration Certificate', - status: isDocumentUploaded('Firm Registration') || ['Statutory Rental', 'Statutory Virtual Code', 'Statutory Domain', 'Statutory MSD', 'Statutory LOI Ack', 'LOA Pending', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Onboarded'].includes(application.status) ? 'completed' : application.status === 'Statutory Firm Reg' ? 'active' : 'pending', - description: 'Firm registration certificate', - documentsUploaded: (documents || []).filter(d => d.documentType === 'Firm Registration').length - }, - { - id: '11b-7', - name: 'Rental agreement/ Lease agreement / Own/ Land agreement', - status: isDocumentUploaded('Rental Agreement') || isDocumentUploaded('Property Documents') || ['Statutory Virtual Code', 'Statutory Domain', 'Statutory MSD', 'Statutory LOI Ack', 'LOA Pending', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Onboarded'].includes(application.status) ? 'completed' : application.status === 'Statutory Rental' ? 'active' : 'pending', - description: 'Property agreement document', - documentsUploaded: (documents || []).filter(d => ['Rental Agreement', 'Property Documents'].includes(d.documentType)).length - }, - { - id: '11b-8', - name: 'Virtual Code', - status: isDocumentUploaded('Virtual Code Confirmation') || ['Statutory Domain', 'Statutory MSD', 'Statutory LOI Ack', 'LOA Pending', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Onboarded'].includes(application.status) ? 'completed' : application.status === 'Statutory Virtual Code' ? 'active' : 'pending', - description: 'Virtual code availability', - documentsUploaded: (documents || []).filter(d => d.documentType === 'Virtual Code Confirmation').length - }, - { - id: '11b-9', - name: 'Domain ID', - status: isDocumentUploaded('Domain ID Setup') || ['Statutory MSD', 'Statutory LOI Ack', 'LOA Pending', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Onboarded'].includes(application.status) ? 'completed' : application.status === 'Statutory Domain' ? 'active' : 'pending', - description: 'Domain ID setup', - documentsUploaded: (documents || []).filter(d => d.documentType === 'Domain ID Setup').length - }, - { - id: '11b-10', - name: 'MSD Configuration', - status: isDocumentUploaded('MSD Configuration') || ['Statutory LOI Ack', 'LOA Pending', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Onboarded'].includes(application.status) ? 'completed' : application.status === 'Statutory MSD' ? 'active' : 'pending', - description: 'Microsoft Dynamics configuration', - documentsUploaded: (documents || []).filter(d => d.documentType === 'MSD Configuration').length - }, - { - id: '11b-11', - name: 'LOI Acknowledgement Copy', - status: isDocumentUploaded('LOI Acknowledgement') || ['LOA Pending', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Onboarded'].includes(application.status) ? 'completed' : application.status === 'Statutory LOI Ack' ? 'active' : 'pending', - description: 'LOI acknowledgement copy', - documentsUploaded: (documents || []).filter(d => d.documentType === 'LOI Acknowledgement').length - } - ] - } - ] - }, - { - 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' || - p.metadata?.allAssignments?.includes('LOA_APPROVAL') - ) - .map((p: any) => `${p.user?.name} (${p.user?.role})`) - )), - documentsUploaded: 1 - }, - { - id: 13, - name: 'EOR Complete', - status: getStageStatus('EOR Complete', () => ['Inauguration', 'Approved', 'Onboarded'].includes(application.status) ? 'completed' : application.status === 'EOR Complete' ? 'active' : 'pending'), - date: application.eorCompleteDate, - description: 'Essential Operating Requirements completed', - documentsUploaded: 6 - }, - { - id: 14, - name: 'Inauguration', - status: getStageStatus('Inauguration', () => ['Approved', 'Onboarded'].includes(application.status) ? 'completed' : application.status === 'Inauguration' ? 'active' : 'pending'), - date: application.inaugurationDate, - description: 'Dealership inauguration ceremony', - documentsUploaded: 2 - }, - { - id: 15, - name: 'Dealership Active', - status: getStageStatus('Onboarded', () => application.status === 'Onboarded' ? 'completed' : 'pending'), - date: application.onboardedDate, - description: 'Dealer profile and login created' - } - ]; - - const eorChecklist = [ - { id: 1, item: 'Sales Standards', completed: false }, - { id: 2, item: 'Service & Spares', completed: false }, - { id: 3, item: 'DMS infra', completed: false }, - { id: 4, item: 'Manpower Training', completed: false }, - { id: 5, item: 'Trade certificate with test ride bikes registration', completed: false }, - { id: 6, item: 'GST certificate including Accessories & Apparels billing', completed: false }, - { id: 7, item: 'Inventory Funding', completed: false }, - { id: 8, item: 'Virtual code availability', completed: false }, - { id: 9, item: 'Vendor payments', completed: false }, - { id: 10, item: 'Details for website submission', completed: false }, - { id: 11, item: 'Infra Insurance both Showroom and Service center', completed: false }, - { id: 12, item: 'Auto ordering', completed: false } - ]; - - const flattenedStages: any[] = processStages.reduce((acc: any[], stage) => { - acc.push({ name: stage.name }); - if (stage.branches) { - stage.branches.forEach((branch: any) => { - branch.stages.forEach((subStage: any) => { - acc.push({ name: subStage.name, parentBranch: branch.name }); - }); - }); - } - if (stage.name === 'EOR In Progress' || stage.name === 'EOR Complete') { - (eorData?.items || eorChecklist).forEach((item: any) => { - acc.push({ name: `EOR: ${item.description || item.item}`, parentBranch: 'EOR' }); - }); - } - return acc; + }; + fetchConfigs(); }, []); - - - - - const getDocumentsForStage = (stageName: string) => { - return documents.filter(doc => - doc.stage === stageName || - (!doc.stage && doc.documentType?.toLowerCase().includes(stageName.toLowerCase().split(' ')[0])) - ); - }; - - const handleApprove = async () => { - 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'); + // Auto-select valid interview level based on application status when scheduling + useEffect(() => { + if (showScheduleModal && application) { + if (application.status === 'Shortlisted' || application.status === 'Questionnaire Completed') { + setInterviewType('level1'); + } else if (application.status === 'Level 1 Approved') { + setInterviewType('level2'); + } else if (application.status === 'Level 2 Approved' || application.status === 'Level 2 Recommended') { + setInterviewType('level3'); } } + }, [showScheduleModal, application?.status]); - if (activeInterview) { + const [isApproving, setIsApproving] = useState(false); + const [isRejecting, setIsRejecting] = useState(false); + + // KT Matrix State + const [ktMatrixScores, setKtMatrixScores] = useState>({}); + const [ktMatrixSelectedValues, setKtMatrixSelectedValues] = useState>({}); + const [ktMatrixRemarks, setKtMatrixRemarks] = useState(''); + const [isSubmittingKT, setIsSubmittingKT] = useState(false); + const [selectedInterviewForFeedback, setSelectedInterviewForFeedback] = useState(null); + + // FDD Partner States + const [showFddFinalizeModal, setShowFddFinalizeModal] = useState(false); + const [showFddFlagModal, setShowFddFlagModal] = useState(false); + const [fddAuditRecommendation, setFddAuditRecommendation] = useState('Green'); + const [fddAuditFindings, setFddAuditFindings] = useState(''); + const [isFinalizingFdd, setIsFinalizingFdd] = useState(false); + const [isFddFlagging, setIsFddFlagging] = useState(false); + + // 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, value: string, score: number) => { + setKtMatrixScores(prev => ({ + ...prev, + [criterionName]: score + })); + setKtMatrixSelectedValues(prev => ({ + ...prev, + [criterionName]: value + })); + }; + + const calculateKTScore = () => { + let totalWeightedScore = 0; + KT_MATRIX_CRITERIA.forEach(criterion => { + const score = ktMatrixScores[criterion.name] || 0; + const weightedScore = (score / criterion.maxScore) * criterion.weight; + totalWeightedScore += weightedScore; + }); + return totalWeightedScore.toFixed(2); + }; + + const handleSubmitKTMatrix = async () => { + if (Object.keys(ktMatrixScores).length < KT_MATRIX_CRITERIA.length) { + toast.warning('Please fill all fields in the KT Matrix'); + return; + } + + // Use the selected interview ID or fallback (though UI now forces selection) + const interviewId = selectedInterviewForFeedback?.id || interviews.find(i => i.status !== 'Completed')?.id || interviews[0]?.id; + + if (!interviewId) { + toast.error('No active interview found to link this KT Matrix to.'); + return; + } + + try { + setIsSubmittingKT(true); + + const criteriaScores = KT_MATRIX_CRITERIA.map(c => ({ + criterionName: c.name, + score: ktMatrixScores[c.name] || 0, + maxScore: c.maxScore, + weightage: c.weight + })); + + await onboardingService.submitKTMatrix({ + interviewId, + criteriaScores, + feedback: ktMatrixRemarks, + recommendation: null // No auto-decision + }); + + toast.success('KT Matrix submitted successfully'); + setShowKTMatrixModal(false); + + // Reset form + setKtMatrixScores({}); + setKtMatrixRemarks(''); + await fetchInterviews(); + await fetchApplication(); // Refresh application status and progress + } catch (error) { + toast.error('Failed to submit KT Matrix'); + } finally { + setIsSubmittingKT(false); + } + }; + + // Level 2 Feedback State + const [level2Feedback, setLevel2Feedback] = useState({ + strategicVision: '', + managementCapabilities: '', + operationalUnderstanding: '', + keyStrengths: '', + areasOfConcern: '', + additionalComments: '', + overallScore: '', + interviewerName: currentUser?.name || '', + interviewDate: new Date().toISOString().split('T')[0] + }); + const [isSubmittingLevel2, setIsSubmittingLevel2] = useState(false); + + const handleLevel2Change = (field: string, value: string) => { + setLevel2Feedback(prev => ({ ...prev, [field]: value })); + }; + + const handleSubmitLevel2Feedback = async () => { + if (!level2Feedback.overallScore) { + toast.warning('Please provide an overall score.'); + return; + } + + const interviewId = selectedInterviewForFeedback?.id || interviews.find(i => i.status !== 'Completed' && i.level === 2)?.id; + + if (!interviewId) { + toast.error('No active Level 2 interview found to link this feedback to.'); + return; + } + + try { + setIsSubmittingLevel2(true); + + const feedbackItems = [ + { type: 'Strategic Vision', comments: level2Feedback.strategicVision }, + { type: 'Management Capabilities', comments: level2Feedback.managementCapabilities }, + { type: 'Operational Understanding', comments: level2Feedback.operationalUnderstanding }, + { type: 'Key Strengths', comments: level2Feedback.keyStrengths }, + { type: 'Areas of Concern', comments: level2Feedback.areasOfConcern }, + { type: 'Additional Comments', comments: level2Feedback.additionalComments } + ].filter(item => item.comments.trim() !== ''); + + await onboardingService.submitLevel2Feedback({ + interviewId, + overallScore: Number(level2Feedback.overallScore), + feedbackItems + }); + + toast.success('Level 2 Feedback submitted successfully'); + setShowLevel2FeedbackModal(false); + + // Reset form + setLevel2Feedback({ + strategicVision: '', + managementCapabilities: '', + operationalUnderstanding: '', + keyStrengths: '', + areasOfConcern: '', + additionalComments: '', + overallScore: '', + interviewerName: currentUser?.name || '', + interviewDate: new Date().toISOString().split('T')[0] + }); + fetchInterviews(); // Refresh to show feedback + fetchApplication(); // Refresh application status + } catch (error) { + toast.error('Failed to submit Level 2 Feedback'); + } finally { + setIsSubmittingLevel2(false); + } + }; + + // Level 3 Feedback State + const [level3Feedback, setLevel3Feedback] = useState({ + strategicVision: '', + managementCapabilities: '', + operationalUnderstanding: '', + brandAlignment: '', + executiveSummary: '', + keyStrengths: '', + areasOfConcern: '', + additionalComments: '', + overallScore: '', + interviewerName: currentUser?.name || '', + interviewDate: new Date().toISOString().split('T')[0] + }); + const [isSubmittingLevel3, setIsSubmittingLevel3] = useState(false); + + const handleLevel3Change = (field: string, value: string) => { + setLevel3Feedback(prev => ({ ...prev, [field]: value })); + }; + + const handleSubmitLevel3Feedback = async () => { + if (!level3Feedback.overallScore) { + toast.warning('Please provide an overall score.'); + return; + } + + const interviewId = selectedInterviewForFeedback?.id || interviews.find(i => i.status !== 'Completed' && i.level === 3)?.id; + + if (!interviewId) { + toast.error('No active Level 3 interview found to link this feedback to.'); + return; + } + + try { + setIsSubmittingLevel3(true); + + // Level 3 might have slightly different fields or same structure. Assuming same for now. + const feedbackItems = [ + { type: 'Business Vision & Strategy', comments: level3Feedback.strategicVision }, + { type: 'Leadership & Decision Making', comments: level3Feedback.managementCapabilities }, + { type: 'Operational & Financial Readiness', comments: level3Feedback.operationalUnderstanding }, + { type: 'Brand Alignment', comments: level3Feedback.brandAlignment }, + { type: 'Key Strengths', comments: level3Feedback.keyStrengths }, + { type: 'Areas of Concern', comments: level3Feedback.areasOfConcern }, + { type: 'Executive Summary', comments: level3Feedback.executiveSummary }, + { type: 'Additional Comments', comments: level3Feedback.additionalComments } + ].filter(item => item.comments.trim() !== ''); + + // Reusing submitLevel2Feedback endpoint as it maps to InterviewFeedback table generic enough for Level 3 too + // Or we can create specific one if needed, but logic is identical so reusing service method + await onboardingService.submitLevel2Feedback({ + interviewId, + overallScore: Number(level3Feedback.overallScore), + feedbackItems + }); + + toast.success('Level 3 Feedback submitted successfully'); + setShowLevel3FeedbackModal(false); + + // Reset form + setLevel3Feedback({ + strategicVision: '', + managementCapabilities: '', + operationalUnderstanding: '', + brandAlignment: '', + executiveSummary: '', + keyStrengths: '', + areasOfConcern: '', + additionalComments: '', + overallScore: '', + interviewerName: currentUser?.name || '', + interviewDate: new Date().toISOString().split('T')[0] + }); + fetchInterviews(); + fetchApplication(); + } catch (error) { + toast.error('Failed to submit Level 3 Feedback'); + } finally { + setIsSubmittingLevel3(false); + } + }; + + // Feedback Details Modal State + const [selectedEvaluationForView, setSelectedEvaluationForView] = useState(null); + const [showFeedbackDetailsModal, setShowFeedbackDetailsModal] = useState(false); + + const fetchInterviews = async () => { + if (applicationId) { try { - await onboardingService.updateInterviewDecision({ - interviewId: activeInterview.id, + const data = await onboardingService.getInterviews(applicationId); + setInterviews(data || []); + } catch (error) { + console.error('Failed to fetch interviews', error); + } + } + }; + + useEffect(() => { + fetchInterviews(); + }, [applicationId]); + + const handleAddInterviewer = () => { + if (!selectedInterviewerId) return; + const usersList = Array.isArray(users) ? users : []; + const userToAdd = usersList.find(u => u.id === selectedInterviewerId); + if (userToAdd && !scheduledInterviewParticipants.find(p => p.id === userToAdd.id)) { + setScheduledInterviewParticipants([...scheduledInterviewParticipants, userToAdd]); + setSelectedInterviewerId(''); + } + }; + + const handleRemoveInterviewer = (userId: string) => { + setScheduledInterviewParticipants(scheduledInterviewParticipants.filter(p => p.id !== userId)); + }; + + useEffect(() => { + if ((activeTab === 'documents' || activeTab === 'progress') && applicationId) { + const fetchDocuments = async () => { + try { + const docs = await onboardingService.getDocuments(applicationId); + setDocuments(docs || []); + } catch (error) { + console.error('Failed to fetch documents', error); + } + }; + fetchDocuments(); + } + }, [activeTab, applicationId]); + + const fetchUsers = async (type?: string) => { + // Only fetch users if user has admin/DD/NBH roles to avoid 403s + if (!currentUser || !['DD Admin', 'Super Admin', 'DD Lead', 'DD Head', 'NBH'].includes(currentUser.role)) { + return; + } + try { + const params: any = {}; + if (type) { + const roleMapping: any = { + 'level1': ['DD-ZM', 'RBM'], + 'level2': ['DD Lead', 'ZBH'], + 'level3': ['NBH', 'DD Head'] + }; + params.roleCode = roleMapping[type]; + + // Include location from the application + if (application) { + params.locationId = application.districtId || application.areaId || application.regionId || application.zoneId; + } + } + + const response = await onboardingService.getUsers(params); + if (Array.isArray(response)) { + setUsers(response); + } else if (response && Array.isArray(response.data)) { + setUsers(response.data); + } else if (response && Array.isArray(response.users)) { + setUsers(response.users); + } else { + console.warn('Unexpected users response:', response); + setUsers([]); + } + } catch (error) { + console.error('Failed to fetch users', error); + setUsers([]); + } + }; + + useEffect(() => { + if (showScheduleModal && application) { + fetchUsers(interviewType); + + // 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 || + p.metadata?.interviewLevel === String(levelNum) || + p.metadata?.allAssignments?.includes(levelNum) || + p.metadata?.allAssignments?.includes(String(levelNum)) + ) + .map((p: any) => p.user) + .filter(Boolean); + + if (preAssigned.length > 0) { + // Ensure uniqueness by user ID + const uniquePreassigned: any[] = []; + const seenIds = new Set(); + preAssigned.forEach((u: any) => { + if (u.id && !seenIds.has(u.id)) { + seenIds.add(u.id); + uniquePreassigned.push(u); + } + }); + setScheduledInterviewParticipants(uniquePreassigned); + } else { + setScheduledInterviewParticipants([]); + } + } else if ((showAssignArchitectureModal || showAssignModal) && application) { + fetchUsers(); // Default fetch for other modals like Assign + } + }, [showScheduleModal, showAssignArchitectureModal, showAssignModal, interviewType, application?.participants]); + + const handleScheduleInterview = async () => { + if (!interviewDate) { + toast.warning('Please select date and time'); + return; + } + try { + setIsScheduling(true); + + const payload = { + applicationId: application?.id, + level: interviewType, + scheduledAt: interviewDate, + type: interviewMode === 'virtual' ? 'Virtual Interview' : 'Physical Interview', + location: interviewMode === 'virtual' ? meetingLink : location, + participants: scheduledInterviewParticipants.map(p => p.id) + }; + + await onboardingService.scheduleInterview(payload); + toast.success('Interview scheduled successfully'); + setShowScheduleModal(false); + // Refresh interviews + await fetchInterviews(); + await fetchApplication(); // Refresh application status + + } catch (error) { + toast.error('Failed to schedule interview'); + console.error(error); + } finally { + setIsScheduling(false); + } + }; + + const handleCancelInterview = async (interviewId: string) => { + if (!window.confirm('Are you sure you want to cancel this interview?')) return; + try { + await onboardingService.updateInterview(interviewId, { status: 'Cancelled' }); + toast.success('Interview cancelled successfully'); + fetchInterviews(); + } catch (error) { + toast.error('Failed to cancel interview'); + console.error(error); + } + }; + + const handleUpload = async () => { + if (!uploadFile || !uploadDocType) { + toast.warning('Please select a file and document type'); + return; + } + + try { + setIsUploading(true); + const formData = new FormData(); + formData.append('file', uploadFile); + formData.append('documentType', uploadDocType); + if (selectedStage) { + formData.append('stage', selectedStage); + } + + await onboardingService.uploadDocument(applicationId!, formData); + + toast.success('Document uploaded successfully'); + setShowUploadForm(false); + setUploadFile(null); + setUploadDocType(''); + + // Refresh documents + const docs = await onboardingService.getDocuments(applicationId); + setDocuments(docs || []); + + // Refresh EOR Data in case an EOR document was uploaded + fetchEorData(); + } catch (error) { + console.error('Upload failed', error); + toast.error('Failed to upload document'); + } finally { + setIsUploading(false); + } + }; + + if (loading) { + return
Loading application details...
; + } + + if (!application) { + return
Application not found
; + } + + const isDocumentUploaded = (docType: string) => { + return (documents || []).some(d => d.documentType === docType); + }; + + const getStageStatus = (stageName: string, fallbackLogic: () => ProcessStage['status']): ProcessStage['status'] => { + const backendStage = (application.progressTracking || []).find((ps: any) => ps.stageName === stageName); + if (backendStage && (backendStage.status === 'completed' || backendStage.status === 'active')) { + return backendStage.status as any; + } + return fallbackLogic(); + }; + + const processStages: ProcessStage[] = [ + { + id: 1, + name: 'Submitted', + status: 'completed', + date: application.submissionDate, + description: 'Application submitted', + documentsUploaded: 3 + }, + { + id: 2, + name: 'Questionnaire', + status: getStageStatus('Questionnaire', () => + ['Questionnaire Completed', 'Shortlisted', 'Level 1 Interview Pending', 'Level 1 Approved', 'Level 2 Interview Pending', 'Level 2 Approved', 'Level 2 Recommended', 'Level 3 Interview Pending', 'Level 3 Approved', 'FDD Verification', 'LOI In Progress', 'Payment Pending', 'LOI Issued', 'Statutory LOI Ack', 'Dealer Code Generation', 'Architecture Team Assigned', 'Architecture Document Upload', 'Architecture Team Completion', 'LOA Pending', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Onboarded'].includes(application.status) ? 'completed' : + application.status === 'Questionnaire Pending' ? 'active' : 'pending' + ), + date: application.questionnaireDate, + description: 'Questionnaire completed', + documentsUploaded: 0 + }, + { + id: 3, + name: 'Shortlist', + status: getStageStatus('Shortlist', () => ['Shortlisted', 'Level 1 Interview Pending', 'Level 1 Approved', 'Level 2 Interview Pending', 'Level 2 Approved', 'Level 2 Recommended', 'Level 3 Interview Pending', 'Level 3 Approved', 'FDD Verification', 'LOI In Progress', 'Payment Pending', 'LOI Issued', 'Statutory LOI Ack', 'Dealer Code Generation', 'Architecture Team Assigned', 'Architecture Document Upload', 'Architecture Team Completion', 'LOA Pending', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Rejected', 'Onboarded'].includes(application.status) ? 'completed' : 'pending'), + date: application.shortlistDate, + description: 'Application shortlisted by DD', + evaluators: Array.from(new Set((application.participants || []) + .filter((p: any) => p.participantType === 'assignee') + .map((p: any) => `${p.user?.fullName || p.user?.name || 'User'} (${p.user?.roleCode || p.participantType})`) + )), + documentsUploaded: 2 + }, + { + id: 4, + name: '1st Level Interview', + status: getStageStatus('1st Level Interview', () => ['Level 1 Approved', 'Level 2 Interview Pending', 'Level 2 Approved', 'Level 2 Recommended', 'Level 3 Interview Pending', 'Level 3 Approved', 'FDD Verification', 'LOI In Progress', 'Payment Pending', 'LOI Issued', 'Statutory LOI Ack', 'Dealer Code Generation', 'Architecture Team Assigned', 'Architecture Document Upload', 'Architecture Team Completion', 'LOA Pending', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Onboarded'].includes(application.status) ? 'completed' : application.status === 'Level 1 Interview Pending' ? 'active' : application.status === 'Rejected' && application.progress >= 40 ? 'completed' : 'pending'), + 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' || + p.metadata?.allAssignments?.includes(1) || + p.metadata?.allAssignments?.includes('1') + ) + .map((p: any) => `${p.user?.name} (${p.user?.role})`) + )), + documentsUploaded: 1 + }, + { + id: 5, + name: '2nd Level Interview', + status: getStageStatus('2nd Level Interview', () => ['Level 2 Approved', 'Level 2 Recommended', 'Level 3 Interview Pending', 'Level 3 Approved', 'FDD Verification', 'LOI In Progress', 'Payment Pending', 'LOI Issued', 'Statutory LOI Ack', 'Dealer Code Generation', 'Architecture Team Assigned', 'Architecture Document Upload', 'Architecture Team Completion', 'LOA Pending', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Onboarded'].includes(application.status) ? 'completed' : application.status === 'Level 2 Interview Pending' ? 'active' : application.status === 'Rejected' && application.progress >= 55 ? 'completed' : 'pending'), + 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' || + p.metadata?.allAssignments?.includes(2) || + p.metadata?.allAssignments?.includes('2') + ) + .map((p: any) => `${p.user?.name} (${p.user?.role})`) + )), + documentsUploaded: 1 + }, + { + id: 6, + name: '3rd Level Interview', + status: getStageStatus('3rd Level Interview', () => ['Level 3 Approved', 'FDD Verification', 'LOI In Progress', 'Payment Pending', 'LOI Issued', 'Statutory LOI Ack', 'Dealer Code Generation', 'Architecture Team Assigned', 'Architecture Document Upload', 'Architecture Team Completion', 'LOA Pending', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Onboarded'].includes(application.status) ? 'completed' : application.status === 'Level 3 Interview Pending' ? 'active' : application.status === 'Rejected' && application.progress >= 70 ? 'completed' : 'pending'), + 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' || + p.metadata?.allAssignments?.includes(3) || + p.metadata?.allAssignments?.includes('3') + ) + .map((p: any) => `${p.user?.name} (${p.user?.role})`) + )), + documentsUploaded: 2 + }, + { + id: 7, + name: 'FDD', + status: getStageStatus('FDD', () => ['LOI In Progress', 'Payment Pending', 'LOI Issued', 'Statutory LOI Ack', 'Dealer Code Generation', 'Architecture Team Assigned', 'Architecture Document Upload', 'Architecture Team Completion', 'LOA Pending', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Onboarded'].includes(application.status) ? 'completed' : application.status === 'FDD Verification' ? 'active' : 'pending'), + date: application.fddDate, + description: 'Financial Due Diligence', + documentsUploaded: 5 + }, + { + id: 8, + name: 'LOI Approval', + status: getStageStatus('LOI Approval', () => ['Payment Pending', 'LOI Issued', 'Statutory LOI Ack', 'Dealer Code Generation', 'Architecture Team Assigned', 'Architecture Document Upload', 'Architecture Team Completion', 'LOA Pending', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Onboarded'].includes(application.status) ? 'completed' : application.status === 'LOI In Progress' ? 'active' : 'pending'), + date: application.loiApprovalDate, + description: 'Letter of Intent approval', + evaluators: Array.from(new Set((application.participants || []) + .filter((p: any) => + p.metadata?.stageCode === 'LOI_APPROVAL' || + p.metadata?.allAssignments?.includes('LOI_APPROVAL') + ) + .map((p: any) => `${p.user?.name} (${p.user?.role})`) + )), + documentsUploaded: 1 + }, + { + id: 9, + name: 'Security Details', + status: getStageStatus('Security Details', () => ['LOI Issued', 'Statutory LOI Ack', 'Dealer Code Generation', 'Architecture Team Assigned', 'Architecture Document Upload', 'Architecture Team Completion', 'LOA Pending', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Onboarded'].includes(application.status) ? 'completed' : application.status === 'Payment Pending' ? 'active' : 'pending'), + date: application.securityDetailsDate, + description: 'Security verification', + documentsUploaded: 3 + }, + { + id: 10, + name: 'LOI Issue', + status: getStageStatus('LOI Issue', () => ['Statutory LOI Ack', 'Dealer Code Generation', 'Architecture Team Assigned', 'Architecture Document Upload', 'Architecture Team Completion', 'LOA Pending', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Onboarded'].includes(application.status) ? 'completed' : application.status === 'LOI Issued' ? 'active' : 'pending'), + date: application.loiIssueDate, + description: 'Letter of Intent issued', + documentsUploaded: 1 + }, + { + id: 11, + name: 'Dealer Code Generation', + status: getStageStatus('Dealer Code Generation', () => (application.dealerCode || ['Architecture Team Assigned', 'Architecture Document Upload', 'Architecture Team Completion', 'Statutory GST', 'Statutory PAN', 'Statutory NODAL', 'Statutory NODAL', 'Statutory Check', 'Statutory Partnership', 'Statutory Firm Reg', 'Statutory Rental', 'Statutory Virtual Code', 'Statutory Domain', 'Statutory MSD', 'Statutory LOI Ack', 'LOA Pending', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Onboarded'].includes(application.status)) ? 'completed' : application.status === 'Dealer Code Generation' ? 'active' : 'pending'), + date: application.dealerCodeDate, + description: 'Dealer code generated and assigned', + documentsUploaded: 0, + isParallel: true, + branches: [ + { + name: 'Architectural Work', + color: 'blue', + stages: [ + { + id: '11a-1', + name: 'Assigned to Architecture Team', + status: application.architectureAssignedTo || ['Architecture Document Upload', 'Architecture Team Completion', 'Statutory GST', 'Statutory PAN', 'Statutory Nodal', 'Statutory Check', 'Statutory Partnership', 'Statutory Firm Reg', 'Statutory Rental', 'Statutory Virtual Code', 'Statutory Domain', 'Statutory MSD', 'Statutory LOI Ack', 'LOA Pending', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Onboarded'].includes(application.status) || application.architectureStatus === 'COMPLETED' || isDocumentUploaded('Architecture Assignment Document') ? 'completed' : application.status === 'Architecture Team Assigned' ? 'active' : 'pending', + date: application.architectureAssignedDate, + description: 'Assigned to architecture team for site planning', + documentsUploaded: 0 + }, + { + id: '11a-2', + name: 'Architectural Document Upload', + status: isDocumentUploaded('Architecture Blueprint') || isDocumentUploaded('Site Plan') || ['Architecture Team Completion', 'LOA Pending', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Onboarded'].includes(application.status) || application.architectureStatus === 'COMPLETED' ? 'completed' : (application.architectureAssignedTo || application.status === 'Architecture Document Upload' || application.architectureStatus === 'IN_PROGRESS') ? 'active' : 'pending', + date: application.architectureDocumentDate, + description: 'Architectural documents and blueprints uploaded', + documentsUploaded: (documents || []).filter(d => ['Architecture Blueprint', 'Site Plan'].includes(d.documentType)).length + }, + { + id: '11a-3', + name: 'Architecture Team Completion', + status: ['LOA Pending', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Onboarded'].includes(application.status) || application.architectureStatus === 'COMPLETED' || isDocumentUploaded('Architecture Completion Certificate') ? 'completed' : application.status === 'Architecture Team Completion' ? 'active' : 'pending', + date: application.architectureCompletionDate, + description: 'Architecture team work completed', + documentsUploaded: 0 + } + ] + }, + { + name: 'Statutory Documents', + color: 'green', + stages: [ + { + id: '11b-1', + name: 'GST', + status: isDocumentUploaded('GST Certificate') || ['Statutory PAN', 'Statutory Nodal', 'Statutory Check', 'Statutory Partnership', 'Statutory Firm Reg', 'Statutory Rental', 'Statutory Virtual Code', 'Statutory Domain', 'Statutory MSD', 'Statutory LOI Ack', 'LOA Pending', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Onboarded'].includes(application.status) ? 'completed' : application.status === 'Statutory GST' ? 'active' : 'pending', + description: 'GST certificate', + documentsUploaded: (documents || []).filter(d => d.documentType === 'GST Certificate').length + }, + { + id: '11b-2', + name: 'PAN', + status: isDocumentUploaded('PAN Card') || ['Statutory Nodal', 'Statutory Check', 'Statutory Partnership', 'Statutory Firm Reg', 'Statutory Rental', 'Statutory Virtual Code', 'Statutory Domain', 'Statutory MSD', 'Statutory LOI Ack', 'LOA Pending', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Onboarded'].includes(application.status) ? 'completed' : application.status === 'Statutory PAN' ? 'active' : 'pending', + description: 'PAN card', + documentsUploaded: (documents || []).filter(d => d.documentType === 'PAN Card').length + }, + { + id: '11b-3', + name: 'Nodal Agreement', + status: isDocumentUploaded('Nodal Agreement') || ['Statutory Check', 'Statutory Partnership', 'Statutory Firm Reg', 'Statutory Rental', 'Statutory Virtual Code', 'Statutory Domain', 'Statutory MSD', 'Statutory LOI Ack', 'LOA Pending', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Onboarded'].includes(application.status) ? 'completed' : application.status === 'Statutory Nodal' ? 'active' : 'pending', + description: 'Nodal agreement document', + documentsUploaded: (documents || []).filter(d => d.documentType === 'Nodal Agreement').length + }, + { + id: '11b-4', + name: 'Cancelled Check', + status: isDocumentUploaded('Cancelled Check') || ['Statutory Partnership', 'Statutory Firm Reg', 'Statutory Rental', 'Statutory Virtual Code', 'Statutory Domain', 'Statutory MSD', 'Statutory LOI Ack', 'LOA Pending', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Onboarded'].includes(application.status) ? 'completed' : application.status === 'Statutory Check' ? 'active' : 'pending', + description: 'Cancelled check copy', + documentsUploaded: (documents || []).filter(d => d.documentType === 'Cancelled Check').length + }, + { + id: '11b-5', + name: 'Partnership Deed/LLP/MOA/AOA/COI', + status: isDocumentUploaded('Partnership Deed') || isDocumentUploaded('LLP Agreement') || isDocumentUploaded('Certificate of Incorporation') || isDocumentUploaded('MOA') || isDocumentUploaded('AOA') || ['Statutory Firm Reg', 'Statutory Rental', 'Statutory Virtual Code', 'Statutory Domain', 'Statutory MSD', 'Statutory LOI Ack', 'LOA Pending', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Onboarded'].includes(application.status) ? 'completed' : application.status === 'Statutory Partnership' ? 'active' : 'pending', + description: 'Business entity documents', + documentsUploaded: (documents || []).filter(d => ['Partnership Deed', 'LLP Agreement', 'Certificate of Incorporation', 'MOA', 'AOA'].includes(d.documentType)).length + }, + { + id: '11b-6', + name: 'Firm Registration Certificate', + status: isDocumentUploaded('Firm Registration') || ['Statutory Rental', 'Statutory Virtual Code', 'Statutory Domain', 'Statutory MSD', 'Statutory LOI Ack', 'LOA Pending', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Onboarded'].includes(application.status) ? 'completed' : application.status === 'Statutory Firm Reg' ? 'active' : 'pending', + description: 'Firm registration certificate', + documentsUploaded: (documents || []).filter(d => d.documentType === 'Firm Registration').length + }, + { + id: '11b-7', + name: 'Rental agreement/ Lease agreement / Own/ Land agreement', + status: isDocumentUploaded('Rental Agreement') || isDocumentUploaded('Property Documents') || ['Statutory Virtual Code', 'Statutory Domain', 'Statutory MSD', 'Statutory LOI Ack', 'LOA Pending', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Onboarded'].includes(application.status) ? 'completed' : application.status === 'Statutory Rental' ? 'active' : 'pending', + description: 'Property agreement document', + documentsUploaded: (documents || []).filter(d => ['Rental Agreement', 'Property Documents'].includes(d.documentType)).length + }, + { + id: '11b-8', + name: 'Virtual Code', + status: isDocumentUploaded('Virtual Code Confirmation') || ['Statutory Domain', 'Statutory MSD', 'Statutory LOI Ack', 'LOA Pending', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Onboarded'].includes(application.status) ? 'completed' : application.status === 'Statutory Virtual Code' ? 'active' : 'pending', + description: 'Virtual code availability', + documentsUploaded: (documents || []).filter(d => d.documentType === 'Virtual Code Confirmation').length + }, + { + id: '11b-9', + name: 'Domain ID', + status: isDocumentUploaded('Domain ID Setup') || ['Statutory MSD', 'Statutory LOI Ack', 'LOA Pending', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Onboarded'].includes(application.status) ? 'completed' : application.status === 'Statutory Domain' ? 'active' : 'pending', + description: 'Domain ID setup', + documentsUploaded: (documents || []).filter(d => d.documentType === 'Domain ID Setup').length + }, + { + id: '11b-10', + name: 'MSD Configuration', + status: isDocumentUploaded('MSD Configuration') || ['Statutory LOI Ack', 'LOA Pending', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Onboarded'].includes(application.status) ? 'completed' : application.status === 'Statutory MSD' ? 'active' : 'pending', + description: 'Microsoft Dynamics configuration', + documentsUploaded: (documents || []).filter(d => d.documentType === 'MSD Configuration').length + }, + { + id: '11b-11', + name: 'LOI Acknowledgement Copy', + status: isDocumentUploaded('LOI Acknowledgement') || ['LOA Pending', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Onboarded'].includes(application.status) ? 'completed' : application.status === 'Statutory LOI Ack' ? 'active' : 'pending', + description: 'LOI acknowledgement copy', + documentsUploaded: (documents || []).filter(d => d.documentType === 'LOI Acknowledgement').length + } + ] + } + ] + }, + { + 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' || + p.metadata?.allAssignments?.includes('LOA_APPROVAL') + ) + .map((p: any) => `${p.user?.name} (${p.user?.role})`) + )), + documentsUploaded: 1 + }, + { + id: 13, + name: 'EOR Complete', + status: getStageStatus('EOR Complete', () => ['Inauguration', 'Approved', 'Onboarded'].includes(application.status) ? 'completed' : application.status === 'EOR Complete' ? 'active' : 'pending'), + date: application.eorCompleteDate, + description: 'Essential Operating Requirements completed', + documentsUploaded: 6 + }, + { + id: 14, + name: 'Inauguration', + status: getStageStatus('Inauguration', () => ['Approved', 'Onboarded'].includes(application.status) ? 'completed' : application.status === 'Inauguration' ? 'active' : 'pending'), + date: application.inaugurationDate, + description: 'Dealership inauguration ceremony', + documentsUploaded: 2 + }, + { + id: 15, + name: 'Dealership Active', + status: getStageStatus('Onboarded', () => application.status === 'Onboarded' ? 'completed' : 'pending'), + date: application.onboardedDate, + description: 'Dealer profile and login created' + } + ]; + + const eorChecklist = [ + { id: 1, item: 'Sales Standards', completed: false }, + { id: 2, item: 'Service & Spares', completed: false }, + { id: 3, item: 'DMS infra', completed: false }, + { id: 4, item: 'Manpower Training', completed: false }, + { id: 5, item: 'Trade certificate with test ride bikes registration', completed: false }, + { id: 6, item: 'GST certificate including Accessories & Apparels billing', completed: false }, + { id: 7, item: 'Inventory Funding', completed: false }, + { id: 8, item: 'Virtual code availability', completed: false }, + { id: 9, item: 'Vendor payments', completed: false }, + { id: 10, item: 'Details for website submission', completed: false }, + { id: 11, item: 'Infra Insurance both Showroom and Service center', completed: false }, + { id: 12, item: 'Auto ordering', completed: false } + ]; + + const flattenedStages: any[] = processStages.reduce((acc: any[], stage) => { + acc.push({ name: stage.name }); + if (stage.branches) { + stage.branches.forEach((branch: any) => { + branch.stages.forEach((subStage: any) => { + acc.push({ name: subStage.name, parentBranch: branch.name }); + }); + }); + } + if (stage.name === 'EOR In Progress' || stage.name === 'EOR Complete') { + (eorData?.items || eorChecklist).forEach((item: any) => { + acc.push({ name: `EOR: ${item.description || item.item}`, parentBranch: 'EOR' }); + }); + } + return acc; + }, []); + + + + + + const getDocumentsForStage = (stageName: string) => { + return documents.filter(doc => + doc.stage === stageName || + (!doc.stage && doc.documentType?.toLowerCase().includes(stageName.toLowerCase().split(' ')[0])) + ); + }; + + const handleApprove = async () => { + 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; + + switch (application.status) { + case 'Shortlisted': + case 'Level 1 Interview Pending': + newStatus = 'Level 1 Approved'; break; + case 'Level 1 Approved': + case 'Level 2 Interview Pending': + newStatus = 'Level 2 Approved'; break; + case 'Level 2 Approved': + case 'Level 3 Interview Pending': + newStatus = 'Level 3 Approved'; break; + case 'Level 3 Approved': + newStatus = 'FDD Verification'; break; + case 'FDD Verification': + newStatus = 'LOI In Progress'; break; + case 'LOI In Progress': + newStatus = 'LOI Issued'; break; + case 'LOI Issued': + newStatus = 'Dealer Code Generation'; break; + case 'Dealer Code Generation': + case 'Architecture Team Assigned': + case 'Architecture Document Upload': + case 'Architecture Team Completion': + newStatus = 'Statutory GST'; break; + case 'Statutory GST': + case 'Statutory PAN': + case 'Statutory Nodal': + case 'Statutory Check': + case 'Statutory Partnership': + case 'Statutory Firm Reg': + case 'Statutory Rental': + case 'Statutory Virtual Code': + case 'Statutory Domain': + case 'Statutory MSD': + case 'Statutory LOI Ack': + newStatus = 'LOA Pending'; break; + case 'LOA Pending': + newStatus = 'EOR In Progress'; break; + case 'EOR In Progress': + newStatus = 'EOR Complete'; break; + case 'EOR Complete': + newStatus = 'Inauguration'; break; + case 'Inauguration': + newStatus = 'Approved'; break; + default: + newStatus = 'Approved'; // Final fallback + } + + const policyManagedStages: { [key: string]: string } = { + 'Level 1 Interview Pending': 'INTERVIEW_LEVEL_1', + 'Level 2 Interview Pending': 'INTERVIEW_LEVEL_2', + 'Level 2 Recommended': 'INTERVIEW_LEVEL_2', + 'Level 3 Interview Pending': 'INTERVIEW_LEVEL_3', + 'LOI In Progress': 'LOI_APPROVAL', + 'LOA Pending': 'LOA_APPROVAL' + }; + + const stageCodeForPolicy = policyManagedStages[application.status]; + + if (stageCodeForPolicy) { + const response = await onboardingService.submitStageDecision({ + applicationId: application.id, + stageCode: stageCodeForPolicy, decision: 'Approved', + remarks: approvalRemark, + nextStatus: newStatus + }); + + if (response.data?.statusUpdated) { + toast.success(response.message || 'Stage completed and moved to next step'); + } else { + toast.info(response.message || 'Approval recorded. Waiting for other mandatory approvers.'); + } + } else { + await onboardingService.updateApplicationStatus(applicationId!, { + status: newStatus, 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; + toast.success(`Application moved to ${newStatus}`); } - } - if (!approvalRemark.trim()) { - toast.warning('Please enter a remark'); - return; - } - - // Application level approval - Robust State Machine - let newStatus = application.status; - - switch (application.status) { - case 'Shortlisted': - case 'Level 1 Interview Pending': - newStatus = 'Level 1 Approved'; break; - case 'Level 1 Approved': - case 'Level 2 Interview Pending': - newStatus = 'Level 2 Approved'; break; - case 'Level 2 Approved': - case 'Level 3 Interview Pending': - newStatus = 'Level 3 Approved'; break; - case 'Level 3 Approved': - newStatus = 'FDD Verification'; break; - case 'FDD Verification': - newStatus = 'LOI In Progress'; break; - case 'LOI In Progress': - newStatus = 'LOI Issued'; break; - case 'LOI Issued': - newStatus = 'Dealer Code Generation'; break; - case 'Dealer Code Generation': - case 'Architecture Team Assigned': - case 'Architecture Document Upload': - case 'Architecture Team Completion': - newStatus = 'Statutory GST'; break; - case 'Statutory GST': - case 'Statutory PAN': - case 'Statutory Nodal': - case 'Statutory Check': - case 'Statutory Partnership': - case 'Statutory Firm Reg': - case 'Statutory Rental': - case 'Statutory Virtual Code': - case 'Statutory Domain': - case 'Statutory MSD': - case 'Statutory LOI Ack': - newStatus = 'LOA Pending'; break; - case 'LOA Pending': - newStatus = 'EOR In Progress'; break; - case 'EOR In Progress': - newStatus = 'EOR Complete'; break; - case 'EOR Complete': - newStatus = 'Inauguration'; break; - case 'Inauguration': - newStatus = 'Approved'; break; - default: - newStatus = 'Approved'; // Final fallback - } - - const policyManagedStages: { [key: string]: string } = { - 'Level 1 Interview Pending': 'INTERVIEW_LEVEL_1', - 'Level 2 Interview Pending': 'INTERVIEW_LEVEL_2', - 'Level 2 Recommended': 'INTERVIEW_LEVEL_2', - 'Level 3 Interview Pending': 'INTERVIEW_LEVEL_3', - 'LOI In Progress': 'LOI_APPROVAL', - 'LOA Pending': 'LOA_APPROVAL' - }; - - const stageCodeForPolicy = policyManagedStages[application.status]; - - if (stageCodeForPolicy) { - const response = await onboardingService.submitStageDecision({ - applicationId: application.id, - stageCode: stageCodeForPolicy, - decision: 'Approved', - remarks: approvalRemark, - nextStatus: newStatus - }); - - if (response.data?.statusUpdated) { - toast.success(response.message || 'Stage completed and moved to next step'); + // Special case: If final approval, create Dealer record + if (newStatus === 'Approved') { + // In a real scenario, we'd have the dealerCodeId from the application's associated DealerCode record + await onboardingService.createDealer({ + applicationId: applicationId, + // dealerCodeId is handled by backend if not provided, or we can fetch it + }); + toast.success('Application approved and Dealer profile created!'); } else { - toast.info(response.message || 'Approval recorded. Waiting for other mandatory approvers.'); + toast.success(`Application moved to ${newStatus}`); } - } else { - await onboardingService.updateApplicationStatus(applicationId!, { - status: newStatus, - remarks: approvalRemark - }); - toast.success(`Application moved to ${newStatus}`); + + setShowApproveModal(false); + setApprovalRemark(''); + setApprovalFile(null); + fetchApplication(); + } catch (error) { + console.error('Approval error:', error); + toast.error('Failed to process approval'); + } finally { + setIsApproving(false); } + }; - // Special case: If final approval, create Dealer record - if (newStatus === 'Approved') { - // In a real scenario, we'd have the dealerCodeId from the application's associated DealerCode record - await onboardingService.createDealer({ - applicationId: applicationId, - // dealerCodeId is handled by backend if not provided, or we can fetch it - }); - toast.success('Application approved and Dealer profile created!'); - } else { - toast.success(`Application moved to ${newStatus}`); - } + const handleReject = async () => { + 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) + ); - setShowApproveModal(false); - setApprovalRemark(''); - setApprovalFile(null); - fetchApplication(); - } catch (error) { - console.error('Approval error:', error); - toast.error('Failed to process approval'); - } finally { - setIsApproving(false); - } - }; + 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; + } + } - const handleReject = async () => { - 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 (!rejectionReason.trim()) { + toast.warning('Please enter a reason for rejection'); + return; + } - if (activeInterview) { - try { - await onboardingService.updateInterviewDecision({ - interviewId: activeInterview.id, + const policyManagedStages: { [key: string]: string } = { + 'Level 1 Interview Pending': 'INTERVIEW_LEVEL_1', + 'Level 2 Interview Pending': 'INTERVIEW_LEVEL_2', + 'Level 2 Recommended': 'INTERVIEW_LEVEL_2', + 'Level 3 Interview Pending': 'INTERVIEW_LEVEL_3', + 'LOI In Progress': 'LOI_APPROVAL', + 'LOA Pending': 'LOA_APPROVAL' + }; + + const stageCodeForPolicy = policyManagedStages[application.status]; + + if (stageCodeForPolicy) { + await onboardingService.submitStageDecision({ + applicationId: application.id, + stageCode: stageCodeForPolicy, decision: 'Rejected', + remarks: rejectionReason, + interviewId: activeInterview?.id + }); + } else { + await onboardingService.updateApplicationStatus(applicationId!, { + status: 'Rejected', remarks: rejectionReason }); - toast.success('Interview rejected'); - setShowRejectModal(false); - setRejectionReason(''); - fetchInterviews(); - fetchApplication(); - return; - } catch (error) { - toast.error('Failed to reject interview'); - return; + } + + toast.success('Application rejected'); + setShowRejectModal(false); + setRejectionReason(''); + fetchApplication(); + } catch (error) { + console.error('Rejection error:', error); + toast.error('Failed to process rejection'); + } finally { + setIsRejecting(false); + } + }; + + const handleGenerateDealerCodes = async () => { + try { + await onboardingService.generateDealerCodes(applicationId!); + toast.success('Dealer codes generated successfully'); + fetchApplication(); + } catch (error) { + console.error('Generate codes error:', error); + toast.error('Failed to generate dealer codes'); + } + }; + + const handleAssignArchitecture = async () => { + if (!architectureLeadId) { + toast.warning('Please select an architecture lead'); + return; + } + try { + setIsAssigningArchitecture(true); + await onboardingService.assignArchitectureTeam(applicationId!, architectureLeadId); + toast.success('Architecture team assigned successfully'); + setShowAssignArchitectureModal(false); + fetchApplication(); // Refresh to update status + } catch (error) { + toast.error('Failed to assign architecture team'); + } finally { + setIsAssigningArchitecture(false); + } + }; + + const handleUpdateArchitectureStatus = async () => { + try { + setIsUpdatingArchitecture(true); + await onboardingService.updateArchitectureStatus(applicationId!, architectureStatus, architectureRemarks); + toast.success('Architecture status updated successfully'); + setShowArchitectureStatusModal(false); + fetchApplication(); + } catch (error) { + toast.error('Failed to update architecture status'); + } finally { + setIsUpdatingArchitecture(false); + } + }; + + const handleAddParticipant = async () => { + if (!selectedUser) { + toast.warning('Please select a user'); + return; + } + try { + setIsAssigningParticipant(true); + await onboardingService.addParticipant({ + requestId: applicationId, + requestType: 'application', + userId: selectedUser, + participantType: participantType || 'contributor' + }); + toast.success('User assigned successfully!'); + // Refresh application data + fetchApplication(); + setSelectedUser(''); + setShowAssignModal(false); + } catch (error) { + toast.error('Failed to assign user'); + } finally { + setIsAssigningParticipant(false); + } + }; + + const handleRetriggerEvaluators = async () => { + try { + setLoading(true); + await onboardingService.retriggerEvaluators(applicationId!); + toast.success('Evaluators re-assigned successfully'); + await fetchApplication(); + } catch (error) { + toast.error('Failed to re-assign evaluators'); + } finally { + setLoading(false); + } + }; + + if (loading) { + return
Loading...
; + } + + if (!application) { + return
Application not found
; + } + + // Determine if current user has an active interview and if they have submitted feedback + const interviewsList = Array.isArray(interviews) ? interviews : []; + + // For action buttons, we only care about pending interviews + const activeInterviewForUser = interviewsList.find(i => + ['Scheduled', 'Rescheduled', 'Pending', 'In Progress'].includes(i.status) && + i.participants?.some((p: any) => p.userId === currentUser?.id) + ); + + // For checking if a decision was ALREADY made, we look at ANY interview the user participated in for the current level + const lastInterviewForUser = [...interviewsList].reverse().find(i => + i.participants?.some((p: any) => p.userId === currentUser?.id) + ); + + const currentUserEvaluation = (activeInterviewForUser || lastInterviewForUser)?.evaluations?.find( + (e: any) => e.evaluatorId === currentUser?.id + ); + + // Helper to check interview level completion + const isInterviewCompleted = (level: number) => { + return interviewsList.some(i => (Number(i.level) === level) && i.status === 'Completed'); + }; + + const isInterviewActive = (level: number) => { + return interviewsList.some(i => (Number(i.level) === level) && i.status === 'Scheduled'); + }; + + // Robust checks for feedback and decision + // 1. If there's an active interview, feedback is required before Approve/Reject + // 2. hasMadeDecision should check if the evaluation has a recommendation + const hasSubmittedFeedback = !!currentUserEvaluation; + + // Specific to the current active interview context + const hasSubmittedFeedbackForActive = activeInterviewForUser && hasSubmittedFeedback; + const policyManagedStages: { [key: string]: string } = { + 'Level 1 Interview Pending': 'INTERVIEW_LEVEL_1', + 'Level 2 Interview Pending': 'INTERVIEW_LEVEL_2', + 'Level 2 Recommended': 'INTERVIEW_LEVEL_2', + 'Level 3 Interview Pending': 'INTERVIEW_LEVEL_3', + 'LOI In Progress': 'LOI_APPROVAL', + 'LOA Pending': 'LOA_APPROVAL' + }; + + const currentStageCode = policyManagedStages[application.status]; + const currentUserStageAction = application.stageApprovals?.find( + (a: any) => + a.stageCode === currentStageCode && + String(a.actorUserId) === String(currentUser?.id) + ); + + const hasMadeStageDecision = !!currentUserStageAction; + + const hasMadeDecisionForUser = !!currentUserStageAction || + currentUserEvaluation?.decision === 'Approved' || + currentUserEvaluation?.decision === 'Rejected' || + ['Approved', 'Rejected', 'Selected'].includes(currentUserEvaluation?.recommendation || ''); + + // Final visibility flags + const isAdmin = currentUser && ['DD Admin', 'Super Admin', 'NBH', 'DD Lead', 'DD Head', 'Finance', 'Finance Admin', 'FDD'].includes(currentUser.role); + const isAdministrativeStage = [ + 'Level 3 Approved', 'FDD Verification', + 'LOI In Progress', 'LOI Issued', 'Statutory LOI Ack', + 'Architecture Team Assigned', 'Architecture Document Upload', 'Architecture Team Completion', + 'Statutory GST', 'Statutory PAN', 'Statutory Nodal', 'Statutory Check', + 'Statutory Partnership', 'Statutory Firm Reg', 'Statutory Rental', + 'Statutory Virtual Code', 'Statutory Domain', 'Statutory MSD', + 'LOA Pending', 'EOR Complete', 'Inauguration' + ].includes(application.status); + + const finalDepositVerified = getDeposit('FINAL')?.status === 'Verified'; + const isLoaLocked = application.status === 'LOA Pending' && !finalDepositVerified; + + // Sequential Enforcement for LOI APPROVAL (SRS 6.16.3.5) + // Approval Chain: Finance -> DD-Head -> NBH + // Sequential Enforcement for LOI APPROVAL (SRS 6.16.3.5) + // Approval Chain: Finance -> DD-Head -> NBH + const financeApproved = application.stageApprovals?.some( + (a: any) => a.stageCode === 'LOI_APPROVAL' && a.actorRole === 'Finance' && a.decision === 'Approved' + ); + const ddHeadApproved = application.stageApprovals?.some( + (a: any) => a.stageCode === 'LOI_APPROVAL' && a.actorRole === 'DD Head' && a.decision === 'Approved' + ); + + // Sequential Enforcement for LOA APPROVAL (SRS 6.18.3.1) + // Approval Chain: DD-Head -> NBH + const ddHeadLoaApproved = application.stageApprovals?.some( + (a: any) => a.stageCode === 'LOA_APPROVAL' && a.actorRole === 'DD Head' && a.decision === 'Approved' + ); + + let sequenceMet = true; + if (!['Super Admin', 'DD Admin'].includes(currentUser?.role || '')) { + // LOI Sequence Enforcement + if (application.status === 'LOI In Progress') { + if (currentUser?.role === 'DD Head') sequenceMet = !!financeApproved; + if (currentUser?.role === 'NBH') { + sequenceMet = !!ddHeadApproved; // NBH can only approve after DD Head + } + // Roles not in the sequence (like DD Lead or FDD) should not see the buttons for LOI issuance decision + if (!['Finance', 'DD Head', 'NBH'].includes(currentUser?.role || '')) { + sequenceMet = false; } } - if (!rejectionReason.trim()) { - toast.warning('Please enter a reason for rejection'); - return; + // LOA Sequence Enforcement + if (application.status === 'LOA Pending') { + if (currentUser?.role === 'NBH') { + sequenceMet = !!ddHeadLoaApproved; // NBH can only approve after DD Head + } + // Roles not in the sequence (like Finance or FDD) should not see the buttons for LOA decision + if (!['DD Head', 'NBH'].includes(currentUser?.role || '')) { + sequenceMet = false; + } } + } - const policyManagedStages: { [key: string]: string } = { - 'Level 1 Interview Pending': 'INTERVIEW_LEVEL_1', - 'Level 2 Interview Pending': 'INTERVIEW_LEVEL_2', - 'Level 2 Recommended': 'INTERVIEW_LEVEL_2', - 'Level 3 Interview Pending': 'INTERVIEW_LEVEL_3', - 'LOI In Progress': 'LOI_APPROVAL', - 'LOA Pending': 'LOA_APPROVAL' + // 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 AND sequence is valid + const shouldShowApproveReject = + !isLoaLocked && ( + (!hasMadeDecisionForUser && !!hasSubmittedFeedbackForActive) || + (!!isAdmin && !!isAdministrativeStage && !hasMadeStageDecision && !!sequenceMet) + ); + + const shouldShowDecisionMessage = !!hasMadeDecisionForUser && (!isAdministrativeStage || !!hasMadeStageDecision); + + + + + const renderFddAuditContent = () => { + const assignments = application?.fddAssignments || []; + + 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) => { + // Look in documents array (which is state) + return (documents || []).find((d: any) => d.documentType === typeName); }; - const stageCodeForPolicy = policyManagedStages[application.status]; - - if (stageCodeForPolicy) { - await onboardingService.submitStageDecision({ - applicationId: application.id, - stageCode: stageCodeForPolicy, - decision: 'Rejected', - remarks: rejectionReason, - interviewId: activeInterview?.id - }); - } else { - await onboardingService.updateApplicationStatus(applicationId!, { - status: 'Rejected', - remarks: rejectionReason - }); + if (assignments.length === 0 && (application.status !== 'FDD Verification' && application.status !== 'LOI In Progress')) { + return ( +
+ +

No FDD Assignment

+

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

+
+ ); } - toast.success('Application rejected'); - setShowRejectModal(false); - setRejectionReason(''); - fetchApplication(); - } catch (error) { - console.error('Rejection error:', error); - toast.error('Failed to process rejection'); - } finally { - setIsRejecting(false); - } - }; - - const handleGenerateDealerCodes = async () => { - try { - await onboardingService.generateDealerCodes(applicationId!); - toast.success('Dealer codes generated successfully'); - fetchApplication(); - } catch (error) { - console.error('Generate codes error:', error); - toast.error('Failed to generate dealer codes'); - } - }; - - const handleAssignArchitecture = async () => { - if (!architectureLeadId) { - toast.warning('Please select an architecture lead'); - return; - } - try { - setIsAssigningArchitecture(true); - await onboardingService.assignArchitectureTeam(applicationId!, architectureLeadId); - toast.success('Architecture team assigned successfully'); - setShowAssignArchitectureModal(false); - fetchApplication(); // Refresh to update status - } catch (error) { - toast.error('Failed to assign architecture team'); - } finally { - setIsAssigningArchitecture(false); - } - }; - - const handleUpdateArchitectureStatus = async () => { - try { - setIsUpdatingArchitecture(true); - await onboardingService.updateArchitectureStatus(applicationId!, architectureStatus, architectureRemarks); - toast.success('Architecture status updated successfully'); - setShowArchitectureStatusModal(false); - fetchApplication(); - } catch (error) { - toast.error('Failed to update architecture status'); - } finally { - setIsUpdatingArchitecture(false); - } - }; - - const handleAddParticipant = async () => { - if (!selectedUser) { - toast.warning('Please select a user'); - return; - } - try { - setIsAssigningParticipant(true); - await onboardingService.addParticipant({ - requestId: applicationId, - requestType: 'application', - userId: selectedUser, - participantType: participantType || 'contributor' - }); - toast.success('User assigned successfully!'); - // Refresh application data - fetchApplication(); - setSelectedUser(''); - setShowAssignModal(false); - } catch (error) { - toast.error('Failed to assign user'); - } finally { - setIsAssigningParticipant(false); - } - }; - - const handleRetriggerEvaluators = async () => { - try { - setLoading(true); - await onboardingService.retriggerEvaluators(applicationId!); - toast.success('Evaluators re-assigned successfully'); - await fetchApplication(); - } catch (error) { - toast.error('Failed to re-assign evaluators'); - } finally { - setLoading(false); - } - }; - - if (loading) { - return
Loading...
; - } - - if (!application) { - return
Application not found
; - } - - // Determine if current user has an active interview and if they have submitted feedback - const interviewsList = Array.isArray(interviews) ? interviews : []; - - // For action buttons, we only care about pending interviews - const activeInterviewForUser = interviewsList.find(i => - ['Scheduled', 'Rescheduled', 'Pending', 'In Progress'].includes(i.status) && - i.participants?.some((p: any) => p.userId === currentUser?.id) - ); - - // For checking if a decision was ALREADY made, we look at ANY interview the user participated in for the current level - const lastInterviewForUser = [...interviewsList].reverse().find(i => - i.participants?.some((p: any) => p.userId === currentUser?.id) - ); - - const currentUserEvaluation = (activeInterviewForUser || lastInterviewForUser)?.evaluations?.find( - (e: any) => e.evaluatorId === currentUser?.id - ); - - // Helper to check interview level completion - const isInterviewCompleted = (level: number) => { - return interviewsList.some(i => (Number(i.level) === level) && i.status === 'Completed'); - }; - - const isInterviewActive = (level: number) => { - return interviewsList.some(i => (Number(i.level) === level) && i.status === 'Scheduled'); - }; - - // Robust checks for feedback and decision - // 1. If there's an active interview, feedback is required before Approve/Reject - // 2. hasMadeDecision should check if the evaluation has a recommendation - const hasSubmittedFeedback = !!currentUserEvaluation; - - // Specific to the current active interview context - const hasSubmittedFeedbackForActive = activeInterviewForUser && hasSubmittedFeedback; - const policyManagedStages: { [key: string]: string } = { - 'Level 1 Interview Pending': 'INTERVIEW_LEVEL_1', - 'Level 2 Interview Pending': 'INTERVIEW_LEVEL_2', - 'Level 2 Recommended': 'INTERVIEW_LEVEL_2', - 'Level 3 Interview Pending': 'INTERVIEW_LEVEL_3', - 'LOI In Progress': 'LOI_APPROVAL', - 'LOA Pending': 'LOA_APPROVAL' - }; - - const currentStageCode = policyManagedStages[application.status]; - const currentUserStageAction = application.stageApprovals?.find( - (a: any) => - a.stageCode === currentStageCode && - String(a.actorUserId) === String(currentUser?.id) - ); - - const hasMadeStageDecision = !!currentUserStageAction; - - const hasMadeDecisionForUser = !!currentUserStageAction || - currentUserEvaluation?.decision === 'Approved' || - currentUserEvaluation?.decision === 'Rejected' || - ['Approved', 'Rejected', 'Selected'].includes(currentUserEvaluation?.recommendation || ''); - - // Final visibility flags - const isAdmin = currentUser && ['DD Admin', 'Super Admin', 'NBH', 'DD Lead', 'DD Head', 'Finance'].includes(currentUser.role); - const isAdministrativeStage = [ - 'Level 3 Approved', 'FDD Verification', - 'LOI In Progress', 'LOI Issued', 'Statutory LOI Ack', - 'Architecture Team Assigned', 'Architecture Document Upload', 'Architecture Team Completion', - 'Statutory GST', 'Statutory PAN', 'Statutory Nodal', 'Statutory Check', - 'Statutory Partnership', 'Statutory Firm Reg', 'Statutory Rental', - 'Statutory Virtual Code', 'Statutory Domain', 'Statutory MSD', - '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 = - !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} - +
+ {/* FDD Partner Actions (Only visible to FDD role in Active FDD stage) */} + {currentUser?.role === 'FDD' && application.status === 'FDD Verification' && ( +
+
+

+ + Audit Workspace +

+

Capture findings and upload the final audit report to progress the LOI issuance.

+
+ { + const file = e.target.files?.[0]; + if (!file) return; + try { + setIsUploading(true); + const formData = new FormData(); + formData.append('file', file); + formData.append('documentType', 'FDD Audit Report'); + formData.append('applicationId', application.id); + await onboardingService.uploadDocument(application.id, formData); + toast.success('FDD Audit Report uploaded successfully'); + fetchApplication(); + } catch (err) { + toast.error('Upload failed'); + } finally { + setIsUploading(false); + } + }} + /> + + + +
+
+ )} + + {/* Financial Document Checklist Section */} + + + +
Financial Artefacts Checklist
+ Verify before sign-off +
- {(!assignment.reports || assignment.reports.length === 0) ? ( -
- -

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

-
- ) : ( -
- {assignment.reports.map((report: any) => ( -
-
-
+
+ {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 && ( +
+ +
+ )} +
+ ); + })} +
+ + + +
+
+

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()}
@@ -1735,25 +1982,25 @@ export function ApplicationDetails() { "{report.findings || 'No detail findings provided by the auditor.'}"
-
+
-
+
{report.reportDocument ? (
-
- -
-
-

{report.reportDocument.fileName}

-

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

-
+
+ +
+
+

{report.reportDocument.fileName}

+

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

+
- -
+
+
-
- ))} -
- )} -
-
- ))} -
- ); - }; - - return ( -
- {/* Header */} -
-
- -
-

{application.name}

-

{application.registrationNumber}

+ ))} +
+ )} + + + ))}
-
- + ); + }; + + + return ( +
+ {/* Header */} +
+
+ +
+

{application.name}

+

{application.registrationNumber}

+
+
+
+ +
-
-
- {/* Main Content */} -
- {/* Core Details Card */} - - - Applicant Information - - -
-
- -
-

Full Name

-

{application.name}

-
-
- -
- -
-

Email

-

{application.email}

-
-
- -
- -
-

Phone

-

{application.phone}

-
-
- -
- -
-

Age

-

{application.age ? `${application.age} years` : 'N/A'}

-
-
- -
- -
-

Education

-

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

-
-
- -
- -
-

Preferred Location

-

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

-
-
- -
- -
-

Location Type

-

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

-
-
- -
- -
-

Owns Bike

-

{application.ownRoyalEnfield === 'yes' ? 'Yes' : 'No'}

-
-
- - {application.ownRoyalEnfield === 'yes' && ( -
- -
-

Bike Model

-

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

-
-
- )} - -
- -
-

Existing Dealer

-

{application.existingDealer === 'yes' ? 'Yes' : 'No'}

-
-
- - {application.existingDealer === 'yes' && ( +
+ {/* Main Content */} +
+ {/* Core Details Card */} + + + Applicant Information + + +
-

Company Name

-

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

+

Full Name

+

{application.name}

- )} -
- -
-

Source

-

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

+
+ +
+

Email

+

{application.email}

+
+ +
+ +
+

Phone

+

{application.phone}

+
+
+ +
+ +
+

Age

+

{application.age ? `${application.age} years` : 'N/A'}

+
+
+ +
+ +
+

Education

+

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

+
+
+ +
+ +
+

Preferred Location

+

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

+
+
+ +
+ +
+

Location Type

+

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

+
+
+ +
+ +
+

{ + setTempFirmType(application.constitutionType || ''); + setShowFirmTypeModal(true); + }}> + Proposed Firm Type + +

+

+ {application.constitutionType || 'Not Provided'} +

+
+
+ +
+ +
+

Owns Bike

+

{application.ownRoyalEnfield === 'yes' ? 'Yes' : 'No'}

+
+
+ + {application.ownRoyalEnfield === 'yes' && ( +
+ +
+

Bike Model

+

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

+
+
+ )} + +
+ +
+

Existing Dealer

+

{application.existingDealer === 'yes' ? 'Yes' : 'No'}

+
+
+ + {application.existingDealer === 'yes' && ( +
+ +
+

Company Name

+

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

+
+
+ )} + +
+ +
+

Source

+

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

+
+
+ + {application.questionnaireMarks !== undefined && ( +
+ +
+

Questionnaire Score

+

{application.questionnaireMarks}/100

+
+
+ )}
- {application.questionnaireMarks !== undefined && ( -
- -
-

Questionnaire Score

-

{application.questionnaireMarks}/100

+ + +
+

Address

+

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

+
+ +
+

Pincode

+

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

+
+ +
+

Description

+

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

+
+ +
+

Past Experience

+

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

+
+ + + + {/* Tabs Section */} + {/* Only show tabs for shortlisted applications (opportunity requests and regular dealership requests) */} + {/* Hide tabs for non-opportunity requests (lead generation) */} + {application.isShortlisted !== false && ( + + + +
+ + Questionnaire + Progress + Documents + Interviews + FDD Audit + EOR Checklist + Payments + Audit Trail +
-
- )} -
+ - + + {/* Questionnaire Response Tab */} + + + -
-

Address

-

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

-
- -
-

Pincode

-

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

-
- -
-

Description

-

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

-
- -
-

Past Experience

-

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

-
-
- - - {/* Tabs Section */} - {/* Only show tabs for shortlisted applications (opportunity requests and regular dealership requests) */} - {/* Hide tabs for non-opportunity requests (lead generation) */} - {application.isShortlisted !== false && ( - - - -
- - Questionnaire - Progress - Documents - Interviews - FDD Audit - EOR Checklist - Payments - Audit Trail - -
-
- - - {/* Questionnaire Response Tab */} - - - - - {/* Progress Tab */} - -
-
-

Application Journey

- {application.progress}% Complete + {/* Progress Tab */} + +
+
+

Application Journey

+ {application.progress}% Complete +
+
- -
-
- {(() => { - 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) +
+ {(() => { + 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 { - name: p.user?.name || 'Unknown', - role: p.user?.role || 'Reviewer', - status: approval ? (approval.decision === 'Approved' ? 'approved' : 'rejected') : 'pending' - }; - }); - }; + 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) + ); - 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 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' + }; - const approvers = getApproverStatus(stageCode); - if (approvers.length === 0) return null; + const stageCode = stageMapping[stageName]; + if (!stageCode) return null; - return ( -
- {approvers.map((approver, i) => ( -
-
- {approver.name.split(' ').map((n: string) => n[0]).join('').substring(0, 2).toUpperCase()} -
-
- {approver.name} - {approver.role} -
+ const approvers = getApproverStatus(stageCode); + if (approvers.length === 0) return null; - {/* Status Dot Overlay */} -
- - {/* Tooltip */} -
- {approver.role}: {approver.status.toUpperCase()} -
-
- ))} -
- ); - }; - - return processStages.map((stage, index) => ( -
-
-
-
- {stage.isParallel ? ( - - ) : stage.isLocked ? ( -
- -
-
- - Stage Locked - - {stage.lockMessage} -
-
-
+ return ( +
+ {approvers.map((approver, i) => ( +
+
+ {approver.name.split(' ').map((n: string) => n[0]).join('').substring(0, 2).toUpperCase()}
- ) : ( - <> - {stage.status === 'completed' ? ( - - ) : stage.status === 'active' ? ( - - ) : ( -
- )} - +
+ {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' ? ( + + ) : stage.status === 'active' ? ( + + ) : ( +
+ )} + + )} +
+ {index < processStages.length - 1 && !stage.isParallel && ( +
)}
- {index < processStages.length - 1 && !stage.isParallel && ( -
- )} -
-
-

{stage.name}

- {stage.description && ( -

{stage.description}

- )} +
+

{stage.name}

+ {stage.description && ( +

{stage.description}

+ )} - {renderApprovers(stage.name as string)} + {renderApprovers(stage.name as string)} - {stage.evaluators && stage.evaluators.length > 0 && !['LOI Approval', 'LOA', '1st Level Interview', '2nd Level Interview', '3rd Level Interview'].includes(stage.name as string) && ( -

- - Evaluators: {stage.evaluators.join(' + ')} -

- )} + {stage.evaluators && stage.evaluators.length > 0 && !['LOI Approval', 'LOA', '1st Level Interview', '2nd Level Interview', '3rd Level Interview'].includes(stage.name as string) && ( +

+ + Evaluators: {stage.evaluators.join(' + ')} +

+ )} - {(() => { - const expectedMap: Record = { - 4: 2, // L1 Interview (ZM + RBM) - 5: 2, // L2 Interview (ZBH + DD Lead) - 6: 2, // L3 Interview (NBH + DD Head) - 8: 3, // LOI Approval (Finance + DD Head + NBH) - 12: 2 // LOA Approval (DD Head + NBH) - }; - const stageId = Number(stage.id); - const expectedCount = expectedMap[stageId]; - const actualCount = stage.evaluators?.length || 0; + {(() => { + const expectedMap: Record = { + 3: 2, // Shortlist (Expected to have auto-mapped interviewers for next steps) + 4: 2, // L1 Interview (ZM + RBM) + 5: 2, // L2 Interview (ZBH + DD Lead) + 6: 2, // L3 Interview (NBH + DD Head) + 8: 3, // LOI Approval (Finance + DD Head + NBH) + 12: 2 // LOA Approval (DD Head + NBH) + }; + const stageId = Number(stage.id); + const expectedCount = expectedMap[stageId]; + + // For Shortlist step, check if Interview Level 1 evaluators are missing + let actualCount = stage.evaluators?.length || 0; + if (stageId === 3) { + const l1Evaluators = (application.participants || []).filter((p: any) => p.metadata?.interviewLevel === 1 || p.metadata?.interviewLevel === '1'); + actualCount = l1Evaluators.length; + } + + // Only show warning if stage is active/completed + // For Shortlist (3), show ONLY after it's finished to avoid clutter during earlier steps + const isEligibleForWarning = stageId === 3 ? (stage.status === 'completed') : (stage.status !== 'pending'); + + if (expectedCount && actualCount < expectedCount && application.status !== 'Rejected' && isEligibleForWarning) { + return ( +
+ + + Missing Evaluators + + {actualCount === 0 + ? "Respective role users were not found for this location." + : `Some roles (${actualCount}/${expectedCount}) are missing for this location.` + } + + + +
+ ); + } + return null; + })()} + + {(() => { + const stageDocsCount = documents.filter(doc => + doc.stage === stage.name || + (!doc.stage && doc.documentType?.toLowerCase().includes(stage.name.toLowerCase().split(' ')[0])) + ).length; - if (expectedCount && actualCount < expectedCount && application.status !== 'Rejected') { return ( -
- - - Missing Evaluators - - {actualCount === 0 - ? "Respective role users were not found for this location." - : `Some roles (${actualCount}/${expectedCount}) are missing for this location.` - } - - - +
+
); - } - return null; - })()} + })()} - {(() => { - const stageDocsCount = documents.filter(doc => - doc.stage === stage.name || - (!doc.stage && doc.documentType?.toLowerCase().includes(stage.name.toLowerCase().split(' ')[0])) - ).length; - - return ( -
- -
- ); - })()} - -

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

+

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

+
-
- {stage.isParallel && stage.branches && ( -
- {stage.branches.map((branch, branchIndex) => { - const branchKey = branch.name.toLowerCase().replace(/\s+/g, '-'); - const isExpanded = expandedBranches[branchKey]; - const branchColor = branch.color === 'blue' ? 'blue' : 'green'; + {stage.isParallel && stage.branches && ( +
+ {stage.branches.map((branch, branchIndex) => { + const branchKey = branch.name.toLowerCase().replace(/\s+/g, '-'); + const isExpanded = expandedBranches[branchKey]; + const branchColor = branch.color === 'blue' ? 'blue' : 'green'; - return ( -
- + return ( +
+ - {isExpanded && ( -
- {branch.stages.map((branchStage) => ( -
-
-
-
- {branchStage.status === 'completed' ? ( - - ) : branchStage.status === 'active' ? ( - - ) : ( -
+ {isExpanded && ( +
+ {branch.stages.map((branchStage) => ( +
+
+
+
+ {branchStage.status === 'completed' ? ( + + ) : branchStage.status === 'active' ? ( + + ) : ( +
+ )} +
+
+
+

{branchStage.name}

+ {branchStage.description && ( +

{branchStage.description}

)} + + {(() => { + const branchDocsCount = documents.filter(doc => + doc.documentType?.toLowerCase().includes(branchStage.name.toLowerCase().split(' ')[0]) || + doc.stage === branchStage.name + ).length; + + return ( +
+ +
+ ); + })()} +

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

-
-

{branchStage.name}

- {branchStage.description && ( -

{branchStage.description}

- )} - - {(() => { - const branchDocsCount = documents.filter(doc => - doc.documentType?.toLowerCase().includes(branchStage.name.toLowerCase().split(' ')[0]) || - doc.stage === branchStage.name - ).length; - - return ( -
- -
- ); - })()} -

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

-
-
- ))} -
- )} -
- ); - })} -
-
- )} -
- )) - })()} -
- + ))} +
+ )} +
+ ); + })} +
+
+ )} +
+ )) + })()} +
+ - {/* Documents Tab */} - -
-

Uploaded Documents

- -
+ {/* Documents Tab */} + +
+

Uploaded Documents

+ +
-
- - - - File Name - Type - Upload Date - Uploader - Actions - - - - {documents.length === 0 ? ( - - - No documents uploaded yet - - - ) : ( - documents.map((doc) => ( - - - - {doc.fileName} - - {doc.documentType} - {new Date(doc.createdAt).toLocaleDateString()} - - {doc.uploader?.fullName || (doc.uploadedBy ? 'Unknown User' : 'Applicant')} - - -
- -
-
-
- )))} -
-
-
-
- - {/* Interviews Tab */} - -
-

Scheduled Interviews

- Level - Date & Time - Type - Location/Link - Status - Scheduled By - Actions + File Name + Type + Upload Date + Uploader + Actions - {(!interviews || interviews.length === 0) ? ( + {documents.length === 0 ? ( - - No interviews scheduled yet + + No documents uploaded yet ) : ( - (Array.isArray(interviews) ? interviews : []).map((interview) => ( - - Level {interview.level} - {interview.scheduleDate ? new Date(interview.scheduleDate).toLocaleString() : 'N/A'} - {interview.interviewType} + documents.map((doc) => ( + + + + {doc.fileName} + + {doc.documentType} + {new Date(doc.createdAt).toLocaleDateString()} - {interview.interviewType?.toLowerCase().includes('virtual') ? ( - - Join Meeting - - ) : ( - interview.linkOrLocation - )} + {doc.uploader?.fullName || (doc.uploadedBy ? 'Unknown User' : 'Applicant')} - - {interview.status} - - - {interview.scheduler?.fullName || interview.scheduledBy || 'N/A'} - - {(interview.status === 'Scheduled' || interview.status === 'scheduled') && ( - - )} + - )) - )} + )))}
-
+
-
-

Interview Feedback

- {(!interviews || interviews.length === 0) ? ( -

No interviews scheduled.

- ) : ( - (Array.isArray(interviews) ? interviews : []).map((interview) => ( -
-

- Level {interview.level} Interview - - ({new Date(interview.scheduleDate).toLocaleDateString()} - {interview.interviewType}) - -

- {interview.evaluations && interview.evaluations.length > 0 ? ( - - - - Interviewer - Role - - {interview.level === 1 ? 'Score (KT Matrix)' : 'Overall Score'} - - Remarks - Recommendation - - - - {interview.evaluations.map((evalItem: any) => ( - - {evalItem.evaluator?.fullName} - {evalItem.evaluator?.role?.roleName || 'N/A'} - - {evalItem.ktMatrixScore ? ( - = 50 ? 'outline' : 'destructive') - : (Number(evalItem.ktMatrixScore) >= 5 ? 'outline' : 'destructive') - }> - {evalItem.ktMatrixScore}/{interview.level === 1 ? '100' : '10'} - - ) : 'N/A'} - - - {evalItem.remarks ? ( -
- {evalItem.remarks} - {evalItem.feedbackDetails && evalItem.feedbackDetails.length > 0 && ( - - )} -
- ) : evalItem.feedbackDetails && evalItem.feedbackDetails.length > 0 ? ( - - ) : ( - evalItem.qualitativeFeedback || '-' - )} -
- {evalItem.recommendation || '-'} -
- ))} -
-
- ) : ( -

No feedback recorded yet.

- )} -
- )) - )} -
- - {['Level 2 Approved', 'Level 3 Interview Pending', 'Approved', 'Onboarded'].includes(application.status) && ( + {/* Interviews Tab */} +
-

Level 2 Interview Summary

-
-

Decision: Approved by both ZBH and DD Lead

-

Overall Assessment: Strong candidate with excellent business plan

+

Scheduled Interviews

+
+ + + + Level + Date & Time + Type + Location/Link + Status + Scheduled By + Actions + + + + {(!interviews || interviews.length === 0) ? ( + + + No interviews scheduled yet + + + ) : ( + (Array.isArray(interviews) ? interviews : []).map((interview) => ( + + Level {interview.level} + {interview.scheduleDate ? new Date(interview.scheduleDate).toLocaleString() : 'N/A'} + {interview.interviewType} + + {interview.interviewType?.toLowerCase().includes('virtual') ? ( + + Join Meeting + + ) : ( + interview.linkOrLocation + )} + + + + {interview.status} + + + {interview.scheduler?.fullName || interview.scheduledBy || 'N/A'} + + {(interview.status === 'Scheduled' || interview.status === 'scheduled') && ( + + )} + + + )) + )} + +
- )} - - - {renderFddAuditContent()} - - - {/* EOR Checklist Tab */} - -
-

Essential Operating Requirements

- {Math.round(eorProgress)}% Complete -
- - -
- {(eorData?.items || eorChecklist).map((item: any) => { - const docType = item.description || item.item; - const hasDocument = !!item.proofDocument; - - return ( -
- - - {/* Clickable Info Area */} -
{ - setSelectedStage(`EOR: ${docType}`); - setUploadDocType(docType); - setShowDocumentsModal(true); - if (!hasDocument) setShowUploadForm(true); - else setShowUploadForm(false); - }} - > -
- - {docType} - - {hasDocument && !item.isCompliant && ( - - Needs Verification - - )} -
- {hasDocument && ( -
- - {item.proofDocument.fileName} -
- )} - {!hasDocument && ( - Click to upload proof - )} -
- - {/* Separate Action Area (No modal trigger) */} -
- {hasDocument && !item.isCompliant && isAdmin && ( -
- - -
- )} - - {(item.isCompliant || item.completed) && ( -
- -
- )} - - {!hasDocument && ( -
- -
- )} -
-
- ); - })} -
- - {eorProgress === 100 && isAdmin && (application.status === 'EOR In Progress' || application.status === 'LOA Pending') && ( -
-
-
- -
-
-

EOR Checklist Complete

-

All 12 mandatory requirements have been verified. You can now complete the audit and move to final inauguration.

-
- -
-
- )} -
- - {/* Payments Tab */} - -
-

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 */} - - -
- {auditLoading ? ( -
-
- Loading audit trail... -
- ) : auditLogs.length === 0 ? ( -
- No audit logs recorded yet for this application. -
+
+

Interview Feedback

+ {(!interviews || interviews.length === 0) ? ( +

No interviews scheduled.

) : ( - auditLogs.map((log: any) => ( -
-
-
-
-

{log.description || log.action}

- - {new Date(log.timestamp).toLocaleString()} - -
-

by {log.userName || 'System'}

- {log.changes && log.changes.length > 0 && ( -
- {log.changes.map((change: string, idx: number) => ( -

{change}

+ (Array.isArray(interviews) ? interviews : []).map((interview) => ( +
+

+ Level {interview.level} Interview + + ({new Date(interview.scheduleDate).toLocaleDateString()} - {interview.interviewType}) + +

+ {interview.evaluations && interview.evaluations.length > 0 ? ( + + + + Interviewer + Role + + {interview.level === 1 ? 'Score (KT Matrix)' : 'Overall Score'} + + Remarks + Recommendation + + + + {interview.evaluations.map((evalItem: any) => ( + + {evalItem.evaluator?.fullName} + {evalItem.evaluator?.role?.roleName || 'N/A'} + + {evalItem.ktMatrixScore ? ( + = 50 ? 'outline' : 'destructive') + : (Number(evalItem.ktMatrixScore) >= 5 ? 'outline' : 'destructive') + }> + {evalItem.ktMatrixScore}/{interview.level === 1 ? '100' : '10'} + + ) : 'N/A'} + + + {evalItem.remarks ? ( +
+ {evalItem.remarks} + {evalItem.feedbackDetails && evalItem.feedbackDetails.length > 0 && ( + + )} +
+ ) : evalItem.feedbackDetails && evalItem.feedbackDetails.length > 0 ? ( + + ) : ( + evalItem.qualitativeFeedback || '-' + )} +
+ {evalItem.recommendation || '-'} +
))} - - )} - +
+
+ ) : ( +

No feedback recorded yet.

+ )}
)) )}
- - - - - - )} -
- {/* Right Sidebar - Summary and Actions */} -
- {/* Summary Card */} - - - Summary - - -
-

Registration ID

-

{application.registrationNumber}

-
-
-

Current Status

- - {application.status} - -
- {application.rank && ( -
-

Rank

-

- {application.rank} of {application.totalApplicantsAtLocation} - in {application.preferredLocation} -

-
- )} -
-

Progress

-
- - {application.progress}% -
-
- {application.deadline && ( -
-

Questionnaire Deadline

-

{new Date(application.deadline).toLocaleDateString()}

-
- )} -
-
+ {['Level 2 Approved', 'Level 3 Interview Pending', 'Approved', 'Onboarded'].includes(application.status) && ( +
+

Level 2 Interview Summary

+
+

Decision: Approved by both ZBH and DD Lead

+

Overall Assessment: Strong candidate with excellent business plan

+
+
+ )} + - {/* Actions Card */} - {/* Only show Actions card for shortlisted applications (opportunity requests and regular dealership requests) */} - {/* Hide Actions for non-opportunity requests (lead generation) - these are read-only records */} - {application.isShortlisted !== false && ( + + {renderFddAuditContent()} + + + {/* EOR Checklist Tab */} + +
+

Essential Operating Requirements

+ {Math.round(eorProgress)}% Complete +
+ + +
+ {(eorData?.items || eorChecklist).map((item: any) => { + const docType = item.description || item.item; + const hasDocument = !!item.proofDocument; + + return ( +
+ + + {/* Clickable Info Area */} +
{ + setSelectedStage(`EOR: ${docType}`); + setUploadDocType(docType); + setShowDocumentsModal(true); + if (!hasDocument) setShowUploadForm(true); + else setShowUploadForm(false); + }} + > +
+ + {docType} + + {hasDocument && !item.isCompliant && ( + + Needs Verification + + )} +
+ {hasDocument && ( +
+ + {item.proofDocument.fileName} +
+ )} + {!hasDocument && ( + Click to upload proof + )} +
+ + {/* Separate Action Area (No modal trigger) */} +
+ {hasDocument && !item.isCompliant && isAdmin && ( +
+ + +
+ )} + + {(item.isCompliant || item.completed) && ( +
+ +
+ )} + + {!hasDocument && ( +
+ +
+ )} +
+
+ ); + })} +
+ + {eorProgress === 100 && isAdmin && (application.status === 'EOR In Progress' || application.status === 'LOA Pending') && ( +
+
+
+ +
+
+

EOR Checklist Complete

+

All 12 mandatory requirements have been verified. You can now complete the audit and move to final inauguration.

+
+ +
+
+ )} +
+ + {/* Payments Tab */} + +
+

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 */} + + +
+ {auditLoading ? ( +
+
+ Loading audit trail... +
+ ) : auditLogs.length === 0 ? ( +
+ No audit logs recorded yet for this application. +
+ ) : ( + auditLogs.map((log: any) => ( +
+
+
+
+

{log.description || log.action}

+ + {new Date(log.timestamp).toLocaleString()} + +
+

by {log.userName || 'System'}

+ {log.changes && log.changes.length > 0 && ( +
+ {log.changes.map((change: string, idx: number) => ( +

{change}

+ ))} +
+ )} +
+
+ )) + )} +
+
+
+ + + + )} +
+ + {/* Right Sidebar - Summary and Actions */} +
+ {/* Summary Card */} - Actions + Summary - - {/* Show Approve/Reject block */} - {isLoaLocked && ( - - - Stage Locked - - Final Security Deposit (₹15L) must be verified by Finance before LOA Approval can proceed. - - - )} - - {shouldShowApproveReject && ( - <> - - - - - )} - - {(shouldShowDecisionMessage) && ( -
- You have {(currentUserStageAction?.decision === 'Approved' || currentUserEvaluation?.decision === 'Approved' || currentUserEvaluation?.recommendation === 'Approved' || currentUserEvaluation?.decision === 'Selected') ? 'Approved' : 'Rejected'} + +
+

Registration ID

+

{application.registrationNumber}

+
+
+

Current Status

+ + {application.status} + +
+ {application.rank && ( +
+

Rank

+

+ {application.rank} of {application.totalApplicantsAtLocation} + in {application.preferredLocation} +

)} - - - - - - {currentUser && ['DD Admin', 'Super Admin', 'DD AM', 'ASM'].includes(currentUser.role) && - !([1, 2, 3].every(level => interviews.some(i => i.level === level))) && ( - - )} - - {currentUser && ['DD Admin', 'Super Admin'].includes(currentUser.role) && application.status === 'Dealer Code Generation' && ( - <> - {!application.dealerCode && ( - - )} - - {application.dealerCode && ( - - )} - - )} - - {((currentUser && currentUser.id === application.architectureAssignedTo) || (currentUser && ['DD Admin', 'Super Admin'].includes(currentUser.role))) && - application.architectureStatus === 'IN_PROGRESS' && ( - - )} - - {/* Show Interview Feedback only if active interview exists AND feedback NOT submitted */} - {activeInterviewForUser && !hasSubmittedFeedback && ( - - - - - - { - setSelectedInterviewForFeedback(activeInterviewForUser); - if (activeInterviewForUser.level === 1) setShowKTMatrixModal(true); - else if (activeInterviewForUser.level === 2) setShowLevel2FeedbackModal(true); - else setShowLevel3FeedbackModal(true); - }} - > - Level {activeInterviewForUser.level} - {activeInterviewForUser.interviewType} - - - - )} - - {application.status === 'Questionnaire Pending' && ( - <> - - - - - )} - - {/* Dedicated Onboarding Button - Appears ONLY when everything is ready (last step) */} - {isAdmin && application.status === 'Inauguration' && !application.dealer && ( -
- {eorProgress < 100 && ( - - - - EOR Checklist must be 100% complete before onboarding. (Current: {eorProgress.toFixed(0)}%) - - - )} - +
+

Progress

+
+ + {application.progress}%
- )} - - {/* Dealer Onboarded Status & Link */} - {application.dealer && ( -
-
- - Dealer Profile Active -
-
- This application has been successfully onboarded as a dealer. A user account has been created for the dealer. -
- {application.dealerCode && ( -
- Dealer Code: - {application.dealerCode.code} -
- )} - +
+ {application.deadline && ( +
+

Questionnaire Deadline

+

{new Date(application.deadline).toLocaleDateString()}

)} - - {currentUser && ['DD Admin', 'Super Admin'].includes(currentUser.role) && ( - - - - - - - Assign User to Application - - Select a user and their role for this application. - - -
-
- - -
-
- - -
- -
-
-
- )} - )} -
-
- {/* Approve Modal */} - - - - Approve Application - - Provide approval remarks and optionally attach supporting documents. - - -
-
- -