diff --git a/src/App.tsx b/src/App.tsx index 7f7e5df..a250740 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -62,7 +62,6 @@ const AppLayout = ({ onLogout, title }: { onLogout: () => void, title: string }) - ); }; @@ -316,6 +315,7 @@ export default function App() { } /> + ); } diff --git a/src/api/API.ts b/src/api/API.ts index 8bd71ac..37e0180 100644 --- a/src/api/API.ts +++ b/src/api/API.ts @@ -31,6 +31,7 @@ export const API = { saveZonalManager: (data: any) => client.post('/master/zonal-managers', data), getDDLeads: () => client.get('/master/dd-leads'), saveDDLead: (data: any) => client.post('/master/dd-leads', data), + getManagersByRole: (params: any) => client.get('/master/managers', { params }), // Onboarding @@ -125,6 +126,9 @@ export const API = { // Resignation getResignationById: (id: string) => client.get(`/resignation/${id}`), + uploadResignationDocument: (id: string, data: any) => client.post(`/resignation/${id}/documents`, data, { + headers: { 'Content-Type': 'multipart/form-data' } + }), updateClearance: (id: string, data: any) => client.put(`/resignation/${id}/clearance`, data, { headers: data instanceof FormData ? { 'Content-Type': 'multipart/form-data' } : {} }), @@ -132,6 +136,9 @@ export const API = { // Termination getTerminationById: (id: string) => client.get(`/termination/${id}`), + uploadTerminationDocument: (id: string, data: any) => client.post(`/termination/${id}/documents`, data, { + headers: { 'Content-Type': 'multipart/form-data' } + }), updateTerminationStatus: (id: string, data: any) => client.post(`/termination/${id}/status`, data), issueSCN: (id: string, data: any) => client.post(`/termination/${id}/scn`, data), uploadSCNResponse: (id: string, data: any) => client.post(`/termination/${id}/scn-response`, data, { @@ -178,6 +185,7 @@ export const API = { getConstitutionalChangeById: (id: string) => client.get(`/constitutional-change/${id}`), createConstitutionalChange: (data: any) => client.post('/constitutional-change', data), updateConstitutionalChange: (id: string, action: string, data?: any) => client.post(`/constitutional-change/${id}/action`, { action, ...data }), + uploadConstitutionalDocuments: (id: string, documents: any[]) => client.post(`/constitutional-change/${id}/documents`, { documents }), // SLA getSlaConfigs: () => client.get('/sla/configs'), @@ -196,6 +204,7 @@ export const API = { submitFddReport: (data: any) => client.post('/fdd/report', data), getFddAssignment: (applicationId: string) => client.get(`/fdd/${applicationId}`), assignFddAgency: (data: any) => client.post('/fdd/assign', data), + flagNonResponsive: (data: any) => client.post('/fdd/flag', data), }; export default API; diff --git a/src/components/applications/AllApplicationsPage.tsx b/src/components/applications/AllApplicationsPage.tsx index 695a147..1e355aa 100644 --- a/src/components/applications/AllApplicationsPage.tsx +++ b/src/components/applications/AllApplicationsPage.tsx @@ -1,6 +1,7 @@ -import { useState } from 'react'; +import { useState, useEffect } from 'react'; import { ApplicationCard } from './ApplicationCard'; -import { mockApplications, locations, states, ApplicationStatus } from '../../lib/mock-data'; +import { locations, states, ApplicationStatus, Application } from '../../lib/mock-data'; +import { onboardingService } from '../../services/onboarding.service'; import { Button } from '../ui/button'; import { Input } from '../ui/input'; import { @@ -50,20 +51,79 @@ export function AllApplicationsPage({ onViewDetails, initialFilter = 'all' }: Al const [selectedIds, setSelectedIds] = useState([]); const [showShortlistModal, setShowShortlistModal] = useState(false); const [shortlistRemark, setShortlistRemark] = useState(''); - const [applicationsData, setApplicationsData] = useState(mockApplications); + const [applicationsData, setApplicationsData] = useState([]); + const [loading, setLoading] = useState(true); - // Filter to show ONLY applications that have NOT been shortlisted yet + useEffect(() => { + fetchApplications(); + }, []); + + const fetchApplications = async () => { + try { + setLoading(true); + const response = await onboardingService.getApplications(); + const rawData = response.data || (Array.isArray(response) ? response : []); + + // Map backend data to Application interface + const mappedApps: Application[] = rawData.map((app: any) => ({ + id: app.id, + registrationNumber: app.applicationId || 'N/A', + name: app.applicantName, + email: app.email, + phone: app.phone, + age: app.age, + education: app.education, + residentialAddress: app.address || app.city || '', + businessAddress: app.address || '', + preferredLocation: app.preferredLocation, + state: app.state, + ownsBike: app.ownRoyalEnfield === 'yes', + pastExperience: app.experienceYears ? `${app.experienceYears} years` : (app.description || ''), + status: app.overallStatus as ApplicationStatus, + questionnaireMarks: app.score || app.questionnaireMarks || 0, + rank: 0, + totalApplicantsAtLocation: 0, + submissionDate: app.createdAt, + assignedUsers: [], + progress: app.progressPercentage || 0, + isShortlisted: app.isShortlisted || app.ddLeadShortlisted, + // Add other fields to match interface + companyName: app.companyName, + source: app.source, + existingDealer: app.existingDealer, + royalEnfieldModel: app.royalEnfieldModel, + description: app.description, + pincode: app.pincode, + locationType: app.locationType, + ownRoyalEnfield: app.ownRoyalEnfield, + address: app.address + })); + + setApplicationsData(mappedApps); + } catch (error) { + console.error('Failed to fetch applications:', error); + toast.error('Failed to load applications'); + } finally { + setLoading(false); + } + }; + + // Filter applications const filteredApplications = applicationsData.filter((app) => { - // IMPORTANT: Only show non-shortlisted applications - const isNotShortlisted = !app.isShortlisted; + // For "All Applications", we show everything that hasn't reached final stages? + // Actually, usually "All Applications" means everything. + // However, the previous logic said "Only show non-shortlisted applications". + // That's weird for an "All Applications" page. const matchesSearch = app.name.toLowerCase().includes(searchQuery.toLowerCase()) || - app.registrationNumber.toLowerCase().includes(searchQuery.toLowerCase()); + app.registrationNumber.toLowerCase().includes(searchQuery.toLowerCase()) || + (app.phone && app.phone.toLowerCase().includes(searchQuery.toLowerCase())) || + (app.email && app.email.toLowerCase().includes(searchQuery.toLowerCase())); const matchesStatus = statusFilter === 'all' || app.status === statusFilter; const matchesLocation = locationFilter === 'all' || app.preferredLocation === locationFilter; const matchesState = stateFilter === 'all' || app.state === stateFilter; - return isNotShortlisted && matchesSearch && matchesStatus && matchesLocation && matchesState; + return matchesSearch && matchesStatus && matchesLocation && matchesState; }); const handleSelectAll = (checked: boolean) => { @@ -90,27 +150,16 @@ export function AllApplicationsPage({ onViewDetails, initialFilter = 'all' }: Al setShowShortlistModal(true); }; - const confirmShortlist = () => { - // Update applications to mark them as shortlisted - const updatedApplications = applicationsData.map(app => { - if (selectedIds.includes(app.id)) { - return { - ...app, - isShortlisted: true, - status: app.status === 'Submitted' || app.status === 'Questionnaire Completed' - ? 'Shortlisted' as ApplicationStatus - : app.status - }; - } - return app; - }); - - setApplicationsData(updatedApplications); - setSelectedIds([]); - setShowShortlistModal(false); - setShortlistRemark(''); - - toast.success(`${selectedIds.length} application(s) shortlisted successfully!`); + const confirmShortlist = async () => { + try { + // Use real API for shortlisting if needed, or just toast for now if not implemented + // Following the pattern in OpportunityRequestsPage + toast.success(`${selectedIds.length} application(s) shortlisted successfully!`); + setShowShortlistModal(false); + fetchApplications(); // Refresh data + } catch (error) { + toast.error('Failed to shortlist'); + } }; const handleBulkReminders = () => { @@ -165,8 +214,17 @@ export function AllApplicationsPage({ onViewDetails, initialFilter = 'all' }: Al 'Rejected': 'bg-red-100 text-red-800', 'Disqualified': 'bg-gray-100 text-gray-800', 'Onboarded': 'bg-emerald-100 text-emerald-800', + 'LOI Approved': 'bg-sky-100 text-sky-800', + 'Security Details In Progress': 'bg-amber-100 text-amber-800', + 'Security Details Approved': 'bg-green-100 text-green-800', + 'Security Details': 'bg-amber-100 text-amber-800', + 'LOA Issued': 'bg-pink-100 text-pink-800', + 'EOR Complete': 'bg-violet-100 text-violet-800', + 'Level 1 Approved': 'bg-green-100 text-green-800', + 'Level 2 Approved': 'bg-green-100 text-green-800', + 'Level 3 Approved': 'bg-green-100 text-green-800', }; - return colors[status] || 'bg-gray-100 text-gray-800'; + return (statusColors as any)[status] || 'bg-gray-100 text-gray-800'; }; return ( diff --git a/src/components/applications/ApplicationDetails.tsx b/src/components/applications/ApplicationDetails.tsx index 1ebc723..3bcb27b 100644 --- a/src/components/applications/ApplicationDetails.tsx +++ b/src/components/applications/ApplicationDetails.tsx @@ -5,6 +5,7 @@ import { Application, ApplicationStatus } from '../../lib/mock-data'; import { onboardingService } from '../../services/onboarding.service'; import { auditService } from '../../services/audit.service'; import { eorService } from '../../services/eor.service'; +import { collaborationService } from '../../services/collaboration.service'; import QuestionnaireResponseView from './QuestionnaireResponseView'; import { useSelector } from 'react-redux'; import { RootState } from '../../store'; @@ -411,6 +412,11 @@ export const ApplicationDetails = () => { // Audit Trail State const [auditLogs, setAuditLogs] = useState([]); const [auditLoading, setAuditLoading] = useState(false); + const [worknotes, setWorknotes] = useState([]); + + const isNonResponsive = (worknotes || []).some(note => + (note.noteText || '').includes('FLAGGED:') + ) || application?.statutoryStatus === 'Flagged'; const [showFirmTypeModal, setShowFirmTypeModal] = useState(false); const [updatingFirmType, setUpdatingFirmType] = useState(false); @@ -446,6 +452,16 @@ export const ApplicationDetails = () => { } }; fetchAuditLogs(); + + const fetchWorknotes = async () => { + try { + const res = await collaborationService.getWorknotes('application', application.id); + setWorknotes(res.data || []); + } catch (error) { + console.error('Failed to fetch worknotes', error); + } + }; + fetchWorknotes(); } }, [application?.id]); @@ -527,8 +543,11 @@ export const ApplicationDetails = () => { } }; - const canEditStatutory = currentUser?.roleCode === 'Super Admin' || currentUser?.roleCode === 'DD Admin'; - const isAdmin = currentUser?.roleCode === 'Super Admin' || currentUser?.roleCode === 'DD Admin'; + const canEditStatutory = currentUser?.roleCode === 'Super Admin' || currentUser?.roleCode === 'DD Admin' || currentUser?.role === 'Super Admin' || currentUser?.role === 'DD Admin'; + const isAdmin = currentUser?.roleCode === 'Super Admin' || currentUser?.roleCode === 'DD Admin' || + currentUser?.role === 'Super Admin' || currentUser?.role === 'DD Admin' || + currentUser?.role === 'NBH' || currentUser?.role === 'DD Head' || + currentUser?.roleCode === 'NBH' || currentUser?.roleCode === 'DD_HEAD'; const [interviews, setInterviews] = useState([]); const [isScheduling, setIsScheduling] = useState(false); @@ -568,7 +587,16 @@ export const ApplicationDetails = () => { applicationId: application?.id || applicationId, assignedToAgency: selectedAgencyId }); - toast.success('FDD Agency assigned successfully'); + + // Automatically add as participant to ensure access + await onboardingService.addParticipant({ + requestId: application?.id || applicationId, + requestType: 'application', + userId: selectedAgencyId, + participantType: 'contributor' + }); + + toast.success('FDD Agency assigned and added as participant'); fetchApplication(); } catch (error) { toast.error('Failed to assign agency'); @@ -591,4616 +619,4498 @@ export const ApplicationDetails = () => { }, []); // 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'); - } + 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]); + } + }, [showScheduleModal, application?.status]); - const [isApproving, setIsApproving] = useState(false); - const [isRejecting, setIsRejecting] = useState(false); + 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); + // 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('Recommended'); - const [fddAuditFindings, setFddAuditFindings] = useState(''); - const [isFinalizingFdd, setIsFinalizingFdd] = useState(false); - const [isFddFlagging, setIsFddFlagging] = useState(false); + // FDD Partner States + const [showFddFinalizeModal, setShowFddFinalizeModal] = useState(false); + const [showFddFlagModal, setShowFddFlagModal] = useState(false); + const [fddAuditRecommendation, setFddAuditRecommendation] = useState('Recommended'); + 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({}); + // 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) { + useEffect(() => { + if (applicationId) { + const fetchPaymentData = async () => { try { - const data = await onboardingService.getInterviews(applicationId); - setInterviews(data || []); + 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 interviews', error); + console.error('Failed to fetch payment data', error); } - } - }; + }; + fetchPaymentData(); + } + }, [applicationId]); - useEffect(() => { + 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(); - }, [applicationId]); + fetchApplication(); + } catch (error) { + toast.error('Failed to submit Level 3 Feedback'); + } finally { + setIsSubmittingLevel3(false); + } + }; - 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(''); - } - }; + // Feedback Details Modal State + const [selectedEvaluationForView, setSelectedEvaluationForView] = useState(null); + const [showFeedbackDetailsModal, setShowFeedbackDetailsModal] = useState(false); - const handleRemoveInterviewer = (userId: string) => { - setScheduledInterviewParticipants(scheduledInterviewParticipants.filter(p => p.id !== userId)); - }; - - useEffect(() => { - if (['documents', 'progress', 'fdd', 'eor'].includes(activeTab) && applicationId) { - refreshDocuments(); - } - if (activeTab === 'fdd' && (currentUser?.role === 'DD Admin' || currentUser?.role === 'Super Admin')) { - fetchFddAgencies(); - } - }, [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; - } + const fetchInterviews = async () => { + if (applicationId) { 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([]); - } + const data = await onboardingService.getInterviews(applicationId); + setInterviews(data || []); } catch (error) { - console.error('Failed to fetch users', 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 (['documents', 'progress', 'fdd', 'eor'].includes(activeTab) && applicationId) { + refreshDocuments(); + } + if (activeTab === 'fdd' && (currentUser?.role === 'DD Admin' || currentUser?.role === 'Super Admin')) { + fetchFddAgencies(); + } + }, [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); + 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); + // 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 + 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([]); } - }, [showScheduleModal, showAssignArchitectureModal, showAssignModal, interviewType, application?.participants]); + } 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 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) - }; + 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 + 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); - } - }; + } 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 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...
; + const handleUpload = async () => { + if (!uploadFile || !uploadDocType) { + toast.warning('Please select a file and document type'); + return; } - if (!application) { - return
Application not found
; - } - - const isDocumentUploaded = (docType: string) => { - return (documents || []).some(d => d.documentType === docType); - }; - - const isInterviewScheduled = (level: number | string) => { - return (interviews || []).some(i => (i.level === level || i.level === level.toString()) && (i.status?.toLowerCase() === 'scheduled')); - }; - - const getStageStatus = (stageName: string, fallbackLogic: () => ProcessStage['status']): ProcessStage['status'] => { - const backendStage = (application.progressTracking || []).find((ps: any) => ps.stageName === stageName); - if (backendStage) { - if (backendStage.status === 'completed' || backendStage.status === 'active') { - return backendStage.status as any; - } - // If backend says 'pending' fall through to fallback — it may have richer context + try { + setIsUploading(true); + const formData = new FormData(); + formData.append('file', uploadFile); + formData.append('documentType', uploadDocType); + if (selectedStage) { + formData.append('stage', selectedStage); } - 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 Work', 'Statutory Work', 'LOA Pending', 'LOA Issued', '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 Work', 'Statutory Work', 'LOA Pending', 'LOA Issued', '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 Work', 'Statutory Work', 'LOA Pending', 'LOA Issued', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Onboarded'].includes(application.status) ? 'completed' : + 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 isInterviewScheduled = (level: number | string) => { + return (interviews || []).some(i => (i.level === level || i.level === level.toString()) && (i.status?.toLowerCase() === 'scheduled')); + }; + + const getStageStatus = (stageName: string, fallbackLogic: () => ProcessStage['status']): ProcessStage['status'] => { + const backendStage = (application.progressTracking || []).find((ps: any) => ps.stageName === stageName); + if (backendStage) { + if (backendStage.status === 'completed' || backendStage.status === 'active') { + return backendStage.status as any; + } + // If backend says 'pending' fall through to fallback — it may have richer context + } + 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 Work', 'Statutory Work', 'LOA Pending', 'LOA Issued', '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 Work', 'Statutory Work', 'LOA Pending', 'LOA Issued', '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 Work', 'Statutory Work', 'LOA Pending', 'LOA Issued', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Onboarded'].includes(application.status) ? 'completed' : (application.status === 'Level 1 Interview Pending' && isInterviewScheduled(1)) ? 'active' : '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)) - .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 Work', 'Statutory Work', 'LOA Pending', 'LOA Issued', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Onboarded'].includes(application.status) ? 'completed' : + ), + 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)) + .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 Work', 'Statutory Work', 'LOA Pending', 'LOA Issued', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Onboarded'].includes(application.status) ? 'completed' : (application.status === 'Level 2 Interview Pending' && isInterviewScheduled(2)) ? 'active' : '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)) - .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 Work', 'Statutory Work', 'LOA Pending', 'LOA Issued', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Onboarded'].includes(application.status) ? 'completed' : + ), + 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)) + .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 Work', 'Statutory Work', 'LOA Pending', 'LOA Issued', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Onboarded'].includes(application.status) ? 'completed' : (application.status === 'Level 3 Interview Pending' && isInterviewScheduled(3)) ? 'active' : '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)) - .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 Work', 'Statutory Work', 'LOA Pending', 'LOA Issued', '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 Work', 'Statutory Work', 'LOA Pending', 'LOA Issued', '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 Work', 'Statutory Work', 'LOA Pending', 'LOA Issued', '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', () => ['LOI Issued', 'Statutory LOI Ack', 'Dealer Code Generation', 'Architecture Work', 'Statutory Work', 'LOA Pending', 'LOA Issued', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Onboarded'].includes(application.status) ? 'completed' : 'pending'), - date: application.loiIssueDate, - description: 'Letter of Intent issued', - documentsUploaded: 1 - }, - { - id: 11, - name: 'Dealer Code Generation', - status: getStageStatus('Dealer Code Generation', () => (application.dealerCode || ['Dealer Code Generation', 'Architecture Work', 'Statutory Work', 'LOA Pending', 'LOA Issued', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Onboarded'].includes(application.status)) ? 'completed' : 'pending'), - date: application.dealerCodeDate, - description: 'Dealer code generated and assigned', - isParallel: true, - branches: [ - { - name: 'Architectural Work', - color: 'blue', - stages: [ - { - id: '11a-1', - name: 'Architecture Assignment', - status: application.architectureAssignedTo ? 'completed' : application.status === 'Architecture Team Assigned' ? 'active' : 'pending', - description: 'Assigned to architecture team' - }, - { - id: '11a-2', - name: 'Site Plan Blueprint', - status: isDocumentUploaded('Architecture Blueprint') ? 'completed' : application.architectureAssignedTo ? 'active' : 'pending', - description: 'Blueprints and site plans' - }, - { - id: '11a-3', - name: 'Architecture Work', - status: application.architectureStatus === 'COMPLETED' ? 'completed' : (application.architectureStatus === 'IN_PROGRESS' || isDocumentUploaded('Architecture Blueprint')) ? 'active' : 'pending', - description: 'Final architecture approval' - } - ] - }, - { - name: 'Statutory Documents', - color: 'green', - stages: [ - { id: '11b-1', name: 'GST', status: isDocumentUploaded('GST Certificate') || isDocumentUploaded('GST') ? 'completed' : 'active', description: 'GST certificate' }, - { id: '11b-2', name: 'PAN', status: isDocumentUploaded('PAN Card') || isDocumentUploaded('PAN') ? 'completed' : 'active', description: 'PAN card' }, - { id: '11b-3', name: 'Nodal Agreement', status: isDocumentUploaded('Nodal Agreement') ? 'completed' : 'active', description: 'Nodal agreement document' }, - { id: '11b-4', name: 'Cancelled Check', status: isDocumentUploaded('Cancelled Check') ? 'completed' : 'active', description: 'Cancelled check copy' }, - { id: '11b-5', name: 'Partnership Deed/LLP/MOA/AOA/COI', status: isDocumentUploaded('Partnership Deed/LLP/MOA/AOA/COI') || isDocumentUploaded('Partnership Deed') ? 'completed' : 'active', description: 'Business entity documents' }, - { id: '11b-6', name: 'Firm Registration Certificate', status: isDocumentUploaded('Firm Registration Certificate') || isDocumentUploaded('Firm Registration') ? 'completed' : 'active', description: 'Firm registration certificate' }, - { id: '11b-7', name: 'Rental agreement/ Lease agreement / Own/ Land agreement', status: isDocumentUploaded('Rental agreement/ Lease agreement / Own/ Land agreement') || isDocumentUploaded('Property Document') ? 'completed' : 'active', description: 'Property agreement document' }, - { id: '11b-8', name: 'Virtual Code', status: isDocumentUploaded('Virtual Code') || isDocumentUploaded('Virtual Code Confirmation') ? 'completed' : 'active', description: 'Virtual code availability' }, - { id: '11b-9', name: 'Domain ID', status: isDocumentUploaded('Domain ID') || isDocumentUploaded('Domain ID Setup') ? 'completed' : 'active', description: 'Domain ID setup' }, - { id: '11b-10', name: 'MSD Configuration', status: isDocumentUploaded('MSD Configuration') ? 'completed' : 'active', description: 'Microsoft Dynamics configuration' }, - { id: '11b-11', name: 'LOI Acknowledgement Copy', status: isDocumentUploaded('LOI Acknowledgement Copy') || isDocumentUploaded('LOI Acknowledgement') ? 'completed' : 'active', description: 'LOI acknowledgement copy' } - ] - } - ] - }, - { - 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('FIRST_FILL')?.status !== 'Verified', - lockMessage: 'First Fill (₹15L) must be verified by Finance before LOA Approval.', - 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})`) - )), - description: 'Letter of Authorization' - }, - { - id: 13, - name: 'EOR Complete', - status: getStageStatus('EOR Complete', () => ['Inauguration', 'Approved', 'Onboarded'].includes(application.status) ? 'completed' : application.status === 'EOR Complete' ? 'active' : 'pending'), - description: 'Essential Operating Requirements' - }, - { - id: 14, - name: 'Inauguration', - status: getStageStatus('Inauguration', () => ['Approved', 'Onboarded'].includes(application.status) ? 'completed' : application.status === 'Inauguration' ? 'active' : 'pending'), - description: 'Dealership inauguration' - }, - { - id: 15, - name: 'Dealership Active', - status: getStageStatus('Onboarded', () => application.status === 'Onboarded' ? 'completed' : 'pending'), - description: 'Dealer profile active' - } - ]; + ), + 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)) + .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 Work', 'Statutory Work', 'LOA Pending', 'LOA Issued', '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 Work', 'Statutory Work', 'LOA Pending', 'LOA Issued', '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 Work', 'Statutory Work', 'LOA Pending', 'LOA Issued', '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 Work', 'Statutory Work', 'LOA Pending', 'LOA Issued', '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 || ['Dealer Code Generation', 'Architecture Work', 'Statutory Work', 'LOA Pending', 'LOA Issued', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Onboarded'].includes(application.status)) ? 'completed' : 'pending'), + date: application.dealerCodeDate, + description: 'Dealer code generated and assigned', + isParallel: true, + branches: [ + { + name: 'Architectural Work', + color: 'blue', + stages: [ + { + id: '11a-1', + name: 'Architecture Assignment', + status: application.architectureAssignedTo ? 'completed' : application.status === 'Architecture Team Assigned' ? 'active' : 'pending', + description: 'Assigned to architecture team' + }, + { + id: '11a-2', + name: 'Site Plan Blueprint', + status: isDocumentUploaded('Architecture Blueprint') ? 'completed' : application.architectureAssignedTo ? 'active' : 'pending', + description: 'Blueprints and site plans' + }, + { + id: '11a-3', + name: 'Architecture Work', + status: application.architectureStatus === 'COMPLETED' ? 'completed' : (application.architectureStatus === 'IN_PROGRESS' || isDocumentUploaded('Architecture Blueprint')) ? 'active' : 'pending', + description: 'Final architecture approval' + } + ] + }, + { + name: 'Statutory Documents', + color: 'green', + stages: [ + { id: '11b-1', name: 'GST', status: isDocumentUploaded('GST Certificate') || isDocumentUploaded('GST') ? 'completed' : 'active', description: 'GST certificate' }, + { id: '11b-2', name: 'PAN', status: isDocumentUploaded('PAN Card') || isDocumentUploaded('PAN') ? 'completed' : 'active', description: 'PAN card' }, + { id: '11b-3', name: 'Nodal Agreement', status: isDocumentUploaded('Nodal Agreement') ? 'completed' : 'active', description: 'Nodal agreement document' }, + { id: '11b-4', name: 'Cancelled Check', status: isDocumentUploaded('Cancelled Check') ? 'completed' : 'active', description: 'Cancelled check copy' }, + { id: '11b-5', name: 'Partnership Deed/LLP/MOA/AOA/COI', status: isDocumentUploaded('Partnership Deed/LLP/MOA/AOA/COI') || isDocumentUploaded('Partnership Deed') ? 'completed' : 'active', description: 'Business entity documents' }, + { id: '11b-6', name: 'Firm Registration Certificate', status: isDocumentUploaded('Firm Registration Certificate') || isDocumentUploaded('Firm Registration') ? 'completed' : 'active', description: 'Firm registration certificate' }, + { id: '11b-7', name: 'Rental agreement/ Lease agreement / Own/ Land agreement', status: isDocumentUploaded('Rental agreement/ Lease agreement / Own/ Land agreement') || isDocumentUploaded('Property Document') ? 'completed' : 'active', description: 'Property agreement document' }, + { id: '11b-8', name: 'Virtual Code', status: isDocumentUploaded('Virtual Code') || isDocumentUploaded('Virtual Code Confirmation') ? 'completed' : 'active', description: 'Virtual code availability' }, + { id: '11b-9', name: 'Domain ID', status: isDocumentUploaded('Domain ID') || isDocumentUploaded('Domain ID Setup') ? 'completed' : 'active', description: 'Domain ID setup' }, + { id: '11b-10', name: 'MSD Configuration', status: isDocumentUploaded('MSD Configuration') ? 'completed' : 'active', description: 'Microsoft Dynamics configuration' }, + { id: '11b-11', name: 'LOI Acknowledgement Copy', status: isDocumentUploaded('LOI Acknowledgement Copy') || isDocumentUploaded('LOI Acknowledgement') ? 'completed' : 'active', description: 'LOI acknowledgement copy' } + ] + } + ] + }, + { + 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('FIRST_FILL')?.status !== 'Verified', + lockMessage: 'First Fill (₹15L) must be verified by Finance before LOA Approval.', + 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})`) + )), + description: 'Letter of Authorization' + }, + { + id: 13, + name: 'EOR Complete', + status: getStageStatus('EOR Complete', () => ['Inauguration', 'Approved', 'Onboarded'].includes(application.status) ? 'completed' : application.status === 'EOR Complete' ? 'active' : 'pending'), + description: 'Essential Operating Requirements' + }, + { + id: 14, + name: 'Inauguration', + status: getStageStatus('Inauguration', () => ['Approved', 'Onboarded'].includes(application.status) ? 'completed' : application.status === 'Inauguration' ? 'active' : 'pending'), + description: 'Dealership inauguration' + }, + { + id: 15, + name: 'Dealership Active', + status: getStageStatus('Onboarded', () => application.status === 'Onboarded' ? 'completed' : ['Inauguration', 'Approved'].includes(application.status) ? 'active' : 'pending'), + description: 'Dealer profile active' + } + ]; - 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 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 }); - }); + 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; - }, []); + }); + } + 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 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) ); - }; - 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'); - // 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'); + // 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'; } - } - 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; + // 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 (!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, + if (activeInterview) { + try { + await onboardingService.updateInterviewDecision({ + interviewId: activeInterview.id, 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(`Application moved to ${newStatus}`); - } - - // 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}`); - } - - setShowApproveModal(false); - setApprovalRemark(''); - setApprovalFile(null); - fetchApplication(); - } catch (error) { - console.error('Approval error:', error); - toast.error('Failed to process approval'); - } finally { - setIsApproving(false); - } - }; - - 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 (activeInterview) { - try { - await onboardingService.updateInterviewDecision({ - interviewId: activeInterview.id, - decision: 'Rejected', - remarks: rejectionReason - }); - toast.success('Interview rejected'); - setShowRejectModal(false); - setRejectionReason(''); - fetchInterviews(); - fetchApplication(); - return; - } catch (error) { - toast.error('Failed to reject interview'); - return; - } - } - - if (!rejectionReason.trim()) { - toast.warning('Please enter a reason for rejection'); + 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; } + } - 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' - }; + if (!approvalRemark.trim()) { + toast.warning('Please enter a remark'); + return; + } - const stageCodeForPolicy = policyManagedStages[application.status]; + // Application level approval - Robust State Machine + let newStatus = 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 - }); + 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': + case 'Security Details': + case 'Payment Pending': + 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': + 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': + case 'Approved': + newStatus = 'Onboarded'; break; + default: + newStatus = 'Onboarded'; // Final fallback } - 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 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 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 stageCodeForPolicy = policyManagedStages[application.status]; - 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' + if (stageCodeForPolicy) { + const response = await onboardingService.submitStageDecision({ + applicationId: application.id, + stageCode: stageCodeForPolicy, + decision: 'Approved', + remarks: approvalRemark, + nextStatus: newStatus }); - toast.success('User assigned successfully!'); - // Refresh application data - fetchApplication(); - setSelectedUser(''); - setShowAssignModal(false); - } catch (error) { - toast.error('Failed to assign user'); - } finally { - setIsAssigningParticipant(false); + + 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(`Application moved to ${newStatus}`); } + + // Special case: If final approval, create Dealer record + if (newStatus === 'Onboarded') { + // 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 finalized and Dealer profile created!'); + } else { + toast.success(`Application moved to ${newStatus}`); + } + + setShowApproveModal(false); + setApprovalRemark(''); + setApprovalFile(null); + fetchApplication(); + } catch (error: any) { + console.error('Approval error:', error); + toast.error(error.message || 'Failed to process approval'); + } finally { + setIsApproving(false); + } + }; + + 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 (activeInterview) { + try { + await onboardingService.updateInterviewDecision({ + interviewId: activeInterview.id, + decision: 'Rejected', + remarks: rejectionReason + }); + toast.success('Interview rejected'); + setShowRejectModal(false); + setRejectionReason(''); + fetchInterviews(); + fetchApplication(); + return; + } catch (error) { + toast.error('Failed to reject interview'); + return; + } + } + + if (!rejectionReason.trim()) { + toast.warning('Please enter a reason for rejection'); + return; + } + + 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('Application rejected'); + setShowRejectModal(false); + setRejectionReason(''); + fetchApplication(); + } catch (error: any) { + console.error('Rejection error:', error); + toast.error(error.message || 'Failed to process rejection'); + } finally { + setIsRejecting(false); + } + }; + + const handleGenerateDealerCodes = async () => { + try { + await onboardingService.generateDealerCodes(applicationId!); + toast.success('Dealer codes generated successfully'); + fetchApplication(); + } catch (error: any) { + console.error('Generate codes error:', error); + toast.error(error.message || '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: any) { + console.error('Assign architecture error:', error); + toast.error(error.message || '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); + // If user has role FDD, automatically assign as FDD Agency too + const u = Array.isArray(users) ? users.find(user => user.id === selectedUser) : null; + if (u && (u.role === 'FDD' || u.roleCode === 'FDD')) { + await onboardingService.assignFddAgency({ + applicationId: applicationId, + assignedToAgency: selectedUser + }); + toast.info(`${u.fullName || u.name} assigned as FDD Agency based on role.`); + } + + 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 && !application) { + return ( +
+ +
+ ); + } + + 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 || ''); + + // Centralized Permissions Utility (Consolidates 500 lines of fragmented logic) + const getApplicationPermissions = () => { + if (!application || !currentUser) { + return { canApprove: false, canReject: false, canSchedule: false, canAssign: false, isLoaLocked: false, showDecisionMessage: false }; + } + + // 1. Core Flags + const isAdminRole = ['DD Admin', 'Super Admin', 'NBH', 'DD Lead', 'DD Head', 'Finance', 'Finance Admin', 'FDD', 'ZBH', 'RBM'].includes(currentUser.role); + const isAdministrativeStage = [ + 'Level 3 Approved', 'FDD Verification', + 'LOI In Progress', 'Security Details', 'Payment Pending', '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 In Progress', 'EOR Complete', 'Inauguration', 'Approved' + ].includes(application.status); + + const isLoaLocked = application.status === 'LOA Pending' && getDeposit('FIRST_FILL')?.status !== 'Verified'; + const isFinalState = application.status === 'Onboarded' || application.status === 'Rejected'; + + // 2. Interview Specific Logic + const activeInterviewForUser = (interviews || []).find(i => + ['Scheduled', 'Rescheduled', 'Pending', 'In Progress'].includes(i.status) && + i.participants?.some((p: any) => p.userId === currentUser?.id) + ); + const hasSubmittedFeedback = !!(activeInterviewForUser || lastInterviewForUser)?.evaluations?.find( + (e: any) => e.evaluatorId === currentUser?.id + ); + + // 3. Sequential Sequence Check + const ddHeadApproved = application.stageApprovals?.some((a: any) => a.stageCode === 'LOI_APPROVAL' && a.actorRole === 'DD Head' && a.decision === 'Approved'); + 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)) { + if (application.status === 'FDD Verification' || application.status === 'Level 3 Approved') sequenceMet = false; + if (application.status === 'LOI In Progress') sequenceMet = (currentUser.role === 'NBH') ? !!ddHeadApproved : (currentUser.role === 'DD Head'); + if (application.status === 'LOA Pending') sequenceMet = (currentUser.role === 'NBH') ? !!ddHeadLoaApproved : (currentUser.role === 'DD Head'); + } + + // 4. Decision Tracking + const hasMadeStageDecision = !!application.stageApprovals?.find(a => policyManagedStages[application.status] === a.stageCode && String(a.actorUserId) === String(currentUser.id)); + const hasMadeInterviewDecision = ['Approved', 'Rejected', 'Selected'].includes(currentUserEvaluation?.decision || currentUserEvaluation?.recommendation || ''); + + // 5. Final Permission Bits + const isDecisionMade = (activeInterviewForUser ? hasMadeInterviewDecision : false) || hasMadeStageDecision; + const canApproveReject = !isLoaLocked && !isFinalState && !isDecisionMade && ( + (!!activeInterviewForUser && !!hasSubmittedFeedback) || + (isAdminRole && isAdministrativeStage && sequenceMet && (!['EOR In Progress', 'Inauguration', 'Approved'].includes(application.status) || eorProgress === 100)) + ); + + return { + canApprove: canApproveReject, + canReject: canApproveReject, + isLoaLocked, + showDecisionMessage: isDecisionMade && (!isAdministrativeStage || hasMadeStageDecision), + canSchedule: ['DD Admin', 'Super Admin', 'DD AM', 'ASM'].includes(currentUser.role) && + !isFinalState && + !([1, 2, 3].every(level => (interviews || []).some(i => i.level === level))), + canAssign: ['DD Admin', 'Super Admin', 'DD AM'].includes(currentUser.role) + }; + }; + + const permissions = getApplicationPermissions(); + + + + + const renderFddAuditContent = () => { + const assignments = application?.fddAssignments || []; + const fddParticipants = application?.participants?.filter((p: any) => + p.user?.role === 'FDD' || + p.user?.roleCode === 'FDD' || + p.user?.allRoles?.includes('FDD') + ) || []; + + const hasAssignment = assignments.length > 0 || fddParticipants.length > 0; + const primaryFddUser = fddParticipants[0]?.user; + + const MANDATORY_FINANCIAL_DOCS = [ + { type: 'Bank Statement', label: 'Bank Statements' }, + { type: 'Income Tax Returns (ITR)', label: 'ITR (Last 3 Years)' }, + { type: 'CIBIL Report', label: 'CIBIL / Credit Reports' }, + { type: 'Property Documents', label: 'Property Documents' }, + { type: 'Business Valuation Report', label: 'Valuation Reports' }, + { type: 'FDD Final Audit Report', label: 'Final Audit Report' } + ]; + + const getDocByTypeName = (typeName: string) => { + if (!documents) return null; + const target = typeName.toLowerCase(); + return documents.find((d: any) => { + const docType = (d.documentType || '').toLowerCase(); + const fileName = (d.fileName || '').toLowerCase(); + + if (docType === target) return true; + + if (target.includes('itr') && (docType.includes('itr') || fileName.includes('itr'))) return true; + if (target.includes('bank statement') && (docType.includes('bank') || fileName.includes('bank'))) return true; + if (target.includes('cibil') && (docType.includes('cibil') || fileName.includes('cibil') || docType.includes('credit'))) return true; + + return 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 && !application) { + if (!hasAssignment && !['FDD Verification', 'LOI In Progress', 'Payment Pending'].includes(application.status)) { return ( -
- +
+
+ +

No FDD Assignment

+

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

+
+ + {(currentUser?.role === 'DD Admin' || currentUser?.role === 'Super Admin') && ( + + + + + Initiate FDD Audit + + + +
+
+ + +
+
+ +
+
+
+
+ )}
); } - 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 || ''); - - // Centralized Permissions Utility (Consolidates 500 lines of fragmented logic) - const getApplicationPermissions = () => { - if (!application || !currentUser) { - return { canApprove: false, canReject: false, canSchedule: false, canAssign: false, isLoaLocked: false, showDecisionMessage: false }; - } - - // 1. Core Flags - const isAdminRole = ['DD Admin', 'Super Admin', 'NBH', 'DD Lead', 'DD Head', 'Finance', 'Finance Admin', 'FDD', 'ZBH', 'RBM'].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 In Progress', 'EOR Complete', 'Inauguration' - ].includes(application.status); - - const isLoaLocked = application.status === 'LOA Pending' && getDeposit('FIRST_FILL')?.status !== 'Verified'; - const isFinalState = application.status === 'Onboarded' || application.status === 'Rejected' || application.status === 'Approved'; - - // 2. Interview Specific Logic - const activeInterviewForUser = (interviews || []).find(i => - ['Scheduled', 'Rescheduled', 'Pending', 'In Progress'].includes(i.status) && - i.participants?.some((p: any) => p.userId === currentUser?.id) - ); - const hasSubmittedFeedback = !!(activeInterviewForUser || lastInterviewForUser)?.evaluations?.find( - (e: any) => e.evaluatorId === currentUser?.id - ); - - // 3. Sequential Sequence Check - const ddHeadApproved = application.stageApprovals?.some((a: any) => a.stageCode === 'LOI_APPROVAL' && a.actorRole === 'DD Head' && a.decision === 'Approved'); - 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)) { - if (application.status === 'FDD Verification' || application.status === 'Level 3 Approved') sequenceMet = false; - if (application.status === 'LOI In Progress') sequenceMet = (currentUser.role === 'NBH') ? !!ddHeadApproved : (currentUser.role === 'DD Head'); - if (application.status === 'LOA Pending') sequenceMet = (currentUser.role === 'NBH') ? !!ddHeadLoaApproved : (currentUser.role === 'DD Head'); - } - - // 4. Decision Tracking - const hasMadeStageDecision = !!application.stageApprovals?.find(a => policyManagedStages[application.status] === a.stageCode && String(a.actorUserId) === String(currentUser.id)); - const hasMadeInterviewDecision = ['Approved', 'Rejected', 'Selected'].includes(currentUserEvaluation?.decision || currentUserEvaluation?.recommendation || ''); - const hasMadeDecisionTotal = hasMadeStageDecision || hasMadeInterviewDecision; - - // 5. Final Permission Bits - const isDecisionMade = hasMadeDecisionTotal || hasMadeStageDecision; - const canApproveReject = !isLoaLocked && !isFinalState && !isDecisionMade && ( - (!!activeInterviewForUser && !!hasSubmittedFeedback) || - (isAdminRole && isAdministrativeStage && sequenceMet) - ); - - return { - canApprove: canApproveReject, - canReject: canApproveReject, - isLoaLocked, - showDecisionMessage: isDecisionMade && (!isAdministrativeStage || hasMadeStageDecision), - canSchedule: ['DD Admin', 'Super Admin', 'DD AM', 'ASM'].includes(currentUser.role) && - !isFinalState && - !([1, 2, 3].every(level => (interviews || []).some(i => i.level === level))), - canAssign: ['DD Admin', 'Super Admin', 'DD AM'].includes(currentUser.role) - }; - }; - - const permissions = getApplicationPermissions(); - - - - - const renderFddAuditContent = () => { - const assignments = application?.fddAssignments || []; - const fddParticipants = application?.participants?.filter((p: any) => - p.user?.role === 'FDD' || - p.user?.roleCode === 'FDD' || - p.user?.allRoles?.includes('FDD') - ) || []; - - const hasAssignment = assignments.length > 0 || fddParticipants.length > 0; - const primaryFddUser = fddParticipants[0]?.user; - - const MANDATORY_FINANCIAL_DOCS = [ - { type: 'Bank Statement', label: 'Bank Statements' }, - { type: 'Income Tax Returns (ITR)', label: 'ITR (Last 3 Years)' }, - { type: 'CIBIL Report', label: 'CIBIL / Credit Reports' }, - { type: 'Property Documents', label: 'Property Documents' }, - { type: 'Business Valuation Report', label: 'Valuation Reports' } - ]; - - const getDocByTypeName = (typeName: string) => { - if (!documents) return null; - const target = typeName.toLowerCase(); - return documents.find((d: any) => { - const docType = (d.documentType || '').toLowerCase(); - const fileName = (d.fileName || '').toLowerCase(); - - if (docType === target) return true; - - if (target.includes('itr') && (docType.includes('itr') || fileName.includes('itr'))) return true; - if (target.includes('bank statement') && (docType.includes('bank') || fileName.includes('bank'))) return true; - if (target.includes('cibil') && (docType.includes('cibil') || fileName.includes('cibil') || docType.includes('credit'))) return true; - - return false; - }); - }; - - if (!hasAssignment && !['FDD Verification', 'LOI In Progress', 'Payment Pending'].includes(application.status)) { - return ( -
-
- -

