From b56a78d62120231243d25ad3023cdab0b7d5fc81 Mon Sep 17 00:00:00 2001 From: laxman h Date: Fri, 17 Apr 2026 20:25:39 +0530 Subject: [PATCH] application detail screen refactored need to test end to end --- .../applications/ApplicationDetails.tsx | 5590 ++--------------- .../ApplicantInformationCard.tsx | 347 + .../ApplicationDetailsActionModals.tsx | 332 + .../ApplicationDetailsExtendedModals.tsx | 485 ++ .../ApplicationDetailsFddAuditContent.tsx | 257 + .../ApplicationDetailsHeader.tsx | 72 + .../ApplicationDetailsSidebar.tsx | 405 ++ .../ApplicationDetailsTabs.tsx | 1070 ++++ .../applicationDetails.shared.ts | 176 + .../useApplicationDetailsAdminActions.ts | 549 ++ .../useApplicationDetailsData.ts | 206 + .../useApplicationDetailsFeedbackActions.ts | 211 + .../useApplicationDetailsLocalActions.ts | 120 + .../useApplicationDetailsPermissions.ts | 138 + .../useApplicationDetailsStageData.ts | 140 + .../useApplicationDetailsUIState.ts | 199 + 16 files changed, 5202 insertions(+), 5095 deletions(-) create mode 100644 src/components/applications/application-details/ApplicantInformationCard.tsx create mode 100644 src/components/applications/application-details/ApplicationDetailsActionModals.tsx create mode 100644 src/components/applications/application-details/ApplicationDetailsExtendedModals.tsx create mode 100644 src/components/applications/application-details/ApplicationDetailsFddAuditContent.tsx create mode 100644 src/components/applications/application-details/ApplicationDetailsHeader.tsx create mode 100644 src/components/applications/application-details/ApplicationDetailsSidebar.tsx create mode 100644 src/components/applications/application-details/ApplicationDetailsTabs.tsx create mode 100644 src/components/applications/application-details/applicationDetails.shared.ts create mode 100644 src/components/applications/application-details/useApplicationDetailsAdminActions.ts create mode 100644 src/components/applications/application-details/useApplicationDetailsData.ts create mode 100644 src/components/applications/application-details/useApplicationDetailsFeedbackActions.ts create mode 100644 src/components/applications/application-details/useApplicationDetailsLocalActions.ts create mode 100644 src/components/applications/application-details/useApplicationDetailsPermissions.ts create mode 100644 src/components/applications/application-details/useApplicationDetailsStageData.ts create mode 100644 src/components/applications/application-details/useApplicationDetailsUIState.ts diff --git a/src/components/applications/ApplicationDetails.tsx b/src/components/applications/ApplicationDetails.tsx index e1a7108..29bc65f 100644 --- a/src/components/applications/ApplicationDetails.tsx +++ b/src/components/applications/ApplicationDetails.tsx @@ -1,280 +1,27 @@ -import { useState, useEffect } from 'react'; +import { useEffect } from 'react'; import { useParams, useNavigate, useLocation } from 'react-router-dom'; -import { toast } from 'sonner'; -import { Application, ApplicationStatus } from '../../lib/mock-data'; import { onboardingService } from '../../services/onboarding.service'; -import { auditService } from '../../services/audit.service'; -import { eorService } from '../../services/eor.service'; -import { collaborationService } from '../../services/collaboration.service'; -import QuestionnaireResponseView from './QuestionnaireResponseView'; +import { ApplicationDetailsHeader } from './application-details/ApplicationDetailsHeader'; +import { ApplicantInformationCard } from './application-details/ApplicantInformationCard'; +import { ApplicationDetailsTabs } from './application-details/ApplicationDetailsTabs'; +import { ApplicationDetailsSidebar } from './application-details/ApplicationDetailsSidebar'; +import { ApplicationDetailsActionModals } from './application-details/ApplicationDetailsActionModals'; +import { ApplicationDetailsExtendedModals } from './application-details/ApplicationDetailsExtendedModals'; +import { ApplicationDetailsFddAuditContent } from './application-details/ApplicationDetailsFddAuditContent'; +import { KT_MATRIX_CRITERIA, auditLogActionBadgeClass } from './application-details/applicationDetails.shared'; +import { useApplicationDetailsPermissions } from './application-details/useApplicationDetailsPermissions'; +import { useApplicationDetailsUIState } from './application-details/useApplicationDetailsUIState'; +import { useApplicationDetailsFeedbackActions } from './application-details/useApplicationDetailsFeedbackActions'; +import { useApplicationDetailsAdminActions } from './application-details/useApplicationDetailsAdminActions'; +import { useApplicationDetailsData } from './application-details/useApplicationDetailsData'; +import { useApplicationDetailsLocalActions } from './application-details/useApplicationDetailsLocalActions'; +import { useApplicationDetailsStageData } from './application-details/useApplicationDetailsStageData'; import { useSelector } from 'react-redux'; import { RootState } from '../../store'; -import { cn, formatDateTime } from '@/components/ui/utils'; -import { DocumentPreviewModal } from '../ui/DocumentPreviewModal'; - -import { Button } from '../ui/button'; -import { Badge } from '../ui/badge'; -import { Tabs, TabsContent, TabsList, TabsTrigger } from '../ui/tabs'; -import { Card, CardContent, CardHeader, CardTitle } from '../ui/card'; -import { - ArrowLeft, - CheckCircle, - XCircle, - MessageSquare, - Calendar, - Clock, - Upload, - Download, - FileText, - User, - MapPin, - Mail, - Phone, - GraduationCap, - Bike, - Award, - ClipboardList, - ChevronDown, - ChevronRight, - GitBranch, - Star, - Zap, - ShieldCheck, - Eye, - Lock, - AlertCircle, - RefreshCw, - Building2, - Pencil, - Check, - Loader2, - Info, - ShieldAlert, - CheckCircle2, - CreditCard, -} 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 { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogTrigger } from '../ui/dialog'; -import { ScrollArea } from '../ui/scroll-area'; -import { - Table, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from '../ui/table'; -import { Checkbox } from '../ui/checkbox'; -import { Separator } from '../ui/separator'; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from '../ui/select'; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, -} from '../ui/dropdown-menu'; +import { Loader2 } from 'lucide-react'; -interface ProcessStage { - id: number | string; - name: string; - status: 'completed' | 'active' | 'pending'; - date?: string; - description?: string; - evaluators?: string[]; - documentsUploaded?: number; - isParallel?: boolean; - isLocked?: boolean; - lockMessage?: string; - branches?: { - name: string; - color: string; - stages: ProcessStage[]; - }[]; -} - - - -const KT_MATRIX_CRITERIA = [ - { - name: "Age", - weight: 5, - maxScore: 10, - options: [ - { label: "20 to 40 years old", value: "20-40", score: 10 }, - { label: "40 to 50 years old", value: "40-50", score: 5 }, - { label: "Above 50 years old", value: "above-50", score: 0 } - ] - }, - { - name: "Qualification", - weight: 5, - maxScore: 10, - options: [ - { label: "Post Graduate", value: "post-graduate", score: 10 }, - { label: "Graduate", value: "graduate", score: 5 }, - { label: "SSLC", value: "sslc", score: 0 } - ] - }, - { - name: "Local Knowledge and Influence", - weight: 5, - maxScore: 10, - options: [ - { label: "Excellent PR", value: "excellent", score: 10 }, - { label: "Good PR", value: "good", score: 5 }, - { label: "Poor PR", value: "poor", score: 0 } - ] - }, - { - name: "Base Location vs Applied Location", - weight: 10, - maxScore: 10, - options: [ - { label: "Native of the Applied location", value: "native", score: 10 }, - { label: "Willing to relocate", value: "relocate", score: 5 }, - { label: "Will manage remotely with occasional visits", value: "remote", score: 0 } - ] - }, - { - name: "Why Interested in Royal Enfield Business?", - weight: 10, - maxScore: 10, - options: [ - { label: "Passion", value: "passion", score: 10 }, - { label: "Business expansion / Status symbol", value: "business", score: 5 } - ] - }, - { - name: "Passion for Royal Enfield", - weight: 10, - maxScore: 10, - options: [ - { label: "Currently owns a Royal Enfield", value: "owns", score: 10 }, - { label: "Owned by Immediate Relative", value: "relative", score: 5 }, - { label: "Does not own Royal Enfield", value: "none", score: 0 } - ] - }, - { - name: "Passion For Rides", - weight: 10, - maxScore: 10, - options: [ - { label: "Goes for long rides regularly", value: "regular", score: 10 }, - { label: "Goes for long rides rarely", value: "rarely", score: 5 }, - { label: "Doesn't go for rides", value: "never", score: 0 } - ] - }, - { - name: "With Whom Partnering?", - weight: 5, - maxScore: 10, - options: [ - { label: "Within family", value: "family", score: 10 }, - { label: "Outside family", value: "outside", score: 0 } - ] - }, - { - name: "Who Will Manage the Firm?", - weight: 10, - maxScore: 10, - options: [ - { label: "Owner managed", value: "owner", score: 10 }, - { label: "Partly owner / partly manager model", value: "partly", score: 5 }, - { label: "Fully manager model", value: "manager", score: 0 } - ] - }, - { - name: "Business Acumen", - weight: 5, - maxScore: 10, - options: [ - { label: "Has similar automobile experience", value: "automobile", score: 10 }, - { label: "Has successful business but not automobile", value: "other-business", score: 5 }, - { label: "No business experience", value: "no-experience", score: 0 } - ] - }, - { - name: "Time Availability", - weight: 5, - maxScore: 10, - options: [ - { label: "Full Time Availability for RE Business", value: "full-time", score: 10 }, - { label: "Part Time Availability for RE Business", value: "part-time", score: 5 }, - { label: "Not Available personally, Manager will handle", value: "manager", score: 0 } - ] - }, - { - name: "Property Ownership", - weight: 5, - maxScore: 10, - options: [ - { label: "Has own property in proposed location", value: "own", score: 10 }, - { label: "Will rent / lease", value: "rent", score: 0 } - ] - }, - { - name: "Investment in the Business", - weight: 5, - maxScore: 10, - options: [ - { label: "Full own funds", value: "own-funds", score: 10 }, - { label: "Partially from the bank", value: "partial-bank", score: 5 }, - { label: "Completely bank funded", value: "full-bank", score: 0 } - ] - }, - { - name: "Will Expand to Other 2W/4W OEMs?", - weight: 5, - maxScore: 10, - options: [ - { label: "No", value: "no", score: 10 }, - { label: "Yes", value: "yes", score: 0 } - ] - }, - { - name: "Plans of Expansion with RE", - weight: 5, - maxScore: 10, - options: [ - { label: "Immediate blood relation will join & expand", value: "blood-relation", score: 10 }, - { label: "Wants to expand by himself into more clusters", value: "self-expand", score: 5 }, - { label: "No plans for expansion", value: "no-plans", score: 0 } - ] - } -]; - -function auditLogActionBadgeClass(action: string): string { - const a = String(action || '').toUpperCase(); - if (a.includes('REJECT') || a.includes('DELET') || a.includes('DISQUALIF')) { - return 'border-red-200 bg-red-50/90 text-red-800'; - } - if (a === 'CREATED' || a.includes('APPROV') || a.includes('COMPLETE')) { - return 'border-emerald-200 bg-emerald-50/90 text-emerald-900'; - } - if (a.includes('DOCUMENT') || a.includes('UPLOAD') || a.includes('ATTACHMENT')) { - return 'border-sky-200 bg-sky-50/80 text-sky-900'; - } - if (a.includes('PAYMENT') || a.includes('SECURITY') || a.includes('DEPOSIT')) { - return 'border-violet-200 bg-violet-50/80 text-violet-900'; - } - if (a.includes('FDD') || a.includes('QUESTIONNAIRE') || a.includes('INTERVIEW')) { - return 'border-amber-200 bg-amber-50/80 text-amber-900'; - } - return 'border-slate-200 bg-slate-50 text-slate-700'; -} - export const ApplicationDetails = () => { const { id } = useParams<{ id: string }>(); const navigate = useNavigate(); @@ -282,286 +29,128 @@ export const ApplicationDetails = () => { const applicationId = id || ''; const onBack = () => navigate(-1); // const application = mockApplications.find(app => app.id === applicationId); - const [application, setApplication] = useState(null); - const [loading, setLoading] = useState(true); - const [documents, setDocuments] = useState([]); - - const refreshDocuments = async () => { - try { - const docs = await onboardingService.getDocuments(applicationId!); - setDocuments(docs || []); - } catch (error) { - console.error('Failed to refresh documents:', error); - } - }; - - const fetchApplication = async (silent = false) => { - try { - if (!silent) setLoading(true); - const data = await onboardingService.getApplicationById(applicationId!); - - // Helper to find stage date - const getStageDate = (stageName: string, fallbackStatus?: string) => { - const stage = data.progressTracking?.find((p: any) => p.stageName === stageName); - if (stage?.stageCompletedAt) return new Date(stage.stageCompletedAt).toISOString(); - if (stage?.stageStartedAt) return new Date(stage.stageStartedAt).toISOString(); - - // Fallback to Status History if progress track is missing - if (fallbackStatus) { - const history = (data.statusHistory || []).find((h: any) => h.newStatus === fallbackStatus); - if (history) return new Date(history.createdAt).toISOString(); - } - return undefined; - }; - - // Map backend data to frontend Application interface - const mappedApp: Application = { - id: data.id, - registrationNumber: data.applicationId || 'N/A', - name: data.applicantName, - email: data.email, - phone: data.phone, - age: data.age, - education: data.education, - residentialAddress: data.address || data.city || '', - businessAddress: data.address || '', - preferredLocation: data.preferredLocation, - state: data.state, - ownsBike: data.ownRoyalEnfield === 'yes', - pastExperience: data.experienceYears ? `${data.experienceYears} years` : (data.description || ''), - status: data.overallStatus as ApplicationStatus, - questionnaireMarks: data.score || data.questionnaireMarks || 0, // Read from score or correct field - questionnaireResponses: data.questionnaireResponses || [], // Map responses - rank: 0, - totalApplicantsAtLocation: 0, - assignedUsers: [], - progress: data.progressPercentage || 0, - isShortlisted: data.isShortlisted || true, // Default to true for now - // Add other fields to match interface - companyName: data.companyName, - source: data.source, - existingDealer: data.existingDealer, - royalEnfieldModel: data.royalEnfieldModel, - description: data.description, - pincode: data.pincode, - locationType: data.locationType, - ownRoyalEnfield: data.ownRoyalEnfield, - address: data.address, - // Map timeline dates from progressTracking - submissionDate: data.createdAt ? new Date(data.createdAt).toISOString() : '', - questionnaireDate: getStageDate('Questionnaire', 'Questionnaire Completed') || getStageDate('Questionnaire', 'Questionnaire Pending'), - shortlistDate: getStageDate('Shortlist', 'Shortlisted'), - level1InterviewDate: getStageDate('1st Level Interview', 'Level 1 Approved'), - level2InterviewDate: getStageDate('2nd Level Interview', 'Level 2 Approved'), - level3InterviewDate: getStageDate('3rd Level Interview', 'Level 3 Approved'), - fddDate: getStageDate('FDD', 'FDD Verification'), - loiApprovalDate: getStageDate('LOI Approval', 'LOI In Progress'), - securityDetailsDate: getStageDate('Security Details', 'Security Details'), - loiIssueDate: getStageDate('LOI Issue', 'LOI Issued'), - dealerCodeDate: getStageDate('Dealer Code Generation', 'Dealer Code Generation'), - architectureAssignedDate: getStageDate('Architecture Team Assigned', 'Architecture Team Assigned'), - architectureDocumentDate: getStageDate('Architecture Document Upload', 'Architecture Document Upload'), - architectureCompletionDate: getStageDate('Architecture Team Completion', 'Architecture Team Completion'), - loaDate: getStageDate('LOA', 'LOA Pending'), - eorCompleteDate: getStageDate('EOR Complete', 'EOR Complete'), - inaugurationDate: getStageDate('Inauguration', 'Inauguration'), - onboardedDate: data.overallStatus === 'Onboarded' ? (data.updatedAt ? new Date(data.updatedAt).toISOString() : new Date().toISOString()) : undefined, - progressTracking: data.progressTracking || [], - participants: data.participants || [], - dealerCode: data.dealerCode, - zoneId: data.zoneId, - regionId: data.regionId, - areaId: data.areaId, - districtId: data.districtId, - stageApprovals: data.stageApprovals || [], - fddAssignments: data.fddAssignments || [], - constitutionType: data.constitutionType, - architectureStatus: data.architectureStatus, - statutoryStatus: data.statutoryStatus, - panNumber: data.panNumber, - gstNumber: data.gstNumber, - bankName: data.bankName, - accountNumber: data.accountNumber, - ifscCode: data.ifscCode, - branchName: data.branchName, - accountHolderName: data.accountHolderName, - registeredAddress: data.registeredAddress, - }; - setApplication(mappedApp); - if (data.uploadedDocuments) { - setDocuments(data.uploadedDocuments || []); - } - } catch (error) { - console.error('Failed to fetch application details', error); - } finally { - setLoading(false); - } - }; - - useEffect(() => { - if (applicationId) { - fetchApplication(); - refreshDocuments(); - } - }, [applicationId]); - - const [eorData, setEorData] = useState(null); - - const fetchEorData = async () => { - if (!applicationId) return; - try { - const resp = await eorService.getChecklist(applicationId); - if (resp.success && resp.data) { - setEorData(resp.data); - } - } catch (err) { - console.log('EOR checklist not found or not yet initiated.'); - setEorData(null); - } - }; - - useEffect(() => { - // Always fetch EOR data regardless of status — documents can be uploaded - // before 'EOR In Progress' and must show as linked in the checklist. - if (applicationId) { - fetchEorData(); - } - }, [applicationId, application?.status]); + const { + application, + loading, + setLoading, + documents, + setDocuments, + eorData, + auditLogs, + auditLoading, + worknotes, + deposits, + paymentConfigs, + refreshDocuments, + fetchApplication, + fetchEorData, + getDeposit, + } = useApplicationDetailsData({ applicationId }); const eorProgress = eorData?.items ? (eorData.items.filter((item: any) => item.isCompliant).length / eorData.items.length) * 100 : 0; - // Audit Trail State - const [auditLogs, setAuditLogs] = useState([]); - const [auditLoading, setAuditLoading] = useState(false); - const [worknotes, setWorknotes] = useState([]); + const routerLocation = useLocation(); + const { + showFirmTypeModal, setShowFirmTypeModal, + updatingFirmType, setUpdatingFirmType, + tempFirmType, setTempFirmType, + activeTab, setActiveTab, + showApproveModal, setShowApproveModal, + showOnboardModal, setShowOnboardModal, + isOnboarding, setIsOnboarding, + showRejectModal, setShowRejectModal, + rejectionReason, setRejectionReason, + scheduledInterviewParticipants, setScheduledInterviewParticipants, + showScheduleModal, setShowScheduleModal, + showKTMatrixModal, setShowKTMatrixModal, + showLevel2FeedbackModal, setShowLevel2FeedbackModal, + showLevel3FeedbackModal, setShowLevel3FeedbackModal, + showDocumentsModal, setShowDocumentsModal, + showAssignModal, setShowAssignModal, + selectedStage, setSelectedStage, + interviewMode, setInterviewMode, + approvalRemark, setApprovalRemark, + expandedBranches, setExpandedBranches, + users, setUsers, + selectedUser, setSelectedUser, + participantType, setParticipantType, + interviewDate, setInterviewDate, + interviewType, setInterviewType, + meetingLink, setMeetingLink, + location, setLocation, + showUploadForm, setShowUploadForm, + uploadFile, setUploadFile, + uploadDocType, setUploadDocType, + approvalFile, setApprovalFile, + isUploading, setIsUploading, + previewDoc, setPreviewDoc, + showPreviewModal, setShowPreviewModal, + selectedInterviewerId, setSelectedInterviewerId, + isEditingStatutory, setIsEditingStatutory, + statutoryForm, setStatutoryForm, + isSavingStatutory, setIsSavingStatutory, + interviews, setInterviews, + isScheduling, setIsScheduling, + showAssignArchitectureModal, setShowAssignArchitectureModal, + architectureLeadId, setArchitectureLeadId, + isAssigningArchitecture, setIsAssigningArchitecture, + showArchitectureStatusModal, setShowArchitectureStatusModal, + architectureStatus, setArchitectureStatus, + architectureRemarks, setArchitectureRemarks, + isUpdatingArchitecture, setIsUpdatingArchitecture, + isAssigningParticipant, setIsAssigningParticipant, + documentConfigs, setDocumentConfigs, + fddAgencies, setFddAgencies, + selectedAgencyId, setSelectedAgencyId, + isAssigningAgency, setIsAssigningAgency, + isApproving, setIsApproving, + isRejecting, setIsRejecting, + ktMatrixScores, setKtMatrixScores, + ktMatrixSelectedValues, setKtMatrixSelectedValues, + ktMatrixRemarks, setKtMatrixRemarks, + isSubmittingKT, setIsSubmittingKT, + selectedInterviewForFeedback, setSelectedInterviewForFeedback, + showFddFinalizeModal, setShowFddFinalizeModal, + showFddFlagModal, setShowFddFlagModal, + fddAuditRecommendation, setFddAuditRecommendation, + fddAuditFindings, setFddAuditFindings, + isFinalizingFdd, setIsFinalizingFdd, + isFddFlagging, setIsFddFlagging, + level2Feedback, setLevel2Feedback, + isSubmittingLevel2, setIsSubmittingLevel2, + level3Feedback, setLevel3Feedback, + isSubmittingLevel3, setIsSubmittingLevel3, + selectedEvaluationForView, setSelectedEvaluationForView, + showFeedbackDetailsModal, setShowFeedbackDetailsModal, + } = useApplicationDetailsUIState({ + currentUser, + initialTab: routerLocation.state?.activeTab || 'questionnaire', + }); const isNonResponsive = (worknotes || []).some(note => (note.noteText || '').includes('FLAGGED:') ) || application?.statutoryStatus === 'Flagged'; - const [showFirmTypeModal, setShowFirmTypeModal] = useState(false); - const [updatingFirmType, setUpdatingFirmType] = useState(false); - const [tempFirmType, setTempFirmType] = useState(''); - - const handleUpdateFirmType = async () => { - try { - setUpdatingFirmType(true); - await onboardingService.updateApplication(applicationId!, { constitutionType: tempFirmType }); - toast.success('Firm type updated successfully'); - setShowFirmTypeModal(false); - fetchApplication(); - } catch (error) { - toast.error('Failed to update firm type'); - } finally { - setUpdatingFirmType(false); - } - }; - - // Fetch audit logs when application loads - useEffect(() => { - if (application?.id) { - const fetchAuditLogs = async () => { - setAuditLoading(true); - try { - const logs = await auditService.getAuditLogs('application', application.id, 1, 100); - setAuditLogs(Array.isArray(logs) ? logs : []); - } catch (error) { - console.error('Failed to fetch audit logs', error); - setAuditLogs([]); - } finally { - setAuditLoading(false); - } - }; - 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]); - - const routerLocation = useLocation(); - const [activeTab, setActiveTab] = useState(routerLocation.state?.activeTab || 'questionnaire'); - const [showApproveModal, setShowApproveModal] = useState(false); - const [showOnboardModal, setShowOnboardModal] = useState(false); - const [isOnboarding, setIsOnboarding] = useState(false); - const [showRejectModal, setShowRejectModal] = useState(false); - const [rejectionReason, setRejectionReason] = useState(''); - const [scheduledInterviewParticipants, setScheduledInterviewParticipants] = useState([]); - - const [showScheduleModal, setShowScheduleModal] = useState(false); - const [showKTMatrixModal, setShowKTMatrixModal] = useState(false); - const [showLevel2FeedbackModal, setShowLevel2FeedbackModal] = useState(false); - const [showLevel3FeedbackModal, setShowLevel3FeedbackModal] = useState(false); - const [showDocumentsModal, setShowDocumentsModal] = useState(false); - const [showAssignModal, setShowAssignModal] = useState(false); - const [selectedStage, setSelectedStage] = useState(null); - const [interviewMode, setInterviewMode] = useState('virtual'); - const [approvalRemark, setApprovalRemark] = useState(''); - const [expandedBranches, setExpandedBranches] = useState<{ [key: string]: boolean }>({ - 'architectural-work': true, - 'statutory-documents': true + const { + handleUpdateFirmType, + handleEditStatutory, + handleSaveStatutory, + fetchFddAgencies, + handleAssignAgency, + } = useApplicationDetailsLocalActions({ + application, + applicationId, + tempFirmType, + setUpdatingFirmType, + setShowFirmTypeModal, + setStatutoryForm, + setIsEditingStatutory, + setIsSavingStatutory, + statutoryForm, + setFddAgencies, + selectedAgencyId, + setIsAssigningAgency, + fetchApplication, }); - const [users, setUsers] = useState([]); - const [selectedUser, setSelectedUser] = useState(''); - const [participantType, setParticipantType] = useState('contributor'); - const [interviewDate, setInterviewDate] = useState(''); - const [interviewType, setInterviewType] = useState('level1'); - const [meetingLink, setMeetingLink] = useState(''); - const [location, setLocation] = useState(''); - const [showUploadForm, setShowUploadForm] = useState(false); // Toggle for upload view - const [uploadFile, setUploadFile] = useState(null); - 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 [isEditingStatutory, setIsEditingStatutory] = useState(false); - const [statutoryForm, setStatutoryForm] = useState({ - accountHolderName: '', - panNumber: '', - gstNumber: '', - bankName: '', - accountNumber: '', - ifscCode: '', - registeredAddress: '' - }); - - const handleEditStatutory = () => { - if (!application) return; - setStatutoryForm({ - accountHolderName: application.accountHolderName || '', - panNumber: application.panNumber || '', - gstNumber: application.gstNumber || '', - bankName: application.bankName || '', - accountNumber: application.accountNumber || '', - ifscCode: application.ifscCode || '', - registeredAddress: application.registeredAddress || '' - }); - setIsEditingStatutory(true); - }; - - const [isSavingStatutory, setIsSavingStatutory] = useState(false); - const handleSaveStatutory = async () => { - try { - setIsSavingStatutory(true); - await onboardingService.updateApplication(applicationId!, statutoryForm); - toast.success('Statutory & Bank details updated successfully'); - setIsEditingStatutory(false); - fetchApplication(true); - } catch (error) { - console.error('Failed to update statutory details', error); - toast.error('Failed to update details'); - } finally { - setIsSavingStatutory(false); - } - }; 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' || @@ -569,61 +158,7 @@ export const ApplicationDetails = () => { currentUser?.role === 'NBH' || currentUser?.role === 'DD Head' || currentUser?.roleCode === 'NBH' || currentUser?.roleCode === 'DD_HEAD'; - const [interviews, setInterviews] = useState([]); - const [isScheduling, setIsScheduling] = useState(false); - const [showAssignArchitectureModal, setShowAssignArchitectureModal] = useState(false); - const [architectureLeadId, setArchitectureLeadId] = useState(''); - const [isAssigningArchitecture, setIsAssigningArchitecture] = useState(false); - const [showArchitectureStatusModal, setShowArchitectureStatusModal] = useState(false); - const [architectureStatus, setArchitectureStatus] = useState('COMPLETED'); - const [architectureRemarks, setArchitectureRemarks] = useState(''); - const [isUpdatingArchitecture, setIsUpdatingArchitecture] = useState(false); - const [isAssigningParticipant, setIsAssigningParticipant] = useState(false); - const [documentConfigs, setDocumentConfigs] = useState([]); - - // Fetch document configurations - const [fddAgencies, setFddAgencies] = useState([]); - const [selectedAgencyId, setSelectedAgencyId] = useState(''); - const [isAssigningAgency, setIsAssigningAgency] = useState(false); - - const fetchFddAgencies = async () => { - try { - const agencies = await onboardingService.getUsers({ roleCode: 'FDD' }); - setFddAgencies(Array.isArray(agencies) ? agencies : []); - } catch (error) { - console.error('Failed to fetch FDD agencies', error); - } - }; - - const handleAssignAgency = async () => { - if (!selectedAgencyId) { - toast.warning('Please select an agency'); - return; - } - try { - setIsAssigningAgency(true); - await onboardingService.assignFddAgency({ - applicationId: application?.id || applicationId, - assignedToAgency: selectedAgencyId - }); - - // 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'); - } finally { - setIsAssigningAgency(false); - } - }; // Fetch document configurations useEffect(() => { const fetchConfigs = async () => { @@ -651,274 +186,10 @@ export const ApplicationDetails = () => { } }, [showScheduleModal, application?.status]); - const [isApproving, setIsApproving] = useState(false); - const [isRejecting, setIsRejecting] = useState(false); - // KT Matrix State - const [ktMatrixScores, setKtMatrixScores] = useState>({}); - const [ktMatrixSelectedValues, setKtMatrixSelectedValues] = useState>({}); - const [ktMatrixRemarks, setKtMatrixRemarks] = useState(''); - const [isSubmittingKT, setIsSubmittingKT] = useState(false); - const [selectedInterviewForFeedback, setSelectedInterviewForFeedback] = useState(null); - - // FDD Partner States - const [showFddFinalizeModal, setShowFddFinalizeModal] = useState(false); - const [showFddFlagModal, setShowFddFlagModal] = useState(false); - const [fddAuditRecommendation, setFddAuditRecommendation] = useState('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({}); - - 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({}); - setKtMatrixSelectedValues({}); - 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) { @@ -935,19 +206,36 @@ export const ApplicationDetails = () => { 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)); - }; + const { + handleKTMatrixChange, + calculateKTScore, + handleSubmitKTMatrix, + handleLevel2Change, + handleSubmitLevel2Feedback, + handleLevel3Change, + handleSubmitLevel3Feedback, + } = useApplicationDetailsFeedbackActions({ + ktMatrixScores, + setKtMatrixScores, + setKtMatrixSelectedValues, + ktMatrixRemarks, + setKtMatrixRemarks, + selectedInterviewForFeedback, + interviews, + setIsSubmittingKT, + setShowKTMatrixModal, + level2Feedback, + setLevel2Feedback, + setIsSubmittingLevel2, + setShowLevel2FeedbackModal, + level3Feedback, + setLevel3Feedback, + setIsSubmittingLevel3, + setShowLevel3FeedbackModal, + currentUser, + fetchInterviews, + fetchApplication, + }); useEffect(() => { if (['documents', 'progress', 'fdd', 'eor'].includes(activeTab) && applicationId) { @@ -957,160 +245,81 @@ export const ApplicationDetails = () => { 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([]); - } - }; + const { + handleAddInterviewer, + handleRemoveInterviewer, + maybeFetchUsersForModal, + handleScheduleInterview, + handleCancelInterview, + handleUpload, + handleApprove, + handleReject, + handleGenerateDealerCodes, + handleAssignArchitecture, + handleUpdateArchitectureStatus, + handleAddParticipant, + handleRetriggerEvaluators, + } = useApplicationDetailsAdminActions({ + application, + applicationId, + currentUser, + interviews, + approvalFile, + approvalRemark, + rejectionReason, + architectureLeadId, + architectureStatus, + architectureRemarks, + selectedUser, + participantType, + users, + interviewDate, + interviewType, + interviewMode, + meetingLink, + location, + scheduledInterviewParticipants, + uploadFile, + uploadDocType, + selectedStage, + setIsApproving, + setShowApproveModal, + setApprovalRemark, + setApprovalFile, + setIsRejecting, + setShowRejectModal, + setRejectionReason, + setIsAssigningArchitecture, + setShowAssignArchitectureModal, + setIsUpdatingArchitecture, + setShowArchitectureStatusModal, + setIsAssigningParticipant, + setSelectedUser, + setShowAssignModal, + setLoading, + setIsScheduling, + setShowScheduleModal, + setIsUploading, + setShowUploadForm, + setUploadFile, + setUploadDocType, + setDocuments, + selectedInterviewerId, + setSelectedInterviewerId, + setScheduledInterviewParticipants, + setUsers, + showScheduleModal, + showAssignArchitectureModal, + showAssignModal, + fetchApplication, + fetchInterviews, + fetchEorData, + }); useEffect(() => { - if (showScheduleModal && application) { - fetchUsers(interviewType); - - // Auto-fill participants based on pre-assigned evaluators for this level - const levelNum = parseInt(interviewType.replace('level', '')) || 1; - const preAssigned = (application?.participants || []) - .filter((p: any) => - p.metadata?.interviewLevel === levelNum || - p.metadata?.interviewLevel === String(levelNum) || - p.metadata?.allAssignments?.includes(levelNum) || - p.metadata?.allAssignments?.includes(String(levelNum)) - ) - .map((p: any) => p.user) - .filter(Boolean); - - if (preAssigned.length > 0) { - // Ensure uniqueness by user ID - const uniquePreassigned: any[] = []; - const seenIds = new Set(); - preAssigned.forEach((u: any) => { - if (u.id && !seenIds.has(u.id)) { - seenIds.add(u.id); - uniquePreassigned.push(u); - } - }); - setScheduledInterviewParticipants(uniquePreassigned); - } else { - setScheduledInterviewParticipants([]); - } - } else if ((showAssignArchitectureModal || showAssignModal) && application) { - fetchUsers(); // Default fetch for other modals like Assign - } + maybeFetchUsersForModal(); }, [showScheduleModal, showAssignArchitectureModal, showAssignModal, interviewType, application?.participants]); - const handleScheduleInterview = async () => { - if (!interviewDate) { - toast.warning('Please select date and time'); - return; - } - try { - setIsScheduling(true); - - const payload = { - applicationId: application?.id, - level: interviewType, - scheduledAt: interviewDate, - type: interviewMode === 'virtual' ? 'Virtual Interview' : 'Physical Interview', - location: interviewMode === 'virtual' ? meetingLink : location, - participants: scheduledInterviewParticipants.map(p => p.id) - }; - - await onboardingService.scheduleInterview(payload); - toast.success('Interview scheduled successfully'); - setShowScheduleModal(false); - // Refresh interviews - await fetchInterviews(); - await fetchApplication(); // Refresh application status - - } catch (error) { - toast.error('Failed to schedule interview'); - console.error(error); - } finally { - setIsScheduling(false); - } - }; - - const handleCancelInterview = async (interviewId: string) => { - if (!window.confirm('Are you sure you want to cancel this interview?')) return; - try { - await onboardingService.updateInterview(interviewId, { status: 'Cancelled' }); - toast.success('Interview cancelled successfully'); - fetchInterviews(); - } catch (error) { - toast.error('Failed to cancel interview'); - console.error(error); - } - }; - - const handleUpload = async () => { - if (!uploadFile || !uploadDocType) { - toast.warning('Please select a file and document type'); - return; - } - - try { - setIsUploading(true); - const formData = new FormData(); - formData.append('file', uploadFile); - formData.append('documentType', uploadDocType); - if (selectedStage) { - formData.append('stage', selectedStage); - } - - await onboardingService.uploadDocument(applicationId!, formData); - - toast.success('Document uploaded successfully'); - setShowUploadForm(false); - setUploadFile(null); - setUploadDocType(''); - - // Refresh documents - const docs = await onboardingService.getDocuments(applicationId); - setDocuments(docs || []); - - // Refresh EOR Data in case an EOR document was uploaded - fetchEorData(); - } catch (error) { - console.error('Upload failed', error); - toast.error('Failed to upload document'); - } finally { - setIsUploading(false); - } - }; - if (loading) { return
Loading application details...
; } @@ -1119,666 +328,13 @@ export const ApplicationDetails = () => { 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' : - (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' : - (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', () => - [ - 'Security Details', - '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 === 'Security Details' || application.status === 'Payment Pending' - ? 'active' - : 'pending', - ), - date: application.securityDetailsDate, - description: 'Security verification', - documentsUploaded: 3 - }, - { - id: 10, - name: 'LOI Issue', - status: getStageStatus('LOI Issue', () => { - if ( - [ - '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) - ) { - return 'completed'; - } - if (application.status === 'LOI Issued') return 'active'; - return '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 flattenedStages: any[] = processStages.reduce((acc: any[], stage) => { - acc.push({ name: stage.name }); - if (stage.branches) { - stage.branches.forEach((branch: any) => { - branch.stages.forEach((subStage: any) => { - acc.push({ name: subStage.name, parentBranch: branch.name }); - }); - }); - } - if (stage.name === 'EOR In Progress' || stage.name === 'EOR Complete') { - (eorData?.items || eorChecklist).forEach((item: any) => { - acc.push({ name: `EOR: ${item.description || item.item}`, parentBranch: 'EOR' }); - }); - } - return acc; - }, []); - - - - - - const getDocumentsForStage = (stageName: string) => { - return documents.filter(doc => - doc.stage === stageName || - (!doc.stage && doc.documentType?.toLowerCase().includes(stageName.toLowerCase().split(' ')[0])) - ); - }; - - const handleApprove = async () => { - try { - setIsApproving(true); - // Check if user has an active interview to approve - const activeInterview = interviews.find(i => - i.status !== 'Completed' && i.status !== 'Cancelled' && - i.participants?.some((p: any) => p.userId === currentUser?.id) - ); - - // Handle File Upload if exists - if (approvalFile && applicationId) { - try { - const formData = new FormData(); - formData.append('file', approvalFile); - formData.append('documentType', 'Approval Attachment'); - - // Determine stage based on active interview - let stageName = null; - if (activeInterview) { - if (activeInterview.level === 1 || activeInterview.level === '1') stageName = '1st Level Interview'; - else if (activeInterview.level === 2 || activeInterview.level === '2') stageName = '2nd Level Interview'; - else if (activeInterview.level === 3 || activeInterview.level === '3') stageName = '3rd Level Interview'; - } - - // Fallback for document stage if it's a general approval - if (!stageName) { - if (application.status === 'Shortlisted' || application.status === 'Level 1 Interview Pending') stageName = '1st Level Interview'; - else if (application.status === 'Level 1 Approved' || application.status === 'Level 2 Interview Pending') stageName = '2nd Level Interview'; - else if (application.status === 'Level 2 Approved' || application.status === 'Level 3 Interview Pending') stageName = '3rd Level Interview'; - } - - if (stageName) { - formData.append('stage', stageName); - } - - await onboardingService.uploadDocument(applicationId, formData); - toast.success('Document uploaded with approval'); - } catch (error) { - console.error('Failed to upload approval document', error); - toast.error('Failed to upload document'); - } - } - - if (activeInterview) { - try { - await onboardingService.updateInterviewDecision({ - interviewId: activeInterview.id, - decision: 'Approved', - remarks: approvalRemark - }); - toast.success('Interview approved successfully'); - setShowApproveModal(false); - setApprovalRemark(''); - setApprovalFile(null); // Reset file - fetchInterviews(); - // Refresh application to check if status updated - fetchApplication(); - return; - } catch (error) { - toast.error('Failed to approve interview'); - return; - } - } - - if (!approvalRemark.trim()) { - toast.warning('Please enter a remark'); - return; - } - - // Application level approval - Robust State Machine - let newStatus = application.status; - - switch (application.status) { - case 'Shortlisted': - case 'Level 1 Interview Pending': - newStatus = 'Level 1 Approved'; break; - case 'Level 1 Approved': - case 'Level 2 Interview Pending': - newStatus = 'Level 2 Approved'; break; - case 'Level 2 Approved': - case 'Level 3 Interview Pending': - newStatus = 'Level 3 Approved'; break; - case 'Level 3 Approved': - newStatus = 'FDD Verification'; break; - case 'FDD Verification': - newStatus = 'LOI In Progress'; break; - case 'LOI In Progress': - newStatus = 'Security Details'; break; - 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 - } - - 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 === '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); - } - }; + const { processStages, eorChecklist, flattenedStages, getDocumentsForStage } = useApplicationDetailsStageData({ + application, + documents, + interviews, + eorData, + getDeposit, + }); if (loading && !application) { return ( @@ -1792,3441 +348,285 @@ export const ApplicationDetails = () => { return
Application not found
; } - // Determine if current user has an active interview and if they have submitted feedback - const interviewsList = Array.isArray(interviews) ? interviews : []; + const { + activeInterviewForUser, + currentUserEvaluation, + hasSubmittedFeedback, + currentUserStageAction, + isInterviewCompleted, + isInterviewActive, + permissions, + } = useApplicationDetailsPermissions({ + application, + interviews, + currentUser, + getDeposit, + eorProgress, + }); - // 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) + + + + const renderFddAuditContent = () => ( + ); - // 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 isSecurityDetailsLocked = ['Security Details', 'Payment Pending'].includes(application.status) && - getDeposit('SECURITY_DEPOSIT')?.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 = !isFinalState && !isDecisionMade && ( - (!!activeInterviewForUser && !!hasSubmittedFeedback) || - (isAdminRole && isAdministrativeStage && sequenceMet && (!['EOR In Progress', 'Inauguration', 'Approved'].includes(application.status) || eorProgress === 100)) - ); - const canApprove = canApproveReject && !isLoaLocked && !isSecurityDetailsLocked; - const canReject = canApproveReject && !isLoaLocked; - - return { - canApprove, - canReject, - isLoaLocked, - isSecurityDetailsLocked, - 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; - }); - }; - - 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 - - - -
-
- - -
-
- -
-
-
-
- )} -
- ); - } - - return ( -
- {/* FDD/Finance Audit Workspace */} - {/* FDD Status Header */} - {hasAssignment && ( -
-
-
- -
-
-

FDD Assignment Active

- {primaryFddUser &&

Assigned to: {primaryFddUser.name}

} -
-
-
- )} - - {/* 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 ? ( -
- -
- ) : ( - - )} -
- ); - })} -
-
-
- -
- {/* 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} 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 (
- {isNonResponsive && ( -
-
-
- -
-
-

Applicant Flagged Non-Responsive

-

Audit process is currently on hold due to missing cooperation

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

{application.name}

-

{application.registrationNumber}

-
-
-
- -
-
+ navigate(`/worknotes/application/${application.id}`, { + state: { + applicationName: application.name, + registrationNumber: application.registrationNumber, + participants: application.participants + } + })} + />
{/* Main Content */}
- {/* Core Details Card */} - - - Applicant Information - - -
-
- -
-

Full Name

-

{application.name}

-
-
- -
- -
-

Email

-

{application.email}

-
-
- -
- -
-

Phone

-

{application.phone}

-
-
- -
- -
-

Age

-

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

-
-
- -
- -
-

Education

-

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

-
-
- -
- -
-

Preferred Location

-

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

-
-
- -
- -
-

Location Type

-

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

-
-
- -
- -
-

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

-

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

-
-
- -
- -
-

Owns Bike

-

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

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

Bike Model

-

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

-
-
- )} - -
- -
-

Existing Dealer

-

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

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

Company Name

-

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

-
-
- )} - -
- -
-

