From 7fa34dd3d6831e31d5ff8eb46e38e6c9820a3962 Mon Sep 17 00:00:00 2001 From: laxman h Date: Wed, 1 Apr 2026 18:03:19 +0530 Subject: [PATCH] multi iteration done in approval flow for progress stsus --- src/api/API.ts | 1 + src/api/client.ts | 10 +- src/components/admin/UserManagementPage.tsx | 49 +- .../applications/ApplicationDetails.tsx | 837 ++++++++++++------ src/components/applications/WorkNotesPage.tsx | 27 +- src/components/auth/LoginPage.tsx | 35 +- src/components/public/ApplicationFormPage.tsx | 18 +- src/lib/mock-data.ts | 8 +- src/pages/public/PublicQuestionnairePage.tsx | 11 - src/services/onboarding.service.ts | 229 ++--- 10 files changed, 730 insertions(+), 495 deletions(-) diff --git a/src/api/API.ts b/src/api/API.ts index 16c21fb..5d8be3d 100644 --- a/src/api/API.ts +++ b/src/api/API.ts @@ -69,6 +69,7 @@ export const API = { getInterviews: (applicationId: string) => client.get(`/assessment/interviews/${applicationId}`), updateRecommendation: (data: any) => client.post('/assessment/recommendation', data), updateInterviewDecision: (data: any) => client.post('/assessment/decision', data), + submitStageDecision: (data: any) => client.post('/assessment/stage-decision', data), getInterviewApprovalStatus: (interviewId: string) => client.get(`/assessment/interviews/${interviewId}/approval-status`), getApprovalPolicies: () => client.get('/assessment/approval-policies'), upsertApprovalPolicy: (stageCode: string, data: any) => client.put(`/assessment/approval-policies/${stageCode}`, data), diff --git a/src/api/client.ts b/src/api/client.ts index 205d432..a95c3d1 100644 --- a/src/api/client.ts +++ b/src/api/client.ts @@ -8,7 +8,7 @@ const client = create({ 'Content-Type': 'application/json', 'Accept': 'application/json', }, - timeout: 10000, + timeout: 30000, }); // Interceptor for Auth Token @@ -23,7 +23,13 @@ client.addRequestTransform((request) => { client.addResponseTransform((response) => { if (!response.ok) { if (response.status === 401) { - console.error('Unauthorized access - potential token expiration'); + const token = localStorage.getItem('token'); + console.error('Unauthorized access - potential token expiration. Token exists in localStorage:', !!token); + console.error('Full 401 Response Details:', { + url: response.config?.url, + method: response.config?.method, + data: response.data + }); // Dispatch global event for App to handle logout window.dispatchEvent(new Event('auth:logout')); } diff --git a/src/components/admin/UserManagementPage.tsx b/src/components/admin/UserManagementPage.tsx index e0645e5..e081512 100644 --- a/src/components/admin/UserManagementPage.tsx +++ b/src/components/admin/UserManagementPage.tsx @@ -46,7 +46,6 @@ export function UserManagementPage() { const [regions, setRegions] = useState([]); const [states, setStates] = useState([]); const [districts, setDistricts] = useState([]); - const [areas, setAreas] = useState([]); const [loading, setLoading] = useState(true); const [searchQuery, setSearchQuery] = useState(''); @@ -69,8 +68,7 @@ export function UserManagementPage() { zoneId: '', regionId: '', stateId: '', - districtId: '', - areaId: '' + districtId: '' }); useEffect(() => { @@ -120,15 +118,9 @@ export function UserManagementPage() { } }, [formData.stateId]); - // Load areas when district changes + // Load areas when district changes (Disabled) useEffect(() => { - if (formData.districtId) { - masterService.getAreas(formData.districtId).then((res: any) => { - if (res && res.success) setAreas(normalizeList(res, 'areas')); - }); - } else { - setAreas([]); - } + // Area selection removed as per user request }, [formData.districtId]); const handleEditUser = (user: any) => { @@ -139,8 +131,7 @@ export function UserManagementPage() { const regionId = user.regionId || (userLocationType === 'region' ? userLocation?.id : getParentIdByType(userLocation, 'region')); const stateId = user.stateId || (userLocationType === 'state' ? userLocation?.id : getParentIdByType(userLocation, 'state')); const districtId = user.districtId || (userLocationType === 'district' ? userLocation?.id : getParentIdByType(userLocation, 'district')); - const areaId = user.areaId || (userLocationType === 'area' ? userLocation?.id : ''); - + setEditingUser(user); setFormData({ fullName: user.fullName || '', @@ -155,8 +146,7 @@ export function UserManagementPage() { zoneId: zoneId || '', regionId: regionId || '', stateId: stateId || '', - districtId: districtId || '', - areaId: areaId || '' + districtId: districtId || '' }); setShowUserModal(true); }; @@ -168,7 +158,7 @@ export function UserManagementPage() { } try { - const userLocationId = formData.areaId || formData.districtId || formData.stateId || formData.regionId || formData.zoneId || null; + const userLocationId = formData.districtId || formData.stateId || formData.regionId || formData.zoneId || null; const submitData = { ...formData, locationId: userLocationId @@ -187,7 +177,7 @@ export function UserManagementPage() { setFormData({ fullName: '', email: '', roleCode: '', status: 'active', isActive: true, mobileNumber: '', department: '', designation: '', employeeId: '', - zoneId: '', regionId: '', stateId: '', districtId: '', areaId: '' + zoneId: '', regionId: '', stateId: '', districtId: '' }); setShowUserModal(false); fetchData(); @@ -236,7 +226,7 @@ export function UserManagementPage() { setEditingUser(null); setFormData({ fullName: '', email: '', roleCode: '', status: 'active', isActive: true, mobileNumber: '', department: '', designation: '', employeeId: '', - zoneId: '', regionId: '', stateId: '', districtId: '', areaId: '' + zoneId: '', regionId: '', stateId: '', districtId: '' }); setShowUserModal(true); }} className="bg-amber-600 hover:bg-amber-700 text-white shrink-0" @@ -506,7 +496,7 @@ export function UserManagementPage() { setFormData({ ...formData, stateId: val, districtId: '', areaId: '' })} + onValueChange={(val) => setFormData({ ...formData, stateId: val, districtId: '' })} disabled={!formData.zoneId} > @@ -556,7 +546,7 @@ export function UserManagementPage() { -
- - -
diff --git a/src/components/applications/ApplicationDetails.tsx b/src/components/applications/ApplicationDetails.tsx index 85a69f4..9d3d89b 100644 --- a/src/components/applications/ApplicationDetails.tsx +++ b/src/components/applications/ApplicationDetails.tsx @@ -8,6 +8,7 @@ import { eorService } from '../../services/eor.service'; import QuestionnaireResponseView from './QuestionnaireResponseView'; import { useSelector } from 'react-redux'; import { RootState } from '../../store'; +import { cn } from '@/components/ui/utils'; import { Button } from '../ui/button'; import { Badge } from '../ui/badge'; @@ -36,14 +37,15 @@ import { GitBranch, Star, Zap, - ShieldCheck + ShieldCheck, + Eye, } from 'lucide-react'; import { Progress } from '../ui/progress'; import { Textarea } from '../ui/textarea'; import { Input } from '../ui/input'; import { Label } from '../ui/label'; import { Alert, AlertDescription, AlertTitle } from '../ui/alert'; -import { AlertCircle, RefreshCw } from 'lucide-react'; +import { AlertCircle, RefreshCw, Check, Loader2 } from 'lucide-react'; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogTrigger } from '../ui/dialog'; import { ScrollArea } from '../ui/scroll-area'; import { @@ -281,7 +283,6 @@ export function ApplicationDetails() { questionnaireResponses: data.questionnaireResponses || [], // Map responses rank: 0, totalApplicantsAtLocation: 0, - submissionDate: data.createdAt, assignedUsers: [], progress: data.progressPercentage || 0, isShortlisted: data.isShortlisted || true, // Default to true for now @@ -296,6 +297,9 @@ export function ApplicationDetails() { ownRoyalEnfield: data.ownRoyalEnfield, address: data.address, // Map timeline dates from progressTracking + submissionDate: data.createdAt ? new Date(data.createdAt).toISOString().split('T')[0] : '', + questionnaireDate: getStageDate('Questionnaire'), + shortlistDate: getStageDate('Shortlist'), level1InterviewDate: getStageDate('1st Level Interview'), level2InterviewDate: getStageDate('2nd Level Interview'), level3InterviewDate: getStageDate('3rd Level Interview'), @@ -310,6 +314,8 @@ export function ApplicationDetails() { loaDate: getStageDate('LOA'), eorCompleteDate: getStageDate('EOR Complete'), inaugurationDate: getStageDate('Inauguration'), + onboardedDate: data.overallStatus === 'Onboarded' ? (data.updatedAt ? new Date(data.updatedAt).toISOString().split('T')[0] : new Date().toISOString().split('T')[0]) : undefined, + progressTracking: data.progressTracking || [], participants: data.participants || [], dealerCode: data.dealerCode, zoneId: data.zoneId, @@ -347,7 +353,7 @@ export function ApplicationDetails() { }; useEffect(() => { - if (['EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved'].includes(application?.status || '')) { + if (['EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Onboarded'].includes(application?.status || '')) { fetchEorData(); } }, [applicationId, application?.status]); @@ -376,8 +382,10 @@ export function ApplicationDetails() { } }, [applicationId]); - const [activeTab, setActiveTab ] = useState('questionnaire'); + const [activeTab, setActiveTab] = useState('questionnaire'); const [showApproveModal, setShowApproveModal] = useState(false); + const [showOnboardModal, setShowOnboardModal] = useState(false); + const [isOnboarding, setIsOnboarding] = useState(false); const [showRejectModal, setShowRejectModal] = useState(false); const [rejectionReason, setRejectionReason] = useState(''); const [scheduledInterviewParticipants, setScheduledInterviewParticipants] = useState([]); @@ -410,6 +418,8 @@ export function ApplicationDetails() { const [uploadDocType, setUploadDocType] = useState(''); const [approvalFile, setApprovalFile] = useState(null); // State for approval modal file const [isUploading, setIsUploading] = useState(false); + const [previewDoc, setPreviewDoc] = useState(null); + const [showPreviewModal, setShowPreviewModal] = useState(false); const [selectedInterviewerId, setSelectedInterviewerId] = useState(''); const [interviews, setInterviews] = useState([]); const [isScheduling, setIsScheduling] = useState(false); @@ -447,7 +457,7 @@ export function ApplicationDetails() { const handleSubmitKTMatrix = async () => { if (Object.keys(ktMatrixScores).length < KT_MATRIX_CRITERIA.length) { - alert('Please fill all fields in the KT Matrix'); + toast.warning('Please fill all fields in the KT Matrix'); return; } @@ -455,7 +465,7 @@ export function ApplicationDetails() { const interviewId = selectedInterviewForFeedback?.id || interviews.find(i => i.status !== 'Completed')?.id || interviews[0]?.id; if (!interviewId) { - alert('No active interview found to link this KT Matrix to.'); + toast.error('No active interview found to link this KT Matrix to.'); return; } @@ -482,7 +492,7 @@ export function ApplicationDetails() { // Reset form setKtMatrixScores({}); setKtMatrixRemarks(''); - await fetchInterviews(); + await fetchInterviews(); await fetchApplication(); // Refresh application status and progress } catch (error) { toast.error('Failed to submit KT Matrix'); @@ -511,14 +521,14 @@ export function ApplicationDetails() { const handleSubmitLevel2Feedback = async () => { if (!level2Feedback.overallScore) { - alert('Please provide an overall score.'); + toast.warning('Please provide an overall score.'); return; } const interviewId = selectedInterviewForFeedback?.id || interviews.find(i => i.status !== 'Completed' && i.level === 2)?.id; if (!interviewId) { - alert('No active Level 2 interview found to link this feedback to.'); + toast.error('No active Level 2 interview found to link this feedback to.'); return; } @@ -586,14 +596,14 @@ export function ApplicationDetails() { const handleSubmitLevel3Feedback = async () => { if (!level3Feedback.overallScore) { - alert('Please provide an overall score.'); + toast.warning('Please provide an overall score.'); return; } const interviewId = selectedInterviewForFeedback?.id || interviews.find(i => i.status !== 'Completed' && i.level === 3)?.id; if (!interviewId) { - alert('No active Level 3 interview found to link this feedback to.'); + toast.error('No active Level 3 interview found to link this feedback to.'); return; } @@ -694,8 +704,8 @@ export function ApplicationDetails() { }, [activeTab, applicationId]); const fetchUsers = async (type?: string) => { - // Only fetch users if user has admin/DD roles to avoid 403s - if (!currentUser || !['DD Admin', 'Super Admin'].includes(currentUser.role)) { + // 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 { @@ -707,7 +717,7 @@ export function ApplicationDetails() { '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; @@ -734,27 +744,41 @@ export function ApplicationDetails() { 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)) + .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) { - setScheduledInterviewParticipants(preAssigned); + // 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 (showScheduleModal && application) { + } else if ((showAssignArchitectureModal || showAssignModal) && application) { fetchUsers(); // Default fetch for other modals like Assign } - }, [showScheduleModal, interviewType, application?.participants]); + }, [showScheduleModal, showAssignArchitectureModal, showAssignModal, interviewType, application?.participants]); const handleScheduleInterview = async () => { if (!interviewDate) { - alert('Please select date and time'); + toast.warning('Please select date and time'); return; } try { @@ -769,12 +793,12 @@ export function ApplicationDetails() { 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'); @@ -784,9 +808,21 @@ export function ApplicationDetails() { } }; + 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) { - alert('Please select a file and document type'); + toast.warning('Please select a file and document type'); return; } @@ -801,7 +837,7 @@ export function ApplicationDetails() { await onboardingService.uploadDocument(applicationId!, formData); - alert('Document uploaded successfully'); + toast.success('Document uploaded successfully'); setShowUploadForm(false); setUploadFile(null); setUploadDocType(''); @@ -809,12 +845,12 @@ export function ApplicationDetails() { // 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); - alert('Failed to upload document'); + toast.error('Failed to upload document'); } finally { setIsUploading(false); } @@ -832,6 +868,14 @@ export function ApplicationDetails() { return (documents || []).some(d => d.documentType === docType); }; + const getStageStatus = (stageName: string, fallbackLogic: () => ProcessStage['status']): ProcessStage['status'] => { + const backendStage = (application.progressTracking || []).find((ps: any) => ps.stageName === stageName); + if (backendStage && (backendStage.status === 'completed' || backendStage.status === 'active')) { + return backendStage.status as any; + } + return fallbackLogic(); + }; + const processStages: ProcessStage[] = [ { id: 1, @@ -844,56 +888,74 @@ export function ApplicationDetails() { { id: 2, name: 'Questionnaire', - status: application.questionnaireMarks ? 'completed' : 'pending', - date: '2025-10-03', + status: getStageStatus('Questionnaire', () => application.questionnaireMarks ? 'completed' : 'pending'), + date: application.questionnaireDate, description: 'Questionnaire completed', documentsUploaded: 0 }, { id: 3, name: 'Shortlist', - status: ['Shortlisted', 'Level 1 Interview Pending', 'Level 1 Approved', 'Level 2 Interview Pending', 'Level 2 Approved', 'Level 2 Recommended', 'Level 3 Interview Pending', 'Level 3 Approved', 'FDD Verification', 'LOI In Progress', 'Payment Pending', 'LOI Issued', 'Statutory LOI Ack', 'Dealer Code Generation', 'Architecture Team Assigned', 'Architecture Document Upload', 'Architecture Team Completion', 'LOA Pending', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Rejected'].includes(application.status) ? 'completed' : 'pending', - date: '2025-10-04', + status: getStageStatus('Shortlist', () => ['Shortlisted', 'Level 1 Interview Pending', 'Level 1 Approved', 'Level 2 Interview Pending', 'Level 2 Approved', 'Level 2 Recommended', 'Level 3 Interview Pending', 'Level 3 Approved', 'FDD Verification', 'LOI In Progress', 'Payment Pending', 'LOI Issued', 'Statutory LOI Ack', 'Dealer Code Generation', 'Architecture Team Assigned', 'Architecture Document Upload', 'Architecture Team Completion', 'LOA Pending', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Rejected', 'Onboarded'].includes(application.status) ? 'completed' : 'pending'), + date: application.shortlistDate, description: 'Application shortlisted by DD', documentsUploaded: 2 }, { id: 4, name: '1st Level Interview', - status: ['Level 1 Approved', 'Level 2 Interview Pending', 'Level 2 Approved', 'Level 2 Recommended', 'Level 3 Interview Pending', 'Level 3 Approved', 'FDD Verification', 'LOI In Progress', 'Payment Pending', 'LOI Issued', 'Statutory LOI Ack', 'Dealer Code Generation', 'Architecture Team Assigned', 'Architecture Document Upload', 'Architecture Team Completion', 'LOA Pending', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved'].includes(application.status) ? 'completed' : application.status === 'Level 1 Interview Pending' ? 'active' : application.status === 'Rejected' && application.progress >= 40 ? 'completed' : 'pending', + status: getStageStatus('1st Level Interview', () => ['Level 1 Approved', 'Level 2 Interview Pending', 'Level 2 Approved', 'Level 2 Recommended', 'Level 3 Interview Pending', 'Level 3 Approved', 'FDD Verification', 'LOI In Progress', 'Payment Pending', 'LOI Issued', 'Statutory LOI Ack', 'Dealer Code Generation', 'Architecture Team Assigned', 'Architecture Document Upload', 'Architecture Team Completion', 'LOA Pending', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Onboarded'].includes(application.status) ? 'completed' : application.status === 'Level 1 Interview Pending' ? 'active' : application.status === 'Rejected' && application.progress >= 40 ? 'completed' : 'pending'), date: application.level1InterviewDate, description: 'DD-ZM + RBM evaluation', - evaluators: (application.participants || []) - .filter((p: any) => p.metadata?.interviewLevel === 1 || (p.metadata?.interviewLevel === '1')) - .map((p: any) => `${p.user?.name} (${p.user?.role})`), + evaluators: Array.from(new Set((application.participants || []) + .filter((p: any) => + p.metadata?.interviewLevel === 1 || + p.metadata?.interviewLevel === '1' || + p.metadata?.allAssignments?.includes(1) || + p.metadata?.allAssignments?.includes('1') + ) + .map((p: any) => `${p.user?.name} (${p.user?.role})`) + )), documentsUploaded: 1 }, { id: 5, name: '2nd Level Interview', - status: ['Level 2 Approved', 'Level 2 Recommended', 'Level 3 Interview Pending', 'Level 3 Approved', 'FDD Verification', 'LOI In Progress', 'Payment Pending', 'LOI Issued', 'Statutory LOI Ack', 'Dealer Code Generation', 'Architecture Team Assigned', 'Architecture Document Upload', 'Architecture Team Completion', 'LOA Pending', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved'].includes(application.status) ? 'completed' : application.status === 'Level 2 Interview Pending' ? 'active' : application.status === 'Rejected' && application.progress >= 55 ? 'completed' : 'pending', + status: getStageStatus('2nd Level Interview', () => ['Level 2 Approved', 'Level 2 Recommended', 'Level 3 Interview Pending', 'Level 3 Approved', 'FDD Verification', 'LOI In Progress', 'Payment Pending', 'LOI Issued', 'Statutory LOI Ack', 'Dealer Code Generation', 'Architecture Team Assigned', 'Architecture Document Upload', 'Architecture Team Completion', 'LOA Pending', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Onboarded'].includes(application.status) ? 'completed' : application.status === 'Level 2 Interview Pending' ? 'active' : application.status === 'Rejected' && application.progress >= 55 ? 'completed' : 'pending'), date: application.level2InterviewDate, description: 'DD Lead + ZBH evaluation', - evaluators: (application.participants || []) - .filter((p: any) => p.metadata?.interviewLevel === 2 || (p.metadata?.interviewLevel === '2')) - .map((p: any) => `${p.user?.name} (${p.user?.role})`), + evaluators: Array.from(new Set((application.participants || []) + .filter((p: any) => + p.metadata?.interviewLevel === 2 || + p.metadata?.interviewLevel === '2' || + p.metadata?.allAssignments?.includes(2) || + p.metadata?.allAssignments?.includes('2') + ) + .map((p: any) => `${p.user?.name} (${p.user?.role})`) + )), documentsUploaded: 1 }, { id: 6, name: '3rd Level Interview', - status: ['Level 3 Approved', 'FDD Verification', 'LOI In Progress', 'Payment Pending', 'LOI Issued', 'Statutory LOI Ack', 'Dealer Code Generation', 'Architecture Team Assigned', 'Architecture Document Upload', 'Architecture Team Completion', 'LOA Pending', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved'].includes(application.status) ? 'completed' : application.status === 'Level 3 Interview Pending' ? 'active' : application.status === 'Rejected' && application.progress >= 70 ? 'completed' : 'pending', + status: getStageStatus('3rd Level Interview', () => ['Level 3 Approved', 'FDD Verification', 'LOI In Progress', 'Payment Pending', 'LOI Issued', 'Statutory LOI Ack', 'Dealer Code Generation', 'Architecture Team Assigned', 'Architecture Document Upload', 'Architecture Team Completion', 'LOA Pending', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Onboarded'].includes(application.status) ? 'completed' : application.status === 'Level 3 Interview Pending' ? 'active' : application.status === 'Rejected' && application.progress >= 70 ? 'completed' : 'pending'), date: application.level3InterviewDate, description: 'NBH + DD Head evaluation', - evaluators: (application.participants || []) - .filter((p: any) => p.metadata?.interviewLevel === 3 || (p.metadata?.interviewLevel === '3')) - .map((p: any) => `${p.user?.name} (${p.user?.role})`), + evaluators: Array.from(new Set((application.participants || []) + .filter((p: any) => + p.metadata?.interviewLevel === 3 || + p.metadata?.interviewLevel === '3' || + p.metadata?.allAssignments?.includes(3) || + p.metadata?.allAssignments?.includes('3') + ) + .map((p: any) => `${p.user?.name} (${p.user?.role})`) + )), documentsUploaded: 2 }, { id: 7, name: 'FDD', - status: ['LOI In Progress', 'Payment Pending', 'LOI Issued', 'Statutory LOI Ack', 'Dealer Code Generation', 'Architecture Team Assigned', 'Architecture Document Upload', 'Architecture Team Completion', 'LOA Pending', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved'].includes(application.status) ? 'completed' : application.status === 'FDD Verification' ? 'active' : 'pending', + status: getStageStatus('FDD', () => ['LOI In Progress', 'Payment Pending', 'LOI Issued', 'Statutory LOI Ack', 'Dealer Code Generation', 'Architecture Team Assigned', 'Architecture Document Upload', 'Architecture Team Completion', 'LOA Pending', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Onboarded'].includes(application.status) ? 'completed' : application.status === 'FDD Verification' ? 'active' : 'pending'), date: application.fddDate, description: 'Financial Due Diligence', documentsUploaded: 5 @@ -901,18 +963,22 @@ export function ApplicationDetails() { { id: 8, name: 'LOI Approval', - status: ['Payment Pending', 'LOI Issued', 'Statutory LOI Ack', 'Dealer Code Generation', 'Architecture Team Assigned', 'Architecture Document Upload', 'Architecture Team Completion', 'LOA Pending', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved'].includes(application.status) ? 'completed' : application.status === 'LOI In Progress' ? 'active' : 'pending', + status: getStageStatus('LOI Approval', () => ['Payment Pending', 'LOI Issued', 'Statutory LOI Ack', 'Dealer Code Generation', 'Architecture Team Assigned', 'Architecture Document Upload', 'Architecture Team Completion', 'LOA Pending', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Onboarded'].includes(application.status) ? 'completed' : application.status === 'LOI In Progress' ? 'active' : 'pending'), date: application.loiApprovalDate, description: 'Letter of Intent approval', - evaluators: (application.participants || []) - .filter((p: any) => p.metadata?.stageCode === 'LOI_APPROVAL') - .map((p: any) => `${p.user?.name} (${p.user?.role})`), + 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: ['Payment Pending', 'Dealer Code Generation', 'Architecture Team Assigned', 'Architecture Document Upload', 'Architecture Team Completion', 'LOA Pending', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved'].includes(application.status) ? 'completed' : 'pending', + status: getStageStatus('Security Details', () => ['Payment Pending', 'Dealer Code Generation', 'Architecture Team Assigned', 'Architecture Document Upload', 'Architecture Team Completion', 'LOA Pending', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Onboarded'].includes(application.status) ? 'completed' : 'pending'), date: application.securityDetailsDate, description: 'Security verification', documentsUploaded: 3 @@ -920,7 +986,7 @@ export function ApplicationDetails() { { id: 10, name: 'LOI Issue', - status: ['Statutory LOI Ack', 'Dealer Code Generation', 'Architecture Team Assigned', 'Architecture Document Upload', 'Architecture Team Completion', 'LOA Pending', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved'].includes(application.status) ? 'completed' : ['Payment Pending', 'LOI Issued'].includes(application.status) ? 'active' : 'pending', + status: getStageStatus('LOI Issue', () => ['Statutory LOI Ack', 'Dealer Code Generation', 'Architecture Team Assigned', 'Architecture Document Upload', 'Architecture Team Completion', 'LOA Pending', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Onboarded'].includes(application.status) ? 'completed' : ['Payment Pending', 'LOI Issued'].includes(application.status) ? 'active' : 'pending'), date: application.loiIssueDate, description: 'Letter of Intent issued', documentsUploaded: 1 @@ -928,7 +994,7 @@ export function ApplicationDetails() { { id: 11, name: 'Dealer Code Generation', - status: (application.dealerCode || ['Architecture Team Assigned', 'Architecture Document Upload', 'Architecture Team Completion', 'Statutory GST', 'Statutory PAN', 'Statutory NODAL', 'Statutory NODAL', 'Statutory Check', 'Statutory Partnership', 'Statutory Firm Reg', 'Statutory Rental', 'Statutory Virtual Code', 'Statutory Domain', 'Statutory MSD', 'Statutory LOI Ack', 'LOA Pending', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved'].includes(application.status)) ? 'completed' : application.status === 'Dealer Code Generation' ? 'active' : 'pending', + status: getStageStatus('Dealer Code Generation', () => (application.dealerCode || ['Architecture Team Assigned', 'Architecture Document Upload', 'Architecture Team Completion', 'Statutory GST', 'Statutory PAN', 'Statutory NODAL', 'Statutory NODAL', 'Statutory Check', 'Statutory Partnership', 'Statutory Firm Reg', 'Statutory Rental', 'Statutory Virtual Code', 'Statutory Domain', 'Statutory MSD', 'Statutory LOI Ack', 'LOA Pending', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Onboarded'].includes(application.status)) ? 'completed' : application.status === 'Dealer Code Generation' ? 'active' : 'pending'), date: application.dealerCodeDate, description: 'Dealer code generated and assigned', documentsUploaded: 0, @@ -941,7 +1007,7 @@ export function ApplicationDetails() { { id: '11a-1', name: 'Assigned to Architecture Team', - status: application.architectureAssignedTo || ['Architecture Document Upload', 'Architecture Team Completion', 'Statutory GST', 'Statutory PAN', 'Statutory Nodal', 'Statutory Check', 'Statutory Partnership', 'Statutory Firm Reg', 'Statutory Rental', 'Statutory Virtual Code', 'Statutory Domain', 'Statutory MSD', 'Statutory LOI Ack', 'LOA Pending', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved'].includes(application.status) || application.architectureStatus === 'COMPLETED' || isDocumentUploaded('Architecture Assignment Document') ? 'completed' : application.status === 'Architecture Team Assigned' ? 'active' : 'pending', + status: application.architectureAssignedTo || ['Architecture Document Upload', 'Architecture Team Completion', 'Statutory GST', 'Statutory PAN', 'Statutory Nodal', 'Statutory Check', 'Statutory Partnership', 'Statutory Firm Reg', 'Statutory Rental', 'Statutory Virtual Code', 'Statutory Domain', 'Statutory MSD', 'Statutory LOI Ack', 'LOA Pending', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Onboarded'].includes(application.status) || application.architectureStatus === 'COMPLETED' || isDocumentUploaded('Architecture Assignment Document') ? 'completed' : application.status === 'Architecture Team Assigned' ? 'active' : 'pending', date: application.architectureAssignedDate, description: 'Assigned to architecture team for site planning', documentsUploaded: 0 @@ -949,7 +1015,7 @@ export function ApplicationDetails() { { id: '11a-2', name: 'Architectural Document Upload', - status: isDocumentUploaded('Architecture Blueprint') || isDocumentUploaded('Site Plan') || ['Architecture Team Completion', 'LOA Pending', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved'].includes(application.status) || application.architectureStatus === 'COMPLETED' ? 'completed' : (application.architectureAssignedTo || application.status === 'Architecture Document Upload' || application.architectureStatus === 'IN_PROGRESS') ? 'active' : 'pending', + status: isDocumentUploaded('Architecture Blueprint') || isDocumentUploaded('Site Plan') || ['Architecture Team Completion', 'LOA Pending', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Onboarded'].includes(application.status) || application.architectureStatus === 'COMPLETED' ? 'completed' : (application.architectureAssignedTo || application.status === 'Architecture Document Upload' || application.architectureStatus === 'IN_PROGRESS') ? 'active' : 'pending', date: application.architectureDocumentDate, description: 'Architectural documents and blueprints uploaded', documentsUploaded: (documents || []).filter(d => ['Architecture Blueprint', 'Site Plan'].includes(d.documentType)).length @@ -957,7 +1023,7 @@ export function ApplicationDetails() { { id: '11a-3', name: 'Architecture Team Completion', - status: ['LOA Pending', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved'].includes(application.status) || application.architectureStatus === 'COMPLETED' || isDocumentUploaded('Architecture Completion Certificate') ? 'completed' : application.status === 'Architecture Team Completion' ? 'active' : 'pending', + status: ['LOA Pending', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Onboarded'].includes(application.status) || application.architectureStatus === 'COMPLETED' || isDocumentUploaded('Architecture Completion Certificate') ? 'completed' : application.status === 'Architecture Team Completion' ? 'active' : 'pending', date: application.architectureCompletionDate, description: 'Architecture team work completed', documentsUploaded: 0 @@ -971,77 +1037,77 @@ export function ApplicationDetails() { { id: '11b-1', name: 'GST', - status: isDocumentUploaded('GST Certificate') || ['Statutory PAN', 'Statutory Nodal', 'Statutory Check', 'Statutory Partnership', 'Statutory Firm Reg', 'Statutory Rental', 'Statutory Virtual Code', 'Statutory Domain', 'Statutory MSD', 'Statutory LOI Ack', 'LOA Pending', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved'].includes(application.status) ? 'completed' : application.status === 'Statutory GST' ? 'active' : 'pending', + status: isDocumentUploaded('GST Certificate') || ['Statutory PAN', 'Statutory Nodal', 'Statutory Check', 'Statutory Partnership', 'Statutory Firm Reg', 'Statutory Rental', 'Statutory Virtual Code', 'Statutory Domain', 'Statutory MSD', 'Statutory LOI Ack', 'LOA Pending', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Onboarded'].includes(application.status) ? 'completed' : application.status === 'Statutory GST' ? 'active' : 'pending', description: 'GST certificate', documentsUploaded: (documents || []).filter(d => d.documentType === 'GST Certificate').length }, { id: '11b-2', name: 'PAN', - status: isDocumentUploaded('PAN Card') || ['Statutory Nodal', 'Statutory Check', 'Statutory Partnership', 'Statutory Firm Reg', 'Statutory Rental', 'Statutory Virtual Code', 'Statutory Domain', 'Statutory MSD', 'Statutory LOI Ack', 'LOA Pending', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved'].includes(application.status) ? 'completed' : application.status === 'Statutory PAN' ? 'active' : 'pending', + status: isDocumentUploaded('PAN Card') || ['Statutory Nodal', 'Statutory Check', 'Statutory Partnership', 'Statutory Firm Reg', 'Statutory Rental', 'Statutory Virtual Code', 'Statutory Domain', 'Statutory MSD', 'Statutory LOI Ack', 'LOA Pending', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Onboarded'].includes(application.status) ? 'completed' : application.status === 'Statutory PAN' ? 'active' : 'pending', description: 'PAN card', documentsUploaded: (documents || []).filter(d => d.documentType === 'PAN Card').length }, { id: '11b-3', name: 'Nodal Agreement', - status: isDocumentUploaded('Nodal Agreement') || ['Statutory Check', 'Statutory Partnership', 'Statutory Firm Reg', 'Statutory Rental', 'Statutory Virtual Code', 'Statutory Domain', 'Statutory MSD', 'Statutory LOI Ack', 'LOA Pending', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved'].includes(application.status) ? 'completed' : application.status === 'Statutory Nodal' ? 'active' : 'pending', + status: isDocumentUploaded('Nodal Agreement') || ['Statutory Check', 'Statutory Partnership', 'Statutory Firm Reg', 'Statutory Rental', 'Statutory Virtual Code', 'Statutory Domain', 'Statutory MSD', 'Statutory LOI Ack', 'LOA Pending', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Onboarded'].includes(application.status) ? 'completed' : application.status === 'Statutory Nodal' ? 'active' : 'pending', description: 'Nodal agreement document', documentsUploaded: (documents || []).filter(d => d.documentType === 'Nodal Agreement').length }, { id: '11b-4', name: 'Cancelled Check', - status: isDocumentUploaded('Cancelled Check') || ['Statutory Partnership', 'Statutory Firm Reg', 'Statutory Rental', 'Statutory Virtual Code', 'Statutory Domain', 'Statutory MSD', 'Statutory LOI Ack', 'LOA Pending', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved'].includes(application.status) ? 'completed' : application.status === 'Statutory Check' ? 'active' : 'pending', + status: isDocumentUploaded('Cancelled Check') || ['Statutory Partnership', 'Statutory Firm Reg', 'Statutory Rental', 'Statutory Virtual Code', 'Statutory Domain', 'Statutory MSD', 'Statutory LOI Ack', 'LOA Pending', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Onboarded'].includes(application.status) ? 'completed' : application.status === 'Statutory Check' ? 'active' : 'pending', description: 'Cancelled check copy', documentsUploaded: (documents || []).filter(d => d.documentType === 'Cancelled Check').length }, { id: '11b-5', name: 'Partnership Deed/LLP/MOA/AOA/COI', - status: isDocumentUploaded('Partnership Deed') || isDocumentUploaded('LLP Agreement') || isDocumentUploaded('Certificate of Incorporation') || isDocumentUploaded('MOA') || isDocumentUploaded('AOA') || ['Statutory Firm Reg', 'Statutory Rental', 'Statutory Virtual Code', 'Statutory Domain', 'Statutory MSD', 'Statutory LOI Ack', 'LOA Pending', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved'].includes(application.status) ? 'completed' : application.status === 'Statutory Partnership' ? 'active' : 'pending', + status: isDocumentUploaded('Partnership Deed') || isDocumentUploaded('LLP Agreement') || isDocumentUploaded('Certificate of Incorporation') || isDocumentUploaded('MOA') || isDocumentUploaded('AOA') || ['Statutory Firm Reg', 'Statutory Rental', 'Statutory Virtual Code', 'Statutory Domain', 'Statutory MSD', 'Statutory LOI Ack', 'LOA Pending', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Onboarded'].includes(application.status) ? 'completed' : application.status === 'Statutory Partnership' ? 'active' : 'pending', description: 'Business entity documents', documentsUploaded: (documents || []).filter(d => ['Partnership Deed', 'LLP Agreement', 'Certificate of Incorporation', 'MOA', 'AOA'].includes(d.documentType)).length }, { id: '11b-6', name: 'Firm Registration Certificate', - status: isDocumentUploaded('Firm Registration') || ['Statutory Rental', 'Statutory Virtual Code', 'Statutory Domain', 'Statutory MSD', 'Statutory LOI Ack', 'LOA Pending', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved'].includes(application.status) ? 'completed' : application.status === 'Statutory Firm Reg' ? 'active' : 'pending', + status: isDocumentUploaded('Firm Registration') || ['Statutory Rental', 'Statutory Virtual Code', 'Statutory Domain', 'Statutory MSD', 'Statutory LOI Ack', 'LOA Pending', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Onboarded'].includes(application.status) ? 'completed' : application.status === 'Statutory Firm Reg' ? 'active' : 'pending', description: 'Firm registration certificate', documentsUploaded: (documents || []).filter(d => d.documentType === 'Firm Registration').length }, { id: '11b-7', name: 'Rental agreement/ Lease agreement / Own/ Land agreement', - status: isDocumentUploaded('Rental Agreement') || isDocumentUploaded('Property Documents') || ['Statutory Virtual Code', 'Statutory Domain', 'Statutory MSD', 'Statutory LOI Ack', 'LOA Pending', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved'].includes(application.status) ? 'completed' : application.status === 'Statutory Rental' ? 'active' : 'pending', + status: isDocumentUploaded('Rental Agreement') || isDocumentUploaded('Property Documents') || ['Statutory Virtual Code', 'Statutory Domain', 'Statutory MSD', 'Statutory LOI Ack', 'LOA Pending', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Onboarded'].includes(application.status) ? 'completed' : application.status === 'Statutory Rental' ? 'active' : 'pending', description: 'Property agreement document', documentsUploaded: (documents || []).filter(d => ['Rental Agreement', 'Property Documents'].includes(d.documentType)).length }, { id: '11b-8', name: 'Virtual Code', - status: isDocumentUploaded('Virtual Code Confirmation') || ['Statutory Domain', 'Statutory MSD', 'Statutory LOI Ack', 'LOA Pending', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved'].includes(application.status) ? 'completed' : application.status === 'Statutory Virtual Code' ? 'active' : 'pending', + status: isDocumentUploaded('Virtual Code Confirmation') || ['Statutory Domain', 'Statutory MSD', 'Statutory LOI Ack', 'LOA Pending', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Onboarded'].includes(application.status) ? 'completed' : application.status === 'Statutory Virtual Code' ? 'active' : 'pending', description: 'Virtual code availability', documentsUploaded: (documents || []).filter(d => d.documentType === 'Virtual Code Confirmation').length }, { id: '11b-9', name: 'Domain ID', - status: isDocumentUploaded('Domain ID Setup') || ['Statutory MSD', 'Statutory LOI Ack', 'LOA Pending', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved'].includes(application.status) ? 'completed' : application.status === 'Statutory Domain' ? 'active' : 'pending', + status: isDocumentUploaded('Domain ID Setup') || ['Statutory MSD', 'Statutory LOI Ack', 'LOA Pending', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Onboarded'].includes(application.status) ? 'completed' : application.status === 'Statutory Domain' ? 'active' : 'pending', description: 'Domain ID setup', documentsUploaded: (documents || []).filter(d => d.documentType === 'Domain ID Setup').length }, { id: '11b-10', name: 'MSD Configuration', - status: isDocumentUploaded('MSD Configuration') || ['Statutory LOI Ack', 'LOA Pending', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved'].includes(application.status) ? 'completed' : application.status === 'Statutory MSD' ? 'active' : 'pending', + status: isDocumentUploaded('MSD Configuration') || ['Statutory LOI Ack', 'LOA Pending', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Onboarded'].includes(application.status) ? 'completed' : application.status === 'Statutory MSD' ? 'active' : 'pending', description: 'Microsoft Dynamics configuration', documentsUploaded: (documents || []).filter(d => d.documentType === 'MSD Configuration').length }, { id: '11b-11', name: 'LOI Acknowledgement Copy', - status: isDocumentUploaded('LOI Acknowledgement') || ['LOA Pending', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved'].includes(application.status) ? 'completed' : application.status === 'Statutory LOI Ack' ? 'active' : 'pending', + status: isDocumentUploaded('LOI Acknowledgement') || ['LOA Pending', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Onboarded'].includes(application.status) ? 'completed' : application.status === 'Statutory LOI Ack' ? 'active' : 'pending', description: 'LOI acknowledgement copy', documentsUploaded: (documents || []).filter(d => d.documentType === 'LOI Acknowledgement').length } @@ -1052,18 +1118,22 @@ export function ApplicationDetails() { { id: 12, name: 'LOA', - status: ['EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved'].includes(application.status) ? 'completed' : application.status === 'LOA Pending' ? 'active' : 'pending', + status: getStageStatus('LOA', () => ['EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Onboarded'].includes(application.status) ? 'completed' : application.status === 'LOA Pending' ? 'active' : 'pending'), date: application.loaDate, description: 'Letter of Authorization', - evaluators: (application.participants || []) - .filter((p: any) => p.metadata?.stageCode === 'LOA_APPROVAL') - .map((p: any) => `${p.user?.name} (${p.user?.role})`), + evaluators: Array.from(new Set((application.participants || []) + .filter((p: any) => + p.metadata?.stageCode === 'LOA_APPROVAL' || + p.metadata?.allAssignments?.includes('LOA_APPROVAL') + ) + .map((p: any) => `${p.user?.name} (${p.user?.role})`) + )), documentsUploaded: 1 }, { id: 13, name: 'EOR Complete', - status: ['Inauguration', 'Approved'].includes(application.status) ? 'completed' : application.status === 'EOR Complete' ? 'active' : 'pending', + status: getStageStatus('EOR Complete', () => ['Inauguration', 'Approved', 'Onboarded'].includes(application.status) ? 'completed' : application.status === 'EOR Complete' ? 'active' : 'pending'), date: application.eorCompleteDate, description: 'Essential Operating Requirements completed', documentsUploaded: 6 @@ -1071,10 +1141,17 @@ export function ApplicationDetails() { { id: 14, name: 'Inauguration', - status: application.status === 'Approved' ? 'completed' : application.status === 'Inauguration' ? 'active' : 'pending', + status: getStageStatus('Inauguration', () => ['Approved', 'Onboarded'].includes(application.status) ? 'completed' : application.status === 'Inauguration' ? 'active' : 'pending'), date: application.inaugurationDate, description: 'Dealership inauguration ceremony', documentsUploaded: 2 + }, + { + id: 15, + name: 'Dealership Active', + status: getStageStatus('Onboarded', () => application.status === 'Onboarded' ? 'completed' : 'pending'), + date: application.onboardedDate, + description: 'Dealer profile and login created' } ]; @@ -1184,14 +1261,14 @@ export function ApplicationDetails() { } if (!approvalRemark.trim()) { - alert('Please enter a remark'); + toast.warning('Please enter a remark'); return; } try { // Application level approval - Robust State Machine let newStatus = application.status; - + switch (application.status) { case 'Shortlisted': case 'Level 1 Interview Pending': @@ -1209,13 +1286,10 @@ export function ApplicationDetails() { case 'LOI In Progress': newStatus = 'LOI Issued'; break; case 'LOI Issued': - newStatus = 'Dealer Code Generation'; break; + newStatus = 'Dealer Code Generation'; break; case 'Dealer Code Generation': - newStatus = 'Architecture Team Assigned'; break; case 'Architecture Team Assigned': - newStatus = 'Architecture Document Upload'; break; case 'Architecture Document Upload': - newStatus = 'Architecture Team Completion'; break; case 'Architecture Team Completion': newStatus = 'Statutory GST'; break; case 'Statutory GST': @@ -1242,10 +1316,38 @@ export function ApplicationDetails() { newStatus = 'Approved'; // Final fallback } - await onboardingService.updateApplicationStatus(applicationId!, { - status: newStatus, - remarks: approvalRemark - }); + const policyManagedStages: { [key: string]: string } = { + 'Level 1 Interview Pending': 'INTERVIEW_LEVEL_1', + 'Level 2 Interview Pending': 'INTERVIEW_LEVEL_2', + 'Level 2 Recommended': 'INTERVIEW_LEVEL_2', + 'Level 3 Interview Pending': 'INTERVIEW_LEVEL_3', + 'LOI In Progress': 'LOI_APPROVAL', + 'LOA Pending': 'LOA_APPROVAL' + }; + + const stageCodeForPolicy = policyManagedStages[application.status]; + + if (stageCodeForPolicy) { + const response = await onboardingService.submitStageDecision({ + applicationId: application.id, + stageCode: stageCodeForPolicy, + decision: 'Approved', + remarks: approvalRemark, + nextStatus: newStatus + }); + + if (response.data?.statusUpdated) { + toast.success(response.message || 'Stage completed and moved to next step'); + } else { + toast.info(response.message || 'Approval recorded. Waiting for other mandatory approvers.'); + } + } else { + await onboardingService.updateApplicationStatus(applicationId!, { + status: newStatus, + remarks: approvalRemark + }); + toast.success(`Application moved to ${newStatus}`); + } // Special case: If final approval, create Dealer record if (newStatus === 'Approved') { @@ -1296,22 +1398,44 @@ export function ApplicationDetails() { } if (!rejectionReason.trim()) { - alert('Please enter a reason for rejection'); + toast.warning('Please enter a reason for rejection'); return; } try { - await onboardingService.updateApplicationStatus(applicationId!, { - status: 'Rejected', - remarks: rejectionReason - }); + 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) { console.error('Rejection error:', error); - toast.error('Failed to reject application'); + toast.error('Failed to process rejection'); } }; @@ -1328,7 +1452,7 @@ export function ApplicationDetails() { const handleAssignArchitecture = async () => { if (!architectureLeadId) { - alert('Please select an architecture lead'); + toast.warning('Please select an architecture lead'); return; } try { @@ -1360,17 +1484,17 @@ export function ApplicationDetails() { const handleWorkNote = () => { if (!workNote.trim()) { - alert('Please enter a note'); + toast.warning('Please enter a note'); return; } - alert(`Work note added: ${workNote}`); + toast.info(`Work note added: ${workNote}`); setShowWorkNoteModal(false); setWorkNote(''); }; const handleAddParticipant = async () => { if (!selectedUser) { - alert('Please select a user'); + toast.warning('Please select a user'); return; } try { @@ -1434,6 +1558,10 @@ export function ApplicationDetails() { 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 @@ -1441,28 +1569,47 @@ export function ApplicationDetails() { // Specific to the current active interview context const hasSubmittedFeedbackForActive = activeInterviewForUser && hasSubmittedFeedback; - const hasMadeDecisionForUser = currentUserEvaluation?.decision === 'Approved' || + 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 && a.actorUserId === currentUser?.id + ); + + const hasMadeStageDecision = !!currentUserStageAction; + + const hasMadeDecisionForUser = !!currentUserStageAction || + currentUserEvaluation?.decision === 'Approved' || currentUserEvaluation?.decision === 'Rejected' || - currentUserEvaluation?.decision === 'Selected'; // Maintain compatibility if needed + ['Approved', 'Rejected', 'Selected'].includes(currentUserEvaluation?.recommendation || ''); // Final visibility flags - const isAdmin = currentUser && ['DD Admin', 'Super Admin'].includes(currentUser.role); + const isAdmin = currentUser && ['DD Admin', 'Super Admin', 'NBH', 'DD Lead', 'DD Head'].includes(currentUser.role); const isAdministrativeStage = [ - 'Shortlisted', 'Level 3 Approved', 'FDD Verification', + '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 GST', 'Statutory PAN', 'Statutory Nodal', 'Statutory Check', + 'Statutory Partnership', 'Statutory Firm Reg', 'Statutory Rental', 'Statutory Virtual Code', 'Statutory Domain', 'Statutory MSD', 'LOA Pending', 'EOR Complete', 'Inauguration' ].includes(application.status); // Show Approve/Reject if: - // 1. It's an interview and feedback is submitted - // 2. OR it's an administrative stage and user is Admin - const shouldShowApproveReject = - (!hasMadeDecisionForUser && hasSubmittedFeedbackForActive) || - (isAdmin && isAdministrativeStage); + // 1. It's an interview and feedback is submitted AND no decision made yet + // 2. OR it's an administrative stage and user is Admin AND hasn't made a decision yet + const shouldShowApproveReject = + (!hasMadeDecisionForUser && hasSubmittedFeedbackForActive) || + (isAdmin && isAdministrativeStage && !hasMadeStageDecision); + + const shouldShowDecisionMessage = hasMadeDecisionForUser && (!isAdministrativeStage || hasMadeStageDecision); @@ -1710,46 +1857,46 @@ export function ApplicationDetails() {

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

- ) : (() => { - // Determine expected count for this stage - const expectedMap: Record = { - 4: 2, // L1 Interview (ZM + RBM) - 5: 2, // L2 Interview (ZBH + DD Lead) - 6: 2, // L3 Interview (NBH + DD Head) - 8: 3, // LOI Approval (Finance + DD Head + NBH) - 12: 2 // LOA Approval (DD Head + NBH) - }; - const stageId = Number(stage.id); - const expectedCount = expectedMap[stageId]; - const actualCount = stage.evaluators?.length || 0; + ) : (() => { + // Determine expected count for this stage + const expectedMap: Record = { + 4: 2, // L1 Interview (ZM + RBM) + 5: 2, // L2 Interview (ZBH + DD Lead) + 6: 2, // L3 Interview (NBH + DD Head) + 8: 3, // LOI Approval (Finance + DD Head + NBH) + 12: 2 // LOA Approval (DD Head + NBH) + }; + const stageId = Number(stage.id); + const expectedCount = expectedMap[stageId]; + const actualCount = stage.evaluators?.length || 0; - if (expectedCount && actualCount < expectedCount && application.status !== 'Rejected') { - return ( -
- - - Missing Evaluators - - {actualCount === 0 - ? "Respective role users were not found for this location." - : `Some roles (${actualCount}/${expectedCount}) are missing for this location.` - } - - - -
- ); - } - return null; - })()} + if (expectedCount && actualCount < expectedCount && application.status !== 'Rejected') { + return ( +
+ + + Missing Evaluators + + {actualCount === 0 + ? "Respective role users were not found for this location." + : `Some roles (${actualCount}/${expectedCount}) are missing for this location.` + } + + + +
+ ); + } + return null; + })()} {/* Stage Docs Link */} {(() => { const stageDocsCount = documents.filter(doc => @@ -1969,12 +2116,13 @@ export function ApplicationDetails() { Location/Link Status Scheduled By + Actions {(!interviews || interviews.length === 0) ? ( - + No interviews scheduled yet @@ -1985,7 +2133,7 @@ export function ApplicationDetails() { {interview.scheduleDate ? new Date(interview.scheduleDate).toLocaleString() : 'N/A'} {interview.interviewType} - {interview.interviewType === 'virtual' ? ( + {interview.interviewType?.toLowerCase().includes('virtual') ? ( Join Meeting @@ -1999,6 +2147,18 @@ export function ApplicationDetails() { {interview.scheduler?.fullName || interview.scheduledBy || 'N/A'} + + {(interview.status === 'Scheduled' || interview.status === 'scheduled') && ( + + )} + )) )} @@ -2094,7 +2254,7 @@ export function ApplicationDetails() { )} - {['Level 2 Approved', 'Level 3 Interview Pending', 'Approved'].includes(application.status) && ( + {['Level 2 Approved', 'Level 3 Interview Pending', 'Approved', 'Onboarded'].includes(application.status) && (

Level 2 Interview Summary

@@ -2117,19 +2277,19 @@ export function ApplicationDetails() { {(eorData?.items || eorChecklist).map((item: any) => { const docType = item.description || item.item; const hasDocument = !!item.proofDocument; - + return ( -
- - + {/* Clickable Info Area */} -
{ setSelectedStage(`EOR: ${docType}`); @@ -2164,8 +2324,8 @@ export function ApplicationDetails() {
{hasDocument && !item.isCompliant && isAdmin && (
- -
)} - + {!hasDocument && (
@@ -2224,7 +2384,7 @@ export function ApplicationDetails() {

EOR Checklist Complete

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

-

Current Status

- {application.status} + + {application.status} +
{application.rank && (
@@ -2393,9 +2560,9 @@ export function ApplicationDetails() { )} - {(hasMadeDecisionForUser) && ( -
- You have {currentUserEvaluation?.recommendation === 'Approved' ? 'Approved' : 'Rejected'} + {(shouldShowDecisionMessage) && ( +
+ You have {(currentUserStageAction?.decision === 'Approved' || currentUserEvaluation?.decision === 'Approved' || currentUserEvaluation?.recommendation === 'Approved' || currentUserEvaluation?.decision === 'Selected') ? 'Approved' : 'Rejected'}
)} @@ -2416,17 +2583,17 @@ export function ApplicationDetails() { Work Note - {currentUser && ['DD Admin', 'Super Admin', 'DD AM', 'ASM'].includes(currentUser.role) && - !([1, 2, 3].every(level => interviews.some(i => i.level === level))) && ( - - )} + {currentUser && ['DD Admin', 'Super Admin', 'DD AM', 'ASM'].includes(currentUser.role) && + !([1, 2, 3].every(level => interviews.some(i => i.level === level))) && ( + + )} {currentUser && ['DD Admin', 'Super Admin'].includes(currentUser.role) && application.status === 'Dealer Code Generation' && ( <> @@ -2504,24 +2671,14 @@ export function ApplicationDetails() { )} - {/* Dedicated Onboarding Button - Appears when logic is ready to onboard as a dealer */} - {isAdmin && ['Dealer Code Generation', 'Architecture Team Completion', 'LOA Pending', 'EOR Complete', 'Inauguration', 'Approved'].includes(application.status) && !application.dealer && ( - )} @@ -2541,7 +2698,7 @@ export function ApplicationDetails() { {application.dealerCode.code}
)} -
@@ -2688,6 +2845,73 @@ export function ApplicationDetails() { + {/* Onboard Confirmation Modal */} + + + +
+ +
+ Finalize Onboarding + + You are about to officially onboard {application.name} as a Royal Enfield dealer. + +
+ +
+
+
+

Official dealer profile will be created.

+
+
+
+

User account will be activated with role Dealer.

+
+
+
+

Primary outlet will be registered in the system.

+
+
+ +
+ + +
+
+
+ {/* Reject Modal */} < Dialog open={showRejectModal} onOpenChange={setShowRejectModal} > @@ -2790,24 +3014,27 @@ export function ApplicationDetails() { - +
Level 1 {isInterviewCompleted(1) && } + {isInterviewActive(1) && }
- +
Level 2 {!isInterviewCompleted(1) && (Prerequisite: L1)} {isInterviewCompleted(2) && } + {isInterviewActive(2) && }
- +
Level 3 {!isInterviewCompleted(2) && (Prerequisite: L2)} {isInterviewCompleted(3) && } + {isInterviewActive(3) && }
@@ -2910,8 +3137,8 @@ export function ApplicationDetails() { +
+ + +
))} @@ -3584,37 +3824,54 @@ export function ApplicationDetails() { - PAN Card - GST Certificate - Aadhaar - Nodal Agreement - Cancelled Check - Partnership Deed - LLP Agreement - Certificate of Incorporation - MOA - AOA - Board Resolution - Firm Registration - Rental Agreement - Property Documents - Virtual Code Confirmation - Domain ID Setup - MSD Configuration - LOI Acknowledgement Copy - Architecture Assignment Document - Architecture Blueprint - Architecture Completion Certificate - Site Plan - Bank Statement - Inauguration Photos - Inauguration Report - Other - {(eorData?.items || eorChecklist).map((item: any, idx: number) => ( - - {item.description || item.item} - - ))} + {(() => { + const STAGE_DOCUMENT_MAP: Record = { + 'GST Certificate': ['GST Certificate'], + 'PAN': ['PAN Card'], + 'Nodal Agreement': ['Nodal Agreement'], + 'Cancelled Check': ['Cancelled Check'], + 'Partnership Deed/LLP/MOA/AOA/COI': ['Partnership Deed', 'LLP Agreement', 'Certificate of Incorporation', 'MOA', 'AOA', 'Board Resolution'], + 'Firm Registration Certificate': ['Firm Registration'], + 'Rental agreement/ Lease agreement / Own/ Land agreement': ['Rental Agreement', 'Property Documents'], + 'Virtual Code': ['Virtual Code Confirmation'], + 'Domain ID': ['Domain ID Setup'], + 'MSD Configuration': ['MSD Configuration'], + 'LOI Acknowledgement Copy': ['LOI Acknowledgement'], + 'Architecture Assignment Document': ['Architecture Assignment Document'], + 'Architecture Blueprint': ['Architecture Blueprint'], + 'Architecture Completion Certificate': ['Architecture Completion Certificate'], + 'Site Plan': ['Site Plan'], + 'FDD': ['FDD Final Audit Report', 'FDD Agency Assignment Letter', 'Statutory Approval Certificate'], + 'FDD Verification': ['FDD Final Audit Report', 'FDD Agency Assignment Letter', 'Statutory Approval Certificate'], + 'LOA': ['LOA Acceptance Copy'], + 'LOI Approval': ['LOI Agreement', 'LOI Acknowledgement'], + 'Inauguration': ['Inauguration Photos', 'Inauguration Report'], + '3rd Level Interview': ['AI Recommendation Summary', 'Interview Evaluation Sheet'], + '2nd Level Interview': ['Interview Evaluation Sheet'], + '1st Level Interview': ['Interview Evaluation Sheet'], + 'Shortlist': ['CIBIL Report', 'Proposed Site City Map'] + }; + + const baseDocs = ['Other']; + let filteredDocs: string[] = []; + + if (!selectedStage) { + // Show standard core docs if no stage select context + filteredDocs = ['PAN Card', 'GST Certificate', 'Aadhaar', 'Partnership Deed', 'LLP Agreement', 'Certificate of Incorporation', 'MOA', 'AOA', 'Bank Statement', 'Other']; + } else if (selectedStage.startsWith('EOR: ')) { + // Map EOR specific item directly + filteredDocs = [selectedStage.replace('EOR: ', ''), 'Other']; + } else { + // Use mapping or fallback to current stage's specific docs + filteredDocs = [...(STAGE_DOCUMENT_MAP[selectedStage] || []), ...baseDocs]; + } + + return Array.from(new Set(filteredDocs)).map((doc, idx) => ( + + {doc} + + )); + })()}
@@ -3663,6 +3920,66 @@ export function ApplicationDetails() { )} + {/* Preview Modal */} + + +
+
+
+ +
+
+ + {previewDoc?.fileName} + + + {previewDoc?.documentType || 'Document'} • {new Date(previewDoc?.createdAt).toLocaleDateString()} + +
+
+
+ +
+ {previewDoc && ( +
+ {previewDoc.fileName?.match(/\.(jpg|jpeg|png|gif|webp)$/i) ? ( + {previewDoc.fileName} + ) : previewDoc.fileName?.match(/\.pdf$/i) ? ( +