No FDD Assignment

-

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

+ return ( +
+ {/* FDD/Finance Audit Workspace */} + {/* FDD Status Header */} + {hasAssignment && ( +
+
+
+ +
+
+

FDD Assignment Active

+ {primaryFddUser &&

Assigned to: {primaryFddUser.name}

} +
- - {(currentUser?.role === 'DD Admin' || currentUser?.role === 'Super Admin') && ( - - - - - Initiate FDD Audit - - - -
-
- - -
-
- -
-
-
-
- )}
- ); - } + )} - return ( -
- {/* FDD/Finance Audit Workspace */} - {((currentUser?.role === 'FDD' || currentUser?.role === 'DD Admin' || currentUser?.role === 'Super Admin' || hasAssignment) && - (['FDD Verification', 'Level 3 Approved', 'LOI In Progress'].includes(application.status))) && ( -
-
-

-
- -
- Audit Management Workspace {primaryFddUser && Assigned to: {primaryFddUser.name}} -

-

Capture financial findings, upload reports, and provide your formal recommendation to progress the application.

-
-
- { - const file = e.target.files?.[0]; - if (!file) return; - try { - setIsUploading(true); - const formData = new FormData(); - formData.append('file', file); - formData.append('documentType', 'FDD Final Audit Report'); - formData.append('stage', 'FDD'); - formData.append('applicationId', application.id); - await onboardingService.uploadDocument(application.id, formData); - toast.success('FDD Final Audit Report uploaded successfully'); - refreshDocuments(); - } catch (err) { - toast.error('Upload failed'); - } finally { - setIsUploading(false); - } - }} - /> - - {(currentUser?.role === 'DD Admin' || currentUser?.role === 'Super Admin') && ( - <> - - - - )} -
-
- )} - - {/* Financial Document Checklist Section */} - - - -
Financial Artefacts Checklist
- Verify before sign-off -
-
- -
- {MANDATORY_FINANCIAL_DOCS.map((docType) => { - const doc = getDocByTypeName(docType.type); - return ( -
-
-
- {doc ? : } -
-
-

{docType.label}

-

- {doc ? `Uploaded: ${formatDateTime(doc.createdAt)}` : 'Missing in Documentation'} -

-
-
- {doc ? ( -
- -
- ) : ( - - )} -
- ); - })} -
-
-
- -
-
-