Source

-

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

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

Questionnaire Score

-

{application.questionnaireMarks}/100

-
-
- )} -
- - - -
-

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

-
-
-

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

-
-
- )} -
-
-
+ { + setTempFirmType(application.constitutionType || ''); + setShowFirmTypeModal(true); + }} + onEditStatutory={handleEditStatutory} + onCancelEditStatutory={() => setIsEditingStatutory(false)} + onSaveStatutory={handleSaveStatutory} + onStatutoryFormChange={setStatutoryForm} + /> {/* Tabs Section */} {/* Only show tabs for shortlisted applications (opportunity requests and regular dealership requests) */} {/* Hide tabs for non-opportunity requests (lead generation) */} {application.isShortlisted !== false && ( - - - -
- - Questionnaire - Progress - Documents - Interviews - FDD Audit - EOR Checklist - Payments - Audit Trail - -
-
- - - {/* Questionnaire Response Tab */} - - - - - {/* Progress Tab */} - -
-
-

Application Journey

- {application.progress}% Complete -
- -
- -
- {(() => { - 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 { - 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' - }; - - 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()} -
-
- ))} -
- ); - }; - - 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

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

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

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} -
- -
- ))} - {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) => ( -
-
-
- - {String(log.action || 'EVENT').replace(/_/g, ' ')} - - {log.stage ? ( - - {log.stage} - - ) : null} -
- -
-

- {log.description || '—'} -

-
- - - - {log.userName || 'System'} - - {log.userEmail ? ( - · {log.userEmail} - ) : null} - -
-
- )) - )} -
- - - - - + )}
{/* 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)}

-
- )} -
-
- - {/* 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 && ( - - - LOA approval locked - - First Fill (later-stage payment) must be verified by Finance - before LOA approval can proceed. This is separate from the initial security deposit before LOI Issued. - - - )} - - {getDeposit('FIRST_FILL')?.status === 'Verified' && - application.status !== 'LOA Pending' && - !['LOA Issued', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Onboarded', 'Rejected'].includes( - application.status, - ) && ( - - - First Fill verified on file - - Finance has verified the First Fill payment. The application - status was not changed until you reach{' '} - LOA Pending. When you get there, LOA approval will not be - blocked by payment (same pattern as recording the initial security deposit before the LOI - security step). - - - )} - - {permissions.isSecurityDetailsLocked && ( - - - Security Details approval locked - - Finance must verify the Security Deposit before this stage can be approved. - You can still use Reject if needed. - - - )} - - {['Security Details', 'Payment Pending'].includes(application.status) && ( - - - Security Details review - - Check the initial security deposit on the Payments tab (Finance - may have already marked it verified). When satisfied, use Approve{' '} - to move to LOI Issued. - - - )} - - {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.canReject && ( - - )} - - {permissions.showDecisionMessage && ( -
- You have {(currentUserStageAction?.decision === 'Approved' || currentUserEvaluation?.decision === 'Approved' || currentUserEvaluation?.recommendation === 'Approved' || currentUserEvaluation?.decision === 'Selected') ? 'Approved' : 'Rejected'} -
- )} - - - - - - {permissions.canSchedule && ( - - )} - - {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 && ( - - )} - - {application.dealerCode && !application.architectureAssignedTo && ( - - )} - - )} - - - - {/* 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' && ( - <> - - - - - )} - - - {/* 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. - - -
-
- -