4342 lines
206 KiB
TypeScript
4342 lines
206 KiB
TypeScript
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,
|
||
} from 'lucide-react';
|
||
import { Progress } from '../ui/progress';
|
||
import { Textarea } from '../ui/textarea';
|
||
import { Input } from '../ui/input';
|
||
import { Label } from '../ui/label';
|
||
import { Alert, AlertDescription, AlertTitle } from '../ui/alert';
|
||
import { AlertCircle, RefreshCw, Check, Loader2 } from 'lucide-react';
|
||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogTrigger } from '../ui/dialog';
|
||
import { ScrollArea } from '../ui/scroll-area';
|
||
import {
|
||
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 function 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 fetchApplication = async () => {
|
||
try {
|
||
setLoading(true);
|
||
const data = await onboardingService.getApplicationById(applicationId!);
|
||
|
||
// Helper to find stage date
|
||
const getStageDate = (stageName: string) => {
|
||
const stage = data.progressTracking?.find((p: any) => p.stageName === stageName);
|
||
return stage?.stageCompletedAt ? new Date(stage.stageCompletedAt).toISOString() :
|
||
stage?.stageStartedAt ? new Date(stage.stageStartedAt).toISOString() : undefined;
|
||
};
|
||
|
||
// 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'),
|
||
shortlistDate: getStageDate('Shortlist'),
|
||
level1InterviewDate: getStageDate('1st Level Interview'),
|
||
level2InterviewDate: getStageDate('2nd Level Interview'),
|
||
level3InterviewDate: getStageDate('3rd Level Interview'),
|
||
fddDate: getStageDate('FDD'),
|
||
loiApprovalDate: getStageDate('LOI Approval'),
|
||
securityDetailsDate: getStageDate('Security Details'),
|
||
loiIssueDate: getStageDate('LOI Issue'),
|
||
dealerCodeDate: getStageDate('Dealer Code Generation'),
|
||
architectureAssignedDate: getStageDate('Architecture Team Assigned'),
|
||
architectureDocumentDate: getStageDate('Architecture Document Upload'),
|
||
architectureCompletionDate: getStageDate('Architecture Team Completion'),
|
||
loaDate: getStageDate('LOA'),
|
||
eorCompleteDate: getStageDate('EOR Complete'),
|
||
inaugurationDate: getStageDate('Inauguration'),
|
||
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 || [],
|
||
};
|
||
setApplication(mappedApp);
|
||
} catch (error) {
|
||
console.error('Failed to fetch application details', error);
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
};
|
||
|
||
useEffect(() => {
|
||
if (applicationId) {
|
||
fetchApplication();
|
||
}
|
||
}, [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(() => {
|
||
if (['EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Onboarded'].includes(application?.status || '')) {
|
||
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);
|
||
|
||
// Fetch audit logs when application loads
|
||
useEffect(() => {
|
||
if (applicationId) {
|
||
const fetchAuditLogs = async () => {
|
||
setAuditLoading(true);
|
||
try {
|
||
const logs = await auditService.getAuditLogs('application', applicationId);
|
||
setAuditLogs(Array.isArray(logs) ? logs : []);
|
||
} catch (error) {
|
||
console.error('Failed to fetch audit logs', error);
|
||
setAuditLogs([]);
|
||
} finally {
|
||
setAuditLoading(false);
|
||
}
|
||
};
|
||
fetchAuditLogs();
|
||
}
|
||
}, [applicationId]);
|
||
|
||
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 [documents, setDocuments] = useState<any[]>([]);
|
||
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 [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 [isApproving, setIsApproving] = useState(false);
|
||
const [isRejecting, setIsRejecting] = useState(false);
|
||
|
||
// KT Matrix State
|
||
const [ktMatrixScores, setKtMatrixScores] = useState<Record<string, number>>({});
|
||
const [ktMatrixRemarks, setKtMatrixRemarks] = useState('');
|
||
const [isSubmittingKT, setIsSubmittingKT] = useState(false);
|
||
const [selectedInterviewForFeedback, setSelectedInterviewForFeedback] = useState<any>(null);
|
||
|
||
// 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, score: number) => {
|
||
setKtMatrixScores(prev => ({
|
||
...prev,
|
||
[criterionName]: score
|
||
}));
|
||
};
|
||
|
||
const calculateKTScore = () => {
|
||
let totalWeightedScore = 0;
|
||
KT_MATRIX_CRITERIA.forEach(criterion => {
|
||
const score = ktMatrixScores[criterion.name] || 0;
|
||
const weightedScore = (score / criterion.maxScore) * criterion.weight;
|
||
totalWeightedScore += weightedScore;
|
||
});
|
||
return totalWeightedScore.toFixed(2);
|
||
};
|
||
|
||
const handleSubmitKTMatrix = async () => {
|
||
if (Object.keys(ktMatrixScores).length < KT_MATRIX_CRITERIA.length) {
|
||
toast.warning('Please fill all fields in the KT Matrix');
|
||
return;
|
||
}
|
||
|
||
// Use the selected interview ID or fallback (though UI now forces selection)
|
||
const interviewId = selectedInterviewForFeedback?.id || interviews.find(i => i.status !== 'Completed')?.id || interviews[0]?.id;
|
||
|
||
if (!interviewId) {
|
||
toast.error('No active interview found to link this KT Matrix to.');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
setIsSubmittingKT(true);
|
||
|
||
const criteriaScores = KT_MATRIX_CRITERIA.map(c => ({
|
||
criterionName: c.name,
|
||
score: ktMatrixScores[c.name] || 0,
|
||
maxScore: c.maxScore,
|
||
weightage: c.weight
|
||
}));
|
||
|
||
await onboardingService.submitKTMatrix({
|
||
interviewId,
|
||
criteriaScores,
|
||
feedback: ktMatrixRemarks,
|
||
recommendation: null // No auto-decision
|
||
});
|
||
|
||
toast.success('KT Matrix submitted successfully');
|
||
setShowKTMatrixModal(false);
|
||
|
||
// Reset form
|
||
setKtMatrixScores({});
|
||
setKtMatrixRemarks('');
|
||
await fetchInterviews();
|
||
await fetchApplication(); // Refresh application status and progress
|
||
} catch (error) {
|
||
toast.error('Failed to submit KT Matrix');
|
||
} finally {
|
||
setIsSubmittingKT(false);
|
||
}
|
||
};
|
||
|
||
// Level 2 Feedback State
|
||
const [level2Feedback, setLevel2Feedback] = useState({
|
||
strategicVision: '',
|
||
managementCapabilities: '',
|
||
operationalUnderstanding: '',
|
||
keyStrengths: '',
|
||
areasOfConcern: '',
|
||
additionalComments: '',
|
||
overallScore: '',
|
||
interviewerName: currentUser?.name || '',
|
||
interviewDate: new Date().toISOString().split('T')[0]
|
||
});
|
||
const [isSubmittingLevel2, setIsSubmittingLevel2] = useState(false);
|
||
|
||
const handleLevel2Change = (field: string, value: string) => {
|
||
setLevel2Feedback(prev => ({ ...prev, [field]: value }));
|
||
};
|
||
|
||
const handleSubmitLevel2Feedback = async () => {
|
||
if (!level2Feedback.overallScore) {
|
||
toast.warning('Please provide an overall score.');
|
||
return;
|
||
}
|
||
|
||
const interviewId = selectedInterviewForFeedback?.id || interviews.find(i => i.status !== 'Completed' && i.level === 2)?.id;
|
||
|
||
if (!interviewId) {
|
||
toast.error('No active Level 2 interview found to link this feedback to.');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
setIsSubmittingLevel2(true);
|
||
|
||
const feedbackItems = [
|
||
{ type: 'Strategic Vision', comments: level2Feedback.strategicVision },
|
||
{ type: 'Management Capabilities', comments: level2Feedback.managementCapabilities },
|
||
{ type: 'Operational Understanding', comments: level2Feedback.operationalUnderstanding },
|
||
{ type: 'Key Strengths', comments: level2Feedback.keyStrengths },
|
||
{ type: 'Areas of Concern', comments: level2Feedback.areasOfConcern },
|
||
{ type: 'Additional Comments', comments: level2Feedback.additionalComments }
|
||
].filter(item => item.comments.trim() !== '');
|
||
|
||
await onboardingService.submitLevel2Feedback({
|
||
interviewId,
|
||
overallScore: Number(level2Feedback.overallScore),
|
||
feedbackItems
|
||
});
|
||
|
||
toast.success('Level 2 Feedback submitted successfully');
|
||
setShowLevel2FeedbackModal(false);
|
||
|
||
// Reset form
|
||
setLevel2Feedback({
|
||
strategicVision: '',
|
||
managementCapabilities: '',
|
||
operationalUnderstanding: '',
|
||
keyStrengths: '',
|
||
areasOfConcern: '',
|
||
additionalComments: '',
|
||
overallScore: '',
|
||
interviewerName: currentUser?.name || '',
|
||
interviewDate: new Date().toISOString().split('T')[0]
|
||
});
|
||
fetchInterviews(); // Refresh to show feedback
|
||
fetchApplication(); // Refresh application status
|
||
} catch (error) {
|
||
toast.error('Failed to submit Level 2 Feedback');
|
||
} finally {
|
||
setIsSubmittingLevel2(false);
|
||
}
|
||
};
|
||
|
||
// Level 3 Feedback State
|
||
const [level3Feedback, setLevel3Feedback] = useState({
|
||
strategicVision: '',
|
||
managementCapabilities: '',
|
||
operationalUnderstanding: '',
|
||
brandAlignment: '',
|
||
executiveSummary: '',
|
||
keyStrengths: '',
|
||
areasOfConcern: '',
|
||
additionalComments: '',
|
||
overallScore: '',
|
||
interviewerName: currentUser?.name || '',
|
||
interviewDate: new Date().toISOString().split('T')[0]
|
||
});
|
||
const [isSubmittingLevel3, setIsSubmittingLevel3] = useState(false);
|
||
|
||
const handleLevel3Change = (field: string, value: string) => {
|
||
setLevel3Feedback(prev => ({ ...prev, [field]: value }));
|
||
};
|
||
|
||
const handleSubmitLevel3Feedback = async () => {
|
||
if (!level3Feedback.overallScore) {
|
||
toast.warning('Please provide an overall score.');
|
||
return;
|
||
}
|
||
|
||
const interviewId = selectedInterviewForFeedback?.id || interviews.find(i => i.status !== 'Completed' && i.level === 3)?.id;
|
||
|
||
if (!interviewId) {
|
||
toast.error('No active Level 3 interview found to link this feedback to.');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
setIsSubmittingLevel3(true);
|
||
|
||
// Level 3 might have slightly different fields or same structure. Assuming same for now.
|
||
const feedbackItems = [
|
||
{ type: 'Business Vision & Strategy', comments: level3Feedback.strategicVision },
|
||
{ type: 'Leadership & Decision Making', comments: level3Feedback.managementCapabilities },
|
||
{ type: 'Operational & Financial Readiness', comments: level3Feedback.operationalUnderstanding },
|
||
{ type: 'Brand Alignment', comments: level3Feedback.brandAlignment },
|
||
{ type: 'Key Strengths', comments: level3Feedback.keyStrengths },
|
||
{ type: 'Areas of Concern', comments: level3Feedback.areasOfConcern },
|
||
{ type: 'Executive Summary', comments: level3Feedback.executiveSummary },
|
||
{ type: 'Additional Comments', comments: level3Feedback.additionalComments }
|
||
].filter(item => item.comments.trim() !== '');
|
||
|
||
// Reusing submitLevel2Feedback endpoint as it maps to InterviewFeedback table generic enough for Level 3 too
|
||
// Or we can create specific one if needed, but logic is identical so reusing service method
|
||
await onboardingService.submitLevel2Feedback({
|
||
interviewId,
|
||
overallScore: Number(level3Feedback.overallScore),
|
||
feedbackItems
|
||
});
|
||
|
||
toast.success('Level 3 Feedback submitted successfully');
|
||
setShowLevel3FeedbackModal(false);
|
||
|
||
// Reset form
|
||
setLevel3Feedback({
|
||
strategicVision: '',
|
||
managementCapabilities: '',
|
||
operationalUnderstanding: '',
|
||
brandAlignment: '',
|
||
executiveSummary: '',
|
||
keyStrengths: '',
|
||
areasOfConcern: '',
|
||
additionalComments: '',
|
||
overallScore: '',
|
||
interviewerName: currentUser?.name || '',
|
||
interviewDate: new Date().toISOString().split('T')[0]
|
||
});
|
||
fetchInterviews();
|
||
fetchApplication();
|
||
} catch (error) {
|
||
toast.error('Failed to submit Level 3 Feedback');
|
||
} finally {
|
||
setIsSubmittingLevel3(false);
|
||
}
|
||
};
|
||
|
||
// Feedback Details Modal State
|
||
const [selectedEvaluationForView, setSelectedEvaluationForView] = useState<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 ((activeTab === 'documents' || activeTab === 'progress') && applicationId) {
|
||
const fetchDocuments = async () => {
|
||
try {
|
||
const docs = await onboardingService.getDocuments(applicationId);
|
||
setDocuments(docs || []);
|
||
} catch (error) {
|
||
console.error('Failed to fetch documents', error);
|
||
}
|
||
};
|
||
fetchDocuments();
|
||
}
|
||
}, [activeTab, applicationId]);
|
||
|
||
const fetchUsers = async (type?: string) => {
|
||
// Only fetch users if user has admin/DD/NBH roles to avoid 403s
|
||
if (!currentUser || !['DD Admin', 'Super Admin', 'DD Lead', 'DD Head', 'NBH'].includes(currentUser.role)) {
|
||
return;
|
||
}
|
||
try {
|
||
const params: any = {};
|
||
if (type) {
|
||
const roleMapping: any = {
|
||
'level1': ['DD-ZM', 'RBM'],
|
||
'level2': ['DD Lead', 'ZBH'],
|
||
'level3': ['NBH', 'DD Head']
|
||
};
|
||
params.roleCode = roleMapping[type];
|
||
|
||
// Include location from the application
|
||
if (application) {
|
||
params.locationId = application.districtId || application.areaId || application.regionId || application.zoneId;
|
||
}
|
||
}
|
||
|
||
const response = await onboardingService.getUsers(params);
|
||
if (Array.isArray(response)) {
|
||
setUsers(response);
|
||
} else if (response && Array.isArray(response.data)) {
|
||
setUsers(response.data);
|
||
} else if (response && Array.isArray(response.users)) {
|
||
setUsers(response.users);
|
||
} else {
|
||
console.warn('Unexpected users response:', response);
|
||
setUsers([]);
|
||
}
|
||
} catch (error) {
|
||
console.error('Failed to fetch users', error);
|
||
setUsers([]);
|
||
}
|
||
};
|
||
|
||
useEffect(() => {
|
||
if (showScheduleModal && application) {
|
||
fetchUsers(interviewType);
|
||
|
||
// Auto-fill participants based on pre-assigned evaluators for this level
|
||
const levelNum = parseInt(interviewType.replace('level', '')) || 1;
|
||
const preAssigned = (application?.participants || [])
|
||
.filter((p: any) =>
|
||
p.metadata?.interviewLevel === levelNum ||
|
||
p.metadata?.interviewLevel === String(levelNum) ||
|
||
p.metadata?.allAssignments?.includes(levelNum) ||
|
||
p.metadata?.allAssignments?.includes(String(levelNum))
|
||
)
|
||
.map((p: any) => p.user)
|
||
.filter(Boolean);
|
||
|
||
if (preAssigned.length > 0) {
|
||
// Ensure uniqueness by user ID
|
||
const uniquePreassigned: any[] = [];
|
||
const seenIds = new Set();
|
||
preAssigned.forEach((u: any) => {
|
||
if (u.id && !seenIds.has(u.id)) {
|
||
seenIds.add(u.id);
|
||
uniquePreassigned.push(u);
|
||
}
|
||
});
|
||
setScheduledInterviewParticipants(uniquePreassigned);
|
||
} else {
|
||
setScheduledInterviewParticipants([]);
|
||
}
|
||
} else if ((showAssignArchitectureModal || showAssignModal) && application) {
|
||
fetchUsers(); // Default fetch for other modals like Assign
|
||
}
|
||
}, [showScheduleModal, showAssignArchitectureModal, showAssignModal, interviewType, application?.participants]);
|
||
|
||
const handleScheduleInterview = async () => {
|
||
if (!interviewDate) {
|
||
toast.warning('Please select date and time');
|
||
return;
|
||
}
|
||
try {
|
||
setIsScheduling(true);
|
||
|
||
const payload = {
|
||
applicationId: application?.id,
|
||
level: interviewType,
|
||
scheduledAt: interviewDate,
|
||
type: interviewMode === 'virtual' ? 'Virtual Interview' : 'Physical Interview',
|
||
location: interviewMode === 'virtual' ? meetingLink : location,
|
||
participants: scheduledInterviewParticipants.map(p => p.id)
|
||
};
|
||
|
||
await onboardingService.scheduleInterview(payload);
|
||
toast.success('Interview scheduled successfully');
|
||
setShowScheduleModal(false);
|
||
// Refresh interviews
|
||
await fetchInterviews();
|
||
await fetchApplication(); // Refresh application status
|
||
|
||
} catch (error) {
|
||
toast.error('Failed to schedule interview');
|
||
console.error(error);
|
||
} finally {
|
||
setIsScheduling(false);
|
||
}
|
||
};
|
||
|
||
const handleCancelInterview = async (interviewId: string) => {
|
||
if (!window.confirm('Are you sure you want to cancel this interview?')) return;
|
||
try {
|
||
await onboardingService.updateInterview(interviewId, { status: 'Cancelled' });
|
||
toast.success('Interview cancelled successfully');
|
||
fetchInterviews();
|
||
} catch (error) {
|
||
toast.error('Failed to cancel interview');
|
||
console.error(error);
|
||
}
|
||
};
|
||
|
||
const handleUpload = async () => {
|
||
if (!uploadFile || !uploadDocType) {
|
||
toast.warning('Please select a file and document type');
|
||
return;
|
||
}
|
||
|
||
try {
|
||
setIsUploading(true);
|
||
const formData = new FormData();
|
||
formData.append('file', uploadFile);
|
||
formData.append('documentType', uploadDocType);
|
||
if (selectedStage) {
|
||
formData.append('stage', selectedStage);
|
||
}
|
||
|
||
await onboardingService.uploadDocument(applicationId!, formData);
|
||
|
||
toast.success('Document uploaded successfully');
|
||
setShowUploadForm(false);
|
||
setUploadFile(null);
|
||
setUploadDocType('');
|
||
|
||
// Refresh documents
|
||
const docs = await onboardingService.getDocuments(applicationId);
|
||
setDocuments(docs || []);
|
||
|
||
// Refresh EOR Data in case an EOR document was uploaded
|
||
fetchEorData();
|
||
} catch (error) {
|
||
console.error('Upload failed', error);
|
||
toast.error('Failed to upload document');
|
||
} finally {
|
||
setIsUploading(false);
|
||
}
|
||
};
|
||
|
||
if (loading) {
|
||
return <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 getStageStatus = (stageName: string, fallbackLogic: () => ProcessStage['status']): ProcessStage['status'] => {
|
||
const backendStage = (application.progressTracking || []).find((ps: any) => ps.stageName === stageName);
|
||
if (backendStage && (backendStage.status === 'completed' || backendStage.status === 'active')) {
|
||
return backendStage.status as any;
|
||
}
|
||
return fallbackLogic();
|
||
};
|
||
|
||
const processStages: ProcessStage[] = [
|
||
{
|
||
id: 1,
|
||
name: 'Submitted',
|
||
status: 'completed',
|
||
date: application.submissionDate,
|
||
description: 'Application submitted',
|
||
documentsUploaded: 3
|
||
},
|
||
{
|
||
id: 2,
|
||
name: 'Questionnaire',
|
||
status: getStageStatus('Questionnaire', () => application.questionnaireMarks ? 'completed' : 'pending'),
|
||
date: application.questionnaireDate,
|
||
description: 'Questionnaire completed',
|
||
documentsUploaded: 0
|
||
},
|
||
{
|
||
id: 3,
|
||
name: 'Shortlist',
|
||
status: getStageStatus('Shortlist', () => ['Shortlisted', 'Level 1 Interview Pending', 'Level 1 Approved', 'Level 2 Interview Pending', 'Level 2 Approved', 'Level 2 Recommended', 'Level 3 Interview Pending', 'Level 3 Approved', 'FDD Verification', 'LOI In Progress', 'Payment Pending', 'LOI Issued', 'Statutory LOI Ack', 'Dealer Code Generation', 'Architecture Team Assigned', 'Architecture Document Upload', 'Architecture Team Completion', 'LOA Pending', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Rejected', 'Onboarded'].includes(application.status) ? 'completed' : 'pending'),
|
||
date: application.shortlistDate,
|
||
description: 'Application shortlisted by DD',
|
||
documentsUploaded: 2
|
||
},
|
||
{
|
||
id: 4,
|
||
name: '1st Level Interview',
|
||
status: getStageStatus('1st Level Interview', () => ['Level 1 Approved', 'Level 2 Interview Pending', 'Level 2 Approved', 'Level 2 Recommended', 'Level 3 Interview Pending', 'Level 3 Approved', 'FDD Verification', 'LOI In Progress', 'Payment Pending', 'LOI Issued', 'Statutory LOI Ack', 'Dealer Code Generation', 'Architecture Team Assigned', 'Architecture Document Upload', 'Architecture Team Completion', 'LOA Pending', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Onboarded'].includes(application.status) ? 'completed' : application.status === 'Level 1 Interview Pending' ? 'active' : application.status === 'Rejected' && application.progress >= 40 ? 'completed' : 'pending'),
|
||
date: application.level1InterviewDate,
|
||
description: 'DD-ZM + RBM evaluation',
|
||
evaluators: Array.from(new Set((application.participants || [])
|
||
.filter((p: any) =>
|
||
p.metadata?.interviewLevel === 1 ||
|
||
p.metadata?.interviewLevel === '1' ||
|
||
p.metadata?.allAssignments?.includes(1) ||
|
||
p.metadata?.allAssignments?.includes('1')
|
||
)
|
||
.map((p: any) => `${p.user?.name} (${p.user?.role})`)
|
||
)),
|
||
documentsUploaded: 1
|
||
},
|
||
{
|
||
id: 5,
|
||
name: '2nd Level Interview',
|
||
status: getStageStatus('2nd Level Interview', () => ['Level 2 Approved', 'Level 2 Recommended', 'Level 3 Interview Pending', 'Level 3 Approved', 'FDD Verification', 'LOI In Progress', 'Payment Pending', 'LOI Issued', 'Statutory LOI Ack', 'Dealer Code Generation', 'Architecture Team Assigned', 'Architecture Document Upload', 'Architecture Team Completion', 'LOA Pending', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Onboarded'].includes(application.status) ? 'completed' : application.status === 'Level 2 Interview Pending' ? 'active' : application.status === 'Rejected' && application.progress >= 55 ? 'completed' : 'pending'),
|
||
date: application.level2InterviewDate,
|
||
description: 'DD Lead + ZBH evaluation',
|
||
evaluators: Array.from(new Set((application.participants || [])
|
||
.filter((p: any) =>
|
||
p.metadata?.interviewLevel === 2 ||
|
||
p.metadata?.interviewLevel === '2' ||
|
||
p.metadata?.allAssignments?.includes(2) ||
|
||
p.metadata?.allAssignments?.includes('2')
|
||
)
|
||
.map((p: any) => `${p.user?.name} (${p.user?.role})`)
|
||
)),
|
||
documentsUploaded: 1
|
||
},
|
||
{
|
||
id: 6,
|
||
name: '3rd Level Interview',
|
||
status: getStageStatus('3rd Level Interview', () => ['Level 3 Approved', 'FDD Verification', 'LOI In Progress', 'Payment Pending', 'LOI Issued', 'Statutory LOI Ack', 'Dealer Code Generation', 'Architecture Team Assigned', 'Architecture Document Upload', 'Architecture Team Completion', 'LOA Pending', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Onboarded'].includes(application.status) ? 'completed' : application.status === 'Level 3 Interview Pending' ? 'active' : application.status === 'Rejected' && application.progress >= 70 ? 'completed' : 'pending'),
|
||
date: application.level3InterviewDate,
|
||
description: 'NBH + DD Head evaluation',
|
||
evaluators: Array.from(new Set((application.participants || [])
|
||
.filter((p: any) =>
|
||
p.metadata?.interviewLevel === 3 ||
|
||
p.metadata?.interviewLevel === '3' ||
|
||
p.metadata?.allAssignments?.includes(3) ||
|
||
p.metadata?.allAssignments?.includes('3')
|
||
)
|
||
.map((p: any) => `${p.user?.name} (${p.user?.role})`)
|
||
)),
|
||
documentsUploaded: 2
|
||
},
|
||
{
|
||
id: 7,
|
||
name: 'FDD',
|
||
status: getStageStatus('FDD', () => ['LOI In Progress', 'Payment Pending', 'LOI Issued', 'Statutory LOI Ack', 'Dealer Code Generation', 'Architecture Team Assigned', 'Architecture Document Upload', 'Architecture Team Completion', 'LOA Pending', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Onboarded'].includes(application.status) ? 'completed' : application.status === 'FDD Verification' ? 'active' : 'pending'),
|
||
date: application.fddDate,
|
||
description: 'Financial Due Diligence',
|
||
documentsUploaded: 5
|
||
},
|
||
{
|
||
id: 8,
|
||
name: 'LOI Approval',
|
||
status: getStageStatus('LOI Approval', () => ['Payment Pending', 'LOI Issued', 'Statutory LOI Ack', 'Dealer Code Generation', 'Architecture Team Assigned', 'Architecture Document Upload', 'Architecture Team Completion', 'LOA Pending', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Onboarded'].includes(application.status) ? 'completed' : application.status === 'LOI In Progress' ? 'active' : 'pending'),
|
||
date: application.loiApprovalDate,
|
||
description: 'Letter of Intent approval',
|
||
evaluators: Array.from(new Set((application.participants || [])
|
||
.filter((p: any) =>
|
||
p.metadata?.stageCode === 'LOI_APPROVAL' ||
|
||
p.metadata?.allAssignments?.includes('LOI_APPROVAL')
|
||
)
|
||
.map((p: any) => `${p.user?.name} (${p.user?.role})`)
|
||
)),
|
||
documentsUploaded: 1
|
||
},
|
||
{
|
||
id: 9,
|
||
name: 'Security Details',
|
||
status: getStageStatus('Security Details', () => ['LOI Issued', 'Statutory LOI Ack', 'Dealer Code Generation', 'Architecture Team Assigned', 'Architecture Document Upload', 'Architecture Team Completion', 'LOA Pending', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Onboarded'].includes(application.status) ? 'completed' : application.status === 'Payment Pending' ? 'active' : 'pending'),
|
||
date: application.securityDetailsDate,
|
||
description: 'Security verification',
|
||
documentsUploaded: 3
|
||
},
|
||
{
|
||
id: 10,
|
||
name: 'LOI Issue',
|
||
status: getStageStatus('LOI Issue', () => ['Statutory LOI Ack', 'Dealer Code Generation', 'Architecture Team Assigned', 'Architecture Document Upload', 'Architecture Team Completion', 'LOA Pending', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Onboarded'].includes(application.status) ? 'completed' : application.status === 'LOI Issued' ? 'active' : 'pending'),
|
||
date: application.loiIssueDate,
|
||
description: 'Letter of Intent issued',
|
||
documentsUploaded: 1
|
||
},
|
||
{
|
||
id: 11,
|
||
name: 'Dealer Code Generation',
|
||
status: getStageStatus('Dealer Code Generation', () => (application.dealerCode || ['Architecture Team Assigned', 'Architecture Document Upload', 'Architecture Team Completion', 'Statutory GST', 'Statutory PAN', 'Statutory NODAL', 'Statutory NODAL', 'Statutory Check', 'Statutory Partnership', 'Statutory Firm Reg', 'Statutory Rental', 'Statutory Virtual Code', 'Statutory Domain', 'Statutory MSD', 'Statutory LOI Ack', 'LOA Pending', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Onboarded'].includes(application.status)) ? 'completed' : application.status === 'Dealer Code Generation' ? 'active' : 'pending'),
|
||
date: application.dealerCodeDate,
|
||
description: 'Dealer code generated and assigned',
|
||
documentsUploaded: 0,
|
||
isParallel: true,
|
||
branches: [
|
||
{
|
||
name: 'Architectural Work',
|
||
color: 'blue',
|
||
stages: [
|
||
{
|
||
id: '11a-1',
|
||
name: 'Assigned to Architecture Team',
|
||
status: application.architectureAssignedTo || ['Architecture Document Upload', 'Architecture Team Completion', 'Statutory GST', 'Statutory PAN', 'Statutory Nodal', 'Statutory Check', 'Statutory Partnership', 'Statutory Firm Reg', 'Statutory Rental', 'Statutory Virtual Code', 'Statutory Domain', 'Statutory MSD', 'Statutory LOI Ack', 'LOA Pending', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Onboarded'].includes(application.status) || application.architectureStatus === 'COMPLETED' || isDocumentUploaded('Architecture Assignment Document') ? 'completed' : application.status === 'Architecture Team Assigned' ? 'active' : 'pending',
|
||
date: application.architectureAssignedDate,
|
||
description: 'Assigned to architecture team for site planning',
|
||
documentsUploaded: 0
|
||
},
|
||
{
|
||
id: '11a-2',
|
||
name: 'Architectural Document Upload',
|
||
status: isDocumentUploaded('Architecture Blueprint') || isDocumentUploaded('Site Plan') || ['Architecture Team Completion', 'LOA Pending', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Onboarded'].includes(application.status) || application.architectureStatus === 'COMPLETED' ? 'completed' : (application.architectureAssignedTo || application.status === 'Architecture Document Upload' || application.architectureStatus === 'IN_PROGRESS') ? 'active' : 'pending',
|
||
date: application.architectureDocumentDate,
|
||
description: 'Architectural documents and blueprints uploaded',
|
||
documentsUploaded: (documents || []).filter(d => ['Architecture Blueprint', 'Site Plan'].includes(d.documentType)).length
|
||
},
|
||
{
|
||
id: '11a-3',
|
||
name: 'Architecture Team Completion',
|
||
status: ['LOA Pending', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Onboarded'].includes(application.status) || application.architectureStatus === 'COMPLETED' || isDocumentUploaded('Architecture Completion Certificate') ? 'completed' : application.status === 'Architecture Team Completion' ? 'active' : 'pending',
|
||
date: application.architectureCompletionDate,
|
||
description: 'Architecture team work completed',
|
||
documentsUploaded: 0
|
||
}
|
||
]
|
||
},
|
||
{
|
||
name: 'Statutory Documents',
|
||
color: 'green',
|
||
stages: [
|
||
{
|
||
id: '11b-1',
|
||
name: 'GST',
|
||
status: isDocumentUploaded('GST Certificate') || ['Statutory PAN', 'Statutory Nodal', 'Statutory Check', 'Statutory Partnership', 'Statutory Firm Reg', 'Statutory Rental', 'Statutory Virtual Code', 'Statutory Domain', 'Statutory MSD', 'Statutory LOI Ack', 'LOA Pending', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Onboarded'].includes(application.status) ? 'completed' : application.status === 'Statutory GST' ? 'active' : 'pending',
|
||
description: 'GST certificate',
|
||
documentsUploaded: (documents || []).filter(d => d.documentType === 'GST Certificate').length
|
||
},
|
||
{
|
||
id: '11b-2',
|
||
name: 'PAN',
|
||
status: isDocumentUploaded('PAN Card') || ['Statutory Nodal', 'Statutory Check', 'Statutory Partnership', 'Statutory Firm Reg', 'Statutory Rental', 'Statutory Virtual Code', 'Statutory Domain', 'Statutory MSD', 'Statutory LOI Ack', 'LOA Pending', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Onboarded'].includes(application.status) ? 'completed' : application.status === 'Statutory PAN' ? 'active' : 'pending',
|
||
description: 'PAN card',
|
||
documentsUploaded: (documents || []).filter(d => d.documentType === 'PAN Card').length
|
||
},
|
||
{
|
||
id: '11b-3',
|
||
name: 'Nodal Agreement',
|
||
status: isDocumentUploaded('Nodal Agreement') || ['Statutory Check', 'Statutory Partnership', 'Statutory Firm Reg', 'Statutory Rental', 'Statutory Virtual Code', 'Statutory Domain', 'Statutory MSD', 'Statutory LOI Ack', 'LOA Pending', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Onboarded'].includes(application.status) ? 'completed' : application.status === 'Statutory Nodal' ? 'active' : 'pending',
|
||
description: 'Nodal agreement document',
|
||
documentsUploaded: (documents || []).filter(d => d.documentType === 'Nodal Agreement').length
|
||
},
|
||
{
|
||
id: '11b-4',
|
||
name: 'Cancelled Check',
|
||
status: isDocumentUploaded('Cancelled Check') || ['Statutory Partnership', 'Statutory Firm Reg', 'Statutory Rental', 'Statutory Virtual Code', 'Statutory Domain', 'Statutory MSD', 'Statutory LOI Ack', 'LOA Pending', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Onboarded'].includes(application.status) ? 'completed' : application.status === 'Statutory Check' ? 'active' : 'pending',
|
||
description: 'Cancelled check copy',
|
||
documentsUploaded: (documents || []).filter(d => d.documentType === 'Cancelled Check').length
|
||
},
|
||
{
|
||
id: '11b-5',
|
||
name: 'Partnership Deed/LLP/MOA/AOA/COI',
|
||
status: isDocumentUploaded('Partnership Deed') || isDocumentUploaded('LLP Agreement') || isDocumentUploaded('Certificate of Incorporation') || isDocumentUploaded('MOA') || isDocumentUploaded('AOA') || ['Statutory Firm Reg', 'Statutory Rental', 'Statutory Virtual Code', 'Statutory Domain', 'Statutory MSD', 'Statutory LOI Ack', 'LOA Pending', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Onboarded'].includes(application.status) ? 'completed' : application.status === 'Statutory Partnership' ? 'active' : 'pending',
|
||
description: 'Business entity documents',
|
||
documentsUploaded: (documents || []).filter(d => ['Partnership Deed', 'LLP Agreement', 'Certificate of Incorporation', 'MOA', 'AOA'].includes(d.documentType)).length
|
||
},
|
||
{
|
||
id: '11b-6',
|
||
name: 'Firm Registration Certificate',
|
||
status: isDocumentUploaded('Firm Registration') || ['Statutory Rental', 'Statutory Virtual Code', 'Statutory Domain', 'Statutory MSD', 'Statutory LOI Ack', 'LOA Pending', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Onboarded'].includes(application.status) ? 'completed' : application.status === 'Statutory Firm Reg' ? 'active' : 'pending',
|
||
description: 'Firm registration certificate',
|
||
documentsUploaded: (documents || []).filter(d => d.documentType === 'Firm Registration').length
|
||
},
|
||
{
|
||
id: '11b-7',
|
||
name: 'Rental agreement/ Lease agreement / Own/ Land agreement',
|
||
status: isDocumentUploaded('Rental Agreement') || isDocumentUploaded('Property Documents') || ['Statutory Virtual Code', 'Statutory Domain', 'Statutory MSD', 'Statutory LOI Ack', 'LOA Pending', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Onboarded'].includes(application.status) ? 'completed' : application.status === 'Statutory Rental' ? 'active' : 'pending',
|
||
description: 'Property agreement document',
|
||
documentsUploaded: (documents || []).filter(d => ['Rental Agreement', 'Property Documents'].includes(d.documentType)).length
|
||
},
|
||
{
|
||
id: '11b-8',
|
||
name: 'Virtual Code',
|
||
status: isDocumentUploaded('Virtual Code Confirmation') || ['Statutory Domain', 'Statutory MSD', 'Statutory LOI Ack', 'LOA Pending', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Onboarded'].includes(application.status) ? 'completed' : application.status === 'Statutory Virtual Code' ? 'active' : 'pending',
|
||
description: 'Virtual code availability',
|
||
documentsUploaded: (documents || []).filter(d => d.documentType === 'Virtual Code Confirmation').length
|
||
},
|
||
{
|
||
id: '11b-9',
|
||
name: 'Domain ID',
|
||
status: isDocumentUploaded('Domain ID Setup') || ['Statutory MSD', 'Statutory LOI Ack', 'LOA Pending', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Onboarded'].includes(application.status) ? 'completed' : application.status === 'Statutory Domain' ? 'active' : 'pending',
|
||
description: 'Domain ID setup',
|
||
documentsUploaded: (documents || []).filter(d => d.documentType === 'Domain ID Setup').length
|
||
},
|
||
{
|
||
id: '11b-10',
|
||
name: 'MSD Configuration',
|
||
status: isDocumentUploaded('MSD Configuration') || ['Statutory LOI Ack', 'LOA Pending', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Onboarded'].includes(application.status) ? 'completed' : application.status === 'Statutory MSD' ? 'active' : 'pending',
|
||
description: 'Microsoft Dynamics configuration',
|
||
documentsUploaded: (documents || []).filter(d => d.documentType === 'MSD Configuration').length
|
||
},
|
||
{
|
||
id: '11b-11',
|
||
name: 'LOI Acknowledgement Copy',
|
||
status: isDocumentUploaded('LOI Acknowledgement') || ['LOA Pending', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Onboarded'].includes(application.status) ? 'completed' : application.status === 'Statutory LOI Ack' ? 'active' : 'pending',
|
||
description: 'LOI acknowledgement copy',
|
||
documentsUploaded: (documents || []).filter(d => d.documentType === 'LOI Acknowledgement').length
|
||
}
|
||
]
|
||
}
|
||
]
|
||
},
|
||
{
|
||
id: 12,
|
||
name: 'LOA',
|
||
status: getStageStatus('LOA', () => ['EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Onboarded'].includes(application.status) ? 'completed' : application.status === 'LOA Pending' ? 'active' : 'pending'),
|
||
isLocked: application.status === 'LOA Pending' &&
|
||
getDeposit('FINAL')?.status !== 'Verified' &&
|
||
!documents.some(d => (d.documentType?.toLowerCase().includes('final') && d.documentType?.toLowerCase().includes('deposit')) && d.status === 'Approved'),
|
||
lockMessage: 'Final Security Deposit (₹15L) must be verified by Finance before LOA Approval.',
|
||
date: application.loaDate,
|
||
description: 'Letter of Authorization',
|
||
evaluators: Array.from(new Set((application.participants || [])
|
||
.filter((p: any) =>
|
||
p.metadata?.stageCode === 'LOA_APPROVAL' ||
|
||
p.metadata?.allAssignments?.includes('LOA_APPROVAL')
|
||
)
|
||
.map((p: any) => `${p.user?.name} (${p.user?.role})`)
|
||
)),
|
||
documentsUploaded: 1
|
||
},
|
||
{
|
||
id: 13,
|
||
name: 'EOR Complete',
|
||
status: getStageStatus('EOR Complete', () => ['Inauguration', 'Approved', 'Onboarded'].includes(application.status) ? 'completed' : application.status === 'EOR Complete' ? 'active' : 'pending'),
|
||
date: application.eorCompleteDate,
|
||
description: 'Essential Operating Requirements completed',
|
||
documentsUploaded: 6
|
||
},
|
||
{
|
||
id: 14,
|
||
name: 'Inauguration',
|
||
status: getStageStatus('Inauguration', () => ['Approved', 'Onboarded'].includes(application.status) ? 'completed' : application.status === 'Inauguration' ? 'active' : 'pending'),
|
||
date: application.inaugurationDate,
|
||
description: 'Dealership inauguration ceremony',
|
||
documentsUploaded: 2
|
||
},
|
||
{
|
||
id: 15,
|
||
name: 'Dealership Active',
|
||
status: getStageStatus('Onboarded', () => application.status === 'Onboarded' ? 'completed' : 'pending'),
|
||
date: application.onboardedDate,
|
||
description: 'Dealer profile and login created'
|
||
}
|
||
];
|
||
|
||
const eorChecklist = [
|
||
{ id: 1, item: 'Sales Standards', completed: false },
|
||
{ id: 2, item: 'Service & Spares', completed: false },
|
||
{ id: 3, item: 'DMS infra', completed: false },
|
||
{ id: 4, item: 'Manpower Training', completed: false },
|
||
{ id: 5, item: 'Trade certificate with test ride bikes registration', completed: false },
|
||
{ id: 6, item: 'GST certificate including Accessories & Apparels billing', completed: false },
|
||
{ id: 7, item: 'Inventory Funding', completed: false },
|
||
{ id: 8, item: 'Virtual code availability', completed: false },
|
||
{ id: 9, item: 'Vendor payments', completed: false },
|
||
{ id: 10, item: 'Details for website submission', completed: false },
|
||
{ id: 11, item: 'Infra Insurance both Showroom and Service center', completed: false },
|
||
{ id: 12, item: 'Auto ordering', completed: false }
|
||
];
|
||
|
||
const flattenedStages: any[] = processStages.reduce((acc: any[], stage) => {
|
||
acc.push({ name: stage.name });
|
||
if (stage.branches) {
|
||
stage.branches.forEach((branch: any) => {
|
||
branch.stages.forEach((subStage: any) => {
|
||
acc.push({ name: subStage.name, parentBranch: branch.name });
|
||
});
|
||
});
|
||
}
|
||
if (stage.name === 'EOR In Progress' || stage.name === 'EOR Complete') {
|
||
(eorData?.items || eorChecklist).forEach((item: any) => {
|
||
acc.push({ name: `EOR: ${item.description || item.item}`, parentBranch: 'EOR' });
|
||
});
|
||
}
|
||
return acc;
|
||
}, []);
|
||
|
||
|
||
|
||
|
||
|
||
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) {
|
||
return <div className="flex justify-center items-center h-96">Loading...</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 || '');
|
||
|
||
// Final visibility flags
|
||
const isAdmin = currentUser && ['DD Admin', 'Super Admin', 'NBH', 'DD Lead', 'DD Head', 'Finance'].includes(currentUser.role);
|
||
const isAdministrativeStage = [
|
||
'Level 3 Approved', 'FDD Verification',
|
||
'LOI In Progress', 'LOI Issued', 'Statutory LOI Ack',
|
||
'Architecture Team Assigned', 'Architecture Document Upload', 'Architecture Team Completion',
|
||
'Statutory GST', 'Statutory PAN', 'Statutory Nodal', 'Statutory Check',
|
||
'Statutory Partnership', 'Statutory Firm Reg', 'Statutory Rental',
|
||
'Statutory Virtual Code', 'Statutory Domain', 'Statutory MSD',
|
||
'LOA Pending', 'EOR Complete', 'Inauguration'
|
||
].includes(application.status);
|
||
|
||
const finalDepositVerified = getDeposit('FINAL')?.status === 'Verified';
|
||
const isLoaLocked = application.status === 'LOA Pending' && !finalDepositVerified;
|
||
|
||
// Show Approve/Reject if:
|
||
// 1. It's an interview and feedback is submitted AND no decision made yet
|
||
// 2. OR it's an administrative stage and user is Admin AND hasn't made a decision yet
|
||
const shouldShowApproveReject =
|
||
!isLoaLocked && (
|
||
(!hasMadeDecisionForUser && hasSubmittedFeedbackForActive) ||
|
||
(isAdmin && isAdministrativeStage && !hasMadeStageDecision)
|
||
);
|
||
|
||
const shouldShowDecisionMessage = hasMadeDecisionForUser && (!isAdministrativeStage || hasMadeStageDecision);
|
||
|
||
|
||
|
||
|
||
const renderFddAuditContent = () => {
|
||
const assignments = application?.fddAssignments || [];
|
||
|
||
if (assignments.length === 0) {
|
||
return (
|
||
<div className="flex flex-col items-center justify-center p-12 bg-slate-50 rounded-xl 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">No FDD Assignment</h3>
|
||
<p className="text-slate-500 text-sm text-center max-w-xs mt-2">
|
||
The Financial Due Diligence process has not been initiated for this application yet.
|
||
</p>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<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">
|
||
<div>
|
||
<Label className="text-[10px] text-slate-400 uppercase tracking-widest font-black mb-2 block">Auditor Recommendation</Label>
|
||
<div className={cn(
|
||
"inline-flex items-center gap-2 px-3 py-1.5 rounded-full border text-xs font-black shadow-sm",
|
||
report.recommendation === 'Green' ? "bg-green-50 border-green-200 text-green-700" :
|
||
report.recommendation === 'Amber' ? "bg-amber-50 border-amber-200 text-amber-700" :
|
||
"bg-red-50 border-red-200 text-red-700"
|
||
)}>
|
||
<div className={cn("w-2 h-2 rounded-full",
|
||
report.recommendation === 'Green' ? "bg-green-500" :
|
||
report.recommendation === 'Amber' ? "bg-amber-500" : "bg-red-500"
|
||
)} />
|
||
{report.recommendation?.toUpperCase()}
|
||
</div>
|
||
</div>
|
||
|
||
<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-900 font-bold text-sm truncate max-w-[180px]">{report.reportDocument.fileName}</p>
|
||
<p className="text-slate-500 text-[10px] font-medium">SUBMITTED {new Date(report.createdAt).toLocaleDateString()}</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 items-center gap-3">
|
||
{report.verifiedAt ? (
|
||
<div className="flex items-center gap-2 bg-green-50 border border-green-100 px-3 py-1.5 rounded-full">
|
||
<CheckCircle className="w-4 h-4 text-green-600" />
|
||
<span className="text-[10px] font-black text-green-700 uppercase">Audit Verified by Finance</span>
|
||
</div>
|
||
) : (
|
||
<div className="flex items-center gap-2 bg-amber-50 border border-amber-100 px-3 py-1.5 rounded-full">
|
||
<Clock className="w-4 h-4 text-amber-600" />
|
||
<span className="text-[10px] font-black text-amber-700 uppercase">Pending Finance Review</span>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
))}
|
||
</div>
|
||
)}
|
||
</CardContent>
|
||
</Card>
|
||
))}
|
||
</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">
|
||
<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>
|
||
</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-sm'
|
||
: 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' ? (
|
||
<Check className="w-5 h-5" />
|
||
) : 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> = {
|
||
4: 2, // L1 Interview (ZM + RBM)
|
||
5: 2, // L2 Interview (ZBH + DD Lead)
|
||
6: 2, // L3 Interview (NBH + DD Head)
|
||
8: 3, // LOI Approval (Finance + DD Head + NBH)
|
||
12: 2 // LOA Approval (DD Head + NBH)
|
||
};
|
||
const stageId = Number(stage.id);
|
||
const expectedCount = expectedMap[stageId];
|
||
const actualCount = stage.evaluators?.length || 0;
|
||
|
||
if (expectedCount && actualCount < expectedCount && application.status !== 'Rejected') {
|
||
return (
|
||
<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);
|
||
}}
|
||
className="text-xs font-medium text-amber-700 hover:text-amber-800 flex items-center gap-1 bg-amber-50 px-2 py-0.5 rounded border border-amber-200"
|
||
>
|
||
<FileText className="w-3 h-3" />
|
||
{stageDocsCount > 0 ? `${stageDocsCount} Documents Uploaded` : 'Upload Document'}
|
||
</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">
|
||
<button
|
||
onClick={() => setExpandedBranches(prev => ({
|
||
...prev,
|
||
[branchKey]: !prev[branchKey]
|
||
}))}
|
||
className={`w-full 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`}>
|
||
{branch.name}
|
||
</p>
|
||
<p className={`text-xs ${branchColor === 'blue' ? 'text-blue-700' : 'text-green-700'}`}>
|
||
{branch.stages.length} steps
|
||
</p>
|
||
</div>
|
||
</button>
|
||
|
||
{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">
|
||
<div className="relative">
|
||
<div className={`w-8 h-8 rounded-full flex items-center justify-center border-2 ${branchStage.status === 'completed'
|
||
? `${branchColor === 'blue' ? 'bg-blue-500 border-blue-500' : 'bg-green-500 border-green-500'}`
|
||
: branchStage.status === 'active'
|
||
? 'bg-amber-500 border-amber-500'
|
||
: 'bg-slate-200 border-slate-300'
|
||
}`}>
|
||
{branchStage.status === 'completed' ? (
|
||
<Check className="w-4 h-4 text-white" />
|
||
) : branchStage.status === 'active' ? (
|
||
<Clock className="w-4 h-4 text-white" />
|
||
) : (
|
||
<div className="w-2 h-2 bg-slate-400 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>
|
||
)}
|
||
|
||
{(() => {
|
||
const branchDocsCount = documents.filter(doc =>
|
||
doc.documentType?.toLowerCase().includes(branchStage.name.toLowerCase().split(' ')[0]) ||
|
||
doc.stage === branchStage.name
|
||
).length;
|
||
|
||
return (
|
||
<div className="flex items-center gap-2 mt-1">
|
||
<button
|
||
onClick={() => {
|
||
setSelectedStage(branchStage.name);
|
||
setShowDocumentsModal(true);
|
||
}}
|
||
className="text-[10px] font-medium text-blue-700 hover:text-blue-800 flex items-center gap-1 bg-blue-50 px-1.5 py-0.5 rounded border border-blue-100"
|
||
>
|
||
<FileText className="w-2.5 h-2.5" />
|
||
{branchDocsCount > 0 ? `${branchDocsCount} Docs` : 'Upload'}
|
||
</button>
|
||
</div>
|
||
);
|
||
})()}
|
||
<p className="text-slate-400 text-[10px] mt-1">
|
||
{branchStage.status === 'completed' && branchStage.date && `Done: ${formatDateTime(branchStage.date)}`}
|
||
{branchStage.status === 'active' && 'Evaluating'}
|
||
{branchStage.status === 'pending' && '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);
|
||
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>{new Date(doc.createdAt).toLocaleDateString()}</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">
|
||
({new Date(interview.scheduleDate).toLocaleDateString()} - {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('INITIAL');
|
||
const config = paymentConfigs.INITIAL_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">Advance Payment</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>{new Date(deposit.verifiedAt).toLocaleDateString()}</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>
|
||
)}
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
);
|
||
})()}
|
||
|
||
{/* Final Security Deposit */}
|
||
{(() => {
|
||
const deposit = getDeposit('FINAL');
|
||
const config = paymentConfigs.FINAL_SECURITY_DEPOSIT;
|
||
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">Final 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>{new Date(deposit.verifiedAt).toLocaleDateString()}</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>
|
||
)}
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
);
|
||
})()}
|
||
</div>
|
||
|
||
{/* Payment Proof Documents */}
|
||
<Card className="mt-6">
|
||
<CardHeader className="pb-3">
|
||
<CardTitle className="text-sm font-medium flex items-center gap-2">
|
||
<FileText className="w-4 h-4 text-amber-600" />
|
||
Verification Documents
|
||
</CardTitle>
|
||
</CardHeader>
|
||
<CardContent>
|
||
{documents.filter((d: any) => d.documentType?.toLowerCase().includes('deposit')).length > 0 ? (
|
||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
|
||
{documents.filter((d: any) => d.documentType?.toLowerCase().includes('deposit')).map((doc: any, index: number) => (
|
||
<div key={index} className="flex items-center justify-between p-3 border border-slate-100 rounded-lg bg-slate-50/50 hover:bg-slate-50 transition-colors">
|
||
<div className="flex items-center gap-3 overflow-hidden">
|
||
<div className="flex-shrink-0 w-8 h-8 rounded bg-white flex items-center justify-center border border-slate-200">
|
||
<FileText className="w-4 h-4 text-slate-400" />
|
||
</div>
|
||
<div className="overflow-hidden">
|
||
<p className="text-xs font-medium text-slate-900 truncate">{doc.fileName || doc.name}</p>
|
||
<p className="text-[10px] text-slate-500 uppercase">{doc.documentType}</p>
|
||
</div>
|
||
</div>
|
||
<Button
|
||
variant="ghost"
|
||
size="sm"
|
||
className="h-7 text-amber-600 hover:text-amber-700 hover:bg-amber-50"
|
||
onClick={() => {
|
||
setPreviewDoc(doc);
|
||
setShowPreviewModal(true);
|
||
}}
|
||
>
|
||
View
|
||
</Button>
|
||
</div>
|
||
))}
|
||
</div>
|
||
) : (
|
||
<div className="text-center py-6 border-2 border-dashed border-slate-100 rounded-lg">
|
||
<p className="text-sm text-slate-400 font-medium italic">No payment proofs uploaded yet.</p>
|
||
</div>
|
||
)}
|
||
</CardContent>
|
||
</Card>
|
||
</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">
|
||
{new Date(log.timestamp).toLocaleString()}
|
||
</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">{new Date(application.deadline).toLocaleDateString()}</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 */}
|
||
{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">
|
||
Final Security Deposit (₹15L) must be verified by Finance before LOA Approval can proceed.
|
||
</AlertDescription>
|
||
</Alert>
|
||
)}
|
||
|
||
{shouldShowApproveReject && (
|
||
<>
|
||
<Button
|
||
className="w-full bg-green-600 hover:bg-green-700"
|
||
onClick={() => setShowApproveModal(true)}
|
||
>
|
||
<CheckCircle className="w-4 h-4 mr-2" />
|
||
Approve
|
||
</Button>
|
||
|
||
<Button
|
||
variant="destructive"
|
||
className="w-full"
|
||
onClick={() => setShowRejectModal(true)}
|
||
>
|
||
<XCircle className="w-4 h-4 mr-2" />
|
||
Reject
|
||
</Button>
|
||
</>
|
||
)}
|
||
|
||
{(shouldShowDecisionMessage) && (
|
||
<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>
|
||
|
||
{currentUser && ['DD Admin', 'Super Admin', 'DD AM', 'ASM'].includes(currentUser.role) &&
|
||
!([1, 2, 3].every(level => interviews.some(i => i.level === level))) && (
|
||
<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>
|
||
)}
|
||
</div >
|
||
</div >
|
||
|
||
{/* 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-3xl max-h-[90vh] overflow-y-auto">
|
||
<DialogHeader>
|
||
<DialogTitle>Fill KT Matrix</DialogTitle>
|
||
<DialogDescription>
|
||
Complete the Knowledge Transfer Matrix evaluation for the applicant.
|
||
</DialogDescription>
|
||
</DialogHeader>
|
||
<div className="space-y-4">
|
||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||
{KT_MATRIX_CRITERIA.map((criterion) => (
|
||
<div key={criterion.name}>
|
||
<Label>{criterion.name} (Weightage: {criterion.weight}%)</Label>
|
||
<Select
|
||
onValueChange={(value) => {
|
||
const selectedOption = criterion.options.find(o => o.value === value);
|
||
if (selectedOption) {
|
||
handleKTMatrixChange(criterion.name, selectedOption.score);
|
||
}
|
||
}}
|
||
>
|
||
<SelectTrigger className="mt-2">
|
||
<SelectValue placeholder="Select option" />
|
||
</SelectTrigger>
|
||
<SelectContent>
|
||
{criterion.options.map((option) => (
|
||
<SelectItem key={option.value} value={option.value}>
|
||
{option.label} ({option.score} points)
|
||
</SelectItem>
|
||
))}
|
||
</SelectContent>
|
||
</Select>
|
||
</div>
|
||
))}
|
||
</div>
|
||
|
||
<Separator />
|
||
|
||
<div className="bg-slate-50 p-4 rounded-lg">
|
||
<p className="text-slate-700">
|
||
<span className="font-medium">Total Weightage:</span> 100%
|
||
</p>
|
||
<p className="text-slate-700 text-lg font-bold mt-2">
|
||
Current Score: {calculateKTScore()}/100
|
||
</p>
|
||
<p className="text-slate-600 text-sm mt-1">
|
||
All parameters will be scored and calculated automatically based on the selected options.
|
||
</p>
|
||
</div>
|
||
|
||
<div>
|
||
<Label>Remarks</Label>
|
||
<Textarea
|
||
placeholder="Enter additional remarks, observations, and recommendations..."
|
||
className="mt-2"
|
||
rows={4}
|
||
value={ktMatrixRemarks}
|
||
onChange={(e) => setKtMatrixRemarks(e.target.value)}
|
||
/>
|
||
</div>
|
||
|
||
<div className="flex gap-3">
|
||
<Button
|
||
variant="outline"
|
||
className="flex-1"
|
||
onClick={() => setShowKTMatrixModal(false)}
|
||
disabled={isSubmittingKT}
|
||
>
|
||
Cancel
|
||
</Button>
|
||
<Button
|
||
className="flex-1 bg-black hover:bg-zinc-800 text-white"
|
||
onClick={handleSubmitKTMatrix}
|
||
disabled={isSubmittingKT}
|
||
>
|
||
{isSubmittingKT ? 'Submitting...' : 'Submit KT Matrix'}
|
||
</Button>
|
||
</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">
|
||
{new Date(doc.createdAt).toLocaleDateString('en-GB')}
|
||
</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 STAGE_DOCUMENT_MAP: Record<string, string[]> = {
|
||
'GST Certificate': ['GST Certificate'],
|
||
'PAN': ['PAN Card'],
|
||
'Nodal Agreement': ['Nodal Agreement'],
|
||
'Cancelled Check': ['Cancelled Check'],
|
||
'Partnership Deed/LLP/MOA/AOA/COI': ['Partnership Deed', 'LLP Agreement', 'Certificate of Incorporation', 'MOA', 'AOA', 'Board Resolution'],
|
||
'Firm Registration Certificate': ['Firm Registration'],
|
||
'Rental agreement/ Lease agreement / Own/ Land agreement': ['Rental Agreement', 'Property Documents'],
|
||
'Virtual Code': ['Virtual Code Confirmation'],
|
||
'Domain ID': ['Domain ID Setup'],
|
||
'MSD Configuration': ['MSD Configuration'],
|
||
'LOI Acknowledgement Copy': ['LOI Acknowledgement'],
|
||
'Architecture Assignment Document': ['Architecture Assignment Document'],
|
||
'Architecture Blueprint': ['Architecture Blueprint'],
|
||
'Architecture Completion Certificate': ['Architecture Completion Certificate'],
|
||
'Site Plan': ['Site Plan'],
|
||
'FDD': ['FDD Final Audit Report', 'FDD Agency Assignment Letter', 'Statutory Approval Certificate'],
|
||
'FDD Verification': ['FDD Final Audit Report', 'FDD Agency Assignment Letter', 'Statutory Approval Certificate'],
|
||
'LOA': ['LOA Acceptance Copy', 'Final Security Deposit Receipt'],
|
||
'LOI Approval': ['Initial Security Deposit Receipt'],
|
||
'LOA Approval': ['Final Security Deposit Receipt'],
|
||
'LOA Acknowledgement': ['Final Security Deposit Receipt'],
|
||
'Inauguration': ['Inauguration Photos', 'Inauguration Report'],
|
||
'3rd Level Interview': ['AI Recommendation Summary', 'Interview Evaluation Sheet'],
|
||
'2nd Level Interview': ['Interview Evaluation Sheet'],
|
||
'1st Level Interview': ['Interview Evaluation Sheet'],
|
||
'Shortlist': ['CIBIL Report', 'Proposed Site City Map', 'PAN Card', 'GST Certificate', 'Aadhaar']
|
||
};
|
||
|
||
const baseDocs = ['Other'];
|
||
let filteredDocs: string[] = [];
|
||
|
||
if (!selectedStage) {
|
||
filteredDocs = ['PAN Card', 'GST Certificate', 'Aadhaar', 'Partnership Deed', 'LLP Agreement', 'Certificate of Incorporation', 'MOA', 'AOA', 'Board Resolution', 'Initial Security Deposit Receipt (₹2L)', 'Final Security Deposit Receipt (₹15L)', 'Rental Agreement', 'Property Documents', 'Bank Statement', 'Cancelled Check', 'Other'];
|
||
} else {
|
||
const stageName = selectedStage as string;
|
||
if (stageName.startsWith('EOR: ')) {
|
||
filteredDocs = [stageName.replace('EOR: ', ''), 'Other'];
|
||
} else {
|
||
filteredDocs = [...(STAGE_DOCUMENT_MAP[stageName] || []), ...baseDocs];
|
||
}
|
||
}
|
||
|
||
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}
|
||
/>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
export default ApplicationDetails;
|