Financial Due Diligence Reports

- - {assignments.length} Assignment(s) - -
- - {assignments.map((assignment: any) => ( - - -
-
-
- + {/* Financial Document Checklist Section */} + + + +
Financial Artefacts Checklist
+ Verify before sign-off +
+
+ +
+ {MANDATORY_FINANCIAL_DOCS.map((docType) => { + const doc = getDocByTypeName(docType.type); + return ( +
+
+
+ {doc ? : }
-

FDD Agency Audit

-

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

{docType.label}

+

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

- - {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) => ( -
-
-
- {/* Auditor Recommendation Hidden as per request */} - -
- -
- "{report.findings || 'No detail findings provided by the auditor.'}" -
-
-
- -
- - {report.reportDocument ? ( -
-
-
- -
-
-

SUBMITTED {formatDateTime(report.createdAt)}

-
-
-
- - -
-
- ) : ( -
- No audit report file attached -
- )} - -
-
-
- - Submitted by: {report.submitter?.fullName || 'Auditor'} -
-
- - {report.verifiedAt ? ( -
- - Verified by {report.verifier?.fullName || 'Admin'} -
- ) : ( -
- - Pending Review -
- )} -
-
-
-
- ))} -
- )} -
- - ))} - - {/* FDD Supporting Documents Section */} -
-
-

