application detail screen refactored need to test end to end
This commit is contained in:
parent
2a0fe71584
commit
b56a78d621
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,347 @@
|
|||||||
|
import {
|
||||||
|
Award,
|
||||||
|
Bike,
|
||||||
|
Building2,
|
||||||
|
ClipboardList,
|
||||||
|
CreditCard,
|
||||||
|
GraduationCap,
|
||||||
|
Loader2,
|
||||||
|
Mail,
|
||||||
|
MapPin,
|
||||||
|
Pencil,
|
||||||
|
Phone,
|
||||||
|
User,
|
||||||
|
} from 'lucide-react';
|
||||||
|
import { Application } from '../../../lib/mock-data';
|
||||||
|
import { Button } from '../../ui/button';
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from '../../ui/card';
|
||||||
|
import { Input } from '../../ui/input';
|
||||||
|
import { Label } from '../../ui/label';
|
||||||
|
import { Separator } from '../../ui/separator';
|
||||||
|
|
||||||
|
interface StatutoryFormState {
|
||||||
|
accountHolderName: string;
|
||||||
|
panNumber: string;
|
||||||
|
gstNumber: string;
|
||||||
|
registeredAddress: string;
|
||||||
|
bankName: string;
|
||||||
|
accountNumber: string;
|
||||||
|
ifscCode: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ApplicantInformationCardProps {
|
||||||
|
application: Application;
|
||||||
|
canEditStatutory: boolean;
|
||||||
|
isEditingStatutory: boolean;
|
||||||
|
isSavingStatutory: boolean;
|
||||||
|
statutoryForm: StatutoryFormState;
|
||||||
|
onEditFirmType: () => void;
|
||||||
|
onEditStatutory: () => void;
|
||||||
|
onCancelEditStatutory: () => void;
|
||||||
|
onSaveStatutory: () => void;
|
||||||
|
onStatutoryFormChange: (nextState: StatutoryFormState) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ApplicantInformationCard({
|
||||||
|
application,
|
||||||
|
canEditStatutory,
|
||||||
|
isEditingStatutory,
|
||||||
|
isSavingStatutory,
|
||||||
|
statutoryForm,
|
||||||
|
onEditFirmType,
|
||||||
|
onEditStatutory,
|
||||||
|
onCancelEditStatutory,
|
||||||
|
onSaveStatutory,
|
||||||
|
onStatutoryFormChange,
|
||||||
|
}: ApplicantInformationCardProps) {
|
||||||
|
return (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Applicant Information</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="p-4 sm:p-6 space-y-4">
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 xl:grid-cols-3 gap-x-6 gap-y-4">
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<User className="w-5 h-5 text-slate-400 mt-1" />
|
||||||
|
<div>
|
||||||
|
<p className="text-slate-600">Full Name</p>
|
||||||
|
<p className="text-slate-900">{application.name}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<Mail className="w-5 h-5 text-slate-400 mt-1" />
|
||||||
|
<div>
|
||||||
|
<p className="text-slate-600">Email</p>
|
||||||
|
<p className="text-slate-900">{application.email}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<Phone className="w-5 h-5 text-slate-400 mt-1" />
|
||||||
|
<div>
|
||||||
|
<p className="text-slate-600">Phone</p>
|
||||||
|
<p className="text-slate-900">{application.phone}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<User className="w-5 h-5 text-slate-400 mt-1" />
|
||||||
|
<div>
|
||||||
|
<p className="text-slate-600">Age</p>
|
||||||
|
<p className="text-slate-900">{application.age ? `${application.age} years` : 'N/A'}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<GraduationCap className="w-5 h-5 text-slate-400 mt-1" />
|
||||||
|
<div>
|
||||||
|
<p className="text-slate-600">Education</p>
|
||||||
|
<p className="text-slate-900">{application.education || 'N/A'}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<MapPin className="w-5 h-5 text-slate-400 mt-1" />
|
||||||
|
<div>
|
||||||
|
<p className="text-slate-600">Preferred Location</p>
|
||||||
|
<p className="text-slate-900">{application.preferredLocation || 'N/A'}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<MapPin className="w-5 h-5 text-slate-400 mt-1" />
|
||||||
|
<div>
|
||||||
|
<p className="text-slate-600">Location Type</p>
|
||||||
|
<p className="text-slate-900">{application.locationType || 'N/A'}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<Building2 className="w-5 h-5 text-slate-400 mt-1" />
|
||||||
|
<div className="flex-1">
|
||||||
|
<p className="text-slate-600 flex items-center justify-between group cursor-pointer" onClick={onEditFirmType}>
|
||||||
|
Proposed Firm Type
|
||||||
|
<Pencil className="w-3 h-3 text-slate-300 group-hover:text-amber-600 transition-colors" />
|
||||||
|
</p>
|
||||||
|
<p className="text-slate-900 font-black text-amber-700 tracking-tight leading-none mt-1">
|
||||||
|
{application.constitutionType || 'Not Provided'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<Bike className="w-5 h-5 text-slate-400 mt-1" />
|
||||||
|
<div>
|
||||||
|
<p className="text-slate-600">Owns Bike</p>
|
||||||
|
<p className="text-slate-900">{application.ownRoyalEnfield === 'yes' ? 'Yes' : 'No'}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{application.ownRoyalEnfield === 'yes' && (
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<Bike className="w-5 h-5 text-slate-400 mt-1" />
|
||||||
|
<div>
|
||||||
|
<p className="text-slate-600">Bike Model</p>
|
||||||
|
<p className="text-slate-900">{application.royalEnfieldModel || 'N/A'}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<User className="w-5 h-5 text-slate-400 mt-1" />
|
||||||
|
<div>
|
||||||
|
<p className="text-slate-600">Existing Dealer</p>
|
||||||
|
<p className="text-slate-900">{application.existingDealer === 'yes' ? 'Yes' : 'No'}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{application.existingDealer === 'yes' && (
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<User className="w-5 h-5 text-slate-400 mt-1" />
|
||||||
|
<div>
|
||||||
|
<p className="text-slate-600">Company Name</p>
|
||||||
|
<p className="text-slate-900">{application.companyName || 'N/A'}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<ClipboardList className="w-5 h-5 text-slate-400 mt-1" />
|
||||||
|
<div>
|
||||||
|
<p className="text-slate-600">Source</p>
|
||||||
|
<p className="text-slate-900">{application.source || 'N/A'}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{application.questionnaireMarks !== undefined && (
|
||||||
|
<div className="flex items-start gap-3">
|
||||||
|
<Award className="w-5 h-5 text-slate-400 mt-1" />
|
||||||
|
<div>
|
||||||
|
<p className="text-slate-600">Questionnaire Score</p>
|
||||||
|
<p className="text-slate-900">{application.questionnaireMarks}/100</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Separator />
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<p className="text-slate-600 mb-2">Address</p>
|
||||||
|
<p className="text-slate-900">{application.address || 'N/A'}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<p className="text-slate-600 mb-2">Pincode</p>
|
||||||
|
<p className="text-slate-900">{application.pincode || 'N/A'}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<p className="text-slate-600 mb-2">Description</p>
|
||||||
|
<p className="text-slate-900">{application.description || 'N/A'}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<p className="text-slate-600 mb-2">Past Experience</p>
|
||||||
|
<p className="text-slate-900">{application.pastExperience || 'N/A'}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="pt-6 border-t mt-6">
|
||||||
|
<div className="flex items-center justify-between mb-4">
|
||||||
|
<h3 className="text-sm font-black text-slate-900 uppercase tracking-widest flex items-center gap-2">
|
||||||
|
<CreditCard className="w-4 h-4 text-amber-600" /> Statutory & Bank Information
|
||||||
|
</h3>
|
||||||
|
{canEditStatutory && !isEditingStatutory && (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={onEditStatutory}
|
||||||
|
className="h-8 text-amber-600 hover:text-amber-700 hover:bg-amber-50 gap-1.5"
|
||||||
|
>
|
||||||
|
<Pencil className="w-3.5 h-3.5" />
|
||||||
|
Edit Details
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{isEditingStatutory ? (
|
||||||
|
<div className="bg-slate-50/50 p-6 rounded-xl border-2 border-amber-100 space-y-4">
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label className="text-[10px] uppercase font-bold text-slate-500">Legal Entity Name</Label>
|
||||||
|
<Input
|
||||||
|
value={statutoryForm.accountHolderName}
|
||||||
|
onChange={(e) => onStatutoryFormChange({ ...statutoryForm, accountHolderName: e.target.value })}
|
||||||
|
placeholder="Enter Legal Entity Name"
|
||||||
|
className="bg-white border-slate-200"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label className="text-[10px] uppercase font-bold text-slate-500">PAN Number</Label>
|
||||||
|
<Input
|
||||||
|
value={statutoryForm.panNumber}
|
||||||
|
onChange={(e) => onStatutoryFormChange({ ...statutoryForm, panNumber: e.target.value.toUpperCase() })}
|
||||||
|
placeholder="10-digit PAN"
|
||||||
|
maxLength={10}
|
||||||
|
className="bg-white border-slate-200 uppercase"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label className="text-[10px] uppercase font-bold text-slate-500">GST Number</Label>
|
||||||
|
<Input
|
||||||
|
value={statutoryForm.gstNumber}
|
||||||
|
onChange={(e) => onStatutoryFormChange({ ...statutoryForm, gstNumber: e.target.value.toUpperCase() })}
|
||||||
|
placeholder="15-digit GSTIN"
|
||||||
|
maxLength={15}
|
||||||
|
className="bg-white border-slate-200 uppercase"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label className="text-[10px] uppercase font-bold text-slate-500">Registered Address</Label>
|
||||||
|
<Input
|
||||||
|
value={statutoryForm.registeredAddress}
|
||||||
|
onChange={(e) => onStatutoryFormChange({ ...statutoryForm, registeredAddress: e.target.value })}
|
||||||
|
placeholder="Enter Registered Office Address"
|
||||||
|
className="bg-white border-slate-200"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label className="text-[10px] uppercase font-bold text-slate-500">Bank Name</Label>
|
||||||
|
<Input
|
||||||
|
value={statutoryForm.bankName}
|
||||||
|
onChange={(e) => onStatutoryFormChange({ ...statutoryForm, bankName: e.target.value })}
|
||||||
|
placeholder="Enter Bank Name"
|
||||||
|
className="bg-white border-slate-200"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label className="text-[10px] uppercase font-bold text-slate-500">Account Number</Label>
|
||||||
|
<Input
|
||||||
|
value={statutoryForm.accountNumber}
|
||||||
|
onChange={(e) => onStatutoryFormChange({ ...statutoryForm, accountNumber: e.target.value })}
|
||||||
|
placeholder="Enter Account Number"
|
||||||
|
className="bg-white border-slate-200"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label className="text-[10px] uppercase font-bold text-slate-500">IFSC Code</Label>
|
||||||
|
<Input
|
||||||
|
value={statutoryForm.ifscCode}
|
||||||
|
onChange={(e) => onStatutoryFormChange({ ...statutoryForm, ifscCode: e.target.value.toUpperCase() })}
|
||||||
|
placeholder="11-digit IFSC"
|
||||||
|
maxLength={11}
|
||||||
|
className="bg-white border-slate-200 uppercase"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex justify-end gap-3 pt-4 border-t">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={onCancelEditStatutory}
|
||||||
|
disabled={isSavingStatutory}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
onClick={onSaveStatutory}
|
||||||
|
disabled={isSavingStatutory}
|
||||||
|
className="bg-amber-600 hover:bg-amber-700"
|
||||||
|
>
|
||||||
|
{isSavingStatutory ? <Loader2 className="w-4 h-4 animate-spin" /> : 'Save Details'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6 bg-slate-50/50 p-4 rounded-xl border border-slate-100">
|
||||||
|
<div className="space-y-1">
|
||||||
|
<p className="text-[10px] text-slate-500 uppercase font-bold tracking-tight">Legal Entity Name</p>
|
||||||
|
<p className="text-xs font-semibold text-slate-900">{application.accountHolderName || 'Pending'}</p>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<p className="text-[10px] text-slate-500 uppercase font-bold tracking-tight">PAN Number</p>
|
||||||
|
<p className="text-xs font-semibold text-slate-900 uppercase">{application.panNumber || 'Pending'}</p>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<p className="text-[10px] text-slate-500 uppercase font-bold tracking-tight">GST Number</p>
|
||||||
|
<p className="text-xs font-semibold text-slate-900 uppercase">{application.gstNumber || 'Pending'}</p>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<p className="text-[10px] text-slate-500 uppercase font-bold tracking-tight">Registered Address</p>
|
||||||
|
<p className="text-xs font-semibold text-slate-900">{application.registeredAddress || 'Pending'}</p>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-1">
|
||||||
|
<p className="text-[10px] text-slate-500 uppercase font-bold tracking-tight">Bank Details</p>
|
||||||
|
<p className="text-xs font-semibold text-slate-900">{application.bankName || 'N/A'}</p>
|
||||||
|
<p className="text-[10px] text-slate-600">A/C: {application.accountNumber || 'N/A'}</p>
|
||||||
|
<p className="text-[10px] text-slate-600">IFSC: {application.ifscCode || 'N/A'}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,332 @@
|
|||||||
|
import { Check, CheckCircle, Clock, Loader2 } from 'lucide-react';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
import { onboardingService } from '../../../services/onboarding.service';
|
||||||
|
import { Button } from '../../ui/button';
|
||||||
|
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '../../ui/dialog';
|
||||||
|
import { Input } from '../../ui/input';
|
||||||
|
import { Label } from '../../ui/label';
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../../ui/select';
|
||||||
|
import { Textarea } from '../../ui/textarea';
|
||||||
|
|
||||||
|
interface ApplicationDetailsActionModalsProps {
|
||||||
|
application: any;
|
||||||
|
fetchApplication: () => void;
|
||||||
|
showApproveModal: boolean;
|
||||||
|
setShowApproveModal: (value: boolean) => void;
|
||||||
|
approvalRemark: string;
|
||||||
|
setApprovalRemark: (value: string) => void;
|
||||||
|
setApprovalFile: (file: File | null) => void;
|
||||||
|
isApproving: boolean;
|
||||||
|
handleApprove: () => void;
|
||||||
|
showOnboardModal: boolean;
|
||||||
|
setShowOnboardModal: (value: boolean) => void;
|
||||||
|
isOnboarding: boolean;
|
||||||
|
setIsOnboarding: (value: boolean) => void;
|
||||||
|
showRejectModal: boolean;
|
||||||
|
setShowRejectModal: (value: boolean) => void;
|
||||||
|
rejectionReason: string;
|
||||||
|
setRejectionReason: (value: string) => void;
|
||||||
|
isRejecting: boolean;
|
||||||
|
handleReject: () => void;
|
||||||
|
showScheduleModal: boolean;
|
||||||
|
setShowScheduleModal: (value: boolean) => void;
|
||||||
|
interviewType: string;
|
||||||
|
setInterviewType: (value: string) => void;
|
||||||
|
interviewMode: string;
|
||||||
|
setInterviewMode: (value: string) => void;
|
||||||
|
interviewDate: string;
|
||||||
|
setInterviewDate: (value: string) => void;
|
||||||
|
meetingLink: string;
|
||||||
|
setMeetingLink: (value: string) => void;
|
||||||
|
location: string;
|
||||||
|
setLocation: (value: string) => void;
|
||||||
|
isInterviewCompleted: (level: number) => boolean;
|
||||||
|
isInterviewActive: (level: number) => boolean;
|
||||||
|
users: any[];
|
||||||
|
selectedInterviewerId: string;
|
||||||
|
setSelectedInterviewerId: (value: string) => void;
|
||||||
|
handleAddInterviewer: () => void;
|
||||||
|
scheduledInterviewParticipants: any[];
|
||||||
|
handleRemoveInterviewer: (id: string) => void;
|
||||||
|
isScheduling: boolean;
|
||||||
|
handleScheduleInterview: () => void;
|
||||||
|
showAssignArchitectureModal: boolean;
|
||||||
|
setShowAssignArchitectureModal: (value: boolean) => void;
|
||||||
|
architectureLeadId: string;
|
||||||
|
setArchitectureLeadId: (value: string) => void;
|
||||||
|
isAssigningArchitecture: boolean;
|
||||||
|
handleAssignArchitecture: () => void;
|
||||||
|
showArchitectureStatusModal: boolean;
|
||||||
|
setShowArchitectureStatusModal: (value: boolean) => void;
|
||||||
|
architectureStatus: string;
|
||||||
|
setArchitectureStatus: (value: string) => void;
|
||||||
|
architectureRemarks: string;
|
||||||
|
setArchitectureRemarks: (value: string) => void;
|
||||||
|
isUpdatingArchitecture: boolean;
|
||||||
|
handleUpdateArchitectureStatus: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ApplicationDetailsActionModals(props: ApplicationDetailsActionModalsProps) {
|
||||||
|
const {
|
||||||
|
application,
|
||||||
|
fetchApplication,
|
||||||
|
showApproveModal,
|
||||||
|
setShowApproveModal,
|
||||||
|
approvalRemark,
|
||||||
|
setApprovalRemark,
|
||||||
|
setApprovalFile,
|
||||||
|
isApproving,
|
||||||
|
handleApprove,
|
||||||
|
showOnboardModal,
|
||||||
|
setShowOnboardModal,
|
||||||
|
isOnboarding,
|
||||||
|
setIsOnboarding,
|
||||||
|
showRejectModal,
|
||||||
|
setShowRejectModal,
|
||||||
|
rejectionReason,
|
||||||
|
setRejectionReason,
|
||||||
|
isRejecting,
|
||||||
|
handleReject,
|
||||||
|
showScheduleModal,
|
||||||
|
setShowScheduleModal,
|
||||||
|
interviewType,
|
||||||
|
setInterviewType,
|
||||||
|
interviewMode,
|
||||||
|
setInterviewMode,
|
||||||
|
interviewDate,
|
||||||
|
setInterviewDate,
|
||||||
|
meetingLink,
|
||||||
|
setMeetingLink,
|
||||||
|
location,
|
||||||
|
setLocation,
|
||||||
|
isInterviewCompleted,
|
||||||
|
isInterviewActive,
|
||||||
|
users,
|
||||||
|
selectedInterviewerId,
|
||||||
|
setSelectedInterviewerId,
|
||||||
|
handleAddInterviewer,
|
||||||
|
scheduledInterviewParticipants,
|
||||||
|
handleRemoveInterviewer,
|
||||||
|
isScheduling,
|
||||||
|
handleScheduleInterview,
|
||||||
|
showAssignArchitectureModal,
|
||||||
|
setShowAssignArchitectureModal,
|
||||||
|
architectureLeadId,
|
||||||
|
setArchitectureLeadId,
|
||||||
|
isAssigningArchitecture,
|
||||||
|
handleAssignArchitecture,
|
||||||
|
showArchitectureStatusModal,
|
||||||
|
setShowArchitectureStatusModal,
|
||||||
|
architectureStatus,
|
||||||
|
setArchitectureStatus,
|
||||||
|
architectureRemarks,
|
||||||
|
setArchitectureRemarks,
|
||||||
|
isUpdatingArchitecture,
|
||||||
|
handleUpdateArchitectureStatus,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<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 {
|
||||||
|
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>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<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>}
|
||||||
|
<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>
|
||||||
|
{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={handleScheduleInterview} disabled={isScheduling}>{isScheduling ? 'Scheduling...' : 'Schedule'}</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
))}
|
||||||
|
{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>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,485 @@
|
|||||||
|
import { AlertCircle, Building2, Download, Eye, FileText, Info, Loader2, ShieldAlert, ShieldCheck, Upload } from 'lucide-react';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
import { onboardingService } from '../../../services/onboarding.service';
|
||||||
|
import { cn, formatDateTime } from '@/components/ui/utils';
|
||||||
|
import { DocumentPreviewModal } from '../../ui/DocumentPreviewModal';
|
||||||
|
import { Badge } from '../../ui/badge';
|
||||||
|
import { Button } from '../../ui/button';
|
||||||
|
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '../../ui/dialog';
|
||||||
|
import { Input } from '../../ui/input';
|
||||||
|
import { Label } from '../../ui/label';
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../../ui/select';
|
||||||
|
import { Separator } from '../../ui/separator';
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableHeader,
|
||||||
|
TableRow,
|
||||||
|
} from '../../ui/table';
|
||||||
|
import { Textarea } from '../../ui/textarea';
|
||||||
|
|
||||||
|
interface ApplicationDetailsExtendedModalsProps {
|
||||||
|
[key: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ApplicationDetailsExtendedModals(props: ApplicationDetailsExtendedModalsProps) {
|
||||||
|
const {
|
||||||
|
application,
|
||||||
|
KT_MATRIX_CRITERIA,
|
||||||
|
showKTMatrixModal,
|
||||||
|
setShowKTMatrixModal,
|
||||||
|
ktMatrixSelectedValues,
|
||||||
|
handleKTMatrixChange,
|
||||||
|
ktMatrixRemarks,
|
||||||
|
setKtMatrixRemarks,
|
||||||
|
calculateKTScore,
|
||||||
|
handleSubmitKTMatrix,
|
||||||
|
isSubmittingKT,
|
||||||
|
showLevel2FeedbackModal,
|
||||||
|
setShowLevel2FeedbackModal,
|
||||||
|
level2Feedback,
|
||||||
|
handleLevel2Change,
|
||||||
|
handleSubmitLevel2Feedback,
|
||||||
|
isSubmittingLevel2,
|
||||||
|
showFeedbackDetailsModal,
|
||||||
|
setShowFeedbackDetailsModal,
|
||||||
|
selectedEvaluationForView,
|
||||||
|
showLevel3FeedbackModal,
|
||||||
|
setShowLevel3FeedbackModal,
|
||||||
|
level3Feedback,
|
||||||
|
handleLevel3Change,
|
||||||
|
handleSubmitLevel3Feedback,
|
||||||
|
isSubmittingLevel3,
|
||||||
|
showDocumentsModal,
|
||||||
|
setShowDocumentsModal,
|
||||||
|
showUploadForm,
|
||||||
|
setShowUploadForm,
|
||||||
|
selectedStage,
|
||||||
|
getDocumentsForStage,
|
||||||
|
setPreviewDoc,
|
||||||
|
setShowPreviewModal,
|
||||||
|
flattenedStages,
|
||||||
|
setSelectedStage,
|
||||||
|
uploadDocType,
|
||||||
|
setUploadDocType,
|
||||||
|
setUploadFile,
|
||||||
|
isUploading,
|
||||||
|
handleUpload,
|
||||||
|
uploadFile,
|
||||||
|
documentConfigs,
|
||||||
|
showPreviewModal,
|
||||||
|
previewDoc,
|
||||||
|
showFddFinalizeModal,
|
||||||
|
setShowFddFinalizeModal,
|
||||||
|
currentUser,
|
||||||
|
fddAuditRecommendation,
|
||||||
|
setFddAuditRecommendation,
|
||||||
|
fddAuditFindings,
|
||||||
|
setFddAuditFindings,
|
||||||
|
isFinalizingFdd,
|
||||||
|
setIsFinalizingFdd,
|
||||||
|
fetchApplication,
|
||||||
|
showFddFlagModal,
|
||||||
|
setShowFddFlagModal,
|
||||||
|
isFddFlagging,
|
||||||
|
setIsFddFlagging,
|
||||||
|
showFirmTypeModal,
|
||||||
|
setShowFirmTypeModal,
|
||||||
|
tempFirmType,
|
||||||
|
setTempFirmType,
|
||||||
|
updatingFirmType,
|
||||||
|
handleUpdateFirmType,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Dialog open={showKTMatrixModal} onOpenChange={setShowKTMatrixModal}>
|
||||||
|
<DialogContent className="flex min-h-0 max-h-[90vh] w-[calc(100%-2rem)] max-w-lg flex-col gap-0 overflow-hidden p-0 sm:max-w-lg">
|
||||||
|
<DialogHeader className="shrink-0 space-y-2 border-b px-5 py-4 text-left">
|
||||||
|
<DialogTitle className="text-base">KT matrix</DialogTitle>
|
||||||
|
<DialogDescription className="text-sm leading-relaxed">
|
||||||
|
Level 1 interview · {application.name}
|
||||||
|
<span className="mt-1 block text-xs text-muted-foreground">
|
||||||
|
{Object.keys(ktMatrixSelectedValues).length} of {KT_MATRIX_CRITERIA.length} criteria answered
|
||||||
|
</span>
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="custom-scrollbar-slim min-h-0 flex-1 overflow-y-auto px-5 py-5">
|
||||||
|
<div className="space-y-6">
|
||||||
|
{KT_MATRIX_CRITERIA.map((criterion: any, idx: number) => (
|
||||||
|
<div key={criterion.name} className="space-y-2">
|
||||||
|
<Label htmlFor={`kt-matrix-${idx}`} className="block text-sm font-medium leading-relaxed text-foreground">
|
||||||
|
<span className="text-muted-foreground">{idx + 1}.</span> {criterion.name}{' '}
|
||||||
|
<span className="font-normal text-muted-foreground">({criterion.weight}%)</span>
|
||||||
|
</Label>
|
||||||
|
<Select
|
||||||
|
value={ktMatrixSelectedValues[criterion.name] ?? undefined}
|
||||||
|
onValueChange={(value) => {
|
||||||
|
const option = criterion.options.find((o: any) => o.value === value);
|
||||||
|
if (option) handleKTMatrixChange(criterion.name, option.value, option.score);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SelectTrigger id={`kt-matrix-${idx}`} className="h-10 w-full text-left text-sm font-normal">
|
||||||
|
<SelectValue placeholder="Choose an option…" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent position="popper" className="max-h-72 w-[var(--radix-select-trigger-width)]">
|
||||||
|
{criterion.options.map((option: any) => (
|
||||||
|
<SelectItem key={option.value} value={option.value} className="py-2.5 text-sm leading-snug">
|
||||||
|
{option.label} <span className="text-muted-foreground">({option.score})</span>
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
<div className="space-y-2 border-t border-border pt-6">
|
||||||
|
<Label htmlFor="kt-matrix-remarks" className="text-sm font-medium">Notes <span className="font-normal text-muted-foreground">(optional)</span></Label>
|
||||||
|
<Textarea id="kt-matrix-remarks" placeholder="Optional remarks…" className="min-h-[96px] resize-y text-sm leading-relaxed" value={ktMatrixRemarks} onChange={(e) => setKtMatrixRemarks(e.target.value)} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex shrink-0 flex-col gap-4 border-t px-5 py-4 sm:flex-row sm:items-center sm:justify-between">
|
||||||
|
<p className="text-sm text-muted-foreground">Weighted total <span className="font-semibold tabular-nums text-foreground">{calculateKTScore()}</span><span className="text-muted-foreground"> / 100</span></p>
|
||||||
|
<div className="flex gap-2 sm:shrink-0">
|
||||||
|
<Button variant="outline" onClick={() => setShowKTMatrixModal(false)}>Cancel</Button>
|
||||||
|
<Button onClick={handleSubmitKTMatrix} disabled={isSubmittingKT || Object.keys(ktMatrixSelectedValues).length < KT_MATRIX_CRITERIA.length}>{isSubmittingKT ? 'Saving…' : 'Submit'}</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<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: any) => (
|
||||||
|
<TableRow key={doc.id} className="hover:bg-slate-50/50 transition-colors">
|
||||||
|
<TableCell className="py-3"><div className="flex items-center gap-2 min-w-0"><FileText className="w-4 h-4 text-slate-400 shrink-0" /><span className="truncate font-medium text-slate-700" title={doc.fileName}>{doc.fileName}</span></div></TableCell>
|
||||||
|
<TableCell className="py-3"><Badge variant="outline" className="capitalize whitespace-nowrap font-normal border-slate-200 bg-white">{doc.documentType?.toLowerCase() || 'Other'}</Badge></TableCell>
|
||||||
|
<TableCell className="py-3 whitespace-nowrap text-slate-600">{formatDateTime(doc.createdAt)}</TableCell>
|
||||||
|
<TableCell className="py-3 text-slate-600">{doc.uploader?.fullName || (doc.uploadedBy ? 'System User' : 'Applicant')}</TableCell>
|
||||||
|
<TableCell className="text-right py-3">
|
||||||
|
<div className="flex gap-1 justify-end">
|
||||||
|
<Button variant="ghost" size="icon" className="h-8 w-8 text-slate-400 hover:text-indigo-600 hover:bg-indigo-50 rounded-full" onClick={() => { setPreviewDoc(doc); setShowPreviewModal(true); }}><Eye className="w-4 h-4" /></Button>
|
||||||
|
<Button variant="ghost" size="icon" className="h-8 w-8 text-slate-400 hover:text-amber-600 hover:bg-amber-50 rounded-full" onClick={() => { const baseUrl = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:5000'; window.open(`${baseUrl}/${doc.filePath}`, '_blank'); }}><Download className="w-4 h-4" /></Button>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex-1 flex flex-col items-center justify-center py-12 text-center border rounded-lg bg-slate-50/30"><div className="w-16 h-16 rounded-full bg-slate-100 flex items-center justify-center mb-4"><FileText className="w-8 h-8 text-slate-300" /></div><h3 className="text-slate-900 font-semibold mb-2">No Documents Found</h3><p className="text-slate-600 text-sm max-w-[250px]">No documents have been uploaded for this stage yet.</p></div>
|
||||||
|
)}
|
||||||
|
<div className="flex flex-col sm:flex-row gap-3 pt-2 mt-auto">
|
||||||
|
<Button className="flex-1 bg-amber-600 hover:bg-amber-700 text-white font-bold py-3 sm:py-5 rounded-xl shadow-lg shadow-amber-600/15 transition-all hover:scale-[1.01] active:scale-[0.99]" onClick={() => setShowUploadForm(true)}><Upload className="w-5 h-5 mr-3" />Upload Document</Button>
|
||||||
|
<Button variant="outline" className="flex-1 sm:flex-none py-3 sm:py-5 px-8 rounded-xl border-slate-200 font-semibold text-slate-600 hover:bg-slate-50" onClick={() => setShowDocumentsModal(false)}>Close</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-6 py-4">
|
||||||
|
<div className="grid gap-6 bg-slate-50/50 p-4 sm:p-6 rounded-2xl border border-slate-200">
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 gap-6">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label className="text-slate-700 font-semibold px-1">Stage context</Label>
|
||||||
|
<Select value={selectedStage || 'null'} onValueChange={(val) => setSelectedStage(val === 'null' ? null : val)}>
|
||||||
|
<SelectTrigger className="bg-white border-slate-200 h-11 rounded-xl focus:ring-amber-500 shadow-sm"><SelectValue placeholder="Select stage" /></SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="null">General / No Stage</SelectItem>
|
||||||
|
{flattenedStages.map((s: any, idx: number) => <SelectItem key={`${s.name}-${idx}`} value={s.name}>{s.parentBranch ? `${s.parentBranch}: ${s.name}` : s.name}</SelectItem>)}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label className="text-slate-700 font-semibold px-1">Document Type</Label>
|
||||||
|
<Select value={uploadDocType} onValueChange={setUploadDocType}>
|
||||||
|
<SelectTrigger className="bg-white border-slate-200 h-11 rounded-xl focus:ring-amber-500 shadow-sm"><SelectValue placeholder="Select type" /></SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{(() => {
|
||||||
|
const baseDocs = ['Other'];
|
||||||
|
const stageConfigs = documentConfigs.filter((c: any) => {
|
||||||
|
const cfgStage = c.stageCode?.trim();
|
||||||
|
const selStage = (selectedStage || 'General').trim();
|
||||||
|
if (cfgStage === selStage) return true;
|
||||||
|
if (selStage.startsWith('EOR:') && cfgStage === 'EOR') return true;
|
||||||
|
if (!selectedStage && cfgStage === 'General') return true;
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
let filteredDocs: string[] = [];
|
||||||
|
if (stageConfigs.length > 0) filteredDocs = stageConfigs.map((c: any) => c.documentType);
|
||||||
|
else if (!selectedStage || selectedStage === 'General') {
|
||||||
|
filteredDocs = ['PAN Card', 'GST Certificate', 'Aadhaar Card', 'Passport Size Photograph', 'Partnership Deed', 'LLP Agreement', 'Certificate of Incorporation', 'Board Resolution', 'Firm Registration Certificate', 'Cancelled Check', 'Bank Statement', 'Other'];
|
||||||
|
} else filteredDocs = baseDocs;
|
||||||
|
if (selectedStage?.startsWith('EOR: ')) {
|
||||||
|
const eorItem = selectedStage.replace('EOR: ', '');
|
||||||
|
if (!filteredDocs.includes(eorItem)) filteredDocs = [eorItem, ...filteredDocs];
|
||||||
|
}
|
||||||
|
return Array.from(new Set(filteredDocs)).map((doc, idx) => <SelectItem key={`${doc}-${idx}`} value={doc}>{doc}</SelectItem>);
|
||||||
|
})()}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label className="text-slate-700 font-semibold px-1">Select File</Label>
|
||||||
|
<Input type="file" className="bg-white border-slate-200 h-12 rounded-xl focus:ring-amber-500 shadow-sm file:mr-4 file:py-2 file:px-4 file:rounded-full file:border-0 file:text-sm file:font-semibold file:bg-amber-50 file:text-amber-700 hover:file:bg-amber-100 cursor-pointer" onChange={(e) => setUploadFile(e.target.files ? e.target.files[0] : null)} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col sm:flex-row gap-3 pt-4">
|
||||||
|
<Button className="flex-1 order-2 sm:order-1 py-3 sm:py-5 rounded-xl border-slate-200 font-semibold text-slate-600 hover:bg-slate-50" variant="outline" onClick={() => setShowUploadForm(false)} disabled={isUploading}>Cancel</Button>
|
||||||
|
<Button className="flex-1 order-1 sm:order-2 bg-amber-600 hover:bg-amber-700 text-white font-bold py-3 sm:py-5 rounded-xl shadow-lg shadow-amber-600/15 transition-all hover:scale-[1.01] active:scale-[0.99]" onClick={async () => { await handleUpload(); setShowUploadForm(false); }} disabled={!uploadFile || !uploadDocType || isUploading}>
|
||||||
|
{isUploading ? <span className="flex items-center gap-2"><div className="w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin" />Uploading...</span> : <span className="flex items-center gap-2"><Upload className="w-5 h-5" />Confirm Upload</span>}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
<DocumentPreviewModal isOpen={showPreviewModal} onClose={() => setShowPreviewModal(false)} document={previewDoc} />
|
||||||
|
|
||||||
|
<Dialog open={showFddFinalizeModal} onOpenChange={setShowFddFinalizeModal}>
|
||||||
|
<DialogContent className="max-w-md p-0 overflow-hidden border-none shadow-2xl rounded-3xl">
|
||||||
|
<div className="bg-slate-950 p-8 flex items-center justify-center relative overflow-hidden"><div className="absolute inset-0 bg-gradient-to-br from-amber-600/20 to-transparent" /><div className="w-20 h-20 bg-amber-600/20 rounded-full flex items-center justify-center animate-pulse relative z-10 shadow-[0_0_40px_rgba(245,158,11,0.2)]"><ShieldCheck className="w-10 h-10 text-amber-500" /></div></div>
|
||||||
|
<div className="p-8 space-y-6 bg-white">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="text-2xl font-black text-slate-900 text-center tracking-tight">Finalize FDD Audit</DialogTitle>
|
||||||
|
<DialogDescription className="text-slate-500 text-center pt-2 leading-relaxed text-sm font-medium">You are about to submit your final findings. This action will <span className="font-bold text-slate-900 underline decoration-amber-500 decoration-2">lock the audit session</span> and trigger the LOI approval workflow.</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="space-y-4">
|
||||||
|
{(currentUser?.role !== 'FDD' && currentUser?.roleCode !== 'FDD') && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label className="text-[10px] font-black uppercase tracking-widest text-slate-400">Auditor Recommendation</Label>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
{['Recommended', 'Qualified with Observations', 'Not Recommended'].map((rec) => (
|
||||||
|
<Button key={rec} variant={fddAuditRecommendation === rec ? 'default' : 'outline'} className={cn("flex-1 h-10 font-bold text-[9px] uppercase tracking-wider rounded-xl transition-all", fddAuditRecommendation === rec && rec === 'Recommended' && "bg-emerald-600 hover:bg-emerald-700", fddAuditRecommendation === rec && rec === 'Qualified with Observations' && "bg-amber-500 hover:bg-amber-600", fddAuditRecommendation === rec && rec === 'Not Recommended' && "bg-red-600 hover:bg-red-700")} onClick={() => setFddAuditRecommendation(rec)}>{rec}</Button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label className="text-[10px] font-black uppercase tracking-widest text-slate-400">Findings Summary</Label>
|
||||||
|
<Textarea placeholder="Summarize key financial findings or discrepancies..." className="min-h-[100px] rounded-xl border-slate-200 focus:ring-amber-500 text-sm" value={fddAuditFindings} onChange={(e) => setFddAuditFindings(e.target.value)} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="bg-amber-50 p-4 rounded-2xl flex gap-3 border border-amber-100"><Info className="w-5 h-5 text-amber-600 shrink-0 mt-0.5" /><p className="text-[11px] text-amber-800 font-medium italic">Ensure the final PDF report is uploaded first. This satisfies the FDD statutory requirement.</p></div>
|
||||||
|
<div className="flex flex-col sm:flex-row gap-3 pt-2">
|
||||||
|
<Button variant="outline" className="w-full sm:flex-1 h-12 rounded-2xl font-bold text-slate-600 hover:bg-slate-50 border-slate-200" onClick={() => setShowFddFinalizeModal(false)} disabled={isFinalizingFdd}>Cancel</Button>
|
||||||
|
<Button
|
||||||
|
className="w-full sm:flex-1 h-12 rounded-2xl font-bold bg-slate-950 hover:bg-slate-900 text-white shadow-lg shadow-slate-200 transition-all active:scale-95 border-b-4 border-amber-500"
|
||||||
|
disabled={isFinalizingFdd || !fddAuditFindings}
|
||||||
|
onClick={async () => {
|
||||||
|
try {
|
||||||
|
setIsFinalizingFdd(true);
|
||||||
|
await onboardingService.submitStageDecision({
|
||||||
|
applicationId: application!.id,
|
||||||
|
stageCode: 'FDD_VERIFICATION',
|
||||||
|
decision: 'Approved',
|
||||||
|
remarks: (currentUser?.role === 'FDD' || currentUser?.roleCode === 'FDD')
|
||||||
|
? `Findings: ${fddAuditFindings}`
|
||||||
|
: `[RECOMMENDATION: ${fddAuditRecommendation}] \nFindings: ${fddAuditFindings}`,
|
||||||
|
nextStatus: 'LOI In Progress',
|
||||||
|
nextProgress: 65
|
||||||
|
});
|
||||||
|
toast.success('FDD Audit finalized and submitted.');
|
||||||
|
setShowFddFinalizeModal(false);
|
||||||
|
fetchApplication();
|
||||||
|
} catch {
|
||||||
|
toast.error('Submission failed');
|
||||||
|
} finally {
|
||||||
|
setIsFinalizingFdd(false);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{isFinalizingFdd ? <Loader2 className="w-5 h-5 animate-spin" /> : 'Confirm & Submit'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
<Dialog open={showFddFlagModal} onOpenChange={setShowFddFlagModal}>
|
||||||
|
<DialogContent className="max-w-md p-0 overflow-hidden border-none shadow-2xl rounded-3xl">
|
||||||
|
<div className="bg-slate-950 p-8 flex items-center justify-center relative overflow-hidden"><div className="absolute inset-0 bg-gradient-to-br from-red-600/20 to-transparent" /><div className="w-20 h-20 bg-red-600/20 rounded-full flex items-center justify-center relative z-10 shadow-[0_0_40px_rgba(220,38,38,0.2)]"><ShieldAlert className="w-10 h-10 text-red-500" /></div></div>
|
||||||
|
<div className="p-8 space-y-6 bg-white text-center">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="text-2xl font-black text-slate-900 tracking-tight">Flag Non-Responsive</DialogTitle>
|
||||||
|
<DialogDescription className="text-slate-500 pt-2 leading-relaxed text-sm font-medium">Are you sure you want to flag this applicant? This will notify the DD Admin that the audit cannot proceed due to applicant's non-cooperation.</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="bg-red-50 p-4 rounded-2xl flex gap-3 border border-red-100"><AlertCircle className="w-5 h-5 text-red-600 shrink-0 mt-0.5" /><p className="text-[11px] text-red-800 text-left font-medium">"Applicant is unresponsive to multiple queries and financial document requests."</p></div>
|
||||||
|
<div className="flex flex-col sm:flex-row gap-3 pt-2">
|
||||||
|
<Button variant="outline" className="w-full sm:flex-1 h-12 rounded-2xl font-bold text-slate-600 hover:bg-slate-50 border-slate-200" onClick={() => setShowFddFlagModal(false)} disabled={isFddFlagging}>Go Back</Button>
|
||||||
|
<Button
|
||||||
|
className="w-full sm:flex-1 h-12 rounded-2xl font-bold bg-slate-950 hover:bg-slate-900 text-white shadow-lg shadow-slate-200 transition-all active:scale-95 border-b-4 border-red-600"
|
||||||
|
disabled={isFddFlagging}
|
||||||
|
onClick={async () => {
|
||||||
|
try {
|
||||||
|
setIsFddFlagging(true);
|
||||||
|
await onboardingService.submitStageDecision({
|
||||||
|
applicationId: application!.id,
|
||||||
|
stageCode: 'FDD_VERIFICATION',
|
||||||
|
decision: 'Rejected',
|
||||||
|
remarks: 'Applicant is non-responsive to FDD queries.'
|
||||||
|
});
|
||||||
|
toast.error('Applicant flagged as non-responsive.');
|
||||||
|
setShowFddFlagModal(false);
|
||||||
|
fetchApplication();
|
||||||
|
} catch {
|
||||||
|
toast.error('Action failed');
|
||||||
|
} finally {
|
||||||
|
setIsFddFlagging(false);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{isFddFlagging ? <Loader2 className="w-5 h-5 animate-spin" /> : 'Confirm Flag'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
|
||||||
|
<Dialog open={showFirmTypeModal} onOpenChange={setShowFirmTypeModal}>
|
||||||
|
<DialogContent className="max-w-md p-0 overflow-hidden rounded-3xl border-none shadow-2xl">
|
||||||
|
<div className="bg-amber-600 p-8 text-white">
|
||||||
|
<div className="w-16 h-16 rounded-2xl bg-white/20 flex items-center justify-center mb-6 backdrop-blur-sm border border-white/30 shadow-inner"><Building2 className="w-8 h-8 text-white" /></div>
|
||||||
|
<h3 className="text-2xl font-black tracking-tight mb-2">Update Firm Type</h3>
|
||||||
|
<p className="text-amber-100/80 text-sm font-medium leading-relaxed">Select the proposed legal constitution for this dealership application.</p>
|
||||||
|
</div>
|
||||||
|
<div className="p-8 space-y-6 bg-white">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label className="text-[10px] text-slate-400 uppercase tracking-widest font-black">Proposed Legal Constitution</Label>
|
||||||
|
<Select value={tempFirmType} onValueChange={setTempFirmType}>
|
||||||
|
<SelectTrigger className="h-12 rounded-xl border-slate-200 focus:ring-amber-500"><SelectValue placeholder="Select Firm Type" /></SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="Proprietorship">Proprietorship</SelectItem>
|
||||||
|
<SelectItem value="Partnership">Partnership</SelectItem>
|
||||||
|
<SelectItem value="Limited Liability partnership">LLP (Limited Liability partnership)</SelectItem>
|
||||||
|
<SelectItem value="Private Limited Company">Private Limited Company</SelectItem>
|
||||||
|
<SelectItem value="Public Limited Company">Public Limited Company</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-3 pt-2">
|
||||||
|
<Button variant="outline" className="flex-1 h-12 rounded-xl font-bold text-slate-600 border-slate-200" onClick={() => setShowFirmTypeModal(false)} disabled={updatingFirmType}>Cancel</Button>
|
||||||
|
<Button className="flex-1 h-12 rounded-xl font-bold bg-amber-600 hover:bg-amber-700 text-white shadow-lg shadow-amber-200 transition-all active:scale-95" disabled={updatingFirmType || !tempFirmType} onClick={handleUpdateFirmType}>{updatingFirmType ? <Loader2 className="w-5 h-5 animate-spin" /> : 'Update Type'}</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,257 @@
|
|||||||
|
import { AlertCircle, CheckCircle, ClipboardList, Download, Eye, FileText, ShieldAlert, ShieldCheck, Upload } from 'lucide-react';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
import { onboardingService } from '../../../services/onboarding.service';
|
||||||
|
import { cn, formatDateTime } from '@/components/ui/utils';
|
||||||
|
import { Badge } from '../../ui/badge';
|
||||||
|
import { Button } from '../../ui/button';
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from '../../ui/card';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
application: any;
|
||||||
|
currentUser: any;
|
||||||
|
documents: any[];
|
||||||
|
fddAgencies: any[];
|
||||||
|
selectedAgencyId: string;
|
||||||
|
setSelectedAgencyId: (v: string) => void;
|
||||||
|
isAssigningAgency: boolean;
|
||||||
|
handleAssignAgency: () => void;
|
||||||
|
setPreviewDoc: (d: any) => void;
|
||||||
|
setShowPreviewModal: (v: boolean) => void;
|
||||||
|
setIsUploading: (v: boolean) => void;
|
||||||
|
fetchApplication: () => void;
|
||||||
|
refreshDocuments: () => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ApplicationDetailsFddAuditContent({
|
||||||
|
application,
|
||||||
|
currentUser,
|
||||||
|
documents,
|
||||||
|
fddAgencies,
|
||||||
|
selectedAgencyId,
|
||||||
|
setSelectedAgencyId,
|
||||||
|
isAssigningAgency,
|
||||||
|
handleAssignAgency,
|
||||||
|
setPreviewDoc,
|
||||||
|
setShowPreviewModal,
|
||||||
|
setIsUploading,
|
||||||
|
fetchApplication,
|
||||||
|
refreshDocuments,
|
||||||
|
}: Props) {
|
||||||
|
const assignments = application?.fddAssignments || [];
|
||||||
|
const fddParticipants = application?.participants?.filter((p: any) =>
|
||||||
|
p.user?.role === 'FDD' || p.user?.roleCode === 'FDD' || p.user?.allRoles?.includes('FDD')
|
||||||
|
) || [];
|
||||||
|
const hasAssignment = assignments.length > 0 || fddParticipants.length > 0;
|
||||||
|
const primaryFddUser = fddParticipants[0]?.user;
|
||||||
|
|
||||||
|
const mandatoryFinancialDocs = [
|
||||||
|
{ type: 'Bank Statement', label: 'Bank Statements' },
|
||||||
|
{ type: 'Income Tax Returns (ITR)', label: 'ITR (Last 3 Years)' },
|
||||||
|
{ type: 'CIBIL Report', label: 'CIBIL / Credit Reports' },
|
||||||
|
{ type: 'Property Documents', label: 'Property Documents' },
|
||||||
|
{ type: 'Business Valuation Report', label: 'Valuation Reports' },
|
||||||
|
{ type: 'FDD Final Audit Report', label: 'Final Audit Report' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const getDocByTypeName = (typeName: string) => {
|
||||||
|
const target = typeName.toLowerCase();
|
||||||
|
return (documents || []).find((d: any) => {
|
||||||
|
const docType = (d.documentType || '').toLowerCase();
|
||||||
|
const fileName = (d.fileName || '').toLowerCase();
|
||||||
|
if (docType === target) return true;
|
||||||
|
if (target.includes('itr') && (docType.includes('itr') || fileName.includes('itr'))) return true;
|
||||||
|
if (target.includes('bank statement') && (docType.includes('bank') || fileName.includes('bank'))) return true;
|
||||||
|
if (target.includes('cibil') && (docType.includes('cibil') || fileName.includes('cibil') || docType.includes('credit'))) return true;
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const isFddSupportDoc = (d: any) => {
|
||||||
|
const type = (d.documentType || '').toLowerCase();
|
||||||
|
const stage = (d.stage || '').toLowerCase();
|
||||||
|
return stage === 'fdd' || type.includes('report') || type.includes('itr') || type.includes('bank') || type.includes('cibil') || type.includes('valuation');
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!hasAssignment && !['FDD Verification', 'LOI In Progress', 'Payment Pending'].includes(application.status)) {
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<div className="flex flex-col items-center justify-center p-12 bg-slate-50 rounded-2xl border-2 border-dashed border-slate-200">
|
||||||
|
<ShieldCheck className="w-12 h-12 text-slate-300 mb-4" />
|
||||||
|
<h3 className="text-slate-900 font-semibold uppercase tracking-widest text-xs">No FDD Assignment</h3>
|
||||||
|
<p className="text-slate-500 text-[10px] text-center max-w-xs mt-2 font-medium leading-relaxed uppercase tracking-tight">
|
||||||
|
The Financial Due Diligence process has not been initiated for this application yet.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{(currentUser?.role === 'DD Admin' || currentUser?.role === 'Super Admin') && (
|
||||||
|
<Card className="border-amber-100 bg-amber-50/30 overflow-hidden rounded-2xl">
|
||||||
|
<CardHeader className="pb-2">
|
||||||
|
<CardTitle className="text-xs font-black uppercase tracking-widest text-amber-800 flex items-center gap-2">
|
||||||
|
<ShieldAlert className="w-4 h-4" />
|
||||||
|
Initiate FDD Audit
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="flex flex-col md:flex-row gap-4">
|
||||||
|
<div className="flex-1">
|
||||||
|
<label className="text-[10px] font-black uppercase tracking-widest text-slate-400 mb-1.5 block">Select FDD Agency</label>
|
||||||
|
<select
|
||||||
|
className="w-full h-11 bg-white border border-slate-200 rounded-xl px-4 text-sm font-medium focus:ring-2 focus:ring-amber-500/20 focus:border-amber-500 outline-none transition-all shadow-sm"
|
||||||
|
value={selectedAgencyId}
|
||||||
|
onChange={(e) => setSelectedAgencyId(e.target.value)}
|
||||||
|
>
|
||||||
|
<option value="">Choose partner agency...</option>
|
||||||
|
{(fddAgencies || []).map((agency: any) => (
|
||||||
|
<option key={agency.id} value={agency.id}>
|
||||||
|
{agency.fullName || agency.name} ({agency.email})
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-end">
|
||||||
|
<Button
|
||||||
|
className="bg-slate-900 text-white hover:bg-slate-800 font-black text-[10px] uppercase tracking-widest px-8 h-11 border-none shadow-lg shadow-slate-900/10 transition-all active:scale-[0.98]"
|
||||||
|
onClick={handleAssignAgency}
|
||||||
|
disabled={isAssigningAgency || !selectedAgencyId}
|
||||||
|
>
|
||||||
|
{isAssigningAgency ? 'Assigning...' : 'Assign & Start Audit'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-8">
|
||||||
|
{hasAssignment && (
|
||||||
|
<div className="flex items-center justify-between p-4 bg-slate-50 border border-slate-200 rounded-xl mb-6">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="p-2 bg-amber-100 rounded-lg">
|
||||||
|
<ShieldCheck className="w-5 h-5 text-amber-600" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h4 className="text-sm font-bold text-slate-900">FDD Assignment Active</h4>
|
||||||
|
{primaryFddUser && <p className="text-xs text-slate-500 font-medium">Assigned to: {primaryFddUser.name}</p>}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Card className="border-slate-200 shadow-sm overflow-hidden rounded-2xl">
|
||||||
|
<CardHeader className="bg-slate-50/50 border-b border-slate-100 py-4">
|
||||||
|
<CardTitle className="text-sm font-black uppercase tracking-widest text-slate-500 flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-2"><ClipboardList className="w-4 h-4" /> Financial Artefacts Checklist</div>
|
||||||
|
<Badge variant="outline" className="text-[10px] bg-white">Verify before sign-off</Badge>
|
||||||
|
</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="p-0">
|
||||||
|
<div className="divide-y divide-slate-100">
|
||||||
|
{mandatoryFinancialDocs.map((docType) => {
|
||||||
|
const doc = getDocByTypeName(docType.type);
|
||||||
|
return (
|
||||||
|
<div key={docType.type} className="flex items-center justify-between p-4 px-6 hover:bg-slate-50/50 transition-colors">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<div className={cn('w-8 h-8 rounded-lg flex items-center justify-center', doc ? 'bg-emerald-50 text-emerald-600' : 'bg-slate-50 text-slate-300')}>
|
||||||
|
{doc ? <CheckCircle className="w-5 h-5" /> : <AlertCircle className="w-5 h-5" />}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-bold text-slate-800">{docType.label}</p>
|
||||||
|
<p className="text-[10px] text-slate-400 font-bold uppercase tracking-tighter">
|
||||||
|
{doc ? `Uploaded: ${formatDateTime(doc.createdAt)}` : 'Missing in Documentation'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{doc ? (
|
||||||
|
<Button variant="ghost" size="sm" className="h-8 text-blue-600 font-black text-[10px] uppercase tracking-widest hover:bg-blue-50" onClick={() => { setPreviewDoc(doc); setShowPreviewModal(true); }}>
|
||||||
|
<Eye className="w-4 h-4 mr-1" /> Preview
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="h-8 border-slate-200 text-slate-500 font-black text-[10px] uppercase tracking-widest hover:bg-slate-50 hover:text-blue-600"
|
||||||
|
onClick={() => {
|
||||||
|
const input = document.createElement('input');
|
||||||
|
input.type = 'file';
|
||||||
|
input.onchange = async (e: any) => {
|
||||||
|
const file = e.target.files[0];
|
||||||
|
if (!file) return;
|
||||||
|
try {
|
||||||
|
setIsUploading(true);
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('file', file);
|
||||||
|
formData.append('documentType', docType.type);
|
||||||
|
formData.append('stage', 'FDD');
|
||||||
|
formData.append('applicationId', application.id);
|
||||||
|
const res = await onboardingService.uploadDocument(application.id, formData);
|
||||||
|
if (docType.type === 'FDD Final Audit Report') {
|
||||||
|
await onboardingService.submitFddReport({
|
||||||
|
applicationId: application.id,
|
||||||
|
reportDocumentId: res.data?.id || res.id,
|
||||||
|
findings: 'Final Audit Report uploaded via checklist.',
|
||||||
|
recommendation: 'REVIEW_PENDING',
|
||||||
|
});
|
||||||
|
fetchApplication();
|
||||||
|
}
|
||||||
|
toast.success(`${docType.label} uploaded successfully`);
|
||||||
|
refreshDocuments();
|
||||||
|
} catch {
|
||||||
|
toast.error('Upload failed');
|
||||||
|
} finally {
|
||||||
|
setIsUploading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
input.click();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Upload className="w-4 h-4 mr-1 text-slate-300" /> Upload
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<h3 className="text-lg font-semibold text-slate-900">Supporting Audit Documents</h3>
|
||||||
|
<Badge variant="outline" className="bg-slate-50 text-slate-500 border-slate-200">
|
||||||
|
{(documents || []).filter(isFddSupportDoc).length} Document(s)
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
{(documents || []).filter(isFddSupportDoc).map((doc: any) => (
|
||||||
|
<div key={doc.id} className="group bg-white border border-slate-200 rounded-xl p-4 flex items-center justify-between hover:border-amber-400 transition-all hover:shadow-md">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<div className="w-10 h-10 rounded-lg bg-slate-50 flex items-center justify-center"><FileText className="w-5 h-5 text-slate-400" /></div>
|
||||||
|
<div className="overflow-hidden">
|
||||||
|
<p className="text-slate-900 font-bold text-sm truncate max-w-[150px]" title={doc.fileName}>{doc.fileName}</p>
|
||||||
|
<p className="text-slate-500 text-[10px] font-medium uppercase">{doc.documentType}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-1">
|
||||||
|
<Button variant="ghost" size="icon" className="h-8 w-8 text-slate-400 hover:text-amber-600 hover:bg-amber-50" onClick={() => window.open(`http://localhost:5000/${doc.filePath}`, '_blank')}>
|
||||||
|
<Download className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
<Button variant="ghost" size="icon" className="h-8 w-8 text-slate-400 hover:text-amber-600 hover:bg-amber-50" onClick={() => { setPreviewDoc(doc); setShowPreviewModal(true); }}>
|
||||||
|
<Eye className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{(documents || []).filter(isFddSupportDoc).length === 0 && (
|
||||||
|
<div className="col-span-full p-8 text-center bg-slate-50 rounded-xl border border-dashed border-slate-200">
|
||||||
|
<p className="text-slate-400 text-sm">No supporting audit documents uploaded yet.</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,72 @@
|
|||||||
|
import { ArrowLeft, MessageSquare, ShieldAlert } from 'lucide-react';
|
||||||
|
import { Button } from '../../ui/button';
|
||||||
|
import { Application } from '../../../lib/mock-data';
|
||||||
|
|
||||||
|
interface ApplicationDetailsHeaderProps {
|
||||||
|
application: Application;
|
||||||
|
isNonResponsive: boolean;
|
||||||
|
isAdmin: boolean;
|
||||||
|
onBack: () => void;
|
||||||
|
onOpenWorknotes: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ApplicationDetailsHeader({
|
||||||
|
application,
|
||||||
|
isNonResponsive,
|
||||||
|
isAdmin,
|
||||||
|
onBack,
|
||||||
|
onOpenWorknotes,
|
||||||
|
}: ApplicationDetailsHeaderProps) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{isNonResponsive && (
|
||||||
|
<div className="bg-red-50 border border-red-200 p-4 rounded-2xl flex items-center justify-between animate-in fade-in slide-in-from-top-4 duration-500">
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
<div className="bg-red-100 p-2 rounded-xl">
|
||||||
|
<ShieldAlert className="w-6 h-6 text-red-600" />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h3 className="text-sm font-black text-red-900 tracking-tight leading-none uppercase">Applicant Flagged Non-Responsive</h3>
|
||||||
|
<p className="text-red-700 text-[11px] font-bold uppercase tracking-widest mt-1 opacity-80">Audit process is currently on hold due to missing cooperation</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{isAdmin && (
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
className="bg-white border-red-200 text-red-600 hover:bg-red-50 font-black text-[10px] uppercase tracking-widest hidden sm:block h-9"
|
||||||
|
onClick={() => {
|
||||||
|
const worknotesTab = document.querySelector('[value="worknotes"]') as HTMLElement;
|
||||||
|
worknotesTab?.click();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Review Audit
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<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={onOpenWorknotes}
|
||||||
|
>
|
||||||
|
<MessageSquare className="w-4 h-4 mr-2" />
|
||||||
|
View Work Notes
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,405 @@
|
|||||||
|
import {
|
||||||
|
AlertCircle,
|
||||||
|
Calendar,
|
||||||
|
CheckCircle,
|
||||||
|
ChevronDown,
|
||||||
|
Clock,
|
||||||
|
GitBranch,
|
||||||
|
Info,
|
||||||
|
Lock,
|
||||||
|
Mail,
|
||||||
|
MessageSquare,
|
||||||
|
Star,
|
||||||
|
User,
|
||||||
|
XCircle,
|
||||||
|
Zap,
|
||||||
|
} from 'lucide-react';
|
||||||
|
import { cn, formatDateTime } from '@/components/ui/utils';
|
||||||
|
import { Alert, AlertDescription, AlertTitle } from '../../ui/alert';
|
||||||
|
import { Badge } from '../../ui/badge';
|
||||||
|
import { Button } from '../../ui/button';
|
||||||
|
import { Card, CardContent, CardHeader, CardTitle } from '../../ui/card';
|
||||||
|
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from '../../ui/dialog';
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from '../../ui/dropdown-menu';
|
||||||
|
import { Input } from '../../ui/input';
|
||||||
|
import { Label } from '../../ui/label';
|
||||||
|
import { Progress } from '../../ui/progress';
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../../ui/select';
|
||||||
|
import { Separator } from '../../ui/separator';
|
||||||
|
|
||||||
|
interface ApplicationDetailsSidebarProps {
|
||||||
|
application: any;
|
||||||
|
permissions: any;
|
||||||
|
getDeposit: (type: string) => any;
|
||||||
|
isNonResponsive: boolean;
|
||||||
|
isAdmin: boolean;
|
||||||
|
currentUserStageAction: any;
|
||||||
|
currentUserEvaluation: any;
|
||||||
|
onOpenApproveModal: () => void;
|
||||||
|
onOpenRejectModal: () => void;
|
||||||
|
onOpenWorknote: () => void;
|
||||||
|
onOpenScheduleModal: () => void;
|
||||||
|
currentUser: any;
|
||||||
|
handleGenerateDealerCodes: () => void;
|
||||||
|
onOpenAssignArchitectureModal: () => void;
|
||||||
|
activeInterviewForUser: any;
|
||||||
|
hasSubmittedFeedback: boolean;
|
||||||
|
setSelectedInterviewForFeedback: (value: any) => void;
|
||||||
|
setShowKTMatrixModal: (value: boolean) => void;
|
||||||
|
setShowLevel2FeedbackModal: (value: boolean) => void;
|
||||||
|
setShowLevel3FeedbackModal: (value: boolean) => void;
|
||||||
|
onGoToDashboard: () => void;
|
||||||
|
showAssignModal: boolean;
|
||||||
|
setShowAssignModal: (value: boolean) => void;
|
||||||
|
selectedUser: string;
|
||||||
|
setSelectedUser: (value: string) => void;
|
||||||
|
users: any[];
|
||||||
|
participantType: string;
|
||||||
|
setParticipantType: (value: string) => void;
|
||||||
|
handleAddParticipant: () => void;
|
||||||
|
isAssigningParticipant: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ApplicationDetailsSidebar(props: ApplicationDetailsSidebarProps) {
|
||||||
|
const {
|
||||||
|
application,
|
||||||
|
permissions,
|
||||||
|
getDeposit,
|
||||||
|
isNonResponsive,
|
||||||
|
isAdmin,
|
||||||
|
currentUserStageAction,
|
||||||
|
currentUserEvaluation,
|
||||||
|
onOpenApproveModal,
|
||||||
|
onOpenRejectModal,
|
||||||
|
onOpenWorknote,
|
||||||
|
onOpenScheduleModal,
|
||||||
|
currentUser,
|
||||||
|
handleGenerateDealerCodes,
|
||||||
|
onOpenAssignArchitectureModal,
|
||||||
|
activeInterviewForUser,
|
||||||
|
hasSubmittedFeedback,
|
||||||
|
setSelectedInterviewForFeedback,
|
||||||
|
setShowKTMatrixModal,
|
||||||
|
setShowLevel2FeedbackModal,
|
||||||
|
setShowLevel3FeedbackModal,
|
||||||
|
onGoToDashboard,
|
||||||
|
showAssignModal,
|
||||||
|
setShowAssignModal,
|
||||||
|
selectedUser,
|
||||||
|
setSelectedUser,
|
||||||
|
users,
|
||||||
|
participantType,
|
||||||
|
setParticipantType,
|
||||||
|
handleAddParticipant,
|
||||||
|
isAssigningParticipant,
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Summary</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
<div>
|
||||||
|
<p className="text-slate-600">Registration ID</p>
|
||||||
|
<p className="text-slate-900">{application.registrationNumber}</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-slate-600">Current Status</p>
|
||||||
|
<Badge className={cn(
|
||||||
|
"mt-1",
|
||||||
|
application.status === 'Onboarded' ? "bg-green-600 hover:bg-green-700 text-white" :
|
||||||
|
application.status === 'Rejected' ? "bg-red-600" :
|
||||||
|
"bg-amber-600"
|
||||||
|
)}>
|
||||||
|
{application.status}
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
{application.rank && (
|
||||||
|
<div>
|
||||||
|
<p className="text-slate-600">Rank</p>
|
||||||
|
<p className="text-slate-900">
|
||||||
|
{application.rank} of {application.totalApplicantsAtLocation}
|
||||||
|
<span className="text-slate-500"> in {application.preferredLocation}</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div>
|
||||||
|
<p className="text-slate-600">Progress</p>
|
||||||
|
<div className="flex items-center gap-2 mt-2">
|
||||||
|
<Progress value={application.progress} className="flex-1" />
|
||||||
|
<span className="text-slate-900">{application.progress}%</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{application.deadline && (
|
||||||
|
<div>
|
||||||
|
<p className="text-slate-600">Questionnaire Deadline</p>
|
||||||
|
<p className="text-slate-900">{formatDateTime(application.deadline)}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{application.isShortlisted !== false && (
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Actions</CardTitle>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-3">
|
||||||
|
{permissions.isLoaLocked && (
|
||||||
|
<Alert variant="destructive" className="mb-4 bg-amber-50 border-amber-200 text-amber-800">
|
||||||
|
<Lock className="w-4 h-4 text-amber-600" />
|
||||||
|
<AlertTitle className="text-amber-900 font-semibold">LOA approval locked</AlertTitle>
|
||||||
|
<AlertDescription className="text-amber-800">
|
||||||
|
<span className="font-medium">First Fill</span> (later-stage payment) must be verified by Finance
|
||||||
|
before LOA approval can proceed. This is separate from the initial security deposit before LOI Issued.
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{getDeposit('FIRST_FILL')?.status === 'Verified' &&
|
||||||
|
application.status !== 'LOA Pending' &&
|
||||||
|
!['LOA Issued', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Onboarded', 'Rejected'].includes(
|
||||||
|
application.status,
|
||||||
|
) && (
|
||||||
|
<Alert className="mb-4 border-violet-200 bg-violet-50/90 text-violet-950">
|
||||||
|
<Info className="h-4 w-4 text-violet-700" />
|
||||||
|
<AlertTitle className="font-semibold">First Fill verified on file</AlertTitle>
|
||||||
|
<AlertDescription className="text-sm text-violet-900/90 leading-relaxed">
|
||||||
|
Finance has verified the <span className="font-medium">First Fill</span> payment. The application
|
||||||
|
status was <span className="font-medium">not</span> changed until you reach{' '}
|
||||||
|
<span className="font-medium">LOA Pending</span>. When you get there, LOA approval will not be
|
||||||
|
blocked by payment (same pattern as recording the initial security deposit before the LOI
|
||||||
|
security step).
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{permissions.isSecurityDetailsLocked && (
|
||||||
|
<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">Security Details approval locked</AlertTitle>
|
||||||
|
<AlertDescription className="text-amber-800">
|
||||||
|
Finance must verify the <span className="font-medium">Security Deposit</span> before this stage can be approved.
|
||||||
|
You can still use <span className="font-medium">Reject</span> if needed.
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{['Security Details', 'Payment Pending'].includes(application.status) && (
|
||||||
|
<Alert className="mb-4 border-sky-200 bg-sky-50/90 text-sky-900">
|
||||||
|
<Info className="h-4 w-4 text-sky-700" />
|
||||||
|
<AlertTitle className="text-sky-950 font-semibold">Security Details review</AlertTitle>
|
||||||
|
<AlertDescription className="text-sm text-sky-900/90 leading-relaxed">
|
||||||
|
Check the initial security deposit on the <span className="font-medium">Payments</span> tab (Finance
|
||||||
|
may have already marked it verified). When satisfied, use <span className="font-medium">Approve</span>{' '}
|
||||||
|
to move to <span className="font-medium">LOI Issued</span>.
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isNonResponsive && isAdmin && (
|
||||||
|
<Alert variant="destructive" className="mb-4 bg-red-50 border-red-200 text-red-800">
|
||||||
|
<AlertCircle className="w-4 h-4 text-red-600" />
|
||||||
|
<AlertTitle className="text-red-900 font-black uppercase tracking-tighter">⚠️ Non-Responsive Flag</AlertTitle>
|
||||||
|
<AlertDescription className="text-red-800 text-xs font-bold leading-tight">
|
||||||
|
FDD Audit has flagged this applicant. Review audit logs before approval.
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{isAdmin && (application.status === 'Level 3 Approved' || application.status === 'FDD Verification') && (!application.fddAssignments || application.fddAssignments.length === 0) && (
|
||||||
|
<Alert className="mb-4 bg-amber-50 border-amber-200 text-amber-800">
|
||||||
|
<AlertCircle className="w-4 h-4 text-amber-600" />
|
||||||
|
<AlertTitle className="text-amber-900 font-bold">FDD Assignment Required</AlertTitle>
|
||||||
|
<AlertDescription className="text-amber-800 font-medium">
|
||||||
|
This application is pending financial due diligence. Please assign an FDD Agency to proceed with the audit.
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{permissions.canApprove && (
|
||||||
|
<Button className="w-full bg-green-600 hover:bg-green-700 font-bold" onClick={onOpenApproveModal}>
|
||||||
|
<CheckCircle className="w-4 h-4 mr-2" />
|
||||||
|
{['Inauguration', 'Approved'].includes(application.status) ? 'Onboard Dealer' : 'Approve'}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{permissions.canReject && (
|
||||||
|
<Button variant="destructive" className="w-full font-bold" onClick={onOpenRejectModal}>
|
||||||
|
<XCircle className="w-4 h-4 mr-2" />
|
||||||
|
Reject
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{permissions.showDecisionMessage && (
|
||||||
|
<div className={`w-full p-2 text-center rounded border ${(currentUserStageAction?.decision === 'Approved' || currentUserEvaluation?.decision === 'Approved' || currentUserEvaluation?.recommendation === 'Approved' || currentUserEvaluation?.decision === 'Selected') ? 'bg-green-50 border-green-200 text-green-700' : 'bg-red-50 border-red-200 text-red-700'}`}>
|
||||||
|
You have {(currentUserStageAction?.decision === 'Approved' || currentUserEvaluation?.decision === 'Approved' || currentUserEvaluation?.recommendation === 'Approved' || currentUserEvaluation?.decision === 'Selected') ? 'Approved' : 'Rejected'}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Separator />
|
||||||
|
|
||||||
|
<Button variant="outline" className="w-full" onClick={onOpenWorknote}>
|
||||||
|
<MessageSquare className="w-4 h-4 mr-2" />
|
||||||
|
Work Note
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
{permissions.canSchedule && (
|
||||||
|
<Button variant="outline" className="w-full" onClick={onOpenScheduleModal}>
|
||||||
|
<Calendar className="w-4 h-4 mr-2" />
|
||||||
|
Schedule Interview
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{currentUser && ['DD Admin', 'Super Admin'].includes(currentUser.role) &&
|
||||||
|
['Dealer Code Generation', 'LOA Pending', 'Architecture Team Assigned', 'Architecture Document Upload', 'Architecture Team Completion'].includes(application.status) && (
|
||||||
|
<>
|
||||||
|
{!application.dealerCode && (
|
||||||
|
<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 && !application.architectureAssignedTo && (
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className="w-full border-blue-200 hover:bg-blue-50 text-blue-700"
|
||||||
|
onClick={onOpenAssignArchitectureModal}
|
||||||
|
>
|
||||||
|
<GitBranch className="w-4 h-4 mr-2" />
|
||||||
|
Assign Architecture Team
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{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>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{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={onGoToDashboard}>
|
||||||
|
<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 ? 'Assigning...' : 'Assign User'}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,176 @@
|
|||||||
|
export 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[];
|
||||||
|
}[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export 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 auditLogActionBadgeClass(action: string): string {
|
||||||
|
const a = String(action || '').toUpperCase();
|
||||||
|
if (a.includes('REJECT') || a.includes('DELET') || a.includes('DISQUALIF')) return 'border-red-200 bg-red-50/90 text-red-800';
|
||||||
|
if (a === 'CREATED' || a.includes('APPROV') || a.includes('COMPLETE')) return 'border-emerald-200 bg-emerald-50/90 text-emerald-900';
|
||||||
|
if (a.includes('DOCUMENT') || a.includes('UPLOAD') || a.includes('ATTACHMENT')) return 'border-sky-200 bg-sky-50/80 text-sky-900';
|
||||||
|
if (a.includes('PAYMENT') || a.includes('SECURITY') || a.includes('DEPOSIT')) return 'border-violet-200 bg-violet-50/80 text-violet-900';
|
||||||
|
if (a.includes('FDD') || a.includes('QUESTIONNAIRE') || a.includes('INTERVIEW')) return 'border-amber-200 bg-amber-50/80 text-amber-900';
|
||||||
|
return 'border-slate-200 bg-slate-50 text-slate-700';
|
||||||
|
}
|
||||||
@ -0,0 +1,549 @@
|
|||||||
|
import { Dispatch, SetStateAction } from 'react';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
import { onboardingService } from '../../../services/onboarding.service';
|
||||||
|
|
||||||
|
interface UseApplicationDetailsAdminActionsParams {
|
||||||
|
application: any;
|
||||||
|
applicationId: string;
|
||||||
|
currentUser: any;
|
||||||
|
interviews: any[];
|
||||||
|
approvalFile: File | null;
|
||||||
|
approvalRemark: string;
|
||||||
|
rejectionReason: string;
|
||||||
|
architectureLeadId: string;
|
||||||
|
architectureStatus: string;
|
||||||
|
architectureRemarks: string;
|
||||||
|
selectedUser: string;
|
||||||
|
participantType: string;
|
||||||
|
users: any[];
|
||||||
|
interviewDate: string;
|
||||||
|
interviewType: string;
|
||||||
|
interviewMode: string;
|
||||||
|
meetingLink: string;
|
||||||
|
location: string;
|
||||||
|
scheduledInterviewParticipants: any[];
|
||||||
|
uploadFile: File | null;
|
||||||
|
uploadDocType: string;
|
||||||
|
selectedStage: string | null;
|
||||||
|
setIsApproving: Dispatch<SetStateAction<boolean>>;
|
||||||
|
setShowApproveModal: Dispatch<SetStateAction<boolean>>;
|
||||||
|
setApprovalRemark: Dispatch<SetStateAction<string>>;
|
||||||
|
setApprovalFile: Dispatch<SetStateAction<File | null>>;
|
||||||
|
setIsRejecting: Dispatch<SetStateAction<boolean>>;
|
||||||
|
setShowRejectModal: Dispatch<SetStateAction<boolean>>;
|
||||||
|
setRejectionReason: Dispatch<SetStateAction<string>>;
|
||||||
|
setIsAssigningArchitecture: Dispatch<SetStateAction<boolean>>;
|
||||||
|
setShowAssignArchitectureModal: Dispatch<SetStateAction<boolean>>;
|
||||||
|
setIsUpdatingArchitecture: Dispatch<SetStateAction<boolean>>;
|
||||||
|
setShowArchitectureStatusModal: Dispatch<SetStateAction<boolean>>;
|
||||||
|
setIsAssigningParticipant: Dispatch<SetStateAction<boolean>>;
|
||||||
|
setSelectedUser: Dispatch<SetStateAction<string>>;
|
||||||
|
setShowAssignModal: Dispatch<SetStateAction<boolean>>;
|
||||||
|
setLoading: Dispatch<SetStateAction<boolean>>;
|
||||||
|
setIsScheduling: Dispatch<SetStateAction<boolean>>;
|
||||||
|
setShowScheduleModal: Dispatch<SetStateAction<boolean>>;
|
||||||
|
setIsUploading: Dispatch<SetStateAction<boolean>>;
|
||||||
|
setShowUploadForm: Dispatch<SetStateAction<boolean>>;
|
||||||
|
setUploadFile: Dispatch<SetStateAction<File | null>>;
|
||||||
|
setUploadDocType: Dispatch<SetStateAction<string>>;
|
||||||
|
setDocuments: Dispatch<SetStateAction<any[]>>;
|
||||||
|
selectedInterviewerId: string;
|
||||||
|
setSelectedInterviewerId: Dispatch<SetStateAction<string>>;
|
||||||
|
setScheduledInterviewParticipants: Dispatch<SetStateAction<any[]>>;
|
||||||
|
setUsers: Dispatch<SetStateAction<any[]>>;
|
||||||
|
showScheduleModal: boolean;
|
||||||
|
showAssignArchitectureModal: boolean;
|
||||||
|
showAssignModal: boolean;
|
||||||
|
fetchApplication: (silent?: boolean) => Promise<void>;
|
||||||
|
fetchInterviews: () => Promise<void>;
|
||||||
|
fetchEorData: () => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useApplicationDetailsAdminActions(params: UseApplicationDetailsAdminActionsParams) {
|
||||||
|
const {
|
||||||
|
application,
|
||||||
|
applicationId,
|
||||||
|
currentUser,
|
||||||
|
interviews,
|
||||||
|
approvalFile,
|
||||||
|
approvalRemark,
|
||||||
|
rejectionReason,
|
||||||
|
architectureLeadId,
|
||||||
|
architectureStatus,
|
||||||
|
architectureRemarks,
|
||||||
|
selectedUser,
|
||||||
|
participantType,
|
||||||
|
users,
|
||||||
|
interviewDate,
|
||||||
|
interviewType,
|
||||||
|
interviewMode,
|
||||||
|
meetingLink,
|
||||||
|
location,
|
||||||
|
scheduledInterviewParticipants,
|
||||||
|
uploadFile,
|
||||||
|
uploadDocType,
|
||||||
|
selectedStage,
|
||||||
|
setIsApproving,
|
||||||
|
setShowApproveModal,
|
||||||
|
setApprovalRemark,
|
||||||
|
setApprovalFile,
|
||||||
|
setIsRejecting,
|
||||||
|
setShowRejectModal,
|
||||||
|
setRejectionReason,
|
||||||
|
setIsAssigningArchitecture,
|
||||||
|
setShowAssignArchitectureModal,
|
||||||
|
setIsUpdatingArchitecture,
|
||||||
|
setShowArchitectureStatusModal,
|
||||||
|
setIsAssigningParticipant,
|
||||||
|
setSelectedUser,
|
||||||
|
setShowAssignModal,
|
||||||
|
setLoading,
|
||||||
|
setIsScheduling,
|
||||||
|
setShowScheduleModal,
|
||||||
|
setIsUploading,
|
||||||
|
setShowUploadForm,
|
||||||
|
setUploadFile,
|
||||||
|
setUploadDocType,
|
||||||
|
setDocuments,
|
||||||
|
selectedInterviewerId,
|
||||||
|
setSelectedInterviewerId,
|
||||||
|
setScheduledInterviewParticipants,
|
||||||
|
setUsers,
|
||||||
|
showScheduleModal,
|
||||||
|
showAssignArchitectureModal,
|
||||||
|
showAssignModal,
|
||||||
|
fetchApplication,
|
||||||
|
fetchInterviews,
|
||||||
|
fetchEorData,
|
||||||
|
} = params;
|
||||||
|
|
||||||
|
const handleAddInterviewer = () => {
|
||||||
|
if (!selectedInterviewerId) return;
|
||||||
|
const usersList = Array.isArray(users) ? users : [];
|
||||||
|
const userToAdd = usersList.find((u) => u.id === selectedInterviewerId);
|
||||||
|
if (userToAdd && !scheduledInterviewParticipants.find((p) => p.id === userToAdd.id)) {
|
||||||
|
setScheduledInterviewParticipants([...scheduledInterviewParticipants, userToAdd]);
|
||||||
|
setSelectedInterviewerId('');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRemoveInterviewer = (userId: string) => {
|
||||||
|
setScheduledInterviewParticipants(scheduledInterviewParticipants.filter((p) => p.id !== userId));
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchUsers = async (type?: string) => {
|
||||||
|
if (!currentUser || !['DD Admin', 'Super Admin', 'DD Lead', 'DD Head', 'NBH'].includes(currentUser.role)) return;
|
||||||
|
try {
|
||||||
|
const reqParams: any = {};
|
||||||
|
if (type) {
|
||||||
|
const roleMapping: any = {
|
||||||
|
level1: ['DD-ZM', 'RBM'],
|
||||||
|
level2: ['DD Lead', 'ZBH'],
|
||||||
|
level3: ['NBH', 'DD Head'],
|
||||||
|
};
|
||||||
|
reqParams.roleCode = roleMapping[type];
|
||||||
|
if (application) {
|
||||||
|
reqParams.locationId = application.districtId || application.areaId || application.regionId || application.zoneId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const response = await onboardingService.getUsers(reqParams);
|
||||||
|
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 setUsers([]);
|
||||||
|
} catch {
|
||||||
|
setUsers([]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const prefillInterviewParticipants = () => {
|
||||||
|
if (!showScheduleModal || !application) return;
|
||||||
|
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) {
|
||||||
|
setScheduledInterviewParticipants([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const unique: any[] = [];
|
||||||
|
const seen = new Set();
|
||||||
|
preAssigned.forEach((u: any) => {
|
||||||
|
if (u.id && !seen.has(u.id)) {
|
||||||
|
seen.add(u.id);
|
||||||
|
unique.push(u);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
setScheduledInterviewParticipants(unique);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleScheduleInterview = async () => {
|
||||||
|
if (!interviewDate) {
|
||||||
|
toast.warning('Please select date and time');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
setIsScheduling(true);
|
||||||
|
await onboardingService.scheduleInterview({
|
||||||
|
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),
|
||||||
|
});
|
||||||
|
toast.success('Interview scheduled successfully');
|
||||||
|
setShowScheduleModal(false);
|
||||||
|
await fetchInterviews();
|
||||||
|
await fetchApplication();
|
||||||
|
} catch {
|
||||||
|
toast.error('Failed to schedule interview');
|
||||||
|
} 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');
|
||||||
|
await fetchInterviews();
|
||||||
|
} catch {
|
||||||
|
toast.error('Failed to cancel interview');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
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('');
|
||||||
|
const docs = await onboardingService.getDocuments(applicationId);
|
||||||
|
setDocuments(docs || []);
|
||||||
|
await fetchEorData();
|
||||||
|
} catch {
|
||||||
|
toast.error('Failed to upload document');
|
||||||
|
} finally {
|
||||||
|
setIsUploading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleApprove = async () => {
|
||||||
|
try {
|
||||||
|
setIsApproving(true);
|
||||||
|
const activeInterview = interviews.find((i) =>
|
||||||
|
i.status !== 'Completed' &&
|
||||||
|
i.status !== 'Cancelled' &&
|
||||||
|
i.participants?.some((p: any) => p.userId === currentUser?.id)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (approvalFile && applicationId) {
|
||||||
|
try {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append('file', approvalFile);
|
||||||
|
formData.append('documentType', 'Approval Attachment');
|
||||||
|
let stageName: string | null = null;
|
||||||
|
if (activeInterview) {
|
||||||
|
if (activeInterview.level === 1 || activeInterview.level === '1') stageName = '1st Level Interview';
|
||||||
|
else if (activeInterview.level === 2 || activeInterview.level === '2') stageName = '2nd Level Interview';
|
||||||
|
else if (activeInterview.level === 3 || activeInterview.level === '3') stageName = '3rd Level Interview';
|
||||||
|
}
|
||||||
|
if (!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);
|
||||||
|
} catch {
|
||||||
|
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);
|
||||||
|
await fetchInterviews();
|
||||||
|
await fetchApplication();
|
||||||
|
return;
|
||||||
|
} catch {
|
||||||
|
toast.error('Failed to approve interview');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!approvalRemark.trim()) {
|
||||||
|
toast.warning('Please enter a remark');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let newStatus = application.status;
|
||||||
|
switch (application.status) {
|
||||||
|
case 'Shortlisted':
|
||||||
|
case 'Level 1 Interview Pending': newStatus = 'Level 1 Approved'; break;
|
||||||
|
case 'Level 1 Approved':
|
||||||
|
case 'Level 2 Interview Pending': newStatus = 'Level 2 Approved'; break;
|
||||||
|
case 'Level 2 Approved':
|
||||||
|
case 'Level 3 Interview Pending': newStatus = 'Level 3 Approved'; break;
|
||||||
|
case 'Level 3 Approved': newStatus = 'FDD Verification'; break;
|
||||||
|
case 'FDD Verification': newStatus = 'LOI In Progress'; break;
|
||||||
|
case 'LOI In Progress': newStatus = 'Security Details'; break;
|
||||||
|
case 'Security Details':
|
||||||
|
case 'Payment Pending': newStatus = 'LOI Issued'; break;
|
||||||
|
case 'LOI Issued': newStatus = 'Dealer Code Generation'; break;
|
||||||
|
case 'Dealer Code Generation':
|
||||||
|
case 'Architecture Team Assigned':
|
||||||
|
case 'Architecture Document Upload':
|
||||||
|
case 'Architecture Team Completion':
|
||||||
|
case 'Statutory GST':
|
||||||
|
case 'Statutory PAN':
|
||||||
|
case 'Statutory Nodal':
|
||||||
|
case 'Statutory Check':
|
||||||
|
case 'Statutory Partnership':
|
||||||
|
case 'Statutory Firm Reg':
|
||||||
|
case 'Statutory Rental':
|
||||||
|
case 'Statutory Virtual Code':
|
||||||
|
case 'Statutory Domain':
|
||||||
|
case 'Statutory MSD':
|
||||||
|
case 'Statutory LOI Ack': newStatus = 'LOA Pending'; break;
|
||||||
|
case 'LOA Pending': newStatus = 'EOR In Progress'; break;
|
||||||
|
case 'EOR In Progress': newStatus = 'EOR Complete'; break;
|
||||||
|
case 'EOR Complete': newStatus = 'Inauguration'; break;
|
||||||
|
case 'Inauguration':
|
||||||
|
case 'Approved': newStatus = 'Onboarded'; break;
|
||||||
|
default: newStatus = 'Onboarded';
|
||||||
|
}
|
||||||
|
|
||||||
|
const policyManagedStages: Record<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 });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newStatus === 'Onboarded') {
|
||||||
|
await onboardingService.createDealer({ applicationId });
|
||||||
|
toast.success('Application finalized and Dealer profile created!');
|
||||||
|
} else {
|
||||||
|
toast.success(`Application moved to ${newStatus}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
setShowApproveModal(false);
|
||||||
|
setApprovalRemark('');
|
||||||
|
setApprovalFile(null);
|
||||||
|
await fetchApplication();
|
||||||
|
} catch (error: any) {
|
||||||
|
toast.error(error.message || 'Failed to process approval');
|
||||||
|
} finally {
|
||||||
|
setIsApproving(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleReject = async () => {
|
||||||
|
try {
|
||||||
|
setIsRejecting(true);
|
||||||
|
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('');
|
||||||
|
await fetchInterviews();
|
||||||
|
await fetchApplication();
|
||||||
|
return;
|
||||||
|
} catch {
|
||||||
|
toast.error('Failed to reject interview');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!rejectionReason.trim()) {
|
||||||
|
toast.warning('Please enter a reason for rejection');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const policyManagedStages: Record<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('');
|
||||||
|
await fetchApplication();
|
||||||
|
} catch (error: any) {
|
||||||
|
toast.error(error.message || 'Failed to process rejection');
|
||||||
|
} finally {
|
||||||
|
setIsRejecting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleGenerateDealerCodes = async () => {
|
||||||
|
try {
|
||||||
|
await onboardingService.generateDealerCodes(applicationId);
|
||||||
|
toast.success('Dealer codes generated successfully');
|
||||||
|
await fetchApplication();
|
||||||
|
} catch (error: any) {
|
||||||
|
toast.error(error.message || 'Failed to generate dealer codes');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAssignArchitecture = async () => {
|
||||||
|
if (!architectureLeadId) {
|
||||||
|
toast.warning('Please select an architecture lead');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
setIsAssigningArchitecture(true);
|
||||||
|
await onboardingService.assignArchitectureTeam(applicationId, architectureLeadId);
|
||||||
|
toast.success('Architecture team assigned successfully');
|
||||||
|
setShowAssignArchitectureModal(false);
|
||||||
|
await fetchApplication();
|
||||||
|
} catch (error: any) {
|
||||||
|
toast.error(error.message || 'Failed to assign architecture team');
|
||||||
|
} finally {
|
||||||
|
setIsAssigningArchitecture(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleUpdateArchitectureStatus = async () => {
|
||||||
|
try {
|
||||||
|
setIsUpdatingArchitecture(true);
|
||||||
|
await onboardingService.updateArchitectureStatus(applicationId, architectureStatus, architectureRemarks);
|
||||||
|
toast.success('Architecture status updated successfully');
|
||||||
|
setShowArchitectureStatusModal(false);
|
||||||
|
await fetchApplication();
|
||||||
|
} catch {
|
||||||
|
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);
|
||||||
|
const u = Array.isArray(users) ? users.find((user) => user.id === selectedUser) : null;
|
||||||
|
if (u && (u.role === 'FDD' || u.roleCode === 'FDD')) {
|
||||||
|
await onboardingService.assignFddAgency({ applicationId, assignedToAgency: selectedUser });
|
||||||
|
toast.info(`${u.fullName || u.name} assigned as FDD Agency based on role.`);
|
||||||
|
}
|
||||||
|
await onboardingService.addParticipant({
|
||||||
|
requestId: applicationId,
|
||||||
|
requestType: 'application',
|
||||||
|
userId: selectedUser,
|
||||||
|
participantType: participantType || 'contributor',
|
||||||
|
});
|
||||||
|
toast.success('User assigned successfully!');
|
||||||
|
await fetchApplication();
|
||||||
|
setSelectedUser('');
|
||||||
|
setShowAssignModal(false);
|
||||||
|
} catch {
|
||||||
|
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 {
|
||||||
|
toast.error('Failed to re-assign evaluators');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const maybeFetchUsersForModal = async () => {
|
||||||
|
if (showScheduleModal && application) {
|
||||||
|
await fetchUsers(interviewType);
|
||||||
|
prefillInterviewParticipants();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if ((showAssignArchitectureModal || showAssignModal) && application) {
|
||||||
|
await fetchUsers();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
handleAddInterviewer,
|
||||||
|
handleRemoveInterviewer,
|
||||||
|
fetchUsers,
|
||||||
|
maybeFetchUsersForModal,
|
||||||
|
handleScheduleInterview,
|
||||||
|
handleCancelInterview,
|
||||||
|
handleUpload,
|
||||||
|
handleApprove,
|
||||||
|
handleReject,
|
||||||
|
handleGenerateDealerCodes,
|
||||||
|
handleAssignArchitecture,
|
||||||
|
handleUpdateArchitectureStatus,
|
||||||
|
handleAddParticipant,
|
||||||
|
handleRetriggerEvaluators,
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -0,0 +1,206 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import { Application, ApplicationStatus } from '../../../lib/mock-data';
|
||||||
|
import { onboardingService } from '../../../services/onboarding.service';
|
||||||
|
import { eorService } from '../../../services/eor.service';
|
||||||
|
import { auditService } from '../../../services/audit.service';
|
||||||
|
import { collaborationService } from '../../../services/collaboration.service';
|
||||||
|
|
||||||
|
interface UseApplicationDetailsDataParams {
|
||||||
|
applicationId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useApplicationDetailsData({ applicationId }: UseApplicationDetailsDataParams) {
|
||||||
|
const [application, setApplication] = useState<Application | null>(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [documents, setDocuments] = useState<any[]>([]);
|
||||||
|
const [eorData, setEorData] = useState<any>(null);
|
||||||
|
const [auditLogs, setAuditLogs] = useState<any[]>([]);
|
||||||
|
const [auditLoading, setAuditLoading] = useState(false);
|
||||||
|
const [worknotes, setWorknotes] = useState<any[]>([]);
|
||||||
|
const [deposits, setDeposits] = useState<any[]>([]);
|
||||||
|
const [paymentConfigs, setPaymentConfigs] = useState<any>({});
|
||||||
|
|
||||||
|
const refreshDocuments = async () => {
|
||||||
|
try {
|
||||||
|
const docs = await onboardingService.getDocuments(applicationId);
|
||||||
|
setDocuments(docs || []);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to refresh documents:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchApplication = async (silent = false) => {
|
||||||
|
try {
|
||||||
|
if (!silent) setLoading(true);
|
||||||
|
const data = await onboardingService.getApplicationById(applicationId);
|
||||||
|
|
||||||
|
const getStageDate = (stageName: string, fallbackStatus?: string) => {
|
||||||
|
const stage = data.progressTracking?.find((p: any) => p.stageName === stageName);
|
||||||
|
if (stage?.stageCompletedAt) return new Date(stage.stageCompletedAt).toISOString();
|
||||||
|
if (stage?.stageStartedAt) return new Date(stage.stageStartedAt).toISOString();
|
||||||
|
if (fallbackStatus) {
|
||||||
|
const history = (data.statusHistory || []).find((h: any) => h.newStatus === fallbackStatus);
|
||||||
|
if (history) return new Date(history.createdAt).toISOString();
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
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,
|
||||||
|
questionnaireResponses: data.questionnaireResponses || [],
|
||||||
|
rank: 0,
|
||||||
|
totalApplicantsAtLocation: 0,
|
||||||
|
assignedUsers: [],
|
||||||
|
progress: data.progressPercentage || 0,
|
||||||
|
isShortlisted: data.isShortlisted || true,
|
||||||
|
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,
|
||||||
|
submissionDate: data.createdAt ? new Date(data.createdAt).toISOString() : '',
|
||||||
|
questionnaireDate: getStageDate('Questionnaire', 'Questionnaire Completed') || getStageDate('Questionnaire', 'Questionnaire Pending'),
|
||||||
|
shortlistDate: getStageDate('Shortlist', 'Shortlisted'),
|
||||||
|
level1InterviewDate: getStageDate('1st Level Interview', 'Level 1 Approved'),
|
||||||
|
level2InterviewDate: getStageDate('2nd Level Interview', 'Level 2 Approved'),
|
||||||
|
level3InterviewDate: getStageDate('3rd Level Interview', 'Level 3 Approved'),
|
||||||
|
fddDate: getStageDate('FDD', 'FDD Verification'),
|
||||||
|
loiApprovalDate: getStageDate('LOI Approval', 'LOI In Progress'),
|
||||||
|
securityDetailsDate: getStageDate('Security Details', 'Security Details'),
|
||||||
|
loiIssueDate: getStageDate('LOI Issue', 'LOI Issued'),
|
||||||
|
dealerCodeDate: getStageDate('Dealer Code Generation', 'Dealer Code Generation'),
|
||||||
|
architectureAssignedDate: getStageDate('Architecture Team Assigned', 'Architecture Team Assigned'),
|
||||||
|
architectureDocumentDate: getStageDate('Architecture Document Upload', 'Architecture Document Upload'),
|
||||||
|
architectureCompletionDate: getStageDate('Architecture Team Completion', 'Architecture Team Completion'),
|
||||||
|
loaDate: getStageDate('LOA', 'LOA Pending'),
|
||||||
|
eorCompleteDate: getStageDate('EOR Complete', 'EOR Complete'),
|
||||||
|
inaugurationDate: getStageDate('Inauguration', 'Inauguration'),
|
||||||
|
onboardedDate: data.overallStatus === 'Onboarded' ? (data.updatedAt ? new Date(data.updatedAt).toISOString() : new Date().toISOString()) : undefined,
|
||||||
|
progressTracking: data.progressTracking || [],
|
||||||
|
participants: data.participants || [],
|
||||||
|
dealerCode: data.dealerCode,
|
||||||
|
zoneId: data.zoneId,
|
||||||
|
regionId: data.regionId,
|
||||||
|
areaId: data.areaId,
|
||||||
|
districtId: data.districtId,
|
||||||
|
stageApprovals: data.stageApprovals || [],
|
||||||
|
fddAssignments: data.fddAssignments || [],
|
||||||
|
constitutionType: data.constitutionType,
|
||||||
|
architectureStatus: data.architectureStatus,
|
||||||
|
statutoryStatus: data.statutoryStatus,
|
||||||
|
panNumber: data.panNumber,
|
||||||
|
gstNumber: data.gstNumber,
|
||||||
|
bankName: data.bankName,
|
||||||
|
accountNumber: data.accountNumber,
|
||||||
|
ifscCode: data.ifscCode,
|
||||||
|
branchName: data.branchName,
|
||||||
|
accountHolderName: data.accountHolderName,
|
||||||
|
registeredAddress: data.registeredAddress,
|
||||||
|
};
|
||||||
|
|
||||||
|
setApplication(mappedApp);
|
||||||
|
if (data.uploadedDocuments) setDocuments(data.uploadedDocuments || []);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to fetch application details', error);
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchEorData = async () => {
|
||||||
|
if (!applicationId) return;
|
||||||
|
try {
|
||||||
|
const resp = await eorService.getChecklist(applicationId);
|
||||||
|
if (resp.success && resp.data) setEorData(resp.data);
|
||||||
|
} catch {
|
||||||
|
setEorData(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getDeposit = (type: string) => deposits.find((d) => d.depositType === type);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!applicationId) return;
|
||||||
|
fetchApplication();
|
||||||
|
refreshDocuments();
|
||||||
|
}, [applicationId]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (applicationId) fetchEorData();
|
||||||
|
}, [applicationId, application?.status]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!application?.id) return;
|
||||||
|
const fetchAuditAndWorknotes = async () => {
|
||||||
|
setAuditLoading(true);
|
||||||
|
try {
|
||||||
|
const logs = await auditService.getAuditLogs('application', application.id, 1, 100);
|
||||||
|
setAuditLogs(Array.isArray(logs) ? logs : []);
|
||||||
|
} catch {
|
||||||
|
setAuditLogs([]);
|
||||||
|
} finally {
|
||||||
|
setAuditLoading(false);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const res = await collaborationService.getWorknotes('application', application.id);
|
||||||
|
setWorknotes(res.data || []);
|
||||||
|
} catch {
|
||||||
|
setWorknotes([]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
fetchAuditAndWorknotes();
|
||||||
|
}, [application?.id]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!applicationId) return;
|
||||||
|
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]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
application,
|
||||||
|
loading,
|
||||||
|
setLoading,
|
||||||
|
documents,
|
||||||
|
setDocuments,
|
||||||
|
eorData,
|
||||||
|
auditLogs,
|
||||||
|
auditLoading,
|
||||||
|
worknotes,
|
||||||
|
deposits,
|
||||||
|
paymentConfigs,
|
||||||
|
refreshDocuments,
|
||||||
|
fetchApplication,
|
||||||
|
fetchEorData,
|
||||||
|
getDeposit,
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -0,0 +1,211 @@
|
|||||||
|
import { toast } from 'sonner';
|
||||||
|
import { Dispatch, SetStateAction } from 'react';
|
||||||
|
import { onboardingService } from '../../../services/onboarding.service';
|
||||||
|
import { KT_MATRIX_CRITERIA } from './applicationDetails.shared';
|
||||||
|
|
||||||
|
interface UseApplicationDetailsFeedbackActionsParams {
|
||||||
|
ktMatrixScores: Record<string, number>;
|
||||||
|
setKtMatrixScores: Dispatch<SetStateAction<Record<string, number>>>;
|
||||||
|
setKtMatrixSelectedValues: Dispatch<SetStateAction<Record<string, string>>>;
|
||||||
|
ktMatrixRemarks: string;
|
||||||
|
setKtMatrixRemarks: Dispatch<SetStateAction<string>>;
|
||||||
|
selectedInterviewForFeedback: any;
|
||||||
|
interviews: any[];
|
||||||
|
setIsSubmittingKT: Dispatch<SetStateAction<boolean>>;
|
||||||
|
setShowKTMatrixModal: Dispatch<SetStateAction<boolean>>;
|
||||||
|
level2Feedback: any;
|
||||||
|
setLevel2Feedback: Dispatch<SetStateAction<any>>;
|
||||||
|
setIsSubmittingLevel2: Dispatch<SetStateAction<boolean>>;
|
||||||
|
setShowLevel2FeedbackModal: Dispatch<SetStateAction<boolean>>;
|
||||||
|
level3Feedback: any;
|
||||||
|
setLevel3Feedback: Dispatch<SetStateAction<any>>;
|
||||||
|
setIsSubmittingLevel3: Dispatch<SetStateAction<boolean>>;
|
||||||
|
setShowLevel3FeedbackModal: Dispatch<SetStateAction<boolean>>;
|
||||||
|
currentUser: any;
|
||||||
|
fetchInterviews: () => Promise<void>;
|
||||||
|
fetchApplication: (silent?: boolean) => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const today = () => new Date().toISOString().split('T')[0];
|
||||||
|
|
||||||
|
const createInitialLevel2Feedback = (currentUser: any) => ({
|
||||||
|
strategicVision: '',
|
||||||
|
managementCapabilities: '',
|
||||||
|
operationalUnderstanding: '',
|
||||||
|
keyStrengths: '',
|
||||||
|
areasOfConcern: '',
|
||||||
|
additionalComments: '',
|
||||||
|
overallScore: '',
|
||||||
|
interviewerName: currentUser?.name || '',
|
||||||
|
interviewDate: today(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const createInitialLevel3Feedback = (currentUser: any) => ({
|
||||||
|
strategicVision: '',
|
||||||
|
managementCapabilities: '',
|
||||||
|
operationalUnderstanding: '',
|
||||||
|
brandAlignment: '',
|
||||||
|
executiveSummary: '',
|
||||||
|
keyStrengths: '',
|
||||||
|
areasOfConcern: '',
|
||||||
|
additionalComments: '',
|
||||||
|
overallScore: '',
|
||||||
|
interviewerName: currentUser?.name || '',
|
||||||
|
interviewDate: today(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export function useApplicationDetailsFeedbackActions({
|
||||||
|
ktMatrixScores,
|
||||||
|
setKtMatrixScores,
|
||||||
|
setKtMatrixSelectedValues,
|
||||||
|
ktMatrixRemarks,
|
||||||
|
setKtMatrixRemarks,
|
||||||
|
selectedInterviewForFeedback,
|
||||||
|
interviews,
|
||||||
|
setIsSubmittingKT,
|
||||||
|
setShowKTMatrixModal,
|
||||||
|
level2Feedback,
|
||||||
|
setLevel2Feedback,
|
||||||
|
setIsSubmittingLevel2,
|
||||||
|
setShowLevel2FeedbackModal,
|
||||||
|
level3Feedback,
|
||||||
|
setLevel3Feedback,
|
||||||
|
setIsSubmittingLevel3,
|
||||||
|
setShowLevel3FeedbackModal,
|
||||||
|
currentUser,
|
||||||
|
fetchInterviews,
|
||||||
|
fetchApplication,
|
||||||
|
}: UseApplicationDetailsFeedbackActionsParams) {
|
||||||
|
const handleKTMatrixChange = (criterionName: string, value: string, score: number) => {
|
||||||
|
setKtMatrixScores((prev) => ({ ...prev, [criterionName]: score }));
|
||||||
|
setKtMatrixSelectedValues((prev) => ({ ...prev, [criterionName]: value }));
|
||||||
|
};
|
||||||
|
|
||||||
|
const calculateKTScore = () => {
|
||||||
|
let totalWeightedScore = 0;
|
||||||
|
KT_MATRIX_CRITERIA.forEach((criterion) => {
|
||||||
|
const score = ktMatrixScores[criterion.name] || 0;
|
||||||
|
totalWeightedScore += (score / criterion.maxScore) * criterion.weight;
|
||||||
|
});
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
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 });
|
||||||
|
toast.success('KT Matrix submitted successfully');
|
||||||
|
setShowKTMatrixModal(false);
|
||||||
|
setKtMatrixScores({});
|
||||||
|
setKtMatrixSelectedValues({});
|
||||||
|
setKtMatrixRemarks('');
|
||||||
|
await fetchInterviews();
|
||||||
|
await fetchApplication();
|
||||||
|
} catch {
|
||||||
|
toast.error('Failed to submit KT Matrix');
|
||||||
|
} finally {
|
||||||
|
setIsSubmittingKT(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLevel2Change = (field: string, value: string) => {
|
||||||
|
setLevel2Feedback((prev: any) => ({ ...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);
|
||||||
|
setLevel2Feedback(createInitialLevel2Feedback(currentUser));
|
||||||
|
await fetchInterviews();
|
||||||
|
await fetchApplication();
|
||||||
|
} catch {
|
||||||
|
toast.error('Failed to submit Level 2 Feedback');
|
||||||
|
} finally {
|
||||||
|
setIsSubmittingLevel2(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLevel3Change = (field: string, value: string) => {
|
||||||
|
setLevel3Feedback((prev: any) => ({ ...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);
|
||||||
|
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() !== '');
|
||||||
|
await onboardingService.submitLevel2Feedback({ interviewId, overallScore: Number(level3Feedback.overallScore), feedbackItems });
|
||||||
|
toast.success('Level 3 Feedback submitted successfully');
|
||||||
|
setShowLevel3FeedbackModal(false);
|
||||||
|
setLevel3Feedback(createInitialLevel3Feedback(currentUser));
|
||||||
|
await fetchInterviews();
|
||||||
|
await fetchApplication();
|
||||||
|
} catch {
|
||||||
|
toast.error('Failed to submit Level 3 Feedback');
|
||||||
|
} finally {
|
||||||
|
setIsSubmittingLevel3(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
handleKTMatrixChange,
|
||||||
|
calculateKTScore,
|
||||||
|
handleSubmitKTMatrix,
|
||||||
|
handleLevel2Change,
|
||||||
|
handleSubmitLevel2Feedback,
|
||||||
|
handleLevel3Change,
|
||||||
|
handleSubmitLevel3Feedback,
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -0,0 +1,120 @@
|
|||||||
|
import { Dispatch, SetStateAction } from 'react';
|
||||||
|
import { toast } from 'sonner';
|
||||||
|
import { onboardingService } from '../../../services/onboarding.service';
|
||||||
|
|
||||||
|
interface UseApplicationDetailsLocalActionsParams {
|
||||||
|
application: any;
|
||||||
|
applicationId: string;
|
||||||
|
tempFirmType: string;
|
||||||
|
setUpdatingFirmType: Dispatch<SetStateAction<boolean>>;
|
||||||
|
setShowFirmTypeModal: Dispatch<SetStateAction<boolean>>;
|
||||||
|
setStatutoryForm: Dispatch<SetStateAction<any>>;
|
||||||
|
setIsEditingStatutory: Dispatch<SetStateAction<boolean>>;
|
||||||
|
setIsSavingStatutory: Dispatch<SetStateAction<boolean>>;
|
||||||
|
statutoryForm: any;
|
||||||
|
setFddAgencies: Dispatch<SetStateAction<any[]>>;
|
||||||
|
selectedAgencyId: string;
|
||||||
|
setIsAssigningAgency: Dispatch<SetStateAction<boolean>>;
|
||||||
|
fetchApplication: (silent?: boolean) => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useApplicationDetailsLocalActions({
|
||||||
|
application,
|
||||||
|
applicationId,
|
||||||
|
tempFirmType,
|
||||||
|
setUpdatingFirmType,
|
||||||
|
setShowFirmTypeModal,
|
||||||
|
setStatutoryForm,
|
||||||
|
setIsEditingStatutory,
|
||||||
|
setIsSavingStatutory,
|
||||||
|
statutoryForm,
|
||||||
|
setFddAgencies,
|
||||||
|
selectedAgencyId,
|
||||||
|
setIsAssigningAgency,
|
||||||
|
fetchApplication,
|
||||||
|
}: UseApplicationDetailsLocalActionsParams) {
|
||||||
|
const handleUpdateFirmType = async () => {
|
||||||
|
try {
|
||||||
|
setUpdatingFirmType(true);
|
||||||
|
await onboardingService.updateApplication(applicationId, { constitutionType: tempFirmType });
|
||||||
|
toast.success('Firm type updated successfully');
|
||||||
|
setShowFirmTypeModal(false);
|
||||||
|
await fetchApplication();
|
||||||
|
} catch {
|
||||||
|
toast.error('Failed to update firm type');
|
||||||
|
} finally {
|
||||||
|
setUpdatingFirmType(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleEditStatutory = () => {
|
||||||
|
if (!application) return;
|
||||||
|
setStatutoryForm({
|
||||||
|
accountHolderName: application.accountHolderName || '',
|
||||||
|
panNumber: application.panNumber || '',
|
||||||
|
gstNumber: application.gstNumber || '',
|
||||||
|
bankName: application.bankName || '',
|
||||||
|
accountNumber: application.accountNumber || '',
|
||||||
|
ifscCode: application.ifscCode || '',
|
||||||
|
registeredAddress: application.registeredAddress || '',
|
||||||
|
});
|
||||||
|
setIsEditingStatutory(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSaveStatutory = async () => {
|
||||||
|
try {
|
||||||
|
setIsSavingStatutory(true);
|
||||||
|
await onboardingService.updateApplication(applicationId, statutoryForm);
|
||||||
|
toast.success('Statutory & Bank details updated successfully');
|
||||||
|
setIsEditingStatutory(false);
|
||||||
|
await fetchApplication(true);
|
||||||
|
} catch {
|
||||||
|
toast.error('Failed to update details');
|
||||||
|
} finally {
|
||||||
|
setIsSavingStatutory(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchFddAgencies = async () => {
|
||||||
|
try {
|
||||||
|
const agencies = await onboardingService.getUsers({ roleCode: 'FDD' });
|
||||||
|
setFddAgencies(Array.isArray(agencies) ? agencies : []);
|
||||||
|
} catch {
|
||||||
|
setFddAgencies([]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleAssignAgency = async () => {
|
||||||
|
if (!selectedAgencyId) {
|
||||||
|
toast.warning('Please select an agency');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
setIsAssigningAgency(true);
|
||||||
|
await onboardingService.assignFddAgency({
|
||||||
|
applicationId: application?.id || applicationId,
|
||||||
|
assignedToAgency: selectedAgencyId,
|
||||||
|
});
|
||||||
|
await onboardingService.addParticipant({
|
||||||
|
requestId: application?.id || applicationId,
|
||||||
|
requestType: 'application',
|
||||||
|
userId: selectedAgencyId,
|
||||||
|
participantType: 'contributor',
|
||||||
|
});
|
||||||
|
toast.success('FDD Agency assigned and added as participant');
|
||||||
|
await fetchApplication();
|
||||||
|
} catch {
|
||||||
|
toast.error('Failed to assign agency');
|
||||||
|
} finally {
|
||||||
|
setIsAssigningAgency(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
handleUpdateFirmType,
|
||||||
|
handleEditStatutory,
|
||||||
|
handleSaveStatutory,
|
||||||
|
fetchFddAgencies,
|
||||||
|
handleAssignAgency,
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -0,0 +1,138 @@
|
|||||||
|
interface UseApplicationDetailsPermissionsParams {
|
||||||
|
application: any;
|
||||||
|
interviews: any[];
|
||||||
|
currentUser: any;
|
||||||
|
getDeposit: (type: string) => any;
|
||||||
|
eorProgress: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useApplicationDetailsPermissions({
|
||||||
|
application,
|
||||||
|
interviews,
|
||||||
|
currentUser,
|
||||||
|
getDeposit,
|
||||||
|
eorProgress,
|
||||||
|
}: UseApplicationDetailsPermissionsParams) {
|
||||||
|
const interviewsList = Array.isArray(interviews) ? interviews : [];
|
||||||
|
|
||||||
|
const activeInterviewForUser = interviewsList.find((i: any) =>
|
||||||
|
['Scheduled', 'Rescheduled', 'Pending', 'In Progress'].includes(i.status) &&
|
||||||
|
i.participants?.some((p: any) => p.userId === currentUser?.id)
|
||||||
|
);
|
||||||
|
|
||||||
|
const lastInterviewForUser = [...interviewsList].reverse().find((i: any) =>
|
||||||
|
i.participants?.some((p: any) => p.userId === currentUser?.id)
|
||||||
|
);
|
||||||
|
|
||||||
|
const currentUserEvaluation = (activeInterviewForUser || lastInterviewForUser)?.evaluations?.find(
|
||||||
|
(e: any) => e.evaluatorId === currentUser?.id
|
||||||
|
);
|
||||||
|
|
||||||
|
const isInterviewCompleted = (level: number) =>
|
||||||
|
interviewsList.some((i: any) => Number(i.level) === level && i.status === 'Completed');
|
||||||
|
|
||||||
|
const isInterviewActive = (level: number) =>
|
||||||
|
interviewsList.some((i: any) => Number(i.level) === level && i.status === 'Scheduled');
|
||||||
|
|
||||||
|
const hasSubmittedFeedback = !!currentUserEvaluation;
|
||||||
|
|
||||||
|
const policyManagedStages: Record<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 getApplicationPermissions = () => {
|
||||||
|
if (!application || !currentUser) {
|
||||||
|
return {
|
||||||
|
canApprove: false,
|
||||||
|
canReject: false,
|
||||||
|
canSchedule: false,
|
||||||
|
canAssign: false,
|
||||||
|
isLoaLocked: false,
|
||||||
|
isSecurityDetailsLocked: false,
|
||||||
|
showDecisionMessage: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const isAdminRole = ['DD Admin', 'Super Admin', 'NBH', 'DD Lead', 'DD Head', 'Finance', 'Finance Admin', 'FDD', 'ZBH', 'RBM'].includes(currentUser.role);
|
||||||
|
const isAdministrativeStage = [
|
||||||
|
'Level 3 Approved', 'FDD Verification',
|
||||||
|
'LOI In Progress', 'Security Details', 'Payment Pending', 'LOI Issued', 'Statutory LOI Ack',
|
||||||
|
'Architecture Team Assigned', 'Architecture Document Upload', 'Architecture Team Completion',
|
||||||
|
'Statutory GST', 'Statutory PAN', 'Statutory Nodal', 'Statutory Check',
|
||||||
|
'Statutory Partnership', 'Statutory Firm Reg', 'Statutory Rental',
|
||||||
|
'Statutory Virtual Code', 'Statutory Domain', 'Statutory MSD',
|
||||||
|
'LOA Pending', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved',
|
||||||
|
].includes(application.status);
|
||||||
|
|
||||||
|
const isLoaLocked = application.status === 'LOA Pending' && getDeposit('FIRST_FILL')?.status !== 'Verified';
|
||||||
|
const isSecurityDetailsLocked =
|
||||||
|
['Security Details', 'Payment Pending'].includes(application.status) &&
|
||||||
|
getDeposit('SECURITY_DEPOSIT')?.status !== 'Verified';
|
||||||
|
const isFinalState = application.status === 'Onboarded' || application.status === 'Rejected';
|
||||||
|
|
||||||
|
const hasFeedbackForActive = !!(activeInterviewForUser || lastInterviewForUser)?.evaluations?.find(
|
||||||
|
(e: any) => e.evaluatorId === currentUser?.id
|
||||||
|
);
|
||||||
|
|
||||||
|
const ddHeadApproved = application.stageApprovals?.some((a: any) => a.stageCode === 'LOI_APPROVAL' && a.actorRole === 'DD Head' && a.decision === 'Approved');
|
||||||
|
const ddHeadLoaApproved = application.stageApprovals?.some((a: any) => a.stageCode === 'LOA_APPROVAL' && a.actorRole === 'DD Head' && a.decision === 'Approved');
|
||||||
|
|
||||||
|
let sequenceMet = true;
|
||||||
|
if (!['Super Admin', 'DD Admin'].includes(currentUser.role)) {
|
||||||
|
if (application.status === 'FDD Verification' || application.status === 'Level 3 Approved') sequenceMet = false;
|
||||||
|
if (application.status === 'LOI In Progress') sequenceMet = currentUser.role === 'NBH' ? !!ddHeadApproved : currentUser.role === 'DD Head';
|
||||||
|
if (application.status === 'LOA Pending') sequenceMet = currentUser.role === 'NBH' ? !!ddHeadLoaApproved : currentUser.role === 'DD Head';
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasMadeStageDecision = !!application.stageApprovals?.find(
|
||||||
|
(a: any) => policyManagedStages[application.status] === a.stageCode && String(a.actorUserId) === String(currentUser.id)
|
||||||
|
);
|
||||||
|
const hasMadeInterviewDecision = ['Approved', 'Rejected', 'Selected'].includes(
|
||||||
|
currentUserEvaluation?.decision || currentUserEvaluation?.recommendation || ''
|
||||||
|
);
|
||||||
|
|
||||||
|
const isDecisionMade = (activeInterviewForUser ? hasMadeInterviewDecision : false) || hasMadeStageDecision;
|
||||||
|
const canApproveReject =
|
||||||
|
!isFinalState &&
|
||||||
|
!isDecisionMade &&
|
||||||
|
((!!activeInterviewForUser && !!hasFeedbackForActive) ||
|
||||||
|
(isAdminRole &&
|
||||||
|
isAdministrativeStage &&
|
||||||
|
sequenceMet &&
|
||||||
|
(!['EOR In Progress', 'Inauguration', 'Approved'].includes(application.status) || eorProgress === 100)));
|
||||||
|
|
||||||
|
return {
|
||||||
|
canApprove: canApproveReject && !isLoaLocked && !isSecurityDetailsLocked,
|
||||||
|
canReject: canApproveReject && !isLoaLocked,
|
||||||
|
isLoaLocked,
|
||||||
|
isSecurityDetailsLocked,
|
||||||
|
showDecisionMessage: isDecisionMade && (!isAdministrativeStage || hasMadeStageDecision),
|
||||||
|
canSchedule:
|
||||||
|
['DD Admin', 'Super Admin', 'DD AM', 'ASM'].includes(currentUser.role) &&
|
||||||
|
!isFinalState &&
|
||||||
|
![1, 2, 3].every((level) => interviewsList.some((i: any) => i.level === level)),
|
||||||
|
canAssign: ['DD Admin', 'Super Admin', 'DD AM'].includes(currentUser.role),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
activeInterviewForUser,
|
||||||
|
lastInterviewForUser,
|
||||||
|
currentUserEvaluation,
|
||||||
|
hasSubmittedFeedback,
|
||||||
|
currentUserStageAction,
|
||||||
|
isInterviewCompleted,
|
||||||
|
isInterviewActive,
|
||||||
|
permissions: getApplicationPermissions(),
|
||||||
|
};
|
||||||
|
}
|
||||||
@ -0,0 +1,140 @@
|
|||||||
|
import { ProcessStage } from './applicationDetails.shared';
|
||||||
|
|
||||||
|
interface UseApplicationDetailsStageDataParams {
|
||||||
|
application: any;
|
||||||
|
documents: any[];
|
||||||
|
interviews: any[];
|
||||||
|
eorData: any;
|
||||||
|
getDeposit: (type: string) => any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useApplicationDetailsStageData({
|
||||||
|
application,
|
||||||
|
documents,
|
||||||
|
interviews,
|
||||||
|
eorData,
|
||||||
|
getDeposit,
|
||||||
|
}: UseApplicationDetailsStageDataParams) {
|
||||||
|
const isDocumentUploaded = (docType: string) => {
|
||||||
|
return (documents || []).some((d) => d.documentType === docType);
|
||||||
|
};
|
||||||
|
|
||||||
|
const isInterviewScheduled = (level: number | string) => {
|
||||||
|
return (interviews || []).some((i) => (i.level === level || i.level === level.toString()) && i.status?.toLowerCase() === 'scheduled');
|
||||||
|
};
|
||||||
|
|
||||||
|
const getStageStatus = (stageName: string, fallbackLogic: () => ProcessStage['status']): ProcessStage['status'] => {
|
||||||
|
const backendStage = (application.progressTracking || []).find((ps: any) => ps.stageName === stageName);
|
||||||
|
if (backendStage && (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', () =>
|
||||||
|
['Questionnaire Completed', 'Shortlisted', 'Level 1 Interview Pending', 'Level 1 Approved', 'Level 2 Interview Pending', 'Level 2 Approved', 'Level 2 Recommended', 'Level 3 Interview Pending', 'Level 3 Approved', 'FDD Verification', 'LOI In Progress', 'Payment Pending', 'LOI Issued', 'Statutory LOI Ack', 'Dealer Code Generation', 'Architecture Work', 'Statutory Work', 'LOA Pending', 'LOA Issued', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Onboarded'].includes(application.status) ? 'completed' : application.status === 'Questionnaire Pending' ? 'active' : 'pending'),
|
||||||
|
date: application.questionnaireDate, description: 'Questionnaire completed', documentsUploaded: 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 3, name: 'Shortlist', status: getStageStatus('Shortlist', () => ['Shortlisted', 'Level 1 Interview Pending', 'Level 1 Approved', 'Level 2 Interview Pending', 'Level 2 Approved', 'Level 2 Recommended', 'Level 3 Interview Pending', 'Level 3 Approved', 'FDD Verification', 'LOI In Progress', 'Payment Pending', 'LOI Issued', 'Statutory LOI Ack', 'Dealer Code Generation', 'Architecture Work', 'Statutory Work', 'LOA Pending', 'LOA Issued', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Rejected', 'Onboarded'].includes(application.status) ? 'completed' : 'pending'),
|
||||||
|
date: application.shortlistDate, description: 'Application shortlisted by DD',
|
||||||
|
evaluators: Array.from(new Set((application.participants || []).filter((p: any) => p.participantType === 'assignee').map((p: any) => `${p.user?.fullName || p.user?.name || 'User'} (${p.user?.roleCode || p.participantType})`))),
|
||||||
|
documentsUploaded: 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 4, name: '1st Level Interview', status: getStageStatus('1st Level Interview', () => ['Level 1 Approved', 'Level 2 Interview Pending', 'Level 2 Approved', 'Level 2 Recommended', 'Level 3 Interview Pending', 'Level 3 Approved', 'FDD Verification', 'LOI In Progress', 'Payment Pending', 'LOI Issued', 'Statutory LOI Ack', 'Dealer Code Generation', 'Architecture Work', 'Statutory Work', 'LOA Pending', 'LOA Issued', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Onboarded'].includes(application.status) ? 'completed' : (application.status === 'Level 1 Interview Pending' && isInterviewScheduled(1)) ? 'active' : 'pending'),
|
||||||
|
date: application.level1InterviewDate, description: 'DD-ZM + RBM evaluation',
|
||||||
|
evaluators: Array.from(new Set((application.participants || []).filter((p: any) => p.metadata?.interviewLevel === 1 || p.metadata?.interviewLevel === '1' || p.metadata?.allAssignments?.includes(1)).map((p: any) => `${p.user?.name} (${p.user?.role})`))),
|
||||||
|
documentsUploaded: 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 5, name: '2nd Level Interview', status: getStageStatus('2nd Level Interview', () => ['Level 2 Approved', 'Level 2 Recommended', 'Level 3 Interview Pending', 'Level 3 Approved', 'FDD Verification', 'LOI In Progress', 'Payment Pending', 'LOI Issued', 'Statutory LOI Ack', 'Dealer Code Generation', 'Architecture Work', 'Statutory Work', 'LOA Pending', 'LOA Issued', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Onboarded'].includes(application.status) ? 'completed' : (application.status === 'Level 2 Interview Pending' && isInterviewScheduled(2)) ? 'active' : 'pending'),
|
||||||
|
date: application.level2InterviewDate, description: 'DD Lead + ZBH evaluation',
|
||||||
|
evaluators: Array.from(new Set((application.participants || []).filter((p: any) => p.metadata?.interviewLevel === 2 || p.metadata?.interviewLevel === '2' || p.metadata?.allAssignments?.includes(2)).map((p: any) => `${p.user?.name} (${p.user?.role})`))),
|
||||||
|
documentsUploaded: 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 6, name: '3rd Level Interview', status: getStageStatus('3rd Level Interview', () => ['Level 3 Approved', 'FDD Verification', 'LOI In Progress', 'Payment Pending', 'LOI Issued', 'Statutory LOI Ack', 'Dealer Code Generation', 'Architecture Work', 'Statutory Work', 'LOA Pending', 'LOA Issued', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Onboarded'].includes(application.status) ? 'completed' : (application.status === 'Level 3 Interview Pending' && isInterviewScheduled(3)) ? 'active' : 'pending'),
|
||||||
|
date: application.level3InterviewDate, description: 'NBH + DD Head evaluation',
|
||||||
|
evaluators: Array.from(new Set((application.participants || []).filter((p: any) => p.metadata?.interviewLevel === 3 || p.metadata?.interviewLevel === '3' || p.metadata?.allAssignments?.includes(3)).map((p: any) => `${p.user?.name} (${p.user?.role})`))),
|
||||||
|
documentsUploaded: 2
|
||||||
|
},
|
||||||
|
{ id: 7, name: 'FDD', status: getStageStatus('FDD', () => ['LOI In Progress', 'Payment Pending', 'LOI Issued', 'Statutory LOI Ack', 'Dealer Code Generation', 'Architecture Work', 'Statutory Work', 'LOA Pending', 'LOA Issued', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Onboarded'].includes(application.status) ? 'completed' : application.status === 'FDD Verification' ? 'active' : 'pending'), date: application.fddDate, description: 'Financial Due Diligence', documentsUploaded: 5 },
|
||||||
|
{
|
||||||
|
id: 8, name: 'LOI Approval', status: getStageStatus('LOI Approval', () => ['Security Details', 'Payment Pending', 'LOI Issued', 'Statutory LOI Ack', 'Dealer Code Generation', 'Architecture Work', 'Statutory Work', 'LOA Pending', 'LOA Issued', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Onboarded'].includes(application.status) ? 'completed' : application.status === 'LOI In Progress' ? 'active' : 'pending'),
|
||||||
|
date: application.loiApprovalDate, description: 'Letter of Intent approval',
|
||||||
|
evaluators: Array.from(new Set((application.participants || []).filter((p: any) => p.metadata?.stageCode === 'LOI_APPROVAL' || p.metadata?.allAssignments?.includes('LOI_APPROVAL')).map((p: any) => `${p.user?.name} (${p.user?.role})`))),
|
||||||
|
documentsUploaded: 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 9, name: 'Security Details', status: getStageStatus('Security Details', () => ['LOI Issued', 'Statutory LOI Ack', 'Dealer Code Generation', 'Architecture Work', 'Statutory Work', 'LOA Pending', 'LOA Issued', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Onboarded'].includes(application.status) ? 'completed' : application.status === 'Security Details' || application.status === 'Payment Pending' ? 'active' : 'pending'),
|
||||||
|
date: application.securityDetailsDate, description: 'Security verification', documentsUploaded: 3
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 10, name: 'LOI Issue', status: getStageStatus('LOI Issue', () => ['Statutory LOI Ack', 'Dealer Code Generation', 'Architecture Work', 'Statutory Work', 'LOA Pending', 'LOA Issued', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Onboarded'].includes(application.status) ? 'completed' : application.status === 'LOI Issued' ? 'active' : 'pending'),
|
||||||
|
date: application.loiIssueDate, description: 'Letter of Intent issued', documentsUploaded: 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 11, name: 'Dealer Code Generation', status: getStageStatus('Dealer Code Generation', () => (application.dealerCode || ['Dealer Code Generation', 'Architecture Work', 'Statutory Work', 'LOA Pending', 'LOA Issued', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Onboarded'].includes(application.status)) ? 'completed' : 'pending'),
|
||||||
|
date: application.dealerCodeDate, description: 'Dealer code generated and assigned', isParallel: true,
|
||||||
|
branches: [
|
||||||
|
{ name: 'Architectural Work', color: 'blue', stages: [
|
||||||
|
{ id: '11a-1', name: 'Architecture Assignment', status: application.architectureAssignedTo ? 'completed' : application.status === 'Architecture Team Assigned' ? 'active' : 'pending', description: 'Assigned to architecture team' },
|
||||||
|
{ id: '11a-2', name: 'Site Plan Blueprint', status: isDocumentUploaded('Architecture Blueprint') ? 'completed' : application.architectureAssignedTo ? 'active' : 'pending', description: 'Blueprints and site plans' },
|
||||||
|
{ id: '11a-3', name: 'Architecture Work', status: application.architectureStatus === 'COMPLETED' ? 'completed' : (application.architectureStatus === 'IN_PROGRESS' || isDocumentUploaded('Architecture Blueprint')) ? 'active' : 'pending', description: 'Final architecture approval' },
|
||||||
|
]},
|
||||||
|
{ name: 'Statutory Documents', color: 'green', stages: [
|
||||||
|
{ id: '11b-1', name: 'GST', status: isDocumentUploaded('GST Certificate') || isDocumentUploaded('GST') ? 'completed' : 'active', description: 'GST certificate' },
|
||||||
|
{ id: '11b-2', name: 'PAN', status: isDocumentUploaded('PAN Card') || isDocumentUploaded('PAN') ? 'completed' : 'active', description: 'PAN card' },
|
||||||
|
{ id: '11b-3', name: 'Nodal Agreement', status: isDocumentUploaded('Nodal Agreement') ? 'completed' : 'active', description: 'Nodal agreement document' },
|
||||||
|
{ id: '11b-4', name: 'Cancelled Check', status: isDocumentUploaded('Cancelled Check') ? 'completed' : 'active', description: 'Cancelled check copy' },
|
||||||
|
{ id: '11b-5', name: 'Partnership Deed/LLP/MOA/AOA/COI', status: isDocumentUploaded('Partnership Deed/LLP/MOA/AOA/COI') || isDocumentUploaded('Partnership Deed') ? 'completed' : 'active', description: 'Business entity documents' },
|
||||||
|
{ id: '11b-6', name: 'Firm Registration Certificate', status: isDocumentUploaded('Firm Registration Certificate') || isDocumentUploaded('Firm Registration') ? 'completed' : 'active', description: 'Firm registration certificate' },
|
||||||
|
{ id: '11b-7', name: 'Rental agreement/ Lease agreement / Own/ Land agreement', status: isDocumentUploaded('Rental agreement/ Lease agreement / Own/ Land agreement') || isDocumentUploaded('Property Document') ? 'completed' : 'active', description: 'Property agreement document' },
|
||||||
|
{ id: '11b-8', name: 'Virtual Code', status: isDocumentUploaded('Virtual Code') || isDocumentUploaded('Virtual Code Confirmation') ? 'completed' : 'active', description: 'Virtual code availability' },
|
||||||
|
{ id: '11b-9', name: 'Domain ID', status: isDocumentUploaded('Domain ID') || isDocumentUploaded('Domain ID Setup') ? 'completed' : 'active', description: 'Domain ID setup' },
|
||||||
|
{ id: '11b-10', name: 'MSD Configuration', status: isDocumentUploaded('MSD Configuration') ? 'completed' : 'active', description: 'Microsoft Dynamics configuration' },
|
||||||
|
{ id: '11b-11', name: 'LOI Acknowledgement Copy', status: isDocumentUploaded('LOI Acknowledgement Copy') || isDocumentUploaded('LOI Acknowledgement') ? 'completed' : 'active', description: 'LOI acknowledgement copy' },
|
||||||
|
]},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 12, name: 'LOA', status: getStageStatus('LOA', () => ['EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Onboarded'].includes(application.status) ? 'completed' : application.status === 'LOA Pending' ? 'active' : 'pending'),
|
||||||
|
isLocked: application.status === 'LOA Pending' && getDeposit('FIRST_FILL')?.status !== 'Verified',
|
||||||
|
lockMessage: 'First Fill (₹15L) must be verified by Finance before LOA Approval.',
|
||||||
|
evaluators: Array.from(new Set((application.participants || []).filter((p: any) => p.metadata?.stageCode === 'LOA_APPROVAL' || p.metadata?.allAssignments?.includes('LOA_APPROVAL')).map((p: any) => `${p.user?.name} (${p.user?.role})`))),
|
||||||
|
description: 'Letter of Authorization'
|
||||||
|
},
|
||||||
|
{ id: 13, name: 'EOR Complete', status: getStageStatus('EOR Complete', () => ['Inauguration', 'Approved', 'Onboarded'].includes(application.status) ? 'completed' : application.status === 'EOR Complete' ? 'active' : 'pending'), description: 'Essential Operating Requirements' },
|
||||||
|
{ id: 14, name: 'Inauguration', status: getStageStatus('Inauguration', () => ['Approved', 'Onboarded'].includes(application.status) ? 'completed' : application.status === 'Inauguration' ? 'active' : 'pending'), description: 'Dealership inauguration' },
|
||||||
|
{ id: 15, name: 'Dealership Active', status: getStageStatus('Onboarded', () => application.status === 'Onboarded' ? 'completed' : ['Inauguration', 'Approved'].includes(application.status) ? 'active' : 'pending'), description: 'Dealer profile active' },
|
||||||
|
];
|
||||||
|
|
||||||
|
const eorChecklist = [
|
||||||
|
{ id: 1, item: 'Sales Standards', completed: false }, { id: 2, item: 'Service & Spares', completed: false }, { id: 3, item: 'DMS infra', completed: false },
|
||||||
|
{ id: 4, item: 'Manpower Training', completed: false }, { id: 5, item: 'Trade certificate with test ride bikes registration', completed: false },
|
||||||
|
{ id: 6, item: 'GST certificate including Accessories & Apparels billing', completed: false }, { id: 7, item: 'Inventory Funding', completed: false },
|
||||||
|
{ id: 8, item: 'Virtual code availability', completed: false }, { id: 9, item: 'Vendor payments', completed: false },
|
||||||
|
{ id: 10, item: 'Details for website submission', completed: false }, { id: 11, item: 'Infra Insurance both Showroom and Service center', completed: false },
|
||||||
|
{ id: 12, item: 'Auto ordering', completed: false },
|
||||||
|
];
|
||||||
|
|
||||||
|
const flattenedStages: any[] = processStages.reduce((acc: any[], stage: any) => {
|
||||||
|
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) =>
|
||||||
|
documents.filter((doc) => doc.stage === stageName || (!doc.stage && doc.documentType?.toLowerCase().includes(stageName.toLowerCase().split(' ')[0])));
|
||||||
|
|
||||||
|
return { processStages, eorChecklist, flattenedStages, getDocumentsForStage };
|
||||||
|
}
|
||||||
@ -0,0 +1,199 @@
|
|||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
interface UseApplicationDetailsUIStateParams {
|
||||||
|
currentUser: any;
|
||||||
|
initialTab?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getToday = () => new Date().toISOString().split('T')[0];
|
||||||
|
|
||||||
|
export function useApplicationDetailsUIState({
|
||||||
|
currentUser,
|
||||||
|
initialTab = 'questionnaire',
|
||||||
|
}: UseApplicationDetailsUIStateParams) {
|
||||||
|
const [showFirmTypeModal, setShowFirmTypeModal] = useState(false);
|
||||||
|
const [updatingFirmType, setUpdatingFirmType] = useState(false);
|
||||||
|
const [tempFirmType, setTempFirmType] = useState('');
|
||||||
|
|
||||||
|
const [activeTab, setActiveTab] = useState(initialTab);
|
||||||
|
const [showApproveModal, setShowApproveModal] = useState(false);
|
||||||
|
const [showOnboardModal, setShowOnboardModal] = useState(false);
|
||||||
|
const [isOnboarding, setIsOnboarding] = useState(false);
|
||||||
|
const [showRejectModal, setShowRejectModal] = useState(false);
|
||||||
|
const [rejectionReason, setRejectionReason] = useState('');
|
||||||
|
const [scheduledInterviewParticipants, setScheduledInterviewParticipants] = useState<any[]>([]);
|
||||||
|
const [showScheduleModal, setShowScheduleModal] = useState(false);
|
||||||
|
const [showKTMatrixModal, setShowKTMatrixModal] = useState(false);
|
||||||
|
const [showLevel2FeedbackModal, setShowLevel2FeedbackModal] = useState(false);
|
||||||
|
const [showLevel3FeedbackModal, setShowLevel3FeedbackModal] = useState(false);
|
||||||
|
const [showDocumentsModal, setShowDocumentsModal] = useState(false);
|
||||||
|
const [showAssignModal, setShowAssignModal] = useState(false);
|
||||||
|
const [selectedStage, setSelectedStage] = useState<string | null>(null);
|
||||||
|
const [interviewMode, setInterviewMode] = useState('virtual');
|
||||||
|
const [approvalRemark, setApprovalRemark] = useState('');
|
||||||
|
const [expandedBranches, setExpandedBranches] = useState<{ [key: string]: boolean }>({
|
||||||
|
'architectural-work': true,
|
||||||
|
'statutory-documents': true,
|
||||||
|
});
|
||||||
|
const [users, setUsers] = useState<any[]>([]);
|
||||||
|
const [selectedUser, setSelectedUser] = useState<string>('');
|
||||||
|
const [participantType, setParticipantType] = useState<string>('contributor');
|
||||||
|
const [interviewDate, setInterviewDate] = useState('');
|
||||||
|
const [interviewType, setInterviewType] = useState('level1');
|
||||||
|
const [meetingLink, setMeetingLink] = useState('');
|
||||||
|
const [location, setLocation] = useState('');
|
||||||
|
const [showUploadForm, setShowUploadForm] = useState(false);
|
||||||
|
const [uploadFile, setUploadFile] = useState<File | null>(null);
|
||||||
|
const [uploadDocType, setUploadDocType] = useState('');
|
||||||
|
const [approvalFile, setApprovalFile] = useState<File | null>(null);
|
||||||
|
const [isUploading, setIsUploading] = useState(false);
|
||||||
|
const [previewDoc, setPreviewDoc] = useState<any>(null);
|
||||||
|
const [showPreviewModal, setShowPreviewModal] = useState(false);
|
||||||
|
const [selectedInterviewerId, setSelectedInterviewerId] = useState<string>('');
|
||||||
|
const [isEditingStatutory, setIsEditingStatutory] = useState(false);
|
||||||
|
const [statutoryForm, setStatutoryForm] = useState({
|
||||||
|
accountHolderName: '',
|
||||||
|
panNumber: '',
|
||||||
|
gstNumber: '',
|
||||||
|
bankName: '',
|
||||||
|
accountNumber: '',
|
||||||
|
ifscCode: '',
|
||||||
|
registeredAddress: '',
|
||||||
|
});
|
||||||
|
const [isSavingStatutory, setIsSavingStatutory] = useState(false);
|
||||||
|
|
||||||
|
const [interviews, setInterviews] = useState<any[]>([]);
|
||||||
|
const [isScheduling, setIsScheduling] = useState(false);
|
||||||
|
const [showAssignArchitectureModal, setShowAssignArchitectureModal] = useState(false);
|
||||||
|
const [architectureLeadId, setArchitectureLeadId] = useState<string>('');
|
||||||
|
const [isAssigningArchitecture, setIsAssigningArchitecture] = useState(false);
|
||||||
|
const [showArchitectureStatusModal, setShowArchitectureStatusModal] = useState(false);
|
||||||
|
const [architectureStatus, setArchitectureStatus] = useState<string>('COMPLETED');
|
||||||
|
const [architectureRemarks, setArchitectureRemarks] = useState<string>('');
|
||||||
|
const [isUpdatingArchitecture, setIsUpdatingArchitecture] = useState(false);
|
||||||
|
const [isAssigningParticipant, setIsAssigningParticipant] = useState(false);
|
||||||
|
const [documentConfigs, setDocumentConfigs] = useState<any[]>([]);
|
||||||
|
const [fddAgencies, setFddAgencies] = useState<any[]>([]);
|
||||||
|
const [selectedAgencyId, setSelectedAgencyId] = useState<string>('');
|
||||||
|
const [isAssigningAgency, setIsAssigningAgency] = useState(false);
|
||||||
|
const [isApproving, setIsApproving] = useState(false);
|
||||||
|
const [isRejecting, setIsRejecting] = useState(false);
|
||||||
|
|
||||||
|
const [ktMatrixScores, setKtMatrixScores] = useState<Record<string, number>>({});
|
||||||
|
const [ktMatrixSelectedValues, setKtMatrixSelectedValues] = useState<Record<string, string>>({});
|
||||||
|
const [ktMatrixRemarks, setKtMatrixRemarks] = useState('');
|
||||||
|
const [isSubmittingKT, setIsSubmittingKT] = useState(false);
|
||||||
|
const [selectedInterviewForFeedback, setSelectedInterviewForFeedback] = useState<any>(null);
|
||||||
|
|
||||||
|
const [showFddFinalizeModal, setShowFddFinalizeModal] = useState(false);
|
||||||
|
const [showFddFlagModal, setShowFddFlagModal] = useState(false);
|
||||||
|
const [fddAuditRecommendation, setFddAuditRecommendation] = useState<string>('Recommended');
|
||||||
|
const [fddAuditFindings, setFddAuditFindings] = useState<string>('');
|
||||||
|
const [isFinalizingFdd, setIsFinalizingFdd] = useState(false);
|
||||||
|
const [isFddFlagging, setIsFddFlagging] = useState(false);
|
||||||
|
|
||||||
|
const [level2Feedback, setLevel2Feedback] = useState({
|
||||||
|
strategicVision: '',
|
||||||
|
managementCapabilities: '',
|
||||||
|
operationalUnderstanding: '',
|
||||||
|
keyStrengths: '',
|
||||||
|
areasOfConcern: '',
|
||||||
|
additionalComments: '',
|
||||||
|
overallScore: '',
|
||||||
|
interviewerName: currentUser?.name || '',
|
||||||
|
interviewDate: getToday(),
|
||||||
|
});
|
||||||
|
const [isSubmittingLevel2, setIsSubmittingLevel2] = useState(false);
|
||||||
|
|
||||||
|
const [level3Feedback, setLevel3Feedback] = useState({
|
||||||
|
strategicVision: '',
|
||||||
|
managementCapabilities: '',
|
||||||
|
operationalUnderstanding: '',
|
||||||
|
brandAlignment: '',
|
||||||
|
executiveSummary: '',
|
||||||
|
keyStrengths: '',
|
||||||
|
areasOfConcern: '',
|
||||||
|
additionalComments: '',
|
||||||
|
overallScore: '',
|
||||||
|
interviewerName: currentUser?.name || '',
|
||||||
|
interviewDate: getToday(),
|
||||||
|
});
|
||||||
|
const [isSubmittingLevel3, setIsSubmittingLevel3] = useState(false);
|
||||||
|
|
||||||
|
const [selectedEvaluationForView, setSelectedEvaluationForView] = useState<any>(null);
|
||||||
|
const [showFeedbackDetailsModal, setShowFeedbackDetailsModal] = useState(false);
|
||||||
|
|
||||||
|
return {
|
||||||
|
showFirmTypeModal, setShowFirmTypeModal,
|
||||||
|
updatingFirmType, setUpdatingFirmType,
|
||||||
|
tempFirmType, setTempFirmType,
|
||||||
|
activeTab, setActiveTab,
|
||||||
|
showApproveModal, setShowApproveModal,
|
||||||
|
showOnboardModal, setShowOnboardModal,
|
||||||
|
isOnboarding, setIsOnboarding,
|
||||||
|
showRejectModal, setShowRejectModal,
|
||||||
|
rejectionReason, setRejectionReason,
|
||||||
|
scheduledInterviewParticipants, setScheduledInterviewParticipants,
|
||||||
|
showScheduleModal, setShowScheduleModal,
|
||||||
|
showKTMatrixModal, setShowKTMatrixModal,
|
||||||
|
showLevel2FeedbackModal, setShowLevel2FeedbackModal,
|
||||||
|
showLevel3FeedbackModal, setShowLevel3FeedbackModal,
|
||||||
|
showDocumentsModal, setShowDocumentsModal,
|
||||||
|
showAssignModal, setShowAssignModal,
|
||||||
|
selectedStage, setSelectedStage,
|
||||||
|
interviewMode, setInterviewMode,
|
||||||
|
approvalRemark, setApprovalRemark,
|
||||||
|
expandedBranches, setExpandedBranches,
|
||||||
|
users, setUsers,
|
||||||
|
selectedUser, setSelectedUser,
|
||||||
|
participantType, setParticipantType,
|
||||||
|
interviewDate, setInterviewDate,
|
||||||
|
interviewType, setInterviewType,
|
||||||
|
meetingLink, setMeetingLink,
|
||||||
|
location, setLocation,
|
||||||
|
showUploadForm, setShowUploadForm,
|
||||||
|
uploadFile, setUploadFile,
|
||||||
|
uploadDocType, setUploadDocType,
|
||||||
|
approvalFile, setApprovalFile,
|
||||||
|
isUploading, setIsUploading,
|
||||||
|
previewDoc, setPreviewDoc,
|
||||||
|
showPreviewModal, setShowPreviewModal,
|
||||||
|
selectedInterviewerId, setSelectedInterviewerId,
|
||||||
|
isEditingStatutory, setIsEditingStatutory,
|
||||||
|
statutoryForm, setStatutoryForm,
|
||||||
|
isSavingStatutory, setIsSavingStatutory,
|
||||||
|
interviews, setInterviews,
|
||||||
|
isScheduling, setIsScheduling,
|
||||||
|
showAssignArchitectureModal, setShowAssignArchitectureModal,
|
||||||
|
architectureLeadId, setArchitectureLeadId,
|
||||||
|
isAssigningArchitecture, setIsAssigningArchitecture,
|
||||||
|
showArchitectureStatusModal, setShowArchitectureStatusModal,
|
||||||
|
architectureStatus, setArchitectureStatus,
|
||||||
|
architectureRemarks, setArchitectureRemarks,
|
||||||
|
isUpdatingArchitecture, setIsUpdatingArchitecture,
|
||||||
|
isAssigningParticipant, setIsAssigningParticipant,
|
||||||
|
documentConfigs, setDocumentConfigs,
|
||||||
|
fddAgencies, setFddAgencies,
|
||||||
|
selectedAgencyId, setSelectedAgencyId,
|
||||||
|
isAssigningAgency, setIsAssigningAgency,
|
||||||
|
isApproving, setIsApproving,
|
||||||
|
isRejecting, setIsRejecting,
|
||||||
|
ktMatrixScores, setKtMatrixScores,
|
||||||
|
ktMatrixSelectedValues, setKtMatrixSelectedValues,
|
||||||
|
ktMatrixRemarks, setKtMatrixRemarks,
|
||||||
|
isSubmittingKT, setIsSubmittingKT,
|
||||||
|
selectedInterviewForFeedback, setSelectedInterviewForFeedback,
|
||||||
|
showFddFinalizeModal, setShowFddFinalizeModal,
|
||||||
|
showFddFlagModal, setShowFddFlagModal,
|
||||||
|
fddAuditRecommendation, setFddAuditRecommendation,
|
||||||
|
fddAuditFindings, setFddAuditFindings,
|
||||||
|
isFinalizingFdd, setIsFinalizingFdd,
|
||||||
|
isFddFlagging, setIsFddFlagging,
|
||||||
|
level2Feedback, setLevel2Feedback,
|
||||||
|
isSubmittingLevel2, setIsSubmittingLevel2,
|
||||||
|
level3Feedback, setLevel3Feedback,
|
||||||
|
isSubmittingLevel3, setIsSubmittingLevel3,
|
||||||
|
selectedEvaluationForView, setSelectedEvaluationForView,
|
||||||
|
showFeedbackDetailsModal, setShowFeedbackDetailsModal,
|
||||||
|
};
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user