Dealer_Onboard_Frontend/src/components/applications/ApplicationDetails.tsx

5208 lines
257 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useState, 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 QuestionnaireResponseView from './QuestionnaireResponseView';
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';
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 }
]
}
];
export const ApplicationDetails = () => {
const { id } = useParams<{ id: string }>();
const navigate = useNavigate();
const { user: currentUser } = useSelector((state: RootState) => state.auth);
const applicationId = id || '';
const onBack = () => navigate(-1);
// const application = mockApplications.find(app => app.id === applicationId);
const [application, setApplication] = useState<Application | null>(null);
const [loading, setLoading] = useState(true);
const [documents, setDocuments] = useState<any[]>([]);
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<any>(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 eorProgress = eorData?.items ? (eorData.items.filter((item: any) => item.isCompliant).length / eorData.items.length) * 100 : 0;
// Audit Trail State
const [auditLogs, setAuditLogs] = useState<any[]>([]);
const [auditLoading, setAuditLoading] = useState(false);
const [showFirmTypeModal, setShowFirmTypeModal] = useState(false);
const [updatingFirmType, setUpdatingFirmType] = useState(false);
const [tempFirmType, setTempFirmType] = useState('');
const handleUpdateFirmType = async () => {
try {
setUpdatingFirmType(true);
await onboardingService.updateApplication(applicationId!, { constitutionType: tempFirmType });
toast.success('Firm type updated successfully');
setShowFirmTypeModal(false);
fetchApplication();
} catch (error) {
toast.error('Failed to update firm type');
} finally {
setUpdatingFirmType(false);
}
};
// Fetch audit logs when application loads
useEffect(() => {
if (application?.id) {
const fetchAuditLogs = async () => {
setAuditLoading(true);
try {
const logs = await auditService.getAuditLogs('application', application.id);
setAuditLogs(Array.isArray(logs) ? logs : []);
} catch (error) {
console.error('Failed to fetch audit logs', error);
setAuditLogs([]);
} finally {
setAuditLoading(false);
}
};
fetchAuditLogs();
}
}, [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<any[]>([]);
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<string | null>(null);
const [interviewMode, setInterviewMode] = useState('virtual');
const [approvalRemark, setApprovalRemark] = useState('');
const [expandedBranches, setExpandedBranches] = useState<{ [key: string]: boolean }>({
'architectural-work': true,
'statutory-documents': true
});
const [users, setUsers] = useState<any[]>([]);
const [selectedUser, setSelectedUser] = useState<string>('');
const [participantType, setParticipantType] = useState<string>('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<File | null>(null);
const [uploadDocType, setUploadDocType] = useState('');
const [approvalFile, setApprovalFile] = useState<File | null>(null); // State for approval modal file
const [isUploading, setIsUploading] = useState(false);
const [previewDoc, setPreviewDoc] = useState<any>(null);
const [showPreviewModal, setShowPreviewModal] = useState(false);
const [selectedInterviewerId, setSelectedInterviewerId] = useState<string>('');
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';
const isAdmin = currentUser?.roleCode === 'Super Admin' || currentUser?.roleCode === 'DD Admin';
const [interviews, setInterviews] = useState<any[]>([]);
const [isScheduling, setIsScheduling] = useState(false);
const [showAssignArchitectureModal, setShowAssignArchitectureModal] = useState(false);
const [architectureLeadId, setArchitectureLeadId] = useState<string>('');
const [isAssigningArchitecture, setIsAssigningArchitecture] = useState(false);
const [showArchitectureStatusModal, setShowArchitectureStatusModal] = useState(false);
const [architectureStatus, setArchitectureStatus] = useState<string>('COMPLETED');
const [architectureRemarks, setArchitectureRemarks] = useState<string>('');
const [isUpdatingArchitecture, setIsUpdatingArchitecture] = useState(false);
const [isAssigningParticipant, setIsAssigningParticipant] = useState(false);
const [documentConfigs, setDocumentConfigs] = useState<any[]>([]);
// Fetch document configurations
const [fddAgencies, setFddAgencies] = useState<any[]>([]);
const [selectedAgencyId, setSelectedAgencyId] = useState<string>('');
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
});
toast.success('FDD Agency assigned successfully');
fetchApplication();
} catch (error) {
toast.error('Failed to assign agency');
} finally {
setIsAssigningAgency(false);
}
};
// Fetch document configurations
useEffect(() => {
const fetchConfigs = async () => {
try {
const res = await onboardingService.getDocumentConfigs({ limit: 1000 }); // Fetch all for lookup
const configs = res.data || (Array.isArray(res) ? res : []);
setDocumentConfigs(configs);
} catch (error) {
console.error('Failed to fetch document configs:', error);
}
};
fetchConfigs();
}, []);
// Auto-select valid interview level based on application status when scheduling
useEffect(() => {
if (showScheduleModal && application) {
if (application.status === 'Shortlisted' || application.status === 'Questionnaire Completed') {
setInterviewType('level1');
} else if (application.status === 'Level 1 Approved') {
setInterviewType('level2');
} else if (application.status === 'Level 2 Approved' || application.status === 'Level 2 Recommended') {
setInterviewType('level3');
}
}
}, [showScheduleModal, application?.status]);
const [isApproving, setIsApproving] = useState(false);
const [isRejecting, setIsRejecting] = useState(false);
// KT Matrix State
const [ktMatrixScores, setKtMatrixScores] = useState<Record<string, number>>({});
const [ktMatrixSelectedValues, setKtMatrixSelectedValues] = useState<Record<string, string>>({});
const [ktMatrixRemarks, setKtMatrixRemarks] = useState('');
const [isSubmittingKT, setIsSubmittingKT] = useState(false);
const [selectedInterviewForFeedback, setSelectedInterviewForFeedback] = useState<any>(null);
// FDD Partner States
const [showFddFinalizeModal, setShowFddFinalizeModal] = useState(false);
const [showFddFlagModal, setShowFddFlagModal] = useState(false);
const [fddAuditRecommendation, setFddAuditRecommendation] = useState<string>('Recommended');
const [fddAuditFindings, setFddAuditFindings] = useState<string>('');
const [isFinalizingFdd, setIsFinalizingFdd] = useState(false);
const [isFddFlagging, setIsFddFlagging] = useState(false);
// Payment Details State
const [deposits, setDeposits] = useState<any[]>([]);
const [paymentConfigs, setPaymentConfigs] = useState<any>({});
useEffect(() => {
if (applicationId) {
const fetchPaymentData = async () => {
try {
const [depositData, configData] = await Promise.all([
onboardingService.getSecurityDeposit(applicationId),
onboardingService.getSystemConfigs({ category: 'SECURITY_DEPOSIT', format: 'map' })
]);
setDeposits(Array.isArray(depositData) ? depositData : [depositData].filter(Boolean));
setPaymentConfigs(configData || {});
} catch (error) {
console.error('Failed to fetch payment data', error);
}
};
fetchPaymentData();
}
}, [applicationId]);
const getDeposit = (type: string) => deposits.find(d => d.depositType === type);
const handleKTMatrixChange = (criterionName: string, value: string, score: number) => {
setKtMatrixScores(prev => ({
...prev,
[criterionName]: score
}));
setKtMatrixSelectedValues(prev => ({
...prev,
[criterionName]: value
}));
};
const calculateKTScore = () => {
let totalWeightedScore = 0;
KT_MATRIX_CRITERIA.forEach(criterion => {
const score = ktMatrixScores[criterion.name] || 0;
const weightedScore = (score / criterion.maxScore) * criterion.weight;
totalWeightedScore += weightedScore;
});
return totalWeightedScore.toFixed(2);
};
const handleSubmitKTMatrix = async () => {
if (Object.keys(ktMatrixScores).length < KT_MATRIX_CRITERIA.length) {
toast.warning('Please fill all fields in the KT Matrix');
return;
}
// Use the selected interview ID or fallback (though UI now forces selection)
const interviewId = selectedInterviewForFeedback?.id || interviews.find(i => i.status !== 'Completed')?.id || interviews[0]?.id;
if (!interviewId) {
toast.error('No active interview found to link this KT Matrix to.');
return;
}
try {
setIsSubmittingKT(true);
const criteriaScores = KT_MATRIX_CRITERIA.map(c => ({
criterionName: c.name,
score: ktMatrixScores[c.name] || 0,
maxScore: c.maxScore,
weightage: c.weight
}));
await onboardingService.submitKTMatrix({
interviewId,
criteriaScores,
feedback: ktMatrixRemarks,
recommendation: null // No auto-decision
});
toast.success('KT Matrix submitted successfully');
setShowKTMatrixModal(false);
// Reset form
setKtMatrixScores({});
setKtMatrixRemarks('');
await fetchInterviews();
await fetchApplication(); // Refresh application status and progress
} catch (error) {
toast.error('Failed to submit KT Matrix');
} finally {
setIsSubmittingKT(false);
}
};
// Level 2 Feedback State
const [level2Feedback, setLevel2Feedback] = useState({
strategicVision: '',
managementCapabilities: '',
operationalUnderstanding: '',
keyStrengths: '',
areasOfConcern: '',
additionalComments: '',
overallScore: '',
interviewerName: currentUser?.name || '',
interviewDate: new Date().toISOString().split('T')[0]
});
const [isSubmittingLevel2, setIsSubmittingLevel2] = useState(false);
const handleLevel2Change = (field: string, value: string) => {
setLevel2Feedback(prev => ({ ...prev, [field]: value }));
};
const handleSubmitLevel2Feedback = async () => {
if (!level2Feedback.overallScore) {
toast.warning('Please provide an overall score.');
return;
}
const interviewId = selectedInterviewForFeedback?.id || interviews.find(i => i.status !== 'Completed' && i.level === 2)?.id;
if (!interviewId) {
toast.error('No active Level 2 interview found to link this feedback to.');
return;
}
try {
setIsSubmittingLevel2(true);
const feedbackItems = [
{ type: 'Strategic Vision', comments: level2Feedback.strategicVision },
{ type: 'Management Capabilities', comments: level2Feedback.managementCapabilities },
{ type: 'Operational Understanding', comments: level2Feedback.operationalUnderstanding },
{ type: 'Key Strengths', comments: level2Feedback.keyStrengths },
{ type: 'Areas of Concern', comments: level2Feedback.areasOfConcern },
{ type: 'Additional Comments', comments: level2Feedback.additionalComments }
].filter(item => item.comments.trim() !== '');
await onboardingService.submitLevel2Feedback({
interviewId,
overallScore: Number(level2Feedback.overallScore),
feedbackItems
});
toast.success('Level 2 Feedback submitted successfully');
setShowLevel2FeedbackModal(false);
// Reset form
setLevel2Feedback({
strategicVision: '',
managementCapabilities: '',
operationalUnderstanding: '',
keyStrengths: '',
areasOfConcern: '',
additionalComments: '',
overallScore: '',
interviewerName: currentUser?.name || '',
interviewDate: new Date().toISOString().split('T')[0]
});
fetchInterviews(); // Refresh to show feedback
fetchApplication(); // Refresh application status
} catch (error) {
toast.error('Failed to submit Level 2 Feedback');
} finally {
setIsSubmittingLevel2(false);
}
};
// Level 3 Feedback State
const [level3Feedback, setLevel3Feedback] = useState({
strategicVision: '',
managementCapabilities: '',
operationalUnderstanding: '',
brandAlignment: '',
executiveSummary: '',
keyStrengths: '',
areasOfConcern: '',
additionalComments: '',
overallScore: '',
interviewerName: currentUser?.name || '',
interviewDate: new Date().toISOString().split('T')[0]
});
const [isSubmittingLevel3, setIsSubmittingLevel3] = useState(false);
const handleLevel3Change = (field: string, value: string) => {
setLevel3Feedback(prev => ({ ...prev, [field]: value }));
};
const handleSubmitLevel3Feedback = async () => {
if (!level3Feedback.overallScore) {
toast.warning('Please provide an overall score.');
return;
}
const interviewId = selectedInterviewForFeedback?.id || interviews.find(i => i.status !== 'Completed' && i.level === 3)?.id;
if (!interviewId) {
toast.error('No active Level 3 interview found to link this feedback to.');
return;
}
try {
setIsSubmittingLevel3(true);
// Level 3 might have slightly different fields or same structure. Assuming same for now.
const feedbackItems = [
{ type: 'Business Vision & Strategy', comments: level3Feedback.strategicVision },
{ type: 'Leadership & Decision Making', comments: level3Feedback.managementCapabilities },
{ type: 'Operational & Financial Readiness', comments: level3Feedback.operationalUnderstanding },
{ type: 'Brand Alignment', comments: level3Feedback.brandAlignment },
{ type: 'Key Strengths', comments: level3Feedback.keyStrengths },
{ type: 'Areas of Concern', comments: level3Feedback.areasOfConcern },
{ type: 'Executive Summary', comments: level3Feedback.executiveSummary },
{ type: 'Additional Comments', comments: level3Feedback.additionalComments }
].filter(item => item.comments.trim() !== '');
// Reusing submitLevel2Feedback endpoint as it maps to InterviewFeedback table generic enough for Level 3 too
// Or we can create specific one if needed, but logic is identical so reusing service method
await onboardingService.submitLevel2Feedback({
interviewId,
overallScore: Number(level3Feedback.overallScore),
feedbackItems
});
toast.success('Level 3 Feedback submitted successfully');
setShowLevel3FeedbackModal(false);
// Reset form
setLevel3Feedback({
strategicVision: '',
managementCapabilities: '',
operationalUnderstanding: '',
brandAlignment: '',
executiveSummary: '',
keyStrengths: '',
areasOfConcern: '',
additionalComments: '',
overallScore: '',
interviewerName: currentUser?.name || '',
interviewDate: new Date().toISOString().split('T')[0]
});
fetchInterviews();
fetchApplication();
} catch (error) {
toast.error('Failed to submit Level 3 Feedback');
} finally {
setIsSubmittingLevel3(false);
}
};
// Feedback Details Modal State
const [selectedEvaluationForView, setSelectedEvaluationForView] = useState<any>(null);
const [showFeedbackDetailsModal, setShowFeedbackDetailsModal] = useState(false);
const fetchInterviews = async () => {
if (applicationId) {
try {
const data = await onboardingService.getInterviews(applicationId);
setInterviews(data || []);
} catch (error) {
console.error('Failed to fetch interviews', error);
}
}
};
useEffect(() => {
fetchInterviews();
}, [applicationId]);
const handleAddInterviewer = () => {
if (!selectedInterviewerId) return;
const usersList = Array.isArray(users) ? users : [];
const userToAdd = usersList.find(u => u.id === selectedInterviewerId);
if (userToAdd && !scheduledInterviewParticipants.find(p => p.id === userToAdd.id)) {
setScheduledInterviewParticipants([...scheduledInterviewParticipants, userToAdd]);
setSelectedInterviewerId('');
}
};
const handleRemoveInterviewer = (userId: string) => {
setScheduledInterviewParticipants(scheduledInterviewParticipants.filter(p => p.id !== userId));
};
useEffect(() => {
if (['documents', 'progress', 'fdd', 'eor'].includes(activeTab) && applicationId) {
refreshDocuments();
}
if (activeTab === 'fdd' && (currentUser?.role === 'DD Admin' || currentUser?.role === 'Super Admin')) {
fetchFddAgencies();
}
}, [activeTab, applicationId]);
const fetchUsers = async (type?: string) => {
// Only fetch users if user has admin/DD/NBH roles to avoid 403s
if (!currentUser || !['DD Admin', 'Super Admin', 'DD Lead', 'DD Head', 'NBH'].includes(currentUser.role)) {
return;
}
try {
const params: any = {};
if (type) {
const roleMapping: any = {
'level1': ['DD-ZM', 'RBM'],
'level2': ['DD Lead', 'ZBH'],
'level3': ['NBH', 'DD Head']
};
params.roleCode = roleMapping[type];
// Include location from the application
if (application) {
params.locationId = application.districtId || application.areaId || application.regionId || application.zoneId;
}
}
const response = await onboardingService.getUsers(params);
if (Array.isArray(response)) {
setUsers(response);
} else if (response && Array.isArray(response.data)) {
setUsers(response.data);
} else if (response && Array.isArray(response.users)) {
setUsers(response.users);
} else {
console.warn('Unexpected users response:', response);
setUsers([]);
}
} catch (error) {
console.error('Failed to fetch users', error);
setUsers([]);
}
};
useEffect(() => {
if (showScheduleModal && application) {
fetchUsers(interviewType);
// Auto-fill participants based on pre-assigned evaluators for this level
const levelNum = parseInt(interviewType.replace('level', '')) || 1;
const preAssigned = (application?.participants || [])
.filter((p: any) =>
p.metadata?.interviewLevel === levelNum ||
p.metadata?.interviewLevel === String(levelNum) ||
p.metadata?.allAssignments?.includes(levelNum) ||
p.metadata?.allAssignments?.includes(String(levelNum))
)
.map((p: any) => p.user)
.filter(Boolean);
if (preAssigned.length > 0) {
// Ensure uniqueness by user ID
const uniquePreassigned: any[] = [];
const seenIds = new Set();
preAssigned.forEach((u: any) => {
if (u.id && !seenIds.has(u.id)) {
seenIds.add(u.id);
uniquePreassigned.push(u);
}
});
setScheduledInterviewParticipants(uniquePreassigned);
} else {
setScheduledInterviewParticipants([]);
}
} else if ((showAssignArchitectureModal || showAssignModal) && application) {
fetchUsers(); // Default fetch for other modals like Assign
}
}, [showScheduleModal, showAssignArchitectureModal, showAssignModal, interviewType, application?.participants]);
const handleScheduleInterview = async () => {
if (!interviewDate) {
toast.warning('Please select date and time');
return;
}
try {
setIsScheduling(true);
const payload = {
applicationId: application?.id,
level: interviewType,
scheduledAt: interviewDate,
type: interviewMode === 'virtual' ? 'Virtual Interview' : 'Physical Interview',
location: interviewMode === 'virtual' ? meetingLink : location,
participants: scheduledInterviewParticipants.map(p => p.id)
};
await onboardingService.scheduleInterview(payload);
toast.success('Interview scheduled successfully');
setShowScheduleModal(false);
// Refresh interviews
await fetchInterviews();
await fetchApplication(); // Refresh application status
} catch (error) {
toast.error('Failed to schedule interview');
console.error(error);
} finally {
setIsScheduling(false);
}
};
const handleCancelInterview = async (interviewId: string) => {
if (!window.confirm('Are you sure you want to cancel this interview?')) return;
try {
await onboardingService.updateInterview(interviewId, { status: 'Cancelled' });
toast.success('Interview cancelled successfully');
fetchInterviews();
} catch (error) {
toast.error('Failed to cancel interview');
console.error(error);
}
};
const handleUpload = async () => {
if (!uploadFile || !uploadDocType) {
toast.warning('Please select a file and document type');
return;
}
try {
setIsUploading(true);
const formData = new FormData();
formData.append('file', uploadFile);
formData.append('documentType', uploadDocType);
if (selectedStage) {
formData.append('stage', selectedStage);
}
await onboardingService.uploadDocument(applicationId!, formData);
toast.success('Document uploaded successfully');
setShowUploadForm(false);
setUploadFile(null);
setUploadDocType('');
// Refresh documents
const docs = await onboardingService.getDocuments(applicationId);
setDocuments(docs || []);
// Refresh EOR Data in case an EOR document was uploaded
fetchEorData();
} catch (error) {
console.error('Upload failed', error);
toast.error('Failed to upload document');
} finally {
setIsUploading(false);
}
};
if (loading) {
return <div>Loading application details...</div>;
}
if (!application) {
return <div>Application not found</div>;
}
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', () => ['Payment Pending', 'LOI Issued', 'Statutory LOI Ack', 'Dealer Code Generation', 'Architecture Work', 'Statutory Work', 'LOA Pending', 'LOA Issued', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Onboarded'].includes(application.status) ? 'completed' : application.status === 'LOI In Progress' ? 'active' : 'pending'),
date: application.loiApprovalDate,
description: 'Letter of Intent approval',
evaluators: Array.from(new Set((application.participants || [])
.filter((p: any) => p.metadata?.stageCode === 'LOI_APPROVAL' || p.metadata?.allAssignments?.includes('LOI_APPROVAL'))
.map((p: any) => `${p.user?.name} (${p.user?.role})`)
)),
documentsUploaded: 1
},
{
id: 9,
name: 'Security Details',
status: getStageStatus('Security Details', () => ['LOI Issued', 'Statutory LOI Ack', 'Dealer Code Generation', 'Architecture Work', 'Statutory Work', 'LOA Pending', 'LOA Issued', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Onboarded'].includes(application.status) ? 'completed' : application.status === 'Payment Pending' ? 'active' : 'pending'),
date: application.securityDetailsDate,
description: 'Security verification',
documentsUploaded: 3
},
{
id: 10,
name: 'LOI Issue',
status: getStageStatus('LOI Issue', () => ['LOI Issued', 'Statutory LOI Ack', 'Dealer Code Generation', 'Architecture Work', 'Statutory Work', 'LOA Pending', 'LOA Issued', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Onboarded'].includes(application.status) ? 'completed' : 'pending'),
date: application.loiIssueDate,
description: 'Letter of Intent issued',
documentsUploaded: 1
},
{
id: 11,
name: 'Dealer Code Generation',
status: getStageStatus('Dealer Code Generation', () => (application.dealerCode || ['Dealer Code Generation', 'Architecture Work', 'Statutory Work', 'LOA Pending', 'LOA Issued', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Onboarded'].includes(application.status)) ? 'completed' : 'pending'),
date: application.dealerCodeDate,
description: 'Dealer code generated and assigned',
isParallel: true,
branches: [
{
name: 'Architectural Work',
color: 'blue',
stages: [
{
id: '11a-1',
name: 'Architecture Assignment',
status: application.architectureAssignedTo ? 'completed' : application.status === 'Architecture Team Assigned' ? 'active' : 'pending',
description: 'Assigned to architecture team'
},
{
id: '11a-2',
name: 'Site Plan Blueprint',
status: isDocumentUploaded('Architecture Blueprint') ? 'completed' : application.architectureAssignedTo ? 'active' : 'pending',
description: 'Blueprints and site plans'
},
{
id: '11a-3',
name: 'Architecture Work',
status: application.architectureStatus === 'COMPLETED' ? 'completed' : (application.architectureStatus === 'IN_PROGRESS' || isDocumentUploaded('Architecture Blueprint')) ? 'active' : 'pending',
description: 'Final architecture approval'
}
]
},
{
name: 'Statutory Documents',
color: 'green',
stages: [
{ id: '11b-1', name: 'GST', status: isDocumentUploaded('GST Certificate') || isDocumentUploaded('GST') ? 'completed' : 'active', description: 'GST certificate' },
{ id: '11b-2', name: 'PAN', status: isDocumentUploaded('PAN Card') || isDocumentUploaded('PAN') ? 'completed' : 'active', description: 'PAN card' },
{ id: '11b-3', name: 'Nodal Agreement', status: isDocumentUploaded('Nodal Agreement') ? 'completed' : 'active', description: 'Nodal agreement document' },
{ id: '11b-4', name: 'Cancelled Check', status: isDocumentUploaded('Cancelled Check') ? 'completed' : 'active', description: 'Cancelled check copy' },
{ id: '11b-5', name: 'Partnership Deed/LLP/MOA/AOA/COI', status: isDocumentUploaded('Partnership Deed/LLP/MOA/AOA/COI') || isDocumentUploaded('Partnership Deed') ? 'completed' : 'active', description: 'Business entity documents' },
{ id: '11b-6', name: 'Firm Registration Certificate', status: isDocumentUploaded('Firm Registration Certificate') || isDocumentUploaded('Firm Registration') ? 'completed' : 'active', description: 'Firm registration certificate' },
{ id: '11b-7', name: 'Rental agreement/ Lease agreement / Own/ Land agreement', status: isDocumentUploaded('Rental agreement/ Lease agreement / Own/ Land agreement') || isDocumentUploaded('Property Document') ? 'completed' : 'active', description: 'Property agreement document' },
{ id: '11b-8', name: 'Virtual Code', status: isDocumentUploaded('Virtual Code') || isDocumentUploaded('Virtual Code Confirmation') ? 'completed' : 'active', description: 'Virtual code availability' },
{ id: '11b-9', name: 'Domain ID', status: isDocumentUploaded('Domain ID') || isDocumentUploaded('Domain ID Setup') ? 'completed' : 'active', description: 'Domain ID setup' },
{ id: '11b-10', name: 'MSD Configuration', status: isDocumentUploaded('MSD Configuration') ? 'completed' : 'active', description: 'Microsoft Dynamics configuration' },
{ id: '11b-11', name: 'LOI Acknowledgement Copy', status: isDocumentUploaded('LOI Acknowledgement Copy') || isDocumentUploaded('LOI Acknowledgement') ? 'completed' : 'active', description: 'LOI acknowledgement copy' }
]
}
]
},
{
id: 12,
name: 'LOA',
status: getStageStatus('LOA', () => ['EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Onboarded'].includes(application.status) ? 'completed' : application.status === 'LOA Pending' ? 'active' : 'pending'),
isLocked: application.status === 'LOA Pending' && getDeposit('FIRST_FILL')?.status !== 'Verified',
lockMessage: 'First Fill (₹15L) must be verified by Finance before LOA Approval.',
evaluators: Array.from(new Set((application.participants || [])
.filter((p: any) => p.metadata?.stageCode === 'LOA_APPROVAL' || p.metadata?.allAssignments?.includes('LOA_APPROVAL'))
.map((p: any) => `${p.user?.name} (${p.user?.role})`)
)),
description: 'Letter of Authorization'
},
{
id: 13,
name: 'EOR Complete',
status: getStageStatus('EOR Complete', () => ['Inauguration', 'Approved', 'Onboarded'].includes(application.status) ? 'completed' : application.status === 'EOR Complete' ? 'active' : 'pending'),
description: 'Essential Operating Requirements'
},
{
id: 14,
name: 'Inauguration',
status: getStageStatus('Inauguration', () => ['Approved', 'Onboarded'].includes(application.status) ? 'completed' : application.status === 'Inauguration' ? 'active' : 'pending'),
description: 'Dealership inauguration'
},
{
id: 15,
name: 'Dealership Active',
status: getStageStatus('Onboarded', () => application.status === 'Onboarded' ? 'completed' : 'pending'),
description: 'Dealer profile active'
}
];
const eorChecklist = [
{ id: 1, item: 'Sales Standards', completed: false },
{ id: 2, item: 'Service & Spares', completed: false },
{ id: 3, item: 'DMS infra', completed: false },
{ id: 4, item: 'Manpower Training', completed: false },
{ id: 5, item: 'Trade certificate with test ride bikes registration', completed: false },
{ id: 6, item: 'GST certificate including Accessories & Apparels billing', completed: false },
{ id: 7, item: 'Inventory Funding', completed: false },
{ id: 8, item: 'Virtual code availability', completed: false },
{ id: 9, item: 'Vendor payments', completed: false },
{ id: 10, item: 'Details for website submission', completed: false },
{ id: 11, item: 'Infra Insurance both Showroom and Service center', completed: false },
{ id: 12, item: 'Auto ordering', completed: false }
];
const flattenedStages: any[] = processStages.reduce((acc: any[], stage) => {
acc.push({ name: stage.name });
if (stage.branches) {
stage.branches.forEach((branch: any) => {
branch.stages.forEach((subStage: any) => {
acc.push({ name: subStage.name, parentBranch: branch.name });
});
});
}
if (stage.name === 'EOR In Progress' || stage.name === 'EOR Complete') {
(eorData?.items || eorChecklist).forEach((item: any) => {
acc.push({ name: `EOR: ${item.description || item.item}`, parentBranch: 'EOR' });
});
}
return acc;
}, []);
const getDocumentsForStage = (stageName: string) => {
return documents.filter(doc =>
doc.stage === stageName ||
(!doc.stage && doc.documentType?.toLowerCase().includes(stageName.toLowerCase().split(' ')[0]))
);
};
const handleApprove = async () => {
try {
setIsApproving(true);
// Check if user has an active interview to approve
const activeInterview = interviews.find(i =>
i.status !== 'Completed' && i.status !== 'Cancelled' &&
i.participants?.some((p: any) => p.userId === currentUser?.id)
);
// Handle File Upload if exists
if (approvalFile && applicationId) {
try {
const formData = new FormData();
formData.append('file', approvalFile);
formData.append('documentType', 'Approval Attachment');
// Determine stage based on active interview
let stageName = null;
if (activeInterview) {
if (activeInterview.level === 1 || activeInterview.level === '1') stageName = '1st Level Interview';
else if (activeInterview.level === 2 || activeInterview.level === '2') stageName = '2nd Level Interview';
else if (activeInterview.level === 3 || activeInterview.level === '3') stageName = '3rd Level Interview';
}
// Fallback for document stage if it's a general approval
if (!stageName) {
if (application.status === 'Shortlisted' || application.status === 'Level 1 Interview Pending') stageName = '1st Level Interview';
else if (application.status === 'Level 1 Approved' || application.status === 'Level 2 Interview Pending') stageName = '2nd Level Interview';
else if (application.status === 'Level 2 Approved' || application.status === 'Level 3 Interview Pending') stageName = '3rd Level Interview';
}
if (stageName) {
formData.append('stage', stageName);
}
await onboardingService.uploadDocument(applicationId, formData);
toast.success('Document uploaded with approval');
} catch (error) {
console.error('Failed to upload approval document', error);
toast.error('Failed to upload document');
}
}
if (activeInterview) {
try {
await onboardingService.updateInterviewDecision({
interviewId: activeInterview.id,
decision: 'Approved',
remarks: approvalRemark
});
toast.success('Interview approved successfully');
setShowApproveModal(false);
setApprovalRemark('');
setApprovalFile(null); // Reset file
fetchInterviews();
// Refresh application to check if status updated
fetchApplication();
return;
} catch (error) {
toast.error('Failed to approve interview');
return;
}
}
if (!approvalRemark.trim()) {
toast.warning('Please enter a remark');
return;
}
// Application level approval - Robust State Machine
let newStatus = application.status;
switch (application.status) {
case 'Shortlisted':
case 'Level 1 Interview Pending':
newStatus = 'Level 1 Approved'; break;
case 'Level 1 Approved':
case 'Level 2 Interview Pending':
newStatus = 'Level 2 Approved'; break;
case 'Level 2 Approved':
case 'Level 3 Interview Pending':
newStatus = 'Level 3 Approved'; break;
case 'Level 3 Approved':
newStatus = 'FDD Verification'; break;
case 'FDD Verification':
newStatus = 'LOI In Progress'; break;
case 'LOI In Progress':
newStatus = 'LOI Issued'; break;
case 'LOI Issued':
newStatus = 'Dealer Code Generation'; break;
case 'Dealer Code Generation':
case 'Architecture Team Assigned':
case 'Architecture Document Upload':
case 'Architecture Team Completion':
newStatus = 'Statutory GST'; break;
case 'Statutory GST':
case 'Statutory PAN':
case 'Statutory Nodal':
case 'Statutory Check':
case 'Statutory Partnership':
case 'Statutory Firm Reg':
case 'Statutory Rental':
case 'Statutory Virtual Code':
case 'Statutory Domain':
case 'Statutory MSD':
case 'Statutory LOI Ack':
newStatus = 'LOA Pending'; break;
case 'LOA Pending':
newStatus = 'EOR In Progress'; break;
case 'EOR In Progress':
newStatus = 'EOR Complete'; break;
case 'EOR Complete':
newStatus = 'Inauguration'; break;
case 'Inauguration':
newStatus = 'Approved'; break;
default:
newStatus = 'Approved'; // Final fallback
}
const policyManagedStages: { [key: string]: string } = {
'Level 1 Interview Pending': 'INTERVIEW_LEVEL_1',
'Level 2 Interview Pending': 'INTERVIEW_LEVEL_2',
'Level 2 Recommended': 'INTERVIEW_LEVEL_2',
'Level 3 Interview Pending': 'INTERVIEW_LEVEL_3',
'LOI In Progress': 'LOI_APPROVAL',
'LOA Pending': 'LOA_APPROVAL'
};
const stageCodeForPolicy = policyManagedStages[application.status];
if (stageCodeForPolicy) {
const response = await onboardingService.submitStageDecision({
applicationId: application.id,
stageCode: stageCodeForPolicy,
decision: 'Approved',
remarks: approvalRemark,
nextStatus: newStatus
});
if (response.data?.statusUpdated) {
toast.success(response.message || 'Stage completed and moved to next step');
} else {
toast.info(response.message || 'Approval recorded. Waiting for other mandatory approvers.');
}
} else {
await onboardingService.updateApplicationStatus(applicationId!, {
status: newStatus,
remarks: approvalRemark
});
toast.success(`Application moved to ${newStatus}`);
}
// Special case: If final approval, create Dealer record
if (newStatus === 'Approved') {
// In a real scenario, we'd have the dealerCodeId from the application's associated DealerCode record
await onboardingService.createDealer({
applicationId: applicationId,
// dealerCodeId is handled by backend if not provided, or we can fetch it
});
toast.success('Application approved and Dealer profile created!');
} else {
toast.success(`Application moved to ${newStatus}`);
}
setShowApproveModal(false);
setApprovalRemark('');
setApprovalFile(null);
fetchApplication();
} catch (error) {
console.error('Approval error:', error);
toast.error('Failed to process approval');
} finally {
setIsApproving(false);
}
};
const handleReject = async () => {
try {
setIsRejecting(true);
// Check if user has an active interview to reject
const activeInterview = interviews.find(i =>
i.status !== 'Completed' && i.status !== 'Cancelled' &&
i.participants?.some((p: any) => p.userId === currentUser?.id)
);
if (activeInterview) {
try {
await onboardingService.updateInterviewDecision({
interviewId: activeInterview.id,
decision: 'Rejected',
remarks: rejectionReason
});
toast.success('Interview rejected');
setShowRejectModal(false);
setRejectionReason('');
fetchInterviews();
fetchApplication();
return;
} catch (error) {
toast.error('Failed to reject interview');
return;
}
}
if (!rejectionReason.trim()) {
toast.warning('Please enter a reason for rejection');
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) {
console.error('Rejection error:', error);
toast.error('Failed to process rejection');
} finally {
setIsRejecting(false);
}
};
const handleGenerateDealerCodes = async () => {
try {
await onboardingService.generateDealerCodes(applicationId!);
toast.success('Dealer codes generated successfully');
fetchApplication();
} catch (error) {
console.error('Generate codes error:', error);
toast.error('Failed to generate dealer codes');
}
};
const handleAssignArchitecture = async () => {
if (!architectureLeadId) {
toast.warning('Please select an architecture lead');
return;
}
try {
setIsAssigningArchitecture(true);
await onboardingService.assignArchitectureTeam(applicationId!, architectureLeadId);
toast.success('Architecture team assigned successfully');
setShowAssignArchitectureModal(false);
fetchApplication(); // Refresh to update status
} catch (error) {
toast.error('Failed to assign architecture team');
} finally {
setIsAssigningArchitecture(false);
}
};
const handleUpdateArchitectureStatus = async () => {
try {
setIsUpdatingArchitecture(true);
await onboardingService.updateArchitectureStatus(applicationId!, architectureStatus, architectureRemarks);
toast.success('Architecture status updated successfully');
setShowArchitectureStatusModal(false);
fetchApplication();
} catch (error) {
toast.error('Failed to update architecture status');
} finally {
setIsUpdatingArchitecture(false);
}
};
const handleAddParticipant = async () => {
if (!selectedUser) {
toast.warning('Please select a user');
return;
}
try {
setIsAssigningParticipant(true);
await onboardingService.addParticipant({
requestId: applicationId,
requestType: 'application',
userId: selectedUser,
participantType: participantType || 'contributor'
});
toast.success('User assigned successfully!');
// Refresh application data
fetchApplication();
setSelectedUser('');
setShowAssignModal(false);
} catch (error) {
toast.error('Failed to assign user');
} finally {
setIsAssigningParticipant(false);
}
};
const handleRetriggerEvaluators = async () => {
try {
setLoading(true);
await onboardingService.retriggerEvaluators(applicationId!);
toast.success('Evaluators re-assigned successfully');
await fetchApplication();
} catch (error) {
toast.error('Failed to re-assign evaluators');
} finally {
setLoading(false);
}
};
if (loading && !application) {
return (
<div className="flex items-center justify-center min-h-[60vh]">
<Loader2 className="w-10 h-10 animate-spin text-amber-600" />
</div>
);
}
if (!application) {
return <div className="flex justify-center items-center h-96">Application not found</div>;
}
// Determine if current user has an active interview and if they have submitted feedback
const interviewsList = Array.isArray(interviews) ? interviews : [];
// For action buttons, we only care about pending interviews
const activeInterviewForUser = interviewsList.find(i =>
['Scheduled', 'Rescheduled', 'Pending', 'In Progress'].includes(i.status) &&
i.participants?.some((p: any) => p.userId === currentUser?.id)
);
// For checking if a decision was ALREADY made, we look at ANY interview the user participated in for the current level
const lastInterviewForUser = [...interviewsList].reverse().find(i =>
i.participants?.some((p: any) => p.userId === currentUser?.id)
);
const currentUserEvaluation = (activeInterviewForUser || lastInterviewForUser)?.evaluations?.find(
(e: any) => e.evaluatorId === currentUser?.id
);
// Helper to check interview level completion
const isInterviewCompleted = (level: number) => {
return interviewsList.some(i => (Number(i.level) === level) && i.status === 'Completed');
};
const isInterviewActive = (level: number) => {
return interviewsList.some(i => (Number(i.level) === level) && i.status === 'Scheduled');
};
// Robust checks for feedback and decision
// 1. If there's an active interview, feedback is required before Approve/Reject
// 2. hasMadeDecision should check if the evaluation has a recommendation
const hasSubmittedFeedback = !!currentUserEvaluation;
// Specific to the current active interview context
const hasSubmittedFeedbackForActive = activeInterviewForUser && hasSubmittedFeedback;
const policyManagedStages: { [key: string]: string } = {
'Level 1 Interview Pending': 'INTERVIEW_LEVEL_1',
'Level 2 Interview Pending': 'INTERVIEW_LEVEL_2',
'Level 2 Recommended': 'INTERVIEW_LEVEL_2',
'Level 3 Interview Pending': 'INTERVIEW_LEVEL_3',
'LOI In Progress': 'LOI_APPROVAL',
'LOA Pending': 'LOA_APPROVAL'
};
const currentStageCode = policyManagedStages[application.status];
const currentUserStageAction = application.stageApprovals?.find(
(a: any) =>
a.stageCode === currentStageCode &&
String(a.actorUserId) === String(currentUser?.id)
);
const hasMadeStageDecision = !!currentUserStageAction;
const hasMadeDecisionForUser = !!currentUserStageAction ||
currentUserEvaluation?.decision === 'Approved' ||
currentUserEvaluation?.decision === 'Rejected' ||
['Approved', 'Rejected', 'Selected'].includes(currentUserEvaluation?.recommendation || '');
// Centralized Permissions Utility (Consolidates 500 lines of fragmented logic)
const getApplicationPermissions = () => {
if (!application || !currentUser) {
return { canApprove: false, canReject: false, canSchedule: false, canAssign: false, isLoaLocked: false, showDecisionMessage: false };
}
// 1. Core Flags
const isAdminRole = ['DD Admin', 'Super Admin', 'NBH', 'DD Lead', 'DD Head', 'Finance', 'Finance Admin', 'FDD', 'ZBH', 'RBM'].includes(currentUser.role);
const isAdministrativeStage = [
'Level 3 Approved', 'FDD Verification',
'LOI In Progress', 'LOI Issued', 'Statutory LOI Ack',
'Architecture Team Assigned', 'Architecture Document Upload', 'Architecture Team Completion',
'Statutory GST', 'Statutory PAN', 'Statutory Nodal', 'Statutory Check',
'Statutory Partnership', 'Statutory Firm Reg', 'Statutory Rental',
'Statutory Virtual Code', 'Statutory Domain', 'Statutory MSD',
'LOA Pending', 'EOR In Progress', 'EOR Complete', 'Inauguration'
].includes(application.status);
const isLoaLocked = application.status === 'LOA Pending' && getDeposit('FIRST_FILL')?.status !== 'Verified';
const isFinalState = application.status === 'Onboarded' || application.status === 'Rejected' || application.status === 'Approved';
// 2. Interview Specific Logic
const activeInterviewForUser = (interviews || []).find(i =>
['Scheduled', 'Rescheduled', 'Pending', 'In Progress'].includes(i.status) &&
i.participants?.some((p: any) => p.userId === currentUser?.id)
);
const hasSubmittedFeedback = !!(activeInterviewForUser || lastInterviewForUser)?.evaluations?.find(
(e: any) => e.evaluatorId === currentUser?.id
);
// 3. Sequential Sequence Check
const ddHeadApproved = application.stageApprovals?.some((a: any) => a.stageCode === 'LOI_APPROVAL' && a.actorRole === 'DD Head' && a.decision === 'Approved');
const ddHeadLoaApproved = application.stageApprovals?.some((a: any) => a.stageCode === 'LOA_APPROVAL' && a.actorRole === 'DD Head' && a.decision === 'Approved');
let sequenceMet = true;
if (!['Super Admin', 'DD Admin'].includes(currentUser.role)) {
if (application.status === 'FDD Verification' || application.status === 'Level 3 Approved') sequenceMet = false;
if (application.status === 'LOI In Progress') sequenceMet = (currentUser.role === 'NBH') ? !!ddHeadApproved : (currentUser.role === 'DD Head');
if (application.status === 'LOA Pending') sequenceMet = (currentUser.role === 'NBH') ? !!ddHeadLoaApproved : (currentUser.role === 'DD Head');
}
// 4. Decision Tracking
const hasMadeStageDecision = !!application.stageApprovals?.find(a => policyManagedStages[application.status] === a.stageCode && String(a.actorUserId) === String(currentUser.id));
const hasMadeInterviewDecision = ['Approved', 'Rejected', 'Selected'].includes(currentUserEvaluation?.decision || currentUserEvaluation?.recommendation || '');
const hasMadeDecisionTotal = hasMadeStageDecision || hasMadeInterviewDecision;
// 5. Final Permission Bits
const isDecisionMade = hasMadeDecisionTotal || hasMadeStageDecision;
const canApproveReject = !isLoaLocked && !isFinalState && !isDecisionMade && (
(!!activeInterviewForUser && !!hasSubmittedFeedback) ||
(isAdminRole && isAdministrativeStage && sequenceMet)
);
return {
canApprove: canApproveReject,
canReject: canApproveReject,
isLoaLocked,
showDecisionMessage: isDecisionMade && (!isAdministrativeStage || hasMadeStageDecision),
canSchedule: ['DD Admin', 'Super Admin', 'DD AM', 'ASM'].includes(currentUser.role) &&
!isFinalState &&
!([1, 2, 3].every(level => (interviews || []).some(i => i.level === level))),
canAssign: ['DD Admin', 'Super Admin', 'DD AM'].includes(currentUser.role)
};
};
const permissions = getApplicationPermissions();
const renderFddAuditContent = () => {
const assignments = application?.fddAssignments || [];
const fddParticipants = application?.participants?.filter((p: any) =>
p.user?.role === 'FDD' ||
p.user?.roleCode === 'FDD' ||
p.user?.allRoles?.includes('FDD')
) || [];
const hasAssignment = assignments.length > 0 || fddParticipants.length > 0;
const primaryFddUser = fddParticipants[0]?.user;
const MANDATORY_FINANCIAL_DOCS = [
{ type: 'Bank Statement', label: 'Bank Statements' },
{ type: 'Income Tax Returns (ITR)', label: 'ITR (Last 3 Years)' },
{ type: 'CIBIL Report', label: 'CIBIL / Credit Reports' },
{ type: 'Property Documents', label: 'Property Documents' },
{ type: 'Business Valuation Report', label: 'Valuation Reports' }
];
const getDocByTypeName = (typeName: string) => {
if (!documents) return null;
const target = typeName.toLowerCase();
return documents.find((d: any) => {
const docType = (d.documentType || '').toLowerCase();
const fileName = (d.fileName || '').toLowerCase();
if (docType === target) return true;
if (target.includes('itr') && (docType.includes('itr') || fileName.includes('itr'))) return true;
if (target.includes('bank statement') && (docType.includes('bank') || fileName.includes('bank'))) return true;
if (target.includes('cibil') && (docType.includes('cibil') || fileName.includes('cibil') || docType.includes('credit'))) return true;
return false;
});
};
if (!hasAssignment && !['FDD Verification', 'LOI In Progress', 'Payment Pending'].includes(application.status)) {
return (
<div className="space-y-6">
<div className="flex flex-col items-center justify-center p-12 bg-slate-50 rounded-2xl border-2 border-dashed border-slate-200">
<ShieldCheck className="w-12 h-12 text-slate-300 mb-4" />
<h3 className="text-slate-900 font-semibold uppercase tracking-widest text-xs">No FDD Assignment</h3>
<p className="text-slate-500 text-[10px] text-center max-w-xs mt-2 font-medium leading-relaxed uppercase tracking-tight">
The Financial Due Diligence process has not been initiated for this application yet.
</p>
</div>
{(currentUser?.role === 'DD Admin' || currentUser?.role === 'Super Admin') && (
<Card className="border-amber-100 bg-amber-50/30 overflow-hidden rounded-2xl">
<CardHeader className="pb-2">
<CardTitle className="text-xs font-black uppercase tracking-widest text-amber-800 flex items-center gap-2">
<ShieldAlert className="w-4 h-4" />
Initiate FDD Audit
</CardTitle>
</CardHeader>
<CardContent>
<div className="flex flex-col md:flex-row gap-4">
<div className="flex-1">
<label className="text-[10px] font-black uppercase tracking-widest text-slate-400 mb-1.5 block">Select FDD Agency</label>
<select
className="w-full h-11 bg-white border border-slate-200 rounded-xl px-4 text-sm font-medium focus:ring-2 focus:ring-amber-500/20 focus:border-amber-500 outline-none transition-all shadow-sm"
value={selectedAgencyId}
onChange={(e) => setSelectedAgencyId(e.target.value)}
>
<option value="">Choose partner agency...</option>
{fddAgencies && fddAgencies.map((agency: any) => (
<option key={agency.id} value={agency.id}>
{agency.fullName || agency.name} ({agency.email})
</option>
))}
</select>
</div>
<div className="flex items-end">
<Button
className="bg-slate-900 text-white hover:bg-slate-800 font-black text-[10px] uppercase tracking-widest px-8 h-11 border-none shadow-lg shadow-slate-900/10 transition-all active:scale-[0.98]"
onClick={() => handleAssignAgency()}
disabled={isAssigningAgency || !selectedAgencyId}
>
{isAssigningAgency ? 'Assigning...' : 'Assign & Start Audit'}
</Button>
</div>
</div>
</CardContent>
</Card>
)}
</div>
);
}
return (
<div className="space-y-8">
{/* FDD/Finance Audit Workspace */}
{((currentUser?.role === 'FDD' || currentUser?.role === 'DD Admin' || currentUser?.role === 'Super Admin' || hasAssignment) &&
(['FDD Verification', 'Level 3 Approved', 'LOI In Progress'].includes(application.status))) && (
<div className="flex flex-col md:flex-row gap-4 p-6 bg-slate-900 rounded-2xl text-white shadow-xl border-b-4 border-amber-500 mb-8">
<div className="flex-1">
<h4 className="text-lg font-bold flex items-center gap-3">
<div className="p-2 bg-amber-500/20 rounded-lg">
<ShieldCheck className="w-5 h-5 text-amber-400" />
</div>
Audit Management Workspace {primaryFddUser && <span className="text-amber-400 text-sm font-normal ml-2">Assigned to: {primaryFddUser.name}</span>}
</h4>
<p className="text-slate-400 text-[11px] mt-1 font-medium leading-relaxed max-w-md">Capture financial findings, upload reports, and provide your formal recommendation to progress the application.</p>
</div>
<div className="flex items-center gap-3">
<input
type="file"
id="fdd-report-upload"
className="hidden"
onChange={async (e) => {
const file = e.target.files?.[0];
if (!file) return;
try {
setIsUploading(true);
const formData = new FormData();
formData.append('file', file);
formData.append('documentType', 'FDD Final Audit Report');
formData.append('stage', 'FDD');
formData.append('applicationId', application.id);
await onboardingService.uploadDocument(application.id, formData);
toast.success('FDD Final Audit Report uploaded successfully');
refreshDocuments();
} catch (err) {
toast.error('Upload failed');
} finally {
setIsUploading(false);
}
}}
/>
<Button
className="bg-white text-slate-900 hover:bg-slate-100 font-black text-[10px] uppercase tracking-widest px-6 h-11 border-none"
disabled={isUploading}
onClick={() => document.getElementById('fdd-report-upload')?.click()}
>
<Upload className="w-4 h-4 mr-2" />
{isUploading ? 'Uploading...' : 'Upload Report'}
</Button>
{(currentUser?.role === 'DD Admin' || currentUser?.role === 'Super Admin') && (
<>
<Button
variant="outline"
className="bg-transparent text-white border-white/20 hover:bg-red-600/20 hover:border-red-500/50 hover:text-red-400 font-black text-[10px] uppercase tracking-widest px-6 h-11"
onClick={() => setShowFddFlagModal(true)}
>
Flag Non-Responsive
</Button>
<Button
className="bg-amber-500 text-white hover:bg-amber-600 font-black text-[10px] uppercase tracking-widest px-6 h-11 border-none shadow-lg shadow-amber-500/20"
onClick={() => setShowFddFinalizeModal(true)}
>
Finalize Audit
</Button>
</>
)}
</div>
</div>
)}
{/* Financial Document Checklist Section */}
<Card className="border-slate-200 shadow-sm overflow-hidden rounded-2xl">
<CardHeader className="bg-slate-50/50 border-b border-slate-100 py-4">
<CardTitle className="text-sm font-black uppercase tracking-widest text-slate-500 flex items-center justify-between">
<div className="flex items-center gap-2"><ClipboardList className="w-4 h-4" /> Financial Artefacts Checklist</div>
<Badge variant="outline" className="text-[10px] bg-white">Verify before sign-off</Badge>
</CardTitle>
</CardHeader>
<CardContent className="p-0">
<div className="divide-y divide-slate-100">
{MANDATORY_FINANCIAL_DOCS.map((docType) => {
const doc = getDocByTypeName(docType.type);
return (
<div key={docType.type} className="flex items-center justify-between p-4 px-6 hover:bg-slate-50/50 transition-colors">
<div className="flex items-center gap-4">
<div className={cn(
"w-8 h-8 rounded-lg flex items-center justify-center",
doc ? "bg-emerald-50 text-emerald-600" : "bg-slate-50 text-slate-300"
)}>
{doc ? <CheckCircle className="w-5 h-5" /> : <AlertCircle className="w-5 h-5" />}
</div>
<div>
<p className="text-sm font-bold text-slate-800">{docType.label}</p>
<p className="text-[10px] text-slate-400 font-bold uppercase tracking-tighter">
{doc ? `Uploaded: ${formatDateTime(doc.createdAt)}` : 'Missing in Documentation'}
</p>
</div>
</div>
{doc ? (
<div className="flex gap-2">
<Button variant="ghost" size="sm" className="h-8 text-blue-600 font-black text-[10px] uppercase tracking-widest hover:bg-blue-50"
onClick={() => {
setPreviewDoc(doc);
setShowPreviewModal(true);
}}
>
<Eye className="w-4 h-4 mr-1" />
Preview
</Button>
</div>
) : (
<Button
variant="outline"
size="sm"
className="h-8 border-slate-200 text-slate-500 font-black text-[10px] uppercase tracking-widest hover:bg-slate-50 hover:text-blue-600"
onClick={() => {
const input = document.createElement('input');
input.type = 'file';
input.onchange = async (e: any) => {
const file = e.target.files[0];
if (!file) return;
try {
setIsUploading(true);
const formData = new FormData();
formData.append('file', file);
formData.append('documentType', docType.type);
formData.append('stage', 'FDD');
formData.append('applicationId', application.id);
await onboardingService.uploadDocument(application.id, formData);
toast.success(`${docType.label} uploaded successfully`);
refreshDocuments();
} catch (err) {
toast.error('Upload failed');
} finally {
setIsUploading(false);
}
};
input.click();
}}
>
<Upload className="w-4 h-4 mr-1 text-slate-300" />
Upload
</Button>
)}
</div>
);
})}
</div>
</CardContent>
</Card>
<div className="space-y-6">
<div className="flex items-center justify-between">
<h3 className="text-lg font-semibold text-slate-900">Financial Due Diligence Reports</h3>
<Badge variant="outline" className="bg-amber-50 text-amber-600 border-amber-200">
{assignments.length} Assignment(s)
</Badge>
</div>
{assignments.map((assignment: any) => (
<Card key={assignment.id} className="overflow-hidden border-slate-200 shadow-sm">
<CardHeader className="bg-slate-50/50 py-4 border-b border-slate-100">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-lg bg-white flex items-center justify-center border border-slate-200 shadow-sm">
<ShieldCheck className="w-6 h-6 text-amber-600" />
</div>
<div>
<h4 className="text-slate-900 font-bold truncate max-w-[200px]">FDD Agency Audit</h4>
<p className="text-slate-500 text-[10px] uppercase tracking-wider font-bold">
Agency ID: {assignment.assignedToAgency || 'Assigned'} Status: {assignment.status}
</p>
</div>
</div>
<Badge className={cn(
assignment.status === 'completed' ? "bg-green-100 text-green-700 hover:bg-green-100" : "bg-amber-100 text-amber-700 hover:bg-amber-100"
)}>
{assignment.status}
</Badge>
</div>
</CardHeader>
<CardContent className="p-0">
{(!assignment.reports || assignment.reports.length === 0) ? (
<div className="p-12 text-center">
<Clock className="w-8 h-8 text-slate-300 mx-auto mb-3" />
<p className="text-slate-500 italic text-sm">Waiting for internal or external agency to submit the final audit report...</p>
</div>
) : (
<div className="divide-y divide-slate-100">
{assignment.reports.map((report: any) => (
<div key={report.id} className="p-6 space-y-6 bg-white">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
<div className="space-y-5">
{/* Auditor Recommendation Hidden as per request */}
<div>
<Label className="text-[10px] text-slate-400 uppercase tracking-widest font-black mb-2 block">Findings Summary</Label>
<div className="bg-slate-50 p-4 rounded-xl border border-slate-100 text-slate-700 text-sm leading-relaxed shadow-inner italic">
"{report.findings || 'No detail findings provided by the auditor.'}"
</div>
</div>
</div>
<div className="space-y-5">
<Label className="text-[10px] text-slate-400 uppercase tracking-widest font-black mb-2 block">Report Document</Label>
{report.reportDocument ? (
<div className="group bg-white border-2 border-slate-100 rounded-xl p-4 flex items-center justify-between hover:border-amber-400 transition-all hover:shadow-md cursor-pointer">
<div className="flex items-center gap-4">
<div className="w-12 h-12 rounded-xl bg-red-50 flex items-center justify-center group-hover:scale-110 transition-all shadow-sm">
<FileText className="w-6 h-6 text-red-500" />
</div>
<div className="overflow-hidden">
<p className="text-slate-500 text-[10px] font-medium">SUBMITTED {formatDateTime(report.createdAt)}</p>
</div>
</div>
<div className="flex gap-1">
<Button
variant="ghost"
size="icon"
className="h-9 w-9 text-slate-400 hover:text-amber-600 hover:bg-amber-50"
onClick={(e) => {
e.stopPropagation();
window.open(`http://localhost:5000/${report.reportDocument.filePath}`, '_blank');
}}
>
<Download className="w-4 h-4" />
</Button>
<Button
variant="ghost"
size="icon"
className="h-9 w-9 text-slate-400 hover:text-amber-600 hover:bg-amber-50"
onClick={(e) => {
e.stopPropagation();
setPreviewDoc(report.reportDocument);
setShowPreviewModal(true);
}}
>
<Eye className="w-4 h-4" />
</Button>
</div>
</div>
) : (
<div className="p-6 bg-slate-50 rounded-xl border border-dashed border-slate-200 text-center text-slate-400 text-xs font-medium">
No audit report file attached
</div>
)}
<div className="pt-4 flex flex-col gap-2">
<div className="flex items-center gap-2">
<div className="flex items-center gap-1.5 bg-slate-50 border border-slate-100 px-3 py-1 rounded-full">
<User className="w-3.5 h-3.5 text-slate-500" />
<span className="text-[10px] font-bold text-slate-600 uppercase">Submitted by: {report.submitter?.fullName || 'Auditor'}</span>
</div>
</div>
{report.verifiedAt ? (
<div className="flex items-center gap-2 bg-green-50 border border-green-100 px-3 py-1.5 rounded-full w-fit">
<CheckCircle className="w-4 h-4 text-green-600" />
<span className="text-[10px] font-black text-green-700 uppercase"> Verified by {report.verifier?.fullName || 'Admin'}</span>
</div>
) : (
<div className="flex items-center gap-2 bg-amber-50 border border-amber-100 px-3 py-1.5 rounded-full w-fit">
<Clock className="w-4 h-4 text-amber-600" />
<span className="text-[10px] font-black text-amber-700 uppercase">Pending Review</span>
</div>
)}
</div>
</div>
</div>
</div>
))}
</div>
)}
</CardContent>
</Card>
))}
{/* FDD Supporting Documents Section */}
<div className="space-y-4">
<div className="flex items-center justify-between">
<h3 className="text-lg font-semibold text-slate-900">Supporting Audit Documents</h3>
<Badge variant="outline" className="bg-slate-50 text-slate-500 border-slate-200">
{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)
</Badge>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{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) => (
<div key={doc.id} className="group bg-white border border-slate-200 rounded-xl p-4 flex items-center justify-between hover:border-amber-400 transition-all hover:shadow-md">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-lg bg-slate-50 flex items-center justify-center">
<FileText className="w-5 h-5 text-slate-400" />
</div>
<div className="overflow-hidden">
<p className="text-slate-900 font-bold text-sm truncate max-w-[150px]" title={doc.fileName}>{doc.fileName}</p>
<p className="text-slate-500 text-[10px] font-medium uppercase">{doc.documentType}</p>
</div>
</div>
<div className="flex gap-1">
<Button
variant="ghost"
size="icon"
className="h-8 w-8 text-slate-400 hover:text-amber-600 hover:bg-amber-50"
onClick={() => window.open(`http://localhost:5000/${doc.filePath}`, '_blank')}
>
<Download className="w-4 h-4" />
</Button>
<Button
variant="ghost"
size="icon"
className="h-8 w-8 text-slate-400 hover:text-amber-600 hover:bg-amber-50"
onClick={() => {
setPreviewDoc(doc);
setShowPreviewModal(true);
}}
>
<Eye className="w-4 h-4" />
</Button>
</div>
</div>
))}
{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 && (
<div className="col-span-full p-8 text-center bg-slate-50 rounded-xl border border-dashed border-slate-200">
<p className="text-slate-400 text-sm">No supporting audit documents uploaded yet.</p>
</div>
)}
</div>
</div>
</div>
</div>
);
};
return (
<div className="space-y-6">
{/* Header */}
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-4">
<div className="flex items-center gap-4">
<Button variant="outline" size="icon" onClick={onBack} className="shrink-0">
<ArrowLeft className="w-4 h-4" />
</Button>
<div className="truncate">
<h1 className="text-slate-900 truncate leading-tight">{application.name}</h1>
<p className="text-slate-600 truncate text-sm">{application.registrationNumber}</p>
</div>
</div>
<div className="flex flex-wrap gap-2">
<Button
variant="outline"
className="relative hover:bg-amber-50 hover:border-amber-300 hover:text-amber-700 transition-all shadow-sm"
onClick={() => navigate(`/worknotes/application/${application.id}`, {
state: {
applicationName: application.name,
registrationNumber: application.registrationNumber,
participants: application.participants
}
})}
>
<MessageSquare className="w-4 h-4 mr-2" />
View Work Notes
</Button>
</div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Main Content */}
<div className="lg:col-span-2 space-y-6">
{/* Core Details Card */}
<Card>
<CardHeader>
<CardTitle>Applicant Information</CardTitle>
</CardHeader>
<CardContent className="p-4 sm:p-6 space-y-4">
<div className="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-3 gap-x-6 gap-y-4">
<div className="flex items-start gap-3">
<User className="w-5 h-5 text-slate-400 mt-1" />
<div>
<p className="text-slate-600">Full Name</p>
<p className="text-slate-900">{application.name}</p>
</div>
</div>
<div className="flex items-start gap-3">
<Mail className="w-5 h-5 text-slate-400 mt-1" />
<div>
<p className="text-slate-600">Email</p>
<p className="text-slate-900">{application.email}</p>
</div>
</div>
<div className="flex items-start gap-3">
<Phone className="w-5 h-5 text-slate-400 mt-1" />
<div>
<p className="text-slate-600">Phone</p>
<p className="text-slate-900">{application.phone}</p>
</div>
</div>
<div className="flex items-start gap-3">
<User className="w-5 h-5 text-slate-400 mt-1" />
<div>
<p className="text-slate-600">Age</p>
<p className="text-slate-900">{application.age ? `${application.age} years` : 'N/A'}</p>
</div>
</div>
<div className="flex items-start gap-3">
<GraduationCap className="w-5 h-5 text-slate-400 mt-1" />
<div>
<p className="text-slate-600">Education</p>
<p className="text-slate-900">{application.education || 'N/A'}</p>
</div>
</div>
<div className="flex items-start gap-3">
<MapPin className="w-5 h-5 text-slate-400 mt-1" />
<div>
<p className="text-slate-600">Preferred Location</p>
<p className="text-slate-900">{application.preferredLocation || 'N/A'}</p>
</div>
</div>
<div className="flex items-start gap-3">
<MapPin className="w-5 h-5 text-slate-400 mt-1" />
<div>
<p className="text-slate-600">Location Type</p>
<p className="text-slate-900">{application.locationType || 'N/A'}</p>
</div>
</div>
<div className="flex items-start gap-3">
<Building2 className="w-5 h-5 text-slate-400 mt-1" />
<div className="flex-1">
<p className="text-slate-600 flex items-center justify-between group cursor-pointer" onClick={() => {
setTempFirmType(application.constitutionType || '');
setShowFirmTypeModal(true);
}}>
Proposed Firm Type
<Pencil className="w-3 h-3 text-slate-300 group-hover:text-amber-600 transition-colors" />
</p>
<p className="text-slate-900 font-black text-amber-700 tracking-tight leading-none mt-1">
{application.constitutionType || 'Not Provided'}
</p>
</div>
</div>
<div className="flex items-start gap-3">
<Bike className="w-5 h-5 text-slate-400 mt-1" />
<div>
<p className="text-slate-600">Owns Bike</p>
<p className="text-slate-900">{application.ownRoyalEnfield === 'yes' ? 'Yes' : 'No'}</p>
</div>
</div>
{application.ownRoyalEnfield === 'yes' && (
<div className="flex items-start gap-3">
<Bike className="w-5 h-5 text-slate-400 mt-1" />
<div>
<p className="text-slate-600">Bike Model</p>
<p className="text-slate-900">{application.royalEnfieldModel || 'N/A'}</p>
</div>
</div>
)}
<div className="flex items-start gap-3">
<User className="w-5 h-5 text-slate-400 mt-1" />
<div>
<p className="text-slate-600">Existing Dealer</p>
<p className="text-slate-900">{application.existingDealer === 'yes' ? 'Yes' : 'No'}</p>
</div>
</div>
{application.existingDealer === 'yes' && (
<div className="flex items-start gap-3">
<User className="w-5 h-5 text-slate-400 mt-1" />
<div>
<p className="text-slate-600">Company Name</p>
<p className="text-slate-900">{application.companyName || 'N/A'}</p>
</div>
</div>
)}
<div className="flex items-start gap-3">
<ClipboardList className="w-5 h-5 text-slate-400 mt-1" />
<div>
<p className="text-slate-600">Source</p>
<p className="text-slate-900">{application.source || 'N/A'}</p>
</div>
</div>
{application.questionnaireMarks !== undefined && (
<div className="flex items-start gap-3">
<Award className="w-5 h-5 text-slate-400 mt-1" />
<div>
<p className="text-slate-600">Questionnaire Score</p>
<p className="text-slate-900">{application.questionnaireMarks}/100</p>
</div>
</div>
)}
</div>
<Separator />
<div>
<p className="text-slate-600 mb-2">Address</p>
<p className="text-slate-900">{application.address || 'N/A'}</p>
</div>
<div>
<p className="text-slate-600 mb-2">Pincode</p>
<p className="text-slate-900">{application.pincode || 'N/A'}</p>
</div>
<div>
<p className="text-slate-600 mb-2">Description</p>
<p className="text-slate-900">{application.description || 'N/A'}</p>
</div>
<div>
<p className="text-slate-600 mb-2">Past Experience</p>
<p className="text-slate-900">{application.pastExperience || 'N/A'}</p>
</div>
<div className="pt-6 border-t mt-6">
<div className="flex items-center justify-between mb-4">
<h3 className="text-sm font-black text-slate-900 uppercase tracking-widest flex items-center gap-2">
<CreditCard className="w-4 h-4 text-amber-600" /> Statutory & Bank Information
</h3>
{canEditStatutory && !isEditingStatutory && (
<Button
variant="ghost"
size="sm"
onClick={handleEditStatutory}
className="h-8 text-amber-600 hover:text-amber-700 hover:bg-amber-50 gap-1.5"
>
<Pencil className="w-3.5 h-3.5" />
Edit Details
</Button>
)}
</div>
{isEditingStatutory ? (
<div className="bg-slate-50/50 p-6 rounded-xl border-2 border-amber-100 space-y-4">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
<div className="space-y-2">
<Label className="text-[10px] uppercase font-bold text-slate-500">Legal Entity Name</Label>
<Input
value={statutoryForm.accountHolderName}
onChange={(e) => setStatutoryForm({...statutoryForm, accountHolderName: e.target.value})}
placeholder="Enter Legal Entity Name"
className="bg-white border-slate-200"
/>
</div>
<div className="space-y-2">
<Label className="text-[10px] uppercase font-bold text-slate-500">PAN Number</Label>
<Input
value={statutoryForm.panNumber}
onChange={(e) => setStatutoryForm({...statutoryForm, panNumber: e.target.value.toUpperCase()})}
placeholder="10-digit PAN"
maxLength={10}
className="bg-white border-slate-200 uppercase"
/>
</div>
<div className="space-y-2">
<Label className="text-[10px] uppercase font-bold text-slate-500">GST Number</Label>
<Input
value={statutoryForm.gstNumber}
onChange={(e) => setStatutoryForm({...statutoryForm, gstNumber: e.target.value.toUpperCase()})}
placeholder="15-digit GSTIN"
maxLength={15}
className="bg-white border-slate-200 uppercase"
/>
</div>
<div className="space-y-2">
<Label className="text-[10px] uppercase font-bold text-slate-500">Registered Address</Label>
<Input
value={statutoryForm.registeredAddress}
onChange={(e) => setStatutoryForm({...statutoryForm, registeredAddress: e.target.value})}
placeholder="Enter Registered Office Address"
className="bg-white border-slate-200"
/>
</div>
<div className="space-y-2">
<Label className="text-[10px] uppercase font-bold text-slate-500">Bank Name</Label>
<Input
value={statutoryForm.bankName}
onChange={(e) => setStatutoryForm({...statutoryForm, bankName: e.target.value})}
placeholder="Enter Bank Name"
className="bg-white border-slate-200"
/>
</div>
<div className="space-y-2">
<Label className="text-[10px] uppercase font-bold text-slate-500">Account Number</Label>
<Input
value={statutoryForm.accountNumber}
onChange={(e) => setStatutoryForm({...statutoryForm, accountNumber: e.target.value})}
placeholder="Enter Account Number"
className="bg-white border-slate-200"
/>
</div>
<div className="space-y-2">
<Label className="text-[10px] uppercase font-bold text-slate-500">IFSC Code</Label>
<Input
value={statutoryForm.ifscCode}
onChange={(e) => setStatutoryForm({...statutoryForm, ifscCode: e.target.value.toUpperCase()})}
placeholder="11-digit IFSC"
maxLength={11}
className="bg-white border-slate-200 uppercase"
/>
</div>
</div>
<div className="flex justify-end gap-3 pt-4 border-t">
<Button
variant="outline"
size="sm"
onClick={() => setIsEditingStatutory(false)}
disabled={isSavingStatutory}
>
Cancel
</Button>
<Button
size="sm"
onClick={handleSaveStatutory}
disabled={isSavingStatutory}
className="bg-amber-600 hover:bg-amber-700"
>
{isSavingStatutory ? <Loader2 className="w-4 h-4 animate-spin" /> : 'Save Details'}
</Button>
</div>
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 bg-slate-50/50 p-4 rounded-xl border border-slate-100">
<div className="space-y-1">
<p className="text-[10px] text-slate-500 uppercase font-bold tracking-tight">Legal Entity Name</p>
<p className="text-xs font-semibold text-slate-900">{application.accountHolderName || 'Pending'}</p>
</div>
<div className="space-y-1">
<p className="text-[10px] text-slate-500 uppercase font-bold tracking-tight">PAN Number</p>
<p className="text-xs font-semibold text-slate-900 uppercase">{application.panNumber || 'Pending'}</p>
</div>
<div className="space-y-1">
<p className="text-[10px] text-slate-500 uppercase font-bold tracking-tight">GST Number</p>
<p className="text-xs font-semibold text-slate-900 uppercase">{application.gstNumber || 'Pending'}</p>
</div>
<div className="space-y-1">
<p className="text-[10px] text-slate-500 uppercase font-bold tracking-tight">Registered Address</p>
<p className="text-xs font-semibold text-slate-900">{application.registeredAddress || 'Pending'}</p>
</div>
<div className="space-y-1">
<p className="text-[10px] text-slate-500 uppercase font-bold tracking-tight">Bank Details</p>
<p className="text-xs font-semibold text-slate-900">{application.bankName || 'N/A'}</p>
<p className="text-[10px] text-slate-600">A/C: {application.accountNumber || 'N/A'}</p>
<p className="text-[10px] text-slate-600">IFSC: {application.ifscCode || 'N/A'}</p>
</div>
</div>
)}
</div>
</CardContent>
</Card>
{/* 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 && (
<Card>
<Tabs value={activeTab} onValueChange={setActiveTab}>
<CardHeader className="pb-4 px-4 sm:px-6">
<div className="overflow-x-auto scrollbar-hide -mx-4 px-4 sm:-mx-6 sm:px-6">
<TabsList className="w-max min-w-full justify-start h-11 bg-slate-100/80 p-1">
<TabsTrigger value="questionnaire" className="min-w-[120px]">Questionnaire</TabsTrigger>
<TabsTrigger value="progress" className="min-w-[80px]">Progress</TabsTrigger>
<TabsTrigger value="documents" className="min-w-[100px]">Documents</TabsTrigger>
<TabsTrigger value="interviews" className="min-w-[100px]">Interviews</TabsTrigger>
<TabsTrigger value="fdd" className="min-w-[120px]">FDD Audit</TabsTrigger>
<TabsTrigger value="eor" className="min-w-[120px]">EOR Checklist</TabsTrigger>
<TabsTrigger value="payments" className="min-w-[100px]">Payments</TabsTrigger>
<TabsTrigger value="audit" className="min-w-[100px]">Audit Trail</TabsTrigger>
</TabsList>
</div>
</CardHeader>
<CardContent>
{/* Questionnaire Response Tab */}
<TabsContent value="questionnaire" className="space-y-6">
<QuestionnaireResponseView application={application} />
</TabsContent>
{/* Progress Tab */}
<TabsContent value="progress" className="space-y-6">
<div>
<div className="flex items-center justify-between mb-4">
<h3 className="text-slate-900">Application Journey</h3>
<Badge className="bg-amber-600">{application.progress}% Complete</Badge>
</div>
<Progress value={application.progress} className="h-3 mb-6" />
</div>
<div className="relative">
{(() => {
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<string, string | number> = {
'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 (
<div className="flex flex-wrap gap-2 mt-3">
{approvers.map((approver, i) => (
<div key={i} className="group relative flex items-center gap-1.5 bg-slate-50 border border-slate-200 rounded-full pl-1 pr-2.5 py-0.5 transition-all hover:bg-white hover:shadow-sm">
<div className={cn(
"w-6 h-6 rounded-full flex items-center justify-center text-[10px] font-bold text-white",
approver.status === 'approved' ? "bg-green-500" : approver.status === 'rejected' ? "bg-red-500" : "bg-slate-300"
)}>
{approver.name.split(' ').map((n: string) => n[0]).join('').substring(0, 2).toUpperCase()}
</div>
<div className="flex flex-col">
<span className="text-[10px] font-medium text-slate-700 leading-none">{approver.name}</span>
<span className="text-[8px] text-slate-500 leading-none mt-0.5">{approver.role}</span>
</div>
{/* Status Dot Overlay */}
<div className={cn(
"absolute -top-0.5 -right-0.5 w-2 h-2 rounded-full border border-white",
approver.status === 'approved' ? "bg-green-500" : approver.status === 'rejected' ? "bg-red-500" : "bg-amber-400"
)} />
{/* Tooltip */}
<div className="absolute bottom-full left-1/2 -translate-x-1/2 mb-2 px-2 py-1 bg-slate-900 text-white text-[10px] rounded opacity-0 group-hover:opacity-100 pointer-events-none transition-opacity whitespace-nowrap z-50">
{approver.role}: {approver.status.toUpperCase()}
</div>
</div>
))}
</div>
);
};
return processStages.map((stage, index) => (
<div key={stage.id}>
<div className="flex gap-4 pb-8">
<div className="relative">
<div className={`w-10 h-10 rounded-full flex items-center justify-center border-2 z-10 relative ${stage.status === 'completed'
? 'bg-green-500 border-green-500 text-white shadow-md'
: stage.status === 'active'
? stage.isLocked ? 'bg-slate-400 border-slate-400 text-white' : 'bg-amber-500 border-amber-500 text-white animate-pulse-subtle'
: 'bg-white border-slate-300 text-slate-400 shadow-none'
}`}>
{stage.isParallel ? (
<GitBranch className="w-5 h-5" />
) : stage.isLocked ? (
<div className="group relative">
<Lock className="w-5 h-5 text-white cursor-help" />
<div className="absolute bottom-full left-1/2 -translate-x-1/2 mb-2 px-3 py-1.5 bg-slate-900 text-white text-[10px] rounded shadow-xl opacity-0 group-hover:opacity-100 pointer-events-none transition-all duration-200 whitespace-nowrap z-[100] border border-slate-700">
<div className="flex flex-col gap-1">
<span className="font-bold text-amber-400 flex items-center gap-1">
<AlertCircle className="w-3 h-3" /> Stage Locked
</span>
<span>{stage.lockMessage}</span>
</div>
<div className="absolute top-full left-1/2 -translate-x-1/2 border-8 border-transparent border-t-slate-900"></div>
</div>
</div>
) : (
<>
{stage.status === 'completed' ? (
<CheckCircle2 className="w-6 h-6" />
) : stage.status === 'active' ? (
<Clock className="w-5 h-5 text-white" />
) : (
<div className="w-3 h-3 bg-slate-300 rounded-full"></div>
)}
</>
)}
</div>
{index < processStages.length - 1 && !stage.isParallel && (
<div className={`absolute top-10 left-1/2 -translate-x-1/2 w-0.5 h-full z-0 ${stage.status === 'completed' ? 'bg-green-500/30' : 'bg-slate-200'
}`}></div>
)}
</div>
<div className="flex-1 pt-1">
<p className={cn(
"font-bold transition-colors",
stage.status === 'completed' ? "text-green-700" : stage.status === 'active' ? "text-amber-700" : "text-slate-900"
)}>{stage.name}</p>
{stage.description && (
<p className="text-slate-600 text-sm mt-0.5 leading-relaxed">{stage.description}</p>
)}
{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) && (
<p className="text-amber-600 text-xs mt-1.5 flex items-center gap-1 bg-amber-50 w-fit px-2 py-0.5 rounded border border-amber-100">
<User className="w-3 h-3" />
Evaluators: {stage.evaluators.join(' + ')}
</p>
)}
{(() => {
const expectedMap: Record<number, number> = {
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 (
<div className="mt-2">
<Alert variant="destructive" className="py-2 px-3 border-amber-200 bg-amber-50 text-amber-800">
<AlertCircle className="h-4 w-4 text-amber-600" />
<AlertTitle className="text-xs font-semibold">Missing Evaluators</AlertTitle>
<AlertDescription className="text-xs">
{actualCount === 0
? "Respective role users were not found for this location."
: `Some roles (${actualCount}/${expectedCount}) are missing for this location.`
}
<Button
variant="link"
size="sm"
className="h-auto p-0 ml-1 text-xs text-amber-700 underline"
onClick={handleRetriggerEvaluators}
>
<RefreshCw className="w-3 h-3 mr-1" />
Re-trigger Assignment
</Button>
</AlertDescription>
</Alert>
</div>
);
}
return null;
})()}
{(() => {
const stageDocsCount = documents.filter(doc =>
doc.stage === stage.name ||
(!doc.stage && doc.documentType?.toLowerCase().includes(stage.name.toLowerCase().split(' ')[0]))
).length;
return (
<div className="flex items-center gap-2 mt-1">
<button
onClick={() => {
setSelectedStage(stage.name);
setShowDocumentsModal(true);
if (stageDocsCount === 0) setShowUploadForm(true);
}}
className="text-xs font-semibold text-blue-600 hover:text-blue-800 flex items-center gap-1.5 px-3 py-1 rounded-full bg-blue-50 border border-blue-100 hover:bg-blue-100 transition-all shadow-sm"
>
<FileText className="w-3.5 h-3.5" />
{stageDocsCount > 0 ? `${stageDocsCount} Documents` : 'Upload'}
</button>
</div>
);
})()}
<p className="text-slate-500 mt-1 text-xs">
{stage.status === 'completed' && stage.date && `Completed: ${formatDateTime(stage.date)}`}
{stage.status === 'active' && 'In Progress'}
{stage.status === 'pending' && 'Pending'}
</p>
</div>
</div>
{stage.isParallel && stage.branches && (
<div className="ml-5 mb-8">
{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 (
<div key={branchIndex} className="mb-6 last:mb-0">
<div className="flex items-center gap-3 mb-2">
<button
onClick={() => setExpandedBranches(prev => ({
...prev,
[branchKey]: !prev[branchKey]
}))}
className={`flex-1 flex items-center gap-3 p-4 rounded-lg border-2 transition-all hover:shadow-md ${branchColor === 'blue'
? 'border-blue-300 bg-blue-50 hover:bg-blue-100'
: 'border-green-300 bg-green-50 hover:bg-green-100'
}`}
>
{isExpanded ? (
<ChevronDown className={`w-5 h-5 ${branchColor === 'blue' ? 'text-blue-600' : 'text-green-600'}`} />
) : (
<ChevronRight className={`w-5 h-5 ${branchColor === 'blue' ? 'text-blue-600' : 'text-green-600'}`} />
)}
<div className={`w-8 h-8 rounded-full flex items-center justify-center ${branchColor === 'blue' ? 'bg-blue-200' : 'bg-green-200'
}`}>
<GitBranch className={`w-4 h-4 ${branchColor === 'blue' ? 'text-blue-700' : 'text-green-700'}`} />
</div>
<div className="flex-1 text-left">
<p className={`${branchColor === 'blue' ? 'text-blue-900' : 'text-green-900'} font-semibold tracking-tight`}>
{branch.name}
</p>
<p className={`text-[10px] uppercase font-bold tracking-wider ${branchColor === 'blue' ? 'text-blue-500' : 'text-green-500'}`}>
{branch.stages.length} SUB-STEPS
</p>
</div>
</button>
</div>
{isExpanded && (
<div className="mt-4 ml-8 border-l-2 border-slate-200 pl-6 space-y-6">
{branch.stages.map((branchStage) => (
<div key={branchStage.id} className="relative">
<div className="flex gap-4 text-xs">
{(() => {
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 (
<>
<div className="relative">
<div className={`w-8 h-8 rounded-full flex items-center justify-center border-2 ${isDone
? `${branchColor === 'blue' ? 'bg-blue-500 border-blue-500' : 'bg-green-500 border-green-500'}`
: branchStage.status === 'active'
? 'bg-amber-500 border-amber-500 text-white shadow-sm'
: 'bg-white border-slate-300 text-slate-400'
}`}>
{isDone ? (
<Check className="w-4 h-4 text-white" strokeWidth={3} />
) : branchStage.status === 'active' ? (
<Clock className="w-4 h-4 text-white" />
) : (
<div className="w-2 h-2 bg-slate-300 rounded-full"></div>
)}
</div>
</div>
<div className="flex-1">
<p className="font-semibold text-slate-800">{branchStage.name}</p>
{branchStage.description && (
<p className="text-slate-500 text-xs mt-0.5">{branchStage.description}</p>
)}
<div className="flex items-center gap-2 mt-1">
<button
onClick={() => {
setSelectedStage(branchStage.name);
setShowDocumentsModal(true);
if (stageDocs.length === 0) setShowUploadForm(true);
}}
className="text-[10px] font-medium text-blue-600 hover:text-blue-800 flex items-center gap-1 transition-colors"
>
<FileText className="w-2.5 h-2.5" />
{stageDocs.length > 0 ? `${stageDocs.length} Docs` : 'Upload'}
</button>
</div>
<p className="text-slate-400 text-[10px] mt-1">
{isDone && branchStage.date ? `Done: ${formatDateTime(branchStage.date)}` : isDone && stageDocs.length > 0 ? `Uploaded: ${formatDateTime(stageDocs[0].updatedAt || stageDocs[0].createdAt)}` : branchStage.status === 'active' ? 'Evaluating' : 'Pending'}
</p>
</div>
</>
);
})()}
</div>
</div>
))}
</div>
)}
</div>
);
})}
<div className="h-8 w-0.5 bg-slate-300 ml-5 opacity-50"></div>
</div>
)}
</div>
))
})()}
</div>
</TabsContent>
{/* Documents Tab */}
<TabsContent value="documents" className="space-y-4">
<div className="flex items-center justify-between">
<h3 className="text-slate-900">Uploaded Documents</h3>
<Button size="sm" className="bg-amber-600 hover:bg-amber-700" onClick={() => {
setSelectedStage(null);
setShowDocumentsModal(true);
setShowUploadForm(true);
}}>
<Upload className="w-4 h-4 mr-2" />
Upload Document
</Button>
</div>
<div className="overflow-x-auto">
<Table>
<TableHeader>
<TableRow>
<TableHead className="min-w-[200px]">File Name</TableHead>
<TableHead className="min-w-[120px]">Type</TableHead>
<TableHead className="min-w-[120px]">Upload Date</TableHead>
<TableHead className="min-w-[150px]">Uploader</TableHead>
<TableHead className="text-right min-w-[100px]">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{documents.length === 0 ? (
<TableRow>
<TableCell colSpan={5} className="text-center py-8 text-slate-500">
No documents uploaded yet
</TableCell>
</TableRow>
) : (
documents.map((doc) => (
<TableRow key={doc.id}>
<TableCell className="flex items-center gap-2">
<FileText className="w-4 h-4 text-slate-400" />
<span className="truncate max-w-[150px] md:max-w-[300px]">{doc.fileName}</span>
</TableCell>
<TableCell>{doc.documentType}</TableCell>
<TableCell>{formatDateTime(doc.createdAt)}</TableCell>
<TableCell>
{doc.uploader?.fullName || (doc.uploadedBy ? 'Unknown User' : 'Applicant')}
</TableCell>
<TableCell>
<div className="flex justify-end gap-2">
<Button size="sm" variant="outline" onClick={() => window.open(`http://localhost:5000/${doc.filePath}`, '_blank')}>
<Download className="w-3 h-3" />
</Button>
</div>
</TableCell>
</TableRow>
)))}
</TableBody>
</Table>
</div>
</TabsContent>
{/* Interviews Tab */}
<TabsContent value="interviews" className="space-y-6">
<div>
<h3 className="text-slate-900 mb-4">Scheduled Interviews</h3>
<div className="overflow-x-auto">
<Table>
<TableHeader>
<TableRow>
<TableHead className="min-w-[100px]">Level</TableHead>
<TableHead className="min-w-[180px]">Date & Time</TableHead>
<TableHead className="min-w-[100px]">Type</TableHead>
<TableHead className="min-w-[200px]">Location/Link</TableHead>
<TableHead className="min-w-[120px]">Status</TableHead>
<TableHead className="min-w-[150px]">Scheduled By</TableHead>
<TableHead className="text-right">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{(!interviews || interviews.length === 0) ? (
<TableRow>
<TableCell colSpan={7} className="text-center py-8 text-slate-500">
No interviews scheduled yet
</TableCell>
</TableRow>
) : (
(Array.isArray(interviews) ? interviews : []).map((interview) => (
<TableRow key={interview.id}>
<TableCell className="font-medium">Level {interview.level}</TableCell>
<TableCell>{interview.scheduleDate ? new Date(interview.scheduleDate).toLocaleString() : 'N/A'}</TableCell>
<TableCell className="capitalize">{interview.interviewType}</TableCell>
<TableCell>
{interview.interviewType?.toLowerCase().includes('virtual') ? (
<a href={interview.linkOrLocation} target="_blank" rel="noopener noreferrer" className="text-blue-600 hover:underline">
Join Meeting
</a>
) : (
interview.linkOrLocation
)}
</TableCell>
<TableCell>
<Badge variant={interview.status === 'Completed' ? 'default' : 'secondary'}>
{interview.status}
</Badge>
</TableCell>
<TableCell>{interview.scheduler?.fullName || interview.scheduledBy || 'N/A'}</TableCell>
<TableCell className="text-right">
{(interview.status === 'Scheduled' || interview.status === 'scheduled') && (
<Button
variant="ghost"
size="sm"
className="text-red-500 hover:text-red-700 hover:bg-red-50 h-8 px-2"
onClick={() => handleCancelInterview(interview.id)}
>
Cancel
</Button>
)}
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</div>
</div>
<div>
<h3 className="text-slate-900 mb-4">Interview Feedback</h3>
{(!interviews || interviews.length === 0) ? (
<p className="text-slate-500 italic">No interviews scheduled.</p>
) : (
(Array.isArray(interviews) ? interviews : []).map((interview) => (
<div key={interview.id} className="mb-6 border p-4 rounded-lg bg-slate-50/50">
<h4 className="font-semibold text-slate-800 mb-2">
Level {interview.level} Interview
<span className="font-normal text-slate-500 text-sm ml-2">
({formatDateTime(interview.scheduleDate)} - {interview.interviewType})
</span>
</h4>
{interview.evaluations && interview.evaluations.length > 0 ? (
<Table>
<TableHeader>
<TableRow>
<TableHead>Interviewer</TableHead>
<TableHead>Role</TableHead>
<TableHead>
{interview.level === 1 ? 'Score (KT Matrix)' : 'Overall Score'}
</TableHead>
<TableHead>Remarks</TableHead>
<TableHead>Recommendation</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{interview.evaluations.map((evalItem: any) => (
<TableRow key={evalItem.id}>
<TableCell className="font-medium">{evalItem.evaluator?.fullName}</TableCell>
<TableCell>{evalItem.evaluator?.role?.roleName || 'N/A'}</TableCell>
<TableCell>
{evalItem.ktMatrixScore ? (
<Badge variant={
interview.level === 1
? (Number(evalItem.ktMatrixScore) >= 50 ? 'outline' : 'destructive')
: (Number(evalItem.ktMatrixScore) >= 5 ? 'outline' : 'destructive')
}>
{evalItem.ktMatrixScore}/{interview.level === 1 ? '100' : '10'}
</Badge>
) : 'N/A'}
</TableCell>
<TableCell className="max-w-xs truncate" title={evalItem.remarks || evalItem.qualitativeFeedback}>
{evalItem.remarks ? (
<div className="flex flex-col gap-1">
<span className="font-medium text-slate-800">{evalItem.remarks}</span>
{evalItem.feedbackDetails && evalItem.feedbackDetails.length > 0 && (
<Button
variant="link"
className="p-0 h-auto font-normal text-blue-600 text-xs w-fit"
onClick={() => {
setSelectedEvaluationForView({ ...evalItem, interview });
setShowFeedbackDetailsModal(true);
}}
>
View Detailed Feedback
</Button>
)}
</div>
) : evalItem.feedbackDetails && evalItem.feedbackDetails.length > 0 ? (
<Button
variant="link"
className="p-0 h-auto font-normal text-blue-600"
onClick={() => {
setSelectedEvaluationForView({ ...evalItem, interview });
setShowFeedbackDetailsModal(true);
}}
>
View Detailed Feedback
</Button>
) : (
evalItem.qualitativeFeedback || '-'
)}
</TableCell>
<TableCell>{evalItem.recommendation || '-'}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
) : (
<p className="text-sm text-slate-500 italic pl-2">No feedback recorded yet.</p>
)}
</div>
))
)}
</div>
{['Level 2 Approved', 'Level 3 Interview Pending', 'Approved', 'Onboarded'].includes(application.status) && (
<div>
<h3 className="text-slate-900 mb-4">Level 2 Interview Summary</h3>
<div className="p-4 bg-slate-50 rounded-lg">
<p className="text-slate-600">Decision: Approved by both ZBH and DD Lead</p>
<p className="text-slate-600 mt-2">Overall Assessment: Strong candidate with excellent business plan</p>
</div>
</div>
)}
</TabsContent>
<TabsContent value="fdd" className="space-y-6">
{renderFddAuditContent()}
</TabsContent>
{/* EOR Checklist Tab */}
<TabsContent value="eor" className="space-y-4">
<div className="flex items-center justify-between mb-4">
<h3 className="text-slate-900">Essential Operating Requirements</h3>
<Badge className="bg-amber-600">{Math.round(eorProgress)}% Complete</Badge>
</div>
<Progress value={eorProgress} className="h-3 mb-6" />
<div className="space-y-3">
{(eorData?.items || eorChecklist).map((item: any) => {
const docType = item.description || item.item;
const hasDocument = !!item.proofDocument;
return (
<div
key={item.id}
className="flex items-center gap-3 p-3 bg-slate-50 rounded-xl transition-all border border-transparent hover:border-slate-200 group"
>
<Checkbox
checked={item.isCompliant || item.completed}
className="pointer-events-none shrink-0"
/>
{/* Clickable Info Area */}
<div
className="flex flex-col flex-1 min-w-0 cursor-pointer"
onClick={() => {
setSelectedStage(`EOR: ${docType}`);
setUploadDocType(docType);
setShowDocumentsModal(true);
if (!hasDocument) setShowUploadForm(true);
else setShowUploadForm(false);
}}
>
<div className="flex items-center gap-2">
<span className={(item.isCompliant || item.completed) ? 'text-slate-900 font-bold' : 'text-slate-600 font-medium'}>
{docType}
</span>
{hasDocument && !item.isCompliant && (
<Badge variant="outline" className="text-[10px] h-4 px-1.5 bg-amber-50 text-amber-600 border-amber-200 uppercase tracking-wider font-bold">
Needs Verification
</Badge>
)}
</div>
{hasDocument && (
<div className="flex items-center gap-1.5 text-xs text-blue-600 font-semibold mt-1">
<FileText className="w-3.5 h-3.5" />
<span className="truncate">{item.proofDocument.fileName}</span>
</div>
)}
{!hasDocument && (
<span className="text-[10px] text-slate-400 mt-1 uppercase tracking-tighter">Click to upload proof</span>
)}
</div>
{/* Separate Action Area (No modal trigger) */}
<div className="flex items-center gap-2 shrink-0">
{hasDocument && !item.isCompliant && isAdmin && (
<div className="flex gap-2">
<Button
size="sm"
className="h-8 px-3 bg-green-600 hover:bg-green-700 text-white font-bold rounded-lg shadow-sm"
onClick={async () => {
await eorService.updateItem(eorData.id, {
...item,
isCompliant: true
});
fetchEorData();
toast.success(`${docType} verified!`);
}}
>
Verify
</Button>
<Button
size="sm"
variant="outline"
className="h-8 px-3 border-red-200 text-red-600 hover:bg-red-50 font-bold rounded-lg"
onClick={async () => {
await eorService.updateItem(eorData.id, {
...item,
isCompliant: false,
proofDocumentId: null
});
fetchEorData();
toast.success(`${docType} rejected.`);
}}
>
Reject
</Button>
</div>
)}
{(item.isCompliant || item.completed) && (
<div className="bg-green-100 p-1.5 rounded-full">
<CheckCircle className="w-4 h-4 text-green-600" />
</div>
)}
{!hasDocument && (
<div className="p-2 text-slate-300 group-hover:text-amber-500 transition-colors">
<Upload className="w-4 h-4" />
</div>
)}
</div>
</div>
);
})}
</div>
{eorProgress === 100 && isAdmin && (application.status === 'EOR In Progress' || application.status === 'LOA Pending') && (
<div className="mt-8 p-6 bg-green-50 rounded-xl border-2 border-green-200 animate-in fade-in slide-in-from-bottom-4 duration-500">
<div className="flex flex-col sm:flex-row items-center gap-4">
<div className="w-12 h-12 rounded-xl bg-green-100 flex items-center justify-center shrink-0">
<ShieldCheck className="w-7 h-7 text-green-600" />
</div>
<div className="flex-1 text-center sm:text-left">
<h4 className="text-green-900 font-bold text-lg">EOR Checklist Complete</h4>
<p className="text-green-700 text-sm">All 12 mandatory requirements have been verified. You can now complete the audit and move to final inauguration.</p>
</div>
<Button
className="w-full sm:w-auto bg-green-600 hover:bg-green-700 text-white font-bold h-12 px-8 rounded-xl shadow-lg shadow-green-600/20 transition-all hover:scale-[1.02] active:scale-[0.98]"
onClick={async () => {
try {
await onboardingService.updateApplicationStatus(application.id, {
status: 'EOR Complete',
remarks: 'EOR Checklist verified and audit completed.'
});
toast.success('EOR Audit completed successfully!');
fetchApplication();
} catch (error) {
toast.error('Failed to complete EOR audit');
}
}}
>
Complete Audit & Proceed
</Button>
</div>
</div>
)}
</TabsContent>
{/* Payments Tab */}
<TabsContent value="payments" className="space-y-6">
<div className="flex items-center justify-between mb-2">
<h3 className="text-lg font-semibold text-slate-900">Security Deposits</h3>
<Badge variant="outline" className="bg-slate-50 text-slate-500 border-slate-200">
{deposits.length} Payment Record(s)
</Badge>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{/* Initial Security Deposit */}
{(() => {
const deposit = getDeposit('SECURITY_DEPOSIT');
const config = paymentConfigs.SECURITY_DEPOSIT;
const expectedAmount = config?.amount || 500000;
return (
<Card className={cn(
"border-l-4",
deposit?.status === 'Verified' ? "border-l-green-500" :
deposit?.status === 'Rejected' ? "border-l-red-500" : "border-l-amber-500"
)}>
<CardContent className="pt-6">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-2">
<div className="w-8 h-8 rounded bg-amber-50 flex items-center justify-center text-amber-600">
<ClipboardList className="w-4 h-4" />
</div>
<span className="font-semibold text-slate-700">Security Deposit</span>
</div>
<Badge className={cn(
deposit?.status === 'Verified' ? "bg-green-100 text-green-700 hover:bg-green-100" :
deposit?.status === 'Rejected' ? "bg-red-100 text-red-700 hover:bg-red-100" :
"bg-amber-100 text-amber-700 hover:bg-amber-100"
)}>
{deposit?.status || 'Awaiting'}
</Badge>
</div>
<div className="space-y-3">
<div className="flex justify-between items-baseline">
<span className="text-xs text-slate-500 uppercase font-bold tracking-wider">Amount Received</span>
<span className="text-lg font-bold text-slate-900">{Number(deposit?.amount || 0).toLocaleString()}</span>
</div>
<div className="flex justify-between items-baseline border-t border-slate-100 pt-2">
<span className="text-xs text-slate-500">Expected Total</span>
<span className="text-sm font-medium text-slate-600">{expectedAmount.toLocaleString()}</span>
</div>
{deposit?.paymentReference && (
<div className="bg-slate-50 p-2 rounded text-xs font-mono text-slate-600 flex justify-between items-center">
<span>Ref: {deposit.paymentReference}</span>
{deposit.verifiedAt && <span>{formatDateTime(deposit.verifiedAt)}</span>}
</div>
)}
{deposit?.remarks && (
<div className="text-[11px] text-slate-500 bg-red-50/50 p-2 rounded border border-red-100 italic">
"{deposit.remarks}"
</div>
)}
{/* Respective Documents */}
<div className="pt-4 mt-2 border-t border-slate-100">
<p className="text-[10px] font-bold text-slate-400 uppercase tracking-wider mb-2">Verification Documents</p>
<div className="space-y-2">
{documents.filter((d: any) => d.documentType?.toLowerCase().includes('security') && d.documentType?.toLowerCase().includes('deposit')).map((doc: any, idx: number) => (
<div key={idx} className="flex items-center justify-between p-2 rounded bg-slate-50/50 border border-slate-100">
<div className="flex items-center gap-2 overflow-hidden">
<FileText className="w-3 h-3 text-slate-400" />
<span className="text-[10px] font-medium text-slate-700 truncate">{doc.fileName || doc.name}</span>
</div>
<Button
variant="ghost"
size="sm"
className="h-6 px-2 text-[10px] text-amber-600 hover:text-amber-700 hover:bg-amber-50"
onClick={() => { setPreviewDoc(doc); setShowPreviewModal(true); }}
>
View
</Button>
</div>
))}
{documents.filter((d: any) => d.documentType?.toLowerCase().includes('security') && d.documentType?.toLowerCase().includes('deposit')).length === 0 && (
<p className="text-[10px] text-slate-400 italic">No proof uploaded</p>
)}
</div>
</div>
</div>
</CardContent>
</Card>
);
})()}
{/* Final Security Deposit */}
{(() => {
const deposit = getDeposit('FIRST_FILL');
const config = paymentConfigs.FIRST_FILL;
const expectedAmount = config?.amount || 1500000;
return (
<Card className={cn(
"border-l-4",
deposit?.status === 'Verified' ? "border-l-green-500" :
deposit?.status === 'Rejected' ? "border-l-red-500" : "border-l-amber-500"
)}>
<CardContent className="pt-6">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-2">
<div className="w-8 h-8 rounded bg-blue-50 flex items-center justify-center text-blue-600">
<ShieldCheck className="w-4 h-4" />
</div>
<span className="font-semibold text-slate-700">First Fill</span>
</div>
<Badge className={cn(
deposit?.status === 'Verified' ? "bg-green-100 text-green-700 hover:bg-green-100" :
deposit?.status === 'Rejected' ? "bg-red-100 text-red-700 hover:bg-red-100" :
"bg-amber-100 text-amber-700 hover:bg-amber-100"
)}>
{deposit?.status || 'Awaiting'}
</Badge>
</div>
<div className="space-y-3">
<div className="flex justify-between items-baseline">
<span className="text-xs text-slate-500 uppercase font-bold tracking-wider">Amount Received</span>
<span className="text-lg font-bold text-slate-900">{Number(deposit?.amount || 0).toLocaleString()}</span>
</div>
<div className="flex justify-between items-baseline border-t border-slate-100 pt-2">
<span className="text-xs text-slate-500">Expected Total</span>
<span className="text-sm font-medium text-slate-600">{expectedAmount.toLocaleString()}</span>
</div>
{deposit?.paymentReference && (
<div className="bg-slate-50 p-2 rounded text-xs font-mono text-slate-600 flex justify-between items-center">
<span>Ref: {deposit.paymentReference}</span>
{deposit.verifiedAt && <span>{formatDateTime(deposit.verifiedAt)}</span>}
</div>
)}
{deposit?.remarks && (
<div className="text-[11px] text-slate-500 bg-red-50/50 p-2 rounded border border-red-100 italic">
"{deposit.remarks}"
</div>
)}
{/* Respective Documents */}
<div className="pt-4 mt-2 border-t border-slate-100">
<p className="text-[10px] font-bold text-slate-400 uppercase tracking-wider mb-2">Verification Documents</p>
<div className="space-y-2">
{documents.filter((d: any) => d.documentType?.toLowerCase().includes('first') && d.documentType?.toLowerCase().includes('fill')).map((doc: any, idx: number) => (
<div key={idx} className="flex items-center justify-between p-2 rounded bg-slate-50/50 border border-slate-100">
<div className="flex items-center gap-2 overflow-hidden">
<FileText className="w-3 h-3 text-slate-400" />
<span className="text-[10px] font-medium text-slate-700 truncate">{doc.fileName || doc.name}</span>
</div>
<Button
variant="ghost"
size="sm"
className="h-6 px-2 text-[10px] text-blue-600 hover:text-blue-700 hover:bg-blue-50"
onClick={() => { setPreviewDoc(doc); setShowPreviewModal(true); }}
>
View
</Button>
</div>
))}
{documents.filter((d: any) => d.documentType?.toLowerCase().includes('first') && d.documentType?.toLowerCase().includes('fill')).length === 0 && (
<p className="text-[10px] text-slate-400 italic">No proof uploaded</p>
)}
</div>
</div>
</div>
</CardContent>
</Card>
);
})()}
</div>
</TabsContent>
{/* Audit Trail Tab */}
<TabsContent value="audit">
<ScrollArea className="h-96">
<div className="space-y-4">
{auditLoading ? (
<div className="flex items-center justify-center py-8">
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-amber-600"></div>
<span className="ml-2 text-slate-500">Loading audit trail...</span>
</div>
) : auditLogs.length === 0 ? (
<div className="text-center py-8 text-slate-500">
No audit logs recorded yet for this application.
</div>
) : (
auditLogs.map((log: any) => (
<div key={log.id} className="flex gap-4 p-3 hover:bg-slate-50 rounded-lg">
<div className="w-2 h-2 bg-amber-600 rounded-full mt-2 flex-shrink-0"></div>
<div className="flex-1">
<div className="flex items-start justify-between">
<p className="text-slate-900 font-medium">{log.description || log.action}</p>
<span className="text-slate-500 text-sm whitespace-nowrap ml-4">
{formatDateTime(log.timestamp)}
</span>
</div>
<p className="text-slate-600 mt-1">by {log.userName || 'System'}</p>
{log.changes && log.changes.length > 0 && (
<div className="mt-1 space-y-0.5">
{log.changes.map((change: string, idx: number) => (
<p key={idx} className="text-slate-500 text-sm">{change}</p>
))}
</div>
)}
</div>
</div>
))
)}
</div>
</ScrollArea>
</TabsContent>
</CardContent>
</Tabs>
</Card>
)}
</div>
{/* Right Sidebar - Summary and Actions */}
<div className="space-y-6">
{/* Summary Card */}
<Card>
<CardHeader>
<CardTitle>Summary</CardTitle>
</CardHeader>
<CardContent className="space-y-4">
<div>
<p className="text-slate-600">Registration ID</p>
<p className="text-slate-900">{application.registrationNumber}</p>
</div>
<div>
<p className="text-slate-600">Current Status</p>
<Badge className={cn(
"mt-1",
application.status === 'Onboarded' ? "bg-green-600 hover:bg-green-700 text-white" :
application.status === 'Rejected' ? "bg-red-600" :
"bg-amber-600"
)}>
{application.status}
</Badge>
</div>
{application.rank && (
<div>
<p className="text-slate-600">Rank</p>
<p className="text-slate-900">
{application.rank} of {application.totalApplicantsAtLocation}
<span className="text-slate-500"> in {application.preferredLocation}</span>
</p>
</div>
)}
<div>
<p className="text-slate-600">Progress</p>
<div className="flex items-center gap-2 mt-2">
<Progress value={application.progress} className="flex-1" />
<span className="text-slate-900">{application.progress}%</span>
</div>
</div>
{application.deadline && (
<div>
<p className="text-slate-600">Questionnaire Deadline</p>
<p className="text-slate-900">{formatDateTime(application.deadline)}</p>
</div>
)}
</CardContent>
</Card>
{/* Actions Card */}
{/* Only show Actions card for shortlisted applications (opportunity requests and regular dealership requests) */}
{/* Hide Actions for non-opportunity requests (lead generation) - these are read-only records */}
{application.isShortlisted !== false && (
<Card>
<CardHeader>
<CardTitle>Actions</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
{/* Show Approve/Reject block */}
{permissions.isLoaLocked && (
<Alert variant="destructive" className="mb-4 bg-amber-50 border-amber-200 text-amber-800">
<Lock className="w-4 h-4 text-amber-600" />
<AlertTitle className="text-amber-900 font-semibold">Stage Locked</AlertTitle>
<AlertDescription className="text-amber-800">
First Fill (15L) must be verified by Finance before LOA Approval can proceed.
</AlertDescription>
</Alert>
)}
{permissions.canApprove && (
<>
<Button
className="w-full bg-green-600 hover:bg-green-700 font-bold"
onClick={() => setShowApproveModal(true)}
>
<CheckCircle className="w-4 h-4 mr-2" />
Approve
</Button>
<Button
variant="destructive"
className="w-full font-bold"
onClick={() => setShowRejectModal(true)}
>
<XCircle className="w-4 h-4 mr-2" />
Reject
</Button>
</>
)}
{permissions.showDecisionMessage && (
<div className={`w-full p-2 text-center rounded border ${(currentUserStageAction?.decision === 'Approved' || currentUserEvaluation?.decision === 'Approved' || currentUserEvaluation?.recommendation === 'Approved' || currentUserEvaluation?.decision === 'Selected') ? 'bg-green-50 border-green-200 text-green-700' : 'bg-red-50 border-red-200 text-red-700'}`}>
You have {(currentUserStageAction?.decision === 'Approved' || currentUserEvaluation?.decision === 'Approved' || currentUserEvaluation?.recommendation === 'Approved' || currentUserEvaluation?.decision === 'Selected') ? 'Approved' : 'Rejected'}
</div>
)}
<Separator />
<Button
variant="outline"
className="w-full"
onClick={() => navigate(`/worknotes/application/${application.id}`, {
state: {
applicationName: application.name,
registrationNumber: application.registrationNumber,
participants: application.participants
}
})}
>
<MessageSquare className="w-4 h-4 mr-2" />
Work Note
</Button>
{permissions.canSchedule && (
<Button
variant="outline"
className="w-full"
onClick={() => setShowScheduleModal(true)}
>
<Calendar className="w-4 h-4 mr-2" />
Schedule Interview
</Button>
)}
{currentUser && ['DD Admin', 'Super Admin'].includes(currentUser.role) && application.status === 'Dealer Code Generation' && (
<>
{!application.dealerCode && (
<Button
className="w-full bg-blue-600 hover:bg-blue-700"
onClick={handleGenerateDealerCodes}
>
<Zap className="w-4 h-4 mr-2" />
Generate Dealer Codes
</Button>
)}
{application.dealerCode && (
<Button
variant="outline"
className="w-full border-blue-200 hover:bg-blue-50 text-blue-700"
onClick={() => setShowAssignArchitectureModal(true)}
>
<GitBranch className="w-4 h-4 mr-2" />
Assign Architecture Team
</Button>
)}
</>
)}
{((currentUser && currentUser.id === application.architectureAssignedTo) || (currentUser && ['DD Admin', 'Super Admin'].includes(currentUser.role))) &&
application.architectureStatus === 'IN_PROGRESS' && (
<Button
className="w-full bg-blue-600 hover:bg-blue-700"
onClick={() => setShowArchitectureStatusModal(true)}
>
<CheckCircle className="w-4 h-4 mr-2" />
Complete Architecture Work
</Button>
)}
{/* Show Interview Feedback only if active interview exists AND feedback NOT submitted */}
{activeInterviewForUser && !hasSubmittedFeedback && (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" className="w-full">
<Star className="w-4 h-4 mr-2" />
Interview Feedback
<ChevronDown className="w-4 h-4 ml-auto" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent className="w-56">
<DropdownMenuItem
key={activeInterviewForUser.id}
onClick={() => {
setSelectedInterviewForFeedback(activeInterviewForUser);
if (activeInterviewForUser.level === 1) setShowKTMatrixModal(true);
else if (activeInterviewForUser.level === 2) setShowLevel2FeedbackModal(true);
else setShowLevel3FeedbackModal(true);
}}
>
Level {activeInterviewForUser.level} - {activeInterviewForUser.interviewType}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)}
{application.status === 'Questionnaire Pending' && (
<>
<Button variant="outline" className="w-full">
<Mail className="w-4 h-4 mr-2" />
Send Reminder
</Button>
<Button variant="outline" className="w-full">
<Clock className="w-4 h-4 mr-2" />
Extend Deadline
</Button>
</>
)}
{/* Dedicated Onboarding Button - Appears ONLY when everything is ready (last step) */}
{isAdmin && application.status === 'Inauguration' && !application.dealer && (
<div className="space-y-2">
{eorProgress < 100 && (
<Alert variant="destructive" className="bg-amber-50 border-amber-200 text-amber-800 py-2">
<AlertCircle className="h-4 w-4 text-amber-600" />
<AlertDescription className="text-xs">
EOR Checklist must be 100% complete before onboarding. (Current: {eorProgress.toFixed(0)}%)
</AlertDescription>
</Alert>
)}
<Button
className="w-full bg-green-600 hover:bg-green-700 font-bold shadow-lg shadow-green-100 disabled:bg-slate-300 disabled:text-slate-500"
onClick={() => setShowOnboardModal(true)}
disabled={eorProgress < 100}
>
<CheckCircle className="w-4 h-4 mr-2" />
Onboard as Dealer (Final Step)
</Button>
</div>
)}
{/* Dealer Onboarded Status & Link */}
{application.dealer && (
<div className="p-4 bg-green-50 border border-green-200 rounded-lg space-y-3">
<div className="flex items-center gap-2 text-green-800 font-semibold">
<CheckCircle className="w-5 h-5 text-green-600" />
Dealer Profile Active
</div>
<div className="text-sm text-green-700">
This application has been successfully onboarded as a dealer. A user account has been created for the dealer.
</div>
{application.dealerCode && (
<div className="flex items-center justify-between text-xs font-mono bg-white p-2 rounded border border-green-100">
<span className="text-slate-500">Dealer Code:</span>
<span className="font-bold text-slate-900">{application.dealerCode.code}</span>
</div>
)}
<Button
className="w-full bg-green-600 hover:bg-green-700 text-white"
onClick={() => navigate('/dashboard')}
>
<Zap className="w-4 h-4 mr-2" />
Go to Dealer Dashboard
</Button>
</div>
)}
{currentUser && ['DD Admin', 'Super Admin'].includes(currentUser.role) && (
<Dialog open={showAssignModal} onOpenChange={setShowAssignModal}>
<DialogTrigger asChild>
<Button variant="outline" className="w-full">
<User className="w-4 h-4 mr-2" />
Assign User
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Assign User to Application</DialogTitle>
<DialogDescription>
Select a user and their role for this application.
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div>
<Label>Select User</Label>
<Select value={selectedUser} onValueChange={setSelectedUser}>
<SelectTrigger className="mt-2">
<SelectValue placeholder="Search users..." />
</SelectTrigger>
<SelectContent>
{users.map((u) => (
<SelectItem key={u.id} value={u.id}>
{u.fullName} ({u.email})
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div>
<Label>Assignment Role</Label>
<Select value={participantType} onValueChange={setParticipantType}>
<SelectTrigger className="mt-2">
<SelectValue placeholder="Select role" />
</SelectTrigger>
<SelectContent>
<SelectItem value="owner">Owner</SelectItem>
<SelectItem value="contributor">Contributor</SelectItem>
<SelectItem value="reviewer">Reviewer</SelectItem>
</SelectContent>
</Select>
</div>
<Button
className="w-full bg-amber-600 hover:bg-amber-700 font-bold h-11"
onClick={handleAddParticipant}
disabled={isAssigningParticipant}
>
{isAssigningParticipant ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
Assigning...
</>
) : (
'Assign User'
)}
</Button>
</div>
</DialogContent>
</Dialog>
)}
</CardContent>
</Card>
)}
{/* Approve Modal */}
<Dialog open={showApproveModal} onOpenChange={setShowApproveModal}>
<DialogContent>
<DialogHeader>
<DialogTitle>Approve Application</DialogTitle>
<DialogDescription>
Provide approval remarks and optionally attach supporting documents.
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div>
<Label>Remark (Required)</Label>
<Textarea
placeholder="Enter approval remarks..."
value={approvalRemark}
onChange={(e) => setApprovalRemark(e.target.value)}
className="mt-2"
rows={4}
/>
</div>
<div>
<Label>Attach File (Optional)</Label>
<Input
type="file"
className="mt-2"
onChange={(e) => setApprovalFile(e.target.files ? e.target.files[0] : null)}
/>
</div>
<div className="flex gap-3">
<Button
variant="outline"
className="flex-1"
onClick={() => setShowApproveModal(false)}
disabled={isApproving}
>
Cancel
</Button>
<Button
className="flex-1 bg-green-600 hover:bg-green-700"
onClick={handleApprove}
disabled={isApproving}
>
{isApproving ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
Approving...
</>
) : (
'Submit Approval'
)}
</Button>
</div>
</div>
</DialogContent>
</Dialog>
{/* Onboard Confirmation Modal */}
<Dialog open={showOnboardModal} onOpenChange={setShowOnboardModal}>
<DialogContent className="max-w-md">
<DialogHeader>
<div className="mx-auto w-12 h-12 bg-green-100 rounded-full flex items-center justify-center mb-4">
<CheckCircle className="w-8 h-8 text-green-600" />
</div>
<DialogTitle className="text-center text-xl font-bold">Finalize Onboarding</DialogTitle>
<DialogDescription className="text-center pt-2">
You are about to officially onboard <span className="font-semibold text-slate-900">{application.name}</span> as a Royal Enfield dealer.
</DialogDescription>
</DialogHeader>
<div className="bg-slate-50 p-4 rounded-lg border border-slate-200 mt-4 space-y-3">
<div className="flex items-start gap-3">
<div className="mt-1 bg-green-500 rounded-full p-0.5"><Check className="w-3 h-3 text-white" /></div>
<p className="text-sm text-slate-600">Official dealer profile will be created.</p>
</div>
<div className="flex items-start gap-3">
<div className="mt-1 bg-green-500 rounded-full p-0.5"><Check className="w-3 h-3 text-white" /></div>
<p className="text-sm text-slate-600">User account will be activated with role <span className="font-medium text-slate-900">Dealer</span>.</p>
</div>
<div className="flex items-start gap-3">
<div className="mt-1 bg-green-500 rounded-full p-0.5"><Check className="w-3 h-3 text-white" /></div>
<p className="text-sm text-slate-600">Primary outlet will be registered in the system.</p>
</div>
</div>
<div className="mt-6 flex flex-col gap-3">
<Button
className="w-full bg-green-600 hover:bg-green-700 h-11 text-lg font-semibold shadow-lg shadow-green-100"
onClick={async () => {
setIsOnboarding(true);
try {
await onboardingService.createDealer({ applicationId: application.id });
toast.success('Dealer profile and login account created successfully!');
setShowOnboardModal(false);
fetchApplication();
} catch (error) {
toast.error('Failed to create dealer profile');
} finally {
setIsOnboarding(false);
}
}}
disabled={isOnboarding}
>
{isOnboarding ? (
<>
<Loader2 className="w-5 h-5 mr-2 animate-spin" />
Processing Onboarding...
</>
) : (
'Confirm & Onboard Dealer'
)}
</Button>
<Button
variant="ghost"
className="w-full text-slate-500 hover:text-slate-700"
onClick={() => setShowOnboardModal(false)}
disabled={isOnboarding}
>
Cancel
</Button>
</div>
</DialogContent>
</Dialog>
{/* Reject Modal */}
<Dialog open={showRejectModal} onOpenChange={setShowRejectModal}>
<DialogContent>
<DialogHeader>
<DialogTitle>Reject Application</DialogTitle>
<DialogDescription>
Please provide a clear reason for rejecting this application.
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div>
<Label>Reason for Rejection (Required)</Label>
<Textarea
placeholder="Enter rejection reason..."
value={rejectionReason}
onChange={(e) => setRejectionReason(e.target.value)}
className="mt-2"
rows={4}
/>
</div>
<div className="flex gap-3">
<Button
variant="outline"
className="flex-1"
onClick={() => setShowRejectModal(false)}
disabled={isRejecting}
>
Cancel
</Button>
<Button
variant="destructive"
className="flex-1"
onClick={handleReject}
disabled={isRejecting}
>
{isRejecting ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
Rejecting...
</>
) : (
'Confirm Rejection'
)}
</Button>
</div>
</div>
</DialogContent>
</Dialog>
{/* Schedule Interview Modal */}
<Dialog open={showScheduleModal} onOpenChange={setShowScheduleModal}>
<DialogContent>
<DialogHeader>
<DialogTitle>Schedule Interview</DialogTitle>
<DialogDescription>
Set up an interview session with the applicant and relevant team members.
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div>
<Label>Interview Type</Label>
<Select value={interviewType} onValueChange={setInterviewType}>
<SelectTrigger className="mt-2">
<SelectValue placeholder="Select interview type" />
</SelectTrigger>
<SelectContent>
<SelectItem value="level1" disabled={isInterviewCompleted(1) || isInterviewActive(1)}>
<div className="flex items-center justify-between w-full">
<span>Level 1</span>
{isInterviewCompleted(1) && <CheckCircle className="w-4 h-4 text-green-500 ml-2 inline" />}
{isInterviewActive(1) && <Clock className="w-4 h-4 text-amber-500 ml-2 inline" />}
</div>
</SelectItem>
<SelectItem value="level2" disabled={!isInterviewCompleted(1) || isInterviewCompleted(2) || isInterviewActive(2)}>
<div className="flex items-center justify-between w-full">
<span>Level 2</span>
{!isInterviewCompleted(1) && <span className="text-[10px] text-slate-400 ml-2">(Prerequisite: L1)</span>}
{isInterviewCompleted(2) && <CheckCircle className="w-4 h-4 text-green-500 ml-2 inline" />}
{isInterviewActive(2) && <Clock className="w-4 h-4 text-amber-500 ml-2 inline" />}
</div>
</SelectItem>
<SelectItem value="level3" disabled={!isInterviewCompleted(2) || isInterviewCompleted(3) || isInterviewActive(3)}>
<div className="flex items-center justify-between w-full">
<span>Level 3</span>
{!isInterviewCompleted(2) && <span className="text-[10px] text-slate-400 ml-2">(Prerequisite: L2)</span>}
{isInterviewCompleted(3) && <CheckCircle className="w-4 h-4 text-green-500 ml-2 inline" />}
{isInterviewActive(3) && <Clock className="w-4 h-4 text-amber-500 ml-2 inline" />}
</div>
</SelectItem>
</SelectContent>
</Select>
</div>
<div>
<Label>Interview Mode</Label>
<Select value={interviewMode} onValueChange={setInterviewMode}>
<SelectTrigger className="mt-2">
<SelectValue placeholder="Select interview mode" />
</SelectTrigger>
<SelectContent>
<SelectItem value="virtual">Virtual</SelectItem>
<SelectItem value="physical">Physical</SelectItem>
</SelectContent>
</Select>
</div>
<div>
<Label>Date & Time</Label>
<Input
type="datetime-local"
className="mt-2"
value={interviewDate}
onChange={(e) => setInterviewDate(e.target.value)}
/>
</div>
{interviewMode === 'virtual' && (
<div>
<Label>Meeting Link</Label>
<Input
placeholder="https://meet.google.com/..."
className="mt-2"
value={meetingLink}
onChange={(e) => setMeetingLink(e.target.value)}
/>
</div>
)}
{interviewMode === 'physical' && (
<div>
<Label>Location</Label>
<Input
placeholder="Enter interview location address"
className="mt-2"
value={location}
onChange={(e) => setLocation(e.target.value)}
/>
</div>
)}
{/* Interviewer Selection */}
<div>
<Label>Interviewers</Label>
<div className="flex gap-2 mt-2">
<Select value={selectedInterviewerId} onValueChange={setSelectedInterviewerId}>
<SelectTrigger className="flex-1">
<SelectValue placeholder="Select interviewer" />
</SelectTrigger>
<SelectContent>
{users.map((user) => (
<SelectItem key={user.id} value={user.id}>
{user.fullName || user.name} ({user.role?.roleName || user.roleCode})
</SelectItem>
))}
</SelectContent>
</Select>
<Button onClick={handleAddInterviewer} type="button" variant="secondary">
Add
</Button>
</div>
{/* Selected Interviewers List */}
{scheduledInterviewParticipants.length > 0 && (
<div className="mt-3 space-y-2">
<Label className="text-xs text-muted-foreground">Selected Interviewers:</Label>
<div className="flex flex-wrap gap-2">
{scheduledInterviewParticipants.map((p) => (
<div key={p.id} className="flex items-center gap-1 bg-secondary px-2 py-1 rounded text-sm">
<span>{p.fullName || p.name || 'Unknown'}</span>
<button
onClick={() => handleRemoveInterviewer(p.id)}
className="text-muted-foreground hover:text-destructive"
>
×
</button>
</div>
))}
</div>
</div>
)}
</div>
<div className="flex gap-3">
<Button
variant="outline"
className="flex-1"
onClick={() => setShowScheduleModal(false)}
disabled={isScheduling}
>
Cancel
</Button>
<Button
className="flex-1 bg-amber-600 hover:bg-amber-700"
onClick={() => {
// Ensure we pass participants to the schedule handler
handleScheduleInterview();
}}
disabled={isScheduling}
>
{isScheduling ? 'Scheduling...' : 'Schedule'}
</Button>
</div>
</div>
</DialogContent>
</Dialog>
{/* Assign Architecture Team Modal */}
<Dialog open={showAssignArchitectureModal} onOpenChange={setShowAssignArchitectureModal}>
<DialogContent>
<DialogHeader>
<DialogTitle>Assign Architecture Team</DialogTitle>
<DialogDescription>
Select an architecture team lead for site planning and blueprints.
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div>
<Label>Select Architecture Lead</Label>
<Select value={architectureLeadId} onValueChange={setArchitectureLeadId}>
<SelectTrigger className="mt-2">
<SelectValue placeholder="Search users..." />
</SelectTrigger>
<SelectContent>
{users.filter(u => u.roleCode === 'ARCHITECTURE' || u.role?.roleCode === 'ARCHITECTURE' || u.role === 'Architecture' || u.role === 'Architecture Team').map((u) => (
<SelectItem key={u.id} value={u.id}>
{u.fullName} ({u.email})
</SelectItem>
))}
{/* Fallback if no specific architecture users found */}
{users.filter(u => u.roleCode === 'ARCHITECTURE' || u.role?.roleCode === 'ARCHITECTURE' || u.role === 'Architecture' || u.role === 'Architecture Team').length === 0 && users.map((u) => (
<SelectItem key={u.id} value={u.id}>
{u.fullName} ({u.roleCode || u.role})
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="flex gap-3">
<Button
variant="outline"
className="flex-1"
onClick={() => setShowAssignArchitectureModal(false)}
disabled={isAssigningArchitecture}
>
Cancel
</Button>
<Button
className="flex-1 bg-blue-600 hover:bg-blue-700"
onClick={handleAssignArchitecture}
disabled={isAssigningArchitecture}
>
{isAssigningArchitecture ? 'Assigning...' : 'Assign Team'}
</Button>
</div>
</div>
</DialogContent>
</Dialog>
{/* Architecture Status Modal */}
<Dialog open={showArchitectureStatusModal} onOpenChange={setShowArchitectureStatusModal}>
<DialogContent>
<DialogHeader>
<DialogTitle>Update Architecture Status</DialogTitle>
<DialogDescription>
Mark the architectural work as completed and optionally add remarks.
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div>
<Label>Status</Label>
<Select value={architectureStatus} onValueChange={setArchitectureStatus}>
<SelectTrigger className="mt-2">
<SelectValue placeholder="Select status" />
</SelectTrigger>
<SelectContent>
<SelectItem value="COMPLETED">Completed</SelectItem>
<SelectItem value="REJECTED">Rejected / Needs Revision</SelectItem>
</SelectContent>
</Select>
</div>
<div>
<Label>Remarks (Optional)</Label>
<Textarea
placeholder="Enter any planning or site-visit remarks..."
value={architectureRemarks}
onChange={(e) => setArchitectureRemarks(e.target.value)}
className="mt-2"
rows={4}
/>
</div>
<div className="flex gap-3">
<Button
variant="outline"
className="flex-1"
onClick={() => setShowArchitectureStatusModal(false)}
disabled={isUpdatingArchitecture}
>
Cancel
</Button>
<Button
className="flex-1 bg-blue-600 hover:bg-blue-700"
onClick={handleUpdateArchitectureStatus}
disabled={isUpdatingArchitecture}
>
{isUpdatingArchitecture ? 'Updating...' : 'Update Status'}
</Button>
</div>
</div>
</DialogContent>
</Dialog>
{/* KT Matrix Modal */}
<Dialog open={showKTMatrixModal} onOpenChange={setShowKTMatrixModal}>
<DialogContent className="max-w-4xl h-[85vh] p-0 overflow-hidden flex flex-col gap-0 border-none shadow-2xl">
{/* Ultra-Simple Header */}
<div className="px-8 py-4 border-b border-slate-100 flex items-center justify-between bg-slate-50 shrink-0">
<div>
<DialogTitle className="text-base font-bold text-slate-900 leading-tight">KT Matrix Assessment</DialogTitle>
<p className="text-slate-400 text-[11px] font-medium tracking-tight">Evaluate technical capability for {application.name}</p>
</div>
<div className="text-right">
<div className="text-xs font-bold text-slate-600 mb-1">{Object.keys(ktMatrixSelectedValues).length} of {KT_MATRIX_CRITERIA.length} Completed</div>
<Progress value={(Object.keys(ktMatrixSelectedValues).length / KT_MATRIX_CRITERIA.length) * 100} className="w-28 h-1.5 bg-slate-100" />
</div>
</div>
<div className="flex-1 overflow-y-auto bg-white">
<div className="p-8 max-w-3xl mx-auto">
{/* Question List - Minimalist Style */}
<div className="divide-y divide-slate-100 border-x border-t border-slate-100 rounded-t-xl overflow-hidden shadow-sm">
{KT_MATRIX_CRITERIA.map((criterion, idx) => (
<div key={criterion.name} className="p-5 hover:bg-slate-50/50 transition-colors">
<div className="flex justify-between items-start gap-4 mb-4">
<h4 className="text-sm font-semibold text-slate-800 leading-snug">
<span className="text-slate-400 mr-2 tabular-nums">{idx + 1}.</span>
{criterion.name}
</h4>
<span className="text-[10px] font-bold text-slate-400 uppercase tracking-widest bg-slate-50 px-2 py-0.5 rounded border border-slate-100">
WT: {criterion.weight}%
</span>
</div>
<div className="flex flex-wrap gap-2">
{criterion.options.map((option) => {
const isSelected = ktMatrixSelectedValues[criterion.name] === option.value;
return (
<div
key={option.value}
onClick={() => handleKTMatrixChange(criterion.name, option.value, option.score)}
className={cn(
"px-3 py-1.5 rounded-lg border text-[11px] font-bold cursor-pointer transition-all flex items-center gap-2 select-none",
isSelected
? "bg-slate-900 border-slate-900 text-white shadow-md"
: "bg-white border-slate-200 text-slate-500 hover:border-slate-400"
)}
>
{isSelected && <Check className="w-3 h-3 text-amber-400" />}
{option.label}
<span className={cn(
"ml-1 font-mono",
isSelected ? "text-slate-400" : "text-slate-300"
)}>
[{option.score}]
</span>
</div>
);
})}
</div>
</div>
))}
</div>
{/* Remarks Component */}
<div className="p-6 border border-slate-100 bg-slate-50 rounded-b-xl space-y-3">
<Label className="text-[10px] font-black text-slate-500 uppercase tracking-widest">Additional Evaluation Notes</Label>
<Textarea
placeholder="Record observations, strengths or concerns..."
className="min-h-[80px] text-sm resize-none border-slate-200 rounded-lg bg-white p-4 focus:ring-1 focus:ring-slate-400 transition-all font-medium"
value={ktMatrixRemarks}
onChange={(e) => setKtMatrixRemarks(e.target.value)}
/>
</div>
{/* Ultra-Simple Summary Line */}
<div className="mt-8 p-6 bg-slate-900 rounded-2xl flex items-center justify-between text-white shadow-xl shadow-slate-200">
<div className="space-y-1">
<p className="text-[10px] font-bold text-slate-400 uppercase tracking-widest">Composite Assessment Score</p>
<div className="flex items-center gap-3">
<div className={cn(
"w-2.5 h-2.5 rounded-full shadow-sm",
Number(calculateKTScore()) >= 60 ? "bg-green-500" :
Number(calculateKTScore()) >= 40 ? "bg-amber-500" : "bg-red-500"
)} />
<span className="text-xs font-bold text-slate-200">
{Number(calculateKTScore()) >= 60 ? "Strong Profile" :
Number(calculateKTScore()) >= 40 ? "Needs Review" : "Low Alignment"}
</span>
</div>
</div>
<div className="text-right">
<div className="flex items-baseline justify-end gap-1">
<span className="text-4xl font-black italic tracking-tighter tabular-nums">{calculateKTScore()}</span>
<span className="text-slate-500 font-bold text-lg">/100</span>
</div>
</div>
</div>
{/* Compact Footer Actions */}
<div className="flex gap-3 mt-8 pb-10">
<Button
className="flex-[2] bg-slate-900 hover:bg-slate-800 text-white font-black rounded-xl h-12 shadow-lg"
onClick={handleSubmitKTMatrix}
disabled={isSubmittingKT || Object.keys(ktMatrixSelectedValues).length < KT_MATRIX_CRITERIA.length}
>
{isSubmittingKT ? 'Saving...' : 'Complete Evaluation'}
</Button>
<Button
variant="outline"
className="flex-1 rounded-xl text-slate-500 border-slate-200 font-bold h-12"
onClick={() => setShowKTMatrixModal(false)}
>
Close
</Button>
</div>
</div>
</div>
</DialogContent>
</Dialog>
{/* Level 2 Feedback Modal */}
<Dialog open={showLevel2FeedbackModal} onOpenChange={setShowLevel2FeedbackModal}>
<DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>Level 2 Interview Feedback</DialogTitle>
<DialogDescription>
Provide detailed feedback from the Level 2 interview (DD Lead + ZBH evaluation).
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div>
<Label>Interview Date</Label>
<Input
type="date"
className="mt-2"
value={level2Feedback.interviewDate}
disabled
/>
</div>
<div>
<Label>Interviewer Name</Label>
<Input
placeholder="Enter your name"
className="mt-2"
value={level2Feedback.interviewerName}
disabled
/>
</div>
<div>
<Label>Overall Performance Score</Label>
<Select
value={level2Feedback.overallScore}
onValueChange={(value) => handleLevel2Change('overallScore', value)}
>
<SelectTrigger className="mt-2">
<SelectValue placeholder="Select score" />
</SelectTrigger>
<SelectContent>
<SelectItem value="10">Outstanding (9-10)</SelectItem>
<SelectItem value="8">Excellent (7-8)</SelectItem>
<SelectItem value="6">Good (5-6)</SelectItem>
<SelectItem value="4">Average (3-4)</SelectItem>
<SelectItem value="2">Below Average (1-2)</SelectItem>
</SelectContent>
</Select>
</div>
<Separator />
<div>
<Label>Strategic Vision</Label>
<Textarea
placeholder="Evaluate the candidate's strategic thinking and long-term vision..."
className="mt-2"
rows={3}
value={level2Feedback.strategicVision}
onChange={(e) => handleLevel2Change('strategicVision', e.target.value)}
/>
</div>
<div>
<Label>Management Capabilities</Label>
<Textarea
placeholder="Assess leadership and team management potential..."
className="mt-2"
rows={3}
value={level2Feedback.managementCapabilities}
onChange={(e) => handleLevel2Change('managementCapabilities', e.target.value)}
/>
</div>
<div>
<Label>Operational Understanding</Label>
<Textarea
placeholder="Review understanding of dealership operations and processes..."
className="mt-2"
rows={3}
value={level2Feedback.operationalUnderstanding}
onChange={(e) => handleLevel2Change('operationalUnderstanding', e.target.value)}
/>
</div>
<div>
<Label>Key Strengths</Label>
<Textarea
placeholder="List the candidate's key strengths and positive attributes..."
className="mt-2"
rows={3}
value={level2Feedback.keyStrengths}
onChange={(e) => handleLevel2Change('keyStrengths', e.target.value)}
/>
</div>
<div>
<Label>Areas of Concern</Label>
<Textarea
placeholder="Highlight any concerns or areas needing improvement..."
className="mt-2"
rows={3}
value={level2Feedback.areasOfConcern}
onChange={(e) => handleLevel2Change('areasOfConcern', e.target.value)}
/>
</div>
<div>
<Label>Additional Comments</Label>
<Textarea
placeholder="Any additional observations or comments..."
className="mt-2"
rows={3}
value={level2Feedback.additionalComments}
onChange={(e) => handleLevel2Change('additionalComments', e.target.value)}
/>
</div>
<div className="flex gap-3">
<Button
variant="outline"
className="flex-1"
onClick={() => setShowLevel2FeedbackModal(false)}
>
Cancel
</Button>
<Button
className="flex-1 bg-black hover:bg-zinc-800 text-white"
onClick={handleSubmitLevel2Feedback}
disabled={isSubmittingLevel2}
>
{isSubmittingLevel2 ? 'Submitting...' : 'Submit Feedback'}
</Button>
</div>
</div>
</DialogContent>
</Dialog>
{/* Feedback Details Modal */}
<Dialog open={showFeedbackDetailsModal} onOpenChange={setShowFeedbackDetailsModal}>
<DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>Interview Feedback Details</DialogTitle>
</DialogHeader>
{selectedEvaluationForView && (
<div className="space-y-4">
<div className="grid grid-cols-2 gap-4 bg-slate-50 p-4 rounded-lg">
<div>
<p className="text-sm font-medium text-slate-500">Interviewer</p>
<p className="font-semibold">{selectedEvaluationForView.evaluator?.fullName}</p>
</div>
<div>
<p className="text-sm font-medium text-slate-500">Role</p>
<p>{selectedEvaluationForView.evaluator?.role?.roleName || 'N/A'}</p>
</div>
<div>
<p className="text-sm font-medium text-slate-500">
{selectedEvaluationForView.interview?.level === 1 ? 'Score (KT Matrix)' : 'Overall Score'}
</p>
<p className="font-bold text-lg">
{selectedEvaluationForView.ktMatrixScore ?
`${selectedEvaluationForView.ktMatrixScore}/${selectedEvaluationForView.interview?.level === 1 ? '100' : '10'}`
: 'N/A'}
</p>
</div>
<div>
<p className="text-sm font-medium text-slate-500">Recommendation</p>
<Badge variant={
selectedEvaluationForView.recommendation?.toLowerCase().includes('reject') ? 'destructive' :
selectedEvaluationForView.recommendation?.toLowerCase().includes('hold') ? 'secondary' : 'default'
}>
{selectedEvaluationForView.recommendation || 'N/A'}
</Badge>
</div>
</div>
<Separator />
<div>
<h4 className="font-semibold mb-3">Detailed Feedback</h4>
{selectedEvaluationForView.feedbackDetails?.length > 0 ? (
<div className="space-y-4">
{selectedEvaluationForView.feedbackDetails.map((detail: any, index: number) => (
<div key={index} className="border-b last:border-0 pb-3 last:pb-0">
<p className="font-medium text-slate-900">{detail.feedbackType}</p>
<p className="text-slate-700 mt-1 whitespace-pre-wrap text-sm">{detail.comments}</p>
</div>
))}
</div>
) : (
<p className="text-slate-500 italic">No detailed feedback available.</p>
)}
</div>
</div>
)}
</DialogContent>
</Dialog>
{/* Level 3 Feedback Modal */}
<Dialog open={showLevel3FeedbackModal} onOpenChange={setShowLevel3FeedbackModal}>
<DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>Level 3 Interview Feedback</DialogTitle>
<DialogDescription>
Provide detailed feedback from the Level 3 interview (NBH + DD-Head evaluation).
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div>
<Label>Interview Date</Label>
<Input
type="date"
className="mt-2"
value={level3Feedback.interviewDate}
disabled
/>
</div>
<div>
<Label>Interviewer Name</Label>
<Input
placeholder="Enter your name"
className="mt-2"
value={level3Feedback.interviewerName}
disabled
/>
</div>
<div>
<Label>Overall Performance Score</Label>
<Select
value={level3Feedback.overallScore}
onValueChange={(value) => handleLevel3Change('overallScore', value)}
>
<SelectTrigger className="mt-2">
<SelectValue placeholder="Select score" />
</SelectTrigger>
<SelectContent>
<SelectItem value="10">Outstanding (9-10)</SelectItem>
<SelectItem value="8">Excellent (7-8)</SelectItem>
<SelectItem value="6">Good (5-6)</SelectItem>
<SelectItem value="4">Average (3-4)</SelectItem>
<SelectItem value="2">Below Average (1-2)</SelectItem>
</SelectContent>
</Select>
</div>
<Separator />
<div>
<Label>Business Vision & Strategy</Label>
<Textarea
placeholder="Evaluate the candidate's long-term business vision and strategic planning..."
className="mt-2"
rows={3}
value={level3Feedback.strategicVision}
onChange={(e) => handleLevel3Change('strategicVision', e.target.value)}
/>
</div>
<div>
<Label>Leadership & Decision Making</Label>
<Textarea
placeholder="Assess leadership qualities and decision-making capabilities..."
className="mt-2"
rows={3}
value={level3Feedback.managementCapabilities}
onChange={(e) => handleLevel3Change('managementCapabilities', e.target.value)}
/>
</div>
<div>
<Label>Operational & Financial Readiness</Label>
<Textarea
placeholder="Review financial commitment and investment readiness..."
className="mt-2"
rows={3}
value={level3Feedback.operationalUnderstanding}
onChange={(e) => handleLevel3Change('operationalUnderstanding', e.target.value)}
/>
</div>
<div>
<Label>Brand Alignment</Label>
<Textarea
placeholder="Evaluate alignment with Royal Enfield brand values and culture..."
className="mt-2"
rows={3}
value={level3Feedback.brandAlignment}
onChange={(e) => handleLevel3Change('brandAlignment', e.target.value)}
/>
</div>
<div>
<Label>Key Strengths</Label>
<Textarea
placeholder="List the candidate's key strengths and exceptional qualities..."
className="mt-2"
rows={3}
value={level3Feedback.keyStrengths}
onChange={(e) => handleLevel3Change('keyStrengths', e.target.value)}
/>
</div>
<div>
<Label>Areas of Concern</Label>
<Textarea
placeholder="Highlight any red flags or major concerns..."
className="mt-2"
rows={3}
value={level3Feedback.areasOfConcern}
onChange={(e) => handleLevel3Change('areasOfConcern', e.target.value)}
/>
</div>
<div>
<Label>Executive Summary</Label>
<Textarea
placeholder="Provide a comprehensive executive summary of the interview and final thoughts..."
className="mt-2"
rows={4}
value={level3Feedback.executiveSummary}
onChange={(e) => handleLevel3Change('executiveSummary', e.target.value)}
/>
</div>
<div className="flex gap-3">
<Button
variant="outline"
className="flex-1"
onClick={() => setShowLevel3FeedbackModal(false)}
>
Cancel
</Button>
<Button
className="flex-1 bg-black hover:bg-zinc-800 text-white"
onClick={handleSubmitLevel3Feedback}
disabled={isSubmittingLevel3}
>
{isSubmittingLevel3 ? 'Submitting...' : 'Submit Feedback'}
</Button>
</div>
</div>
</DialogContent>
</Dialog>
{/* Documents Modal */}
<Dialog
open={showDocumentsModal}
onOpenChange={(open) => {
setShowDocumentsModal(open);
if (!open) setShowUploadForm(false);
}}
>
<DialogContent className="max-w-[95vw] sm:max-w-2xl md:max-w-3xl lg:max-w-4xl max-h-[90vh] overflow-hidden flex flex-col p-4 sm:p-6">
<DialogHeader className="pb-4">
<DialogTitle className="text-xl font-bold flex items-center gap-2">
<FileText className="w-5 h-5 text-amber-600" />
Documents - {selectedStage || 'General'}
</DialogTitle>
<DialogDescription className="text-slate-500">
View and manage documents uploaded for this stage.
</DialogDescription>
</DialogHeader>
{!showUploadForm ? (
<div className="flex-1 flex flex-col min-h-0 space-y-4">
{getDocumentsForStage(selectedStage || '').length > 0 ? (
<div className="flex-1 overflow-auto border rounded-lg border-slate-200">
<Table className="w-full table-auto">
<TableHeader className="bg-slate-50/80 sticky top-0 z-10">
<TableRow className="hover:bg-transparent border-b">
<TableHead className="w-[45%] min-w-[150px] font-semibold text-slate-900 py-3">Document Name</TableHead>
<TableHead className="w-[15%] min-w-[100px] font-semibold text-slate-900 py-3">Type</TableHead>
<TableHead className="w-[15%] min-w-[100px] font-semibold text-slate-900 py-3">Upload Date</TableHead>
<TableHead className="w-[15%] min-w-[140px] font-semibold text-slate-900 py-3">Uploaded By</TableHead>
<TableHead className="text-right w-[10%] min-w-[80px] font-semibold text-slate-900 py-3">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{getDocumentsForStage(selectedStage || '').map((doc) => (
<TableRow key={doc.id} className="hover:bg-slate-50/50 transition-colors">
<TableCell className="py-3">
<div className="flex items-center gap-2 min-w-0">
<FileText className="w-4 h-4 text-slate-400 shrink-0" />
<span className="truncate font-medium text-slate-700" title={doc.fileName}>{doc.fileName}</span>
</div>
</TableCell>
<TableCell className="py-3">
<Badge variant="outline" className="capitalize whitespace-nowrap font-normal border-slate-200 bg-white">
{doc.documentType?.toLowerCase() || 'Other'}
</Badge>
</TableCell>
<TableCell className="py-3 whitespace-nowrap text-slate-600">
{formatDateTime(doc.createdAt)}
</TableCell>
<TableCell className="py-3 text-slate-600">
{doc.uploader?.fullName || (doc.uploadedBy ? 'System User' : 'Applicant')}
</TableCell>
<TableCell className="text-right py-3">
<div className="flex gap-1 justify-end">
<Button
variant="ghost"
size="icon"
className="h-8 w-8 text-slate-400 hover:text-indigo-600 hover:bg-indigo-50 rounded-full"
onClick={() => {
setPreviewDoc(doc);
setShowPreviewModal(true);
}}
>
<Eye className="w-4 h-4" />
</Button>
<Button
variant="ghost"
size="icon"
className="h-8 w-8 text-slate-400 hover:text-amber-600 hover:bg-amber-50 rounded-full"
onClick={() => {
const baseUrl = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:5000';
window.open(`${baseUrl}/${doc.filePath}`, '_blank');
}}
>
<Download className="w-4 h-4" />
</Button>
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
) : (
<div className="flex-1 flex flex-col items-center justify-center py-12 text-center border rounded-lg bg-slate-50/30">
<div className="w-16 h-16 rounded-full bg-slate-100 flex items-center justify-center mb-4">
<FileText className="w-8 h-8 text-slate-300" />
</div>
<h3 className="text-slate-900 font-semibold mb-2">No Documents Found</h3>
<p className="text-slate-600 text-sm max-w-[250px]">No documents have been uploaded for this stage yet.</p>
</div>
)}
<div className="flex flex-col sm:flex-row gap-3 pt-2 mt-auto">
<Button
className="flex-1 bg-amber-600 hover:bg-amber-700 text-white font-bold py-3 sm:py-5 rounded-xl shadow-lg shadow-amber-600/15 transition-all hover:scale-[1.01] active:scale-[0.99]"
onClick={() => setShowUploadForm(true)}
>
<Upload className="w-5 h-5 mr-3" />
Upload Document
</Button>
<Button
variant="outline"
className="flex-1 sm:flex-none py-3 sm:py-5 px-8 rounded-xl border-slate-200 font-semibold text-slate-600 hover:bg-slate-50"
onClick={() => setShowDocumentsModal(false)}
>
Close
</Button>
</div>
</div>
) : (
<div className="space-y-6 py-4">
<div className="grid gap-6 bg-slate-50/50 p-4 sm:p-6 rounded-2xl border border-slate-200">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-6">
<div className="space-y-2">
<Label className="text-slate-700 font-semibold px-1">Stage context</Label>
<Select value={selectedStage || 'null'} onValueChange={(val) => setSelectedStage(val === 'null' ? null : val)}>
<SelectTrigger className="bg-white border-slate-200 h-11 rounded-xl focus:ring-amber-500 shadow-sm">
<SelectValue placeholder="Select stage" />
</SelectTrigger>
<SelectContent>
<SelectItem value="null">General / No Stage</SelectItem>
{flattenedStages.map((s, idx) => (
<SelectItem key={`${s.name}-${idx}`} value={s.name}>
{s.parentBranch ? `${s.parentBranch}: ${s.name}` : s.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label className="text-slate-700 font-semibold px-1">Document Type</Label>
<Select value={uploadDocType} onValueChange={setUploadDocType}>
<SelectTrigger className="bg-white border-slate-200 h-11 rounded-xl focus:ring-amber-500 shadow-sm">
<SelectValue placeholder="Select type" />
</SelectTrigger>
<SelectContent>
{(() => {
const baseDocs = ['Other'];
// Stage-based filtering from DB configs
const stageConfigs = documentConfigs.filter(c => {
// Normalize stage codes for comparison
const cfgStage = c.stageCode?.trim();
const selStage = (selectedStage || 'General').trim();
// Exact match
if (cfgStage === selStage) return true;
// EOR sub-stage mapping
if (selStage.startsWith('EOR:') && cfgStage === 'EOR') return true;
// Special case for General
if (!selectedStage && cfgStage === 'General') return true;
return false;
});
let filteredDocs: string[] = [];
if (stageConfigs.length > 0) {
filteredDocs = stageConfigs.map(c => c.documentType);
} else if (!selectedStage || selectedStage === 'General') {
// Unified KYC checklist fallback
filteredDocs = [
'PAN Card', 'GST Certificate', 'Aadhaar Card', 'Passport Size Photograph',
'Partnership Deed', 'LLP Agreement', 'Certificate of Incorporation',
'Board Resolution', 'Firm Registration Certificate', 'Cancelled Check',
'Bank Statement', 'Other'
];
} else {
filteredDocs = baseDocs;
}
// If EOR specifically, ensure we can upload the actual item
if (selectedStage?.startsWith('EOR: ')) {
const eorItem = selectedStage.replace('EOR: ', '');
if (!filteredDocs.includes(eorItem)) {
filteredDocs = [eorItem, ...filteredDocs];
}
}
return Array.from(new Set(filteredDocs)).map((doc, idx) => (
<SelectItem key={`${doc}-${idx}`} value={doc}>
{doc}
</SelectItem>
));
})()}
</SelectContent>
</Select>
</div>
</div>
<div className="space-y-2">
<Label className="text-slate-700 font-semibold px-1">Select File</Label>
<Input
type="file"
className="bg-white border-slate-200 h-12 rounded-xl focus:ring-amber-500 shadow-sm file:mr-4 file:py-2 file:px-4 file:rounded-full file:border-0 file:text-sm file:font-semibold file:bg-amber-50 file:text-amber-700 hover:file:bg-amber-100 cursor-pointer"
onChange={(e) => setUploadFile(e.target.files ? e.target.files[0] : null)}
/>
</div>
</div>
<div className="flex flex-col sm:flex-row gap-3 pt-4">
<Button
className="flex-1 order-2 sm:order-1 py-3 sm:py-5 rounded-xl border-slate-200 font-semibold text-slate-600 hover:bg-slate-50"
variant="outline"
onClick={() => setShowUploadForm(false)}
disabled={isUploading}
>
Cancel
</Button>
<Button
className="flex-1 order-1 sm:order-2 bg-amber-600 hover:bg-amber-700 text-white font-bold py-3 sm:py-5 rounded-xl shadow-lg shadow-amber-600/15 transition-all hover:scale-[1.01] active:scale-[0.99]"
onClick={async () => {
await handleUpload();
setShowUploadForm(false);
}}
disabled={!uploadFile || !uploadDocType || isUploading}
>
{isUploading ? (
<span className="flex items-center gap-2">
<div className="w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin" />
Uploading...
</span>
) : (
<span className="flex items-center gap-2">
<Upload className="w-5 h-5" />
Confirm Upload
</span>
)}
</Button>
</div>
</div>
)}
</DialogContent>
</Dialog>
<DocumentPreviewModal
isOpen={showPreviewModal}
onClose={() => setShowPreviewModal(false)}
document={previewDoc}
/>
{/* FDD Finalize Confirmation Modal */}
<Dialog open={showFddFinalizeModal} onOpenChange={setShowFddFinalizeModal}>
<DialogContent className="max-w-md p-0 overflow-hidden border-none shadow-2xl rounded-3xl">
<div className="bg-slate-950 p-8 flex items-center justify-center relative overflow-hidden">
<div className="absolute inset-0 bg-gradient-to-br from-amber-600/20 to-transparent" />
<div className="w-20 h-20 bg-amber-600/20 rounded-full flex items-center justify-center animate-pulse relative z-10 shadow-[0_0_40px_rgba(245,158,11,0.2)]">
<ShieldCheck className="w-10 h-10 text-amber-500" />
</div>
</div>
<div className="p-8 space-y-6 bg-white">
<DialogHeader>
<DialogTitle className="text-2xl font-black text-slate-900 text-center tracking-tight">Finalize FDD Audit</DialogTitle>
<DialogDescription className="text-slate-500 text-center pt-2 leading-relaxed text-sm font-medium">
You are about to submit your final findings. This action will <span className="font-bold text-slate-900 underline decoration-amber-500 decoration-2">lock the audit session</span> and trigger the LOI approval workflow.
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
{(currentUser?.role !== 'FDD' && currentUser?.roleCode !== 'FDD') && (
<div className="space-y-2">
<Label className="text-[10px] font-black uppercase tracking-widest text-slate-400">Auditor Recommendation</Label>
<div className="flex gap-2">
{['Recommended', 'Qualified with Observations', 'Not Recommended'].map((rec) => (
<Button
key={rec}
variant={fddAuditRecommendation === rec ? 'default' : 'outline'}
className={cn(
"flex-1 h-10 font-bold text-[9px] uppercase tracking-wider rounded-xl transition-all",
fddAuditRecommendation === rec && rec === 'Recommended' && "bg-emerald-600 hover:bg-emerald-700",
fddAuditRecommendation === rec && rec === 'Qualified with Observations' && "bg-amber-500 hover:bg-amber-600",
fddAuditRecommendation === rec && rec === 'Not Recommended' && "bg-red-600 hover:bg-red-700"
)}
onClick={() => setFddAuditRecommendation(rec)}
>
{rec}
</Button>
))}
</div>
</div>
)}
<div className="space-y-2">
<Label className="text-[10px] font-black uppercase tracking-widest text-slate-400">Findings Summary</Label>
<Textarea
placeholder="Summarize key financial findings or discrepancies..."
className="min-h-[100px] rounded-xl border-slate-200 focus:ring-amber-500 text-sm"
value={fddAuditFindings}
onChange={(e) => setFddAuditFindings(e.target.value)}
/>
</div>
</div>
<div className="bg-amber-50 p-4 rounded-2xl flex gap-3 border border-amber-100">
<Info className="w-5 h-5 text-amber-600 shrink-0 mt-0.5" />
<p className="text-[11px] text-amber-800 font-medium italic">
Ensure the final PDF report is uploaded first. This satisfies the FDD statutory requirement.
</p>
</div>
<div className="flex flex-col sm:flex-row gap-3 pt-2">
<Button
variant="outline"
className="w-full sm:flex-1 h-12 rounded-2xl font-bold text-slate-600 hover:bg-slate-50 border-slate-200"
onClick={() => setShowFddFinalizeModal(false)}
disabled={isFinalizingFdd}
>
Cancel
</Button>
<Button
className="w-full sm:flex-1 h-12 rounded-2xl font-bold bg-slate-950 hover:bg-slate-900 text-white shadow-lg shadow-slate-200 transition-all active:scale-95 border-b-4 border-amber-500"
disabled={isFinalizingFdd || !fddAuditFindings}
onClick={async () => {
try {
setIsFinalizingFdd(true);
await onboardingService.submitStageDecision({
applicationId: application!.id,
stageCode: 'FDD_VERIFICATION',
decision: 'Approved',
remarks: (currentUser?.role === 'FDD' || currentUser?.roleCode === 'FDD')
? `Findings: ${fddAuditFindings}`
: `[RECOMMENDATION: ${fddAuditRecommendation}] \nFindings: ${fddAuditFindings}`,
nextStatus: 'LOI In Progress',
nextProgress: 65
});
toast.success('FDD Audit finalized and submitted.');
setShowFddFinalizeModal(false);
fetchApplication();
} catch (err) {
toast.error('Submission failed');
} finally {
setIsFinalizingFdd(false);
}
}}
>
{isFinalizingFdd ? <Loader2 className="w-5 h-5 animate-spin" /> : 'Confirm & Submit'}
</Button>
</div>
</div>
</DialogContent>
</Dialog>
{/* FDD Flag Modal */}
<Dialog open={showFddFlagModal} onOpenChange={setShowFddFlagModal}>
<DialogContent className="max-w-md p-0 overflow-hidden border-none shadow-2xl rounded-3xl">
<div className="bg-slate-950 p-8 flex items-center justify-center relative overflow-hidden">
<div className="absolute inset-0 bg-gradient-to-br from-red-600/20 to-transparent" />
<div className="w-20 h-20 bg-red-600/20 rounded-full flex items-center justify-center relative z-10 shadow-[0_0_40px_rgba(220,38,38,0.2)]">
<ShieldAlert className="w-10 h-10 text-red-500" />
</div>
</div>
<div className="p-8 space-y-6 bg-white text-center">
<DialogHeader>
<DialogTitle className="text-2xl font-black text-slate-900 tracking-tight">Flag Non-Responsive</DialogTitle>
<DialogDescription className="text-slate-500 pt-2 leading-relaxed text-sm font-medium">
Are you sure you want to flag this applicant? This will notify the DD Admin that the audit cannot proceed due to applicant's non-cooperation.
</DialogDescription>
</DialogHeader>
<div className="bg-red-50 p-4 rounded-2xl flex gap-3 border border-red-100">
<AlertCircle className="w-5 h-5 text-red-600 shrink-0 mt-0.5" />
<p className="text-[11px] text-red-800 text-left font-medium">
"Applicant is unresponsive to multiple queries and financial document requests."
</p>
</div>
<div className="flex flex-col sm:flex-row gap-3 pt-2">
<Button
variant="outline"
className="w-full sm:flex-1 h-12 rounded-2xl font-bold text-slate-600 hover:bg-slate-50 border-slate-200"
onClick={() => setShowFddFlagModal(false)}
disabled={isFddFlagging}
>
Go Back
</Button>
<Button
className="w-full sm:flex-1 h-12 rounded-2xl font-bold bg-slate-950 hover:bg-slate-900 text-white shadow-lg shadow-slate-200 transition-all active:scale-95 border-b-4 border-red-600"
disabled={isFddFlagging}
onClick={async () => {
try {
setIsFddFlagging(true);
await onboardingService.submitStageDecision({
applicationId: application!.id,
stageCode: 'FDD_VERIFICATION',
decision: 'Rejected',
remarks: 'Applicant is non-responsive to FDD queries.'
});
toast.error('Applicant flagged as non-responsive.');
setShowFddFlagModal(false);
fetchApplication();
} catch (err) {
toast.error('Action failed');
} finally {
setIsFddFlagging(false);
}
}}
>
{isFddFlagging ? <Loader2 className="w-5 h-5 animate-spin" /> : 'Confirm Flag'}
</Button>
</div>
</div>
</DialogContent>
</Dialog>
{/* Firm Type Update Modal */}
<Dialog open={showFirmTypeModal} onOpenChange={setShowFirmTypeModal}>
<DialogContent className="max-w-md p-0 overflow-hidden rounded-3xl border-none shadow-2xl">
<div className="bg-amber-600 p-8 text-white">
<div className="w-16 h-16 rounded-2xl bg-white/20 flex items-center justify-center mb-6 backdrop-blur-sm border border-white/30 shadow-inner">
<Building2 className="w-8 h-8 text-white" />
</div>
<h3 className="text-2xl font-black tracking-tight mb-2">Update Firm Type</h3>
<p className="text-amber-100/80 text-sm font-medium leading-relaxed">
Select the proposed legal constitution for this dealership application.
</p>
</div>
<div className="p-8 space-y-6 bg-white">
<div className="space-y-2">
<Label className="text-[10px] text-slate-400 uppercase tracking-widest font-black">Proposed Legal Constitution</Label>
<Select value={tempFirmType} onValueChange={setTempFirmType}>
<SelectTrigger className="h-12 rounded-xl border-slate-200 focus:ring-amber-500">
<SelectValue placeholder="Select Firm Type" />
</SelectTrigger>
<SelectContent>
<SelectItem value="Proprietorship">Proprietorship</SelectItem>
<SelectItem value="Partnership">Partnership</SelectItem>
<SelectItem value="Limited Liability partnership">LLP (Limited Liability partnership)</SelectItem>
<SelectItem value="Private Limited Company">Private Limited Company</SelectItem>
<SelectItem value="Public Limited Company">Public Limited Company</SelectItem>
</SelectContent>
</Select>
</div>
<div className="flex gap-3 pt-2">
<Button
variant="outline"
className="flex-1 h-12 rounded-xl font-bold text-slate-600 border-slate-200"
onClick={() => setShowFirmTypeModal(false)}
disabled={updatingFirmType}
>
Cancel
</Button>
<Button
className="flex-1 h-12 rounded-xl font-bold bg-amber-600 hover:bg-amber-700 text-white shadow-lg shadow-amber-200 transition-all active:scale-95"
disabled={updatingFirmType || !tempFirmType}
onClick={handleUpdateFirmType}
>
{updatingFirmType ? <Loader2 className="w-5 h-5 animate-spin" /> : 'Update Type'}
</Button>
</div>
</div>
</DialogContent>
</Dialog>
</div>
</div>
</div>
);
};
export default ApplicationDetails;