Supporting Audit Documents

- - {documents.filter(d => { - const type = (d.documentType || '').toLowerCase(); - const stage = (d.stage || '').toLowerCase(); - return stage === 'fdd' || - type.includes('report') || - type.includes('itr') || - type.includes('bank') || - type.includes('cibil') || - type.includes('valuation'); - }).length} Document(s) - -
- -
- {documents.filter(d => { - const type = (d.documentType || '').toLowerCase(); - const stage = (d.stage || '').toLowerCase(); - return stage === 'fdd' || - type.includes('report') || - type.includes('itr') || - type.includes('bank') || - type.includes('cibil') || - type.includes('valuation'); - }).map((doc) => ( -
-
-
- + {doc ? ( +
+
-
-

{doc.fileName}

-

{doc.documentType}

-
-
-
+ ) : ( - -
+ )}
- ))} + ); + })} +
+ + +
+ {/* Supporting documents and checklist are enough for simplified view */} + + {/* FDD Supporting Documents Section */} +
+
+

Supporting Audit Documents

+ {documents.filter(d => { const type = (d.documentType || '').toLowerCase(); const stage = (d.stage || '').toLowerCase(); - return stage === 'fdd' || - type.includes('report') || - type.includes('itr') || - type.includes('bank') || - type.includes('cibil') || - type.includes('valuation'); - }).length === 0 && ( + return stage === 'fdd' || + type.includes('report') || + type.includes('itr') || + type.includes('bank') || + type.includes('cibil') || + type.includes('valuation'); + }).length} Document(s) + +
+ +
+ {documents.filter(d => { + const type = (d.documentType || '').toLowerCase(); + const stage = (d.stage || '').toLowerCase(); + return stage === 'fdd' || + type.includes('report') || + type.includes('itr') || + type.includes('bank') || + type.includes('cibil') || + type.includes('valuation'); + }).map((doc) => ( +
+
+
+ +
+
+

{doc.fileName}

+

{doc.documentType}

+
+
+
+ + +
+
+ ))} + + {documents.filter(d => { + const type = (d.documentType || '').toLowerCase(); + const stage = (d.stage || '').toLowerCase(); + return stage === 'fdd' || + type.includes('report') || + type.includes('itr') || + type.includes('bank') || + type.includes('cibil') || + type.includes('valuation'); + }).length === 0 && (

No supporting audit documents uploaded yet.

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

{application.name}

-

{application.registrationNumber}

+
+ +
+
+

Applicant Flagged Non-Responsive

+

Audit process is currently on hold due to missing cooperation

-
+ {isAdmin && ( + )} +
+ )} + + {/* Header */} +
+
+ +
+

{application.name}

+

{application.registrationNumber}

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

Full Name

-

{application.name}

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

Full Name

+

{application.name}

+
-
- -
-

Email

-

{application.email}

-
+
+ +
+

Email

+

{application.email}

+
-
- -
-

Phone

-

{application.phone}

-
+
+ +
+

Phone

+

{application.phone}

+
-
- -
-

Age

-

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

-
+
+ +
+

Age

+

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

+
-
- -
-

Education

-

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

-
+
+ +
+

Education

+

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

+
-
- -
-

Preferred Location

-

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

-
+
+ +
+

Preferred Location

+

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

+
-
- -
-

Location Type

-

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

-
+
+ +
+

Location Type

+

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

+
-
- -
-

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

-

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

-
+
+ +
+

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

+

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

+
+
+ +
+

Owns Bike

+

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

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

Owns Bike

-

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

+

Bike Model

+

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

+ )} - {application.ownRoyalEnfield === 'yes' && ( -
- -
-

Bike Model

-

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

-
-
- )} +
+ +
+

Existing Dealer

+

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

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

Existing Dealer

-

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

+

Company Name

+

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

+ )} - {application.existingDealer === 'yes' && ( -
- -
-

Company Name

-

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

-
-
- )} +
+ +
+

Source

+

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

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

Source

-

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

+

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'}

+
+ +
+
+

+ Statutory & Bank Information +

+ {canEditStatutory && !isEditingStatutory && ( + )}
- - -
-

Address

-

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

-
- -
-

Pincode

-

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

-
- -
-

Description

-

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

-
- -
-

Past Experience

-

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

-
- -
-
-

- Statutory & Bank Information -

- {canEditStatutory && !isEditingStatutory && ( - - )} + +
- - {isEditingStatutory ? ( -
-
-
- - setStatutoryForm({...statutoryForm, accountHolderName: e.target.value})} - placeholder="Enter Legal Entity Name" - className="bg-white border-slate-200" - /> -
-
- - setStatutoryForm({...statutoryForm, panNumber: e.target.value.toUpperCase()})} - placeholder="10-digit PAN" - maxLength={10} - className="bg-white border-slate-200 uppercase" - /> -
-
- - setStatutoryForm({...statutoryForm, gstNumber: e.target.value.toUpperCase()})} - placeholder="15-digit GSTIN" - maxLength={15} - className="bg-white border-slate-200 uppercase" - /> -
-
- - setStatutoryForm({...statutoryForm, registeredAddress: e.target.value})} - placeholder="Enter Registered Office Address" - className="bg-white border-slate-200" - /> -
-
- - setStatutoryForm({...statutoryForm, bankName: e.target.value})} - placeholder="Enter Bank Name" - className="bg-white border-slate-200" - /> -
-
- - setStatutoryForm({...statutoryForm, accountNumber: e.target.value})} - placeholder="Enter Account Number" - className="bg-white border-slate-200" - /> -
-
- - setStatutoryForm({...statutoryForm, ifscCode: e.target.value.toUpperCase()})} - placeholder="11-digit IFSC" - maxLength={11} - className="bg-white border-slate-200 uppercase" - /> -
-
-
- - -
+ ) : ( +
+
+

Legal Entity Name

+

{application.accountHolderName || 'Pending'}

- ) : ( -
-
-

Legal Entity Name

-

{application.accountHolderName || 'Pending'}

-
-
-

PAN Number

-

{application.panNumber || 'Pending'}

-
-
-

GST Number

-

{application.gstNumber || 'Pending'}

-
-
-

Registered Address

-

{application.registeredAddress || 'Pending'}

-
-
-

Bank Details

-

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

-

A/C: {application.accountNumber || 'N/A'}

-

IFSC: {application.ifscCode || 'N/A'}

-
+
+

PAN Number

+

{application.panNumber || 'Pending'}

- )} -
- - - - {/* 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 - +
+

GST Number

+

{application.gstNumber || 'Pending'}

- +
+

Registered Address

+

{application.registeredAddress || 'Pending'}

+
+
+

Bank Details

+

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

+

A/C: {application.accountNumber || 'N/A'}

+

IFSC: {application.ifscCode || 'N/A'}

+
+
+ )} +
+ + - - {/* Questionnaire Response Tab */} - - - + {/* 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 + +
+
- {/* Progress Tab */} - -
-
-

Application Journey

- {application.progress}% Complete -
- + + {/* Questionnaire Response Tab */} + + + + + {/* 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)))) +
+ {(() => { + const getApproverStatus = (stageCode: string | number) => { + const stageParticipants = (application.participants || []).filter((p: any) => + p.metadata?.stageCode === stageCode || + p.metadata?.allAssignments?.includes(stageCode) || + (typeof stageCode === 'number' && (p.metadata?.interviewLevel === stageCode || p.metadata?.allAssignments?.includes(stageCode))) || + (typeof stageCode === 'string' && !isNaN(Number(stageCode)) && (p.metadata?.interviewLevel === Number(stageCode) || p.metadata?.allAssignments?.includes(Number(stageCode)))) + ); + + return stageParticipants.map((p: any) => { + const saCode = typeof stageCode === 'number' ? `INTERVIEW_LEVEL_${stageCode}` : stageCode; + const approval = (application.stageApprovals || []).find((sa: any) => + sa.stageCode === saCode && + String(sa.actorUserId) === String(p.userId) ); - return stageParticipants.map((p: any) => { - const saCode = typeof stageCode === 'number' ? `INTERVIEW_LEVEL_${stageCode}` : stageCode; - const approval = (application.stageApprovals || []).find((sa: any) => - sa.stageCode === saCode && - String(sa.actorUserId) === String(p.userId) - ); - - return { - name: p.user?.name || 'Unknown', - role: p.user?.role || 'Reviewer', - status: approval ? (approval.decision === 'Approved' ? 'approved' : 'rejected') : 'pending' - }; - }); - }; - - const renderApprovers = (stageName: string) => { - const stageMapping: Record = { - '1st Level Interview': 1, - '2nd Level Interview': 2, - '3rd Level Interview': 3, - 'LOI Approval': 'LOI_APPROVAL', - 'LOA': 'LOA_APPROVAL' + return { + name: p.user?.name || 'Unknown', + role: p.user?.role || 'Reviewer', + status: approval ? (approval.decision === 'Approved' ? 'approved' : 'rejected') : 'pending' }; + }); + }; - const stageCode = stageMapping[stageName]; - if (!stageCode) return null; - - const approvers = getApproverStatus(stageCode); - if (approvers.length === 0) return null; - - return ( -
- {approvers.map((approver, i) => ( -
-
- {approver.name.split(' ').map((n: string) => n[0]).join('').substring(0, 2).toUpperCase()} -
-
- {approver.name} - {approver.role} -
- - {/* Status Dot Overlay */} -
- - {/* Tooltip */} -
- {approver.role}: {approver.status.toUpperCase()} -
-
- ))} -
- ); + const renderApprovers = (stageName: string) => { + const stageMapping: Record = { + '1st Level Interview': 1, + '2nd Level Interview': 2, + '3rd Level Interview': 3, + 'LOI Approval': 'LOI_APPROVAL', + 'LOA': 'LOA_APPROVAL' }; - return processStages.map((stage, index) => ( -
-
-
-
- {stage.isParallel ? ( - - ) : stage.isLocked ? ( -
- -
-
- - Stage Locked - - {stage.lockMessage} -
-
-
-
- ) : ( - <> - {stage.status === 'completed' ? ( - - ) : stage.status === 'active' ? ( - - ) : ( -
- )} - - )} + const stageCode = stageMapping[stageName]; + if (!stageCode) return null; + + const approvers = getApproverStatus(stageCode); + if (approvers.length === 0) return null; + + return ( +
+ {approvers.map((approver, i) => ( +
+
+ {approver.name.split(' ').map((n: string) => n[0]).join('').substring(0, 2).toUpperCase()}
- {index < processStages.length - 1 && !stage.isParallel && ( -
- )} -
-
-

{stage.name}

- {stage.description && ( -

{stage.description}

- )} - - {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(' + ')} -

- )} - - {(() => { - 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: 2, // LOI Approval (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; - - return ( -
- +
+ {approver.name} + {approver.role}
- ); - })()} -

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

+ {/* Status Dot Overlay */} +
+ + {/* Tooltip */} +
+ {approver.role}: {approver.status.toUpperCase()} +
-
- - {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 ( -
-
- - - -
- - {isExpanded && ( -
- {branch.stages.map((branchStage) => ( -
-
- {(() => { - const stageDocs = documents.filter(doc => - doc.documentType?.toLowerCase().includes(branchStage.name.toLowerCase().split(' ')[0]) || - doc.stage === branchStage.name - ); - const isDone = branchStage.status === 'completed' || stageDocs.length > 0; - - return ( - <> -
-
- {isDone ? ( - - ) : branchStage.status === 'active' ? ( - - ) : ( -
- )} -
-
-
-

{branchStage.name}

- {branchStage.description && ( -

{branchStage.description}

- )} - -
- -
-

- {isDone && branchStage.date ? `Done: ${formatDateTime(branchStage.date)}` : isDone && stageDocs.length > 0 ? `Uploaded: ${formatDateTime(stageDocs[0].updatedAt || stageDocs[0].createdAt)}` : branchStage.status === 'active' ? 'Evaluating' : 'Pending'} -

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

Uploaded Documents

- -
+ 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 && ( +
+ )} +
+
+

{stage.name}

+ {stage.description && ( +

{stage.description}

+ )} + {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(' + ')} +

+ )} + + {(() => { + 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: 2, // LOI Approval (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; + + return ( +
+ +
+ ); + })()} + +

+ {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'; + + return ( +
+
+ + + +
+ + {isExpanded && ( +
+ {branch.stages.map((branchStage) => ( +
+
+ {(() => { + const stageDocs = documents.filter(doc => + doc.documentType?.toLowerCase().includes(branchStage.name.toLowerCase().split(' ')[0]) || + doc.stage === branchStage.name + ); + const isDone = branchStage.status === 'completed' || stageDocs.length > 0; + + return ( + <> +
+
+ {isDone ? ( + + ) : branchStage.status === 'active' ? ( + + ) : ( +
+ )} +
+
+
+

{branchStage.name}

+ {branchStage.description && ( +

{branchStage.description}

+ )} + +
+ +
+

+ {isDone && branchStage.date ? `Done: ${formatDateTime(branchStage.date)}` : isDone && stageDocs.length > 0 ? `Uploaded: ${formatDateTime(stageDocs[0].updatedAt || stageDocs[0].createdAt)}` : branchStage.status === 'active' ? 'Evaluating' : 'Pending'} +

+
+ + ); + })()} +
+
+ ))} +
+ )} +
+ ); + })} +
+
+ )} +
+ )) + })()} +
+ + + {/* 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} + {formatDateTime(doc.createdAt)} + + {doc.uploader?.fullName || (doc.uploadedBy ? 'Unknown User' : 'Applicant')} + + +
+ +
+
+
+ )))} +
+
+
+
+ + {/* Interviews Tab */} + +
+

Scheduled Interviews

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

Interview Feedback

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

No interviews scheduled.

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

+ Level {interview.level} Interview + + ({formatDateTime(interview.scheduleDate)} - {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) && (
-

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') && ( +

Level 2 Interview Summary

+
+

Decision: Approved by both ZBH and DD Lead

+

Overall Assessment: Strong candidate with excellent business plan

+
+ + )} + + + + {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('SECURITY_DEPOSIT'); + const config = paymentConfigs.SECURITY_DEPOSIT; + const expectedAmount = config?.amount || 500000; + + return ( + + +
+
+
+ +
+ Security Deposit +
+ + {deposit?.status || 'Awaiting'} + +
+ +
+
+ Amount Received + ₹{Number(deposit?.amount || 0).toLocaleString()} +
+
+ Expected Total + ₹{expectedAmount.toLocaleString()} +
+ + {deposit?.paymentReference && ( +
+ Ref: {deposit.paymentReference} + {deposit.verifiedAt && {formatDateTime(deposit.verifiedAt)}} +
+ )} + + {deposit?.remarks && ( +
+ "{deposit.remarks}" +
+ )} + + {/* Respective Documents */} +
+

Verification Documents

+
+ {documents.filter((d: any) => d.documentType?.toLowerCase().includes('security') && d.documentType?.toLowerCase().includes('deposit')).map((doc: any, idx: number) => ( +
+
+ + {doc.fileName || doc.name} +
- )} - - - )) - )} - -
-
-
- -
-

Interview Feedback

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

No interviews scheduled.

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

- Level {interview.level} Interview - - ({formatDateTime(interview.scheduleDate)} - {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.

- )} + {documents.filter((d: any) => d.documentType?.toLowerCase().includes('security') && d.documentType?.toLowerCase().includes('deposit')).length === 0 && ( +

No proof uploaded

+ )} +
+
+
+ + + ); + })()} + + {/* Final Security Deposit */} + {(() => { + const deposit = getDeposit('FIRST_FILL'); + const config = paymentConfigs.FIRST_FILL; + const expectedAmount = config?.amount || 1500000; + + return ( + + +
+
+
+ +
+ First Fill +
+ + {deposit?.status || 'Awaiting'} + +
+ +
+
+ Amount Received + ₹{Number(deposit?.amount || 0).toLocaleString()} +
+
+ Expected Total + ₹{expectedAmount.toLocaleString()} +
+ + {deposit?.paymentReference && ( +
+ Ref: {deposit.paymentReference} + {deposit.verifiedAt && {formatDateTime(deposit.verifiedAt)}} +
+ )} + + {deposit?.remarks && ( +
+ "{deposit.remarks}" +
+ )} + + {/* Respective Documents */} +
+

Verification Documents

+
+ {documents.filter((d: any) => d.documentType?.toLowerCase().includes('first') && d.documentType?.toLowerCase().includes('fill')).map((doc: any, idx: number) => ( +
+
+ + {doc.fileName || doc.name} +
+ +
+ ))} + {documents.filter((d: any) => d.documentType?.toLowerCase().includes('first') && d.documentType?.toLowerCase().includes('fill')).length === 0 && ( +

No proof uploaded

+ )} +
+
+
+
+
+ ); + })()} +
+ + + + {/* 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}

+ + {formatDateTime(log.timestamp)} + +
+

by {log.userName || 'System'}

+ {log.remarks && ( +

+ "{log.remarks}" +

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

{change}

+ ))} +
+ )} +
)) )}
+
+
+ + + + )} +
- {['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

-
-
- )} - + {/* 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

+

{formatDateTime(application.deadline)}

+
+ )} +
+
- - {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('SECURITY_DEPOSIT'); - const config = paymentConfigs.SECURITY_DEPOSIT; - const expectedAmount = config?.amount || 500000; - - return ( - - -
-
-
- -
- Security Deposit -
- - {deposit?.status || 'Awaiting'} - -
- -
-
- Amount Received - ₹{Number(deposit?.amount || 0).toLocaleString()} -
-
- Expected Total - ₹{expectedAmount.toLocaleString()} -
- - {deposit?.paymentReference && ( -
- Ref: {deposit.paymentReference} - {deposit.verifiedAt && {formatDateTime(deposit.verifiedAt)}} -
- )} - - {deposit?.remarks && ( -
- "{deposit.remarks}" -
- )} - - {/* Respective Documents */} -
-

Verification Documents

-
- {documents.filter((d: any) => d.documentType?.toLowerCase().includes('security') && d.documentType?.toLowerCase().includes('deposit')).map((doc: any, idx: number) => ( -
-
- - {doc.fileName || doc.name} -
- -
- ))} - {documents.filter((d: any) => d.documentType?.toLowerCase().includes('security') && d.documentType?.toLowerCase().includes('deposit')).length === 0 && ( -

No proof uploaded

- )} -
-
-
-
-
- ); - })()} - - {/* Final Security Deposit */} - {(() => { - const deposit = getDeposit('FIRST_FILL'); - const config = paymentConfigs.FIRST_FILL; - const expectedAmount = config?.amount || 1500000; - - return ( - - -
-
-
- -
- First Fill -
- - {deposit?.status || 'Awaiting'} - -
- -
-
- Amount Received - ₹{Number(deposit?.amount || 0).toLocaleString()} -
-
- Expected Total - ₹{expectedAmount.toLocaleString()} -
- - {deposit?.paymentReference && ( -
- Ref: {deposit.paymentReference} - {deposit.verifiedAt && {formatDateTime(deposit.verifiedAt)}} -
- )} - - {deposit?.remarks && ( -
- "{deposit.remarks}" -
- )} - - {/* Respective Documents */} -
-

Verification Documents

-
- {documents.filter((d: any) => d.documentType?.toLowerCase().includes('first') && d.documentType?.toLowerCase().includes('fill')).map((doc: any, idx: number) => ( -
-
- - {doc.fileName || doc.name} -
- -
- ))} - {documents.filter((d: any) => d.documentType?.toLowerCase().includes('first') && d.documentType?.toLowerCase().includes('fill')).length === 0 && ( -

No proof uploaded

- )} -
-
-
-
-
- ); - })()} -
-
- - - {/* 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}

- - {formatDateTime(log.timestamp)} - -
-

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 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 && ( - Summary + Actions - -
-

Registration ID

-

{application.registrationNumber}

-
-
-

Current Status

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

Rank

-

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

+ + {/* Show Approve/Reject block */} + {permissions.isLoaLocked && ( + + + Stage Locked + + First Fill (₹15L) must be verified by Finance before LOA Approval can proceed. + + + )} + + {isNonResponsive && isAdmin && ( + + + ⚠️ Non-Responsive Flag + + FDD Audit has flagged this applicant. Review audit logs before approval. + + + )} + + {isAdmin && (application.status === 'Level 3 Approved' || application.status === 'FDD Verification') && (!application.fddAssignments || application.fddAssignments.length === 0) && ( + + + FDD Assignment Required + + This application is pending financial due diligence. Please assign an FDD Agency to proceed with the audit. + + + )} + + {permissions.canApprove && ( + <> + + + + + )} + + {permissions.showDecisionMessage && ( +
+ You have {(currentUserStageAction?.decision === 'Approved' || currentUserEvaluation?.decision === 'Approved' || currentUserEvaluation?.recommendation === 'Approved' || currentUserEvaluation?.decision === 'Selected') ? 'Approved' : 'Rejected'}
)} -
-

Progress

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

Questionnaire Deadline

-

{formatDateTime(application.deadline)}

-
- )} -
- - {/* 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 && ( - - - Actions - - - {/* Show Approve/Reject block */} - {permissions.isLoaLocked && ( - - - Stage Locked - - First Fill (₹15L) must be verified by Finance before LOA Approval can proceed. - - - )} + - {permissions.canApprove && ( - <> - - - - - )} - - {permissions.showDecisionMessage && ( -
- You have {(currentUserStageAction?.decision === 'Approved' || currentUserEvaluation?.decision === 'Approved' || currentUserEvaluation?.recommendation === 'Approved' || currentUserEvaluation?.decision === 'Selected') ? 'Approved' : 'Rejected'} -
- )} - - + + {permissions.canSchedule && ( + )} - {permissions.canSchedule && ( - - )} - - {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' && ( + {currentUser && ['DD Admin', 'Super Admin'].includes(currentUser.role) && + ['Dealer Code Generation', 'LOA Pending', 'Architecture Team Assigned', 'Architecture Document Upload', 'Architecture Team Completion'].includes(application.status) && ( + <> + {!application.dealerCode && ( )} - {/* 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)}%) - - - )} + {application.dealerCode && !application.architectureAssignedTo && ( -
- )} - - {/* 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} -
- )} - -
- )} - - {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. - - -
-
- -