after the 3rd demo fe points covered like templates improvd ui enahanced to match original theme check revokemiddlware added stagwe transistion even after one rejection flow added

This commit is contained in:
laxman h 2026-04-27 19:10:49 +05:30
parent 5fbf06d827
commit 5170ab6c5a
22 changed files with 786 additions and 149 deletions

View File

@ -222,6 +222,8 @@ export const API = {
// System Configs // System Configs
getSystemConfigs: (params?: any) => client.get('/master/system-configs', params), getSystemConfigs: (params?: any) => client.get('/master/system-configs', params),
saveSystemConfig: (data: any) => client.post('/master/system-configs', data), saveSystemConfig: (data: any) => client.post('/master/system-configs', data),
getDealerAsmMappings: () => client.get('/master/dealer-asm-mappings'),
saveDealerAsmMapping: (data: { dealerId: string; asmUserId?: string | null }) => client.post('/master/dealer-asm-mappings', data),
// EOR Checklist // EOR Checklist
getEorChecklistForApplication: (applicationId: string) => client.get(`/eor/application/${applicationId}`), getEorChecklistForApplication: (applicationId: string) => client.get(`/eor/application/${applicationId}`),

View File

@ -146,7 +146,20 @@ const QuestionnaireForm: React.FC<QuestionnaireFormProps> = ({
return ( return (
<div className="space-y-6"> <div className="space-y-6">
<h3 className="text-xl font-semibold">Dealership Assessment Questionnaire</h3> <div className="rounded-lg overflow-hidden border border-slate-200 shadow-sm">
<div className="bg-black px-5 py-4">
<div className="flex items-center gap-3">
<img src="/assets/images/Re_Logo.png" alt="Royal Enfield" className="h-8 w-auto" />
<div>
<p className="text-white text-lg font-bold tracking-wide leading-tight">ROYAL ENFIELD</p>
<p className="text-slate-300 text-sm leading-tight">Dealership Partner Application</p>
</div>
</div>
</div>
<div className="bg-white px-5 py-3 border-t border-slate-200">
<h3 className="text-xl font-semibold">Dealership Assessment Questionnaire</h3>
</div>
</div>
{Object.entries(sections).map(([sectionName, sectionQuestions]) => ( {Object.entries(sections).map(([sectionName, sectionQuestions]) => (
<div key={sectionName} className="border p-4 rounded bg-white shadow-sm"> <div key={sectionName} className="border p-4 rounded bg-white shadow-sm">

View File

@ -108,7 +108,7 @@ export function Header({ title, onRefresh }: HeaderProps) {
{/* Current User Info */} {/* Current User Info */}
{currentUser && ( {currentUser && (
<div className="flex items-center gap-3 px-3 py-2 bg-slate-100 rounded-lg"> <div className="flex items-center gap-3 px-3 py-2 bg-slate-100 rounded-lg">
<div className="w-8 h-8 bg-amber-600 rounded-full flex items-center justify-center"> <div className="w-8 h-8 bg-re-red rounded-full flex items-center justify-center">
<UserIcon className="w-4 h-4 text-white" /> <UserIcon className="w-4 h-4 text-white" />
</div> </div>
<div className="text-left"> <div className="text-left">

View File

@ -43,12 +43,9 @@ export const ASMDialog: React.FC<ASMDialogProps> = ({
const { zones, regionalOffices } = useSelector((state: RootState) => state.master); const { zones, regionalOffices } = useSelector((state: RootState) => state.master);
const filteredASMUsers = userAssignedData.filter(u => { const filteredASMUsers = userAssignedData.filter(u => {
const roles = u.allRoles || []; const roles = (u.allRoles || []).map((r: string) => String(r || '').toUpperCase());
return roles.some((r: string) => { const roleCode = String(u.roleCode || '').toUpperCase();
const roleStr = (r || '').toUpperCase(); return roles.includes('DD-AM') || roleCode === 'DD-AM';
return ['ASM', 'AREA SALES MANAGER', 'DD-AM', 'AREA MANAGER', 'RM', 'RBM', 'REGIONAL MANAGER', 'ZBH', 'ZONE BUSINESS HEAD', 'ZONAL BUSINESS HEAD'].includes(roleStr) ||
roleStr.includes('AREA SALES') || roleStr.includes('REGIONAL') || roleStr.includes('ZONAL');
});
}); });
// PRE-FILLING LOGIC: When manager or role changes, pre-select their districts // PRE-FILLING LOGIC: When manager or role changes, pre-select their districts
@ -71,8 +68,8 @@ export const ASMDialog: React.FC<ASMDialogProps> = ({
<Dialog open={isOpen} onOpenChange={onOpenChange}> <Dialog open={isOpen} onOpenChange={onOpenChange}>
<DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto"> <DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto">
<DialogHeader> <DialogHeader>
<DialogTitle>{editingASMId ? 'Edit' : 'Add'} Area Sales Manager</DialogTitle> <DialogTitle>{editingASMId ? 'Edit' : 'Add'} DD Area Manager</DialogTitle>
<DialogDescription>Configure ASM details and assignment</DialogDescription> <DialogDescription>Configure DD-AM details and district assignment</DialogDescription>
</DialogHeader> </DialogHeader>
<div className="space-y-4"> <div className="space-y-4">
<div> <div>
@ -116,24 +113,11 @@ export const ASMDialog: React.FC<ASMDialogProps> = ({
</Select> </Select>
</div> </div>
<div className="space-y-2">
<Label>Assignment Role</Label>
<Select value={asmRoleCode} onValueChange={(val: any) => setAsmRoleCode(val)}>
<SelectTrigger className="mt-2 text-slate-900">
<SelectValue placeholder="Select role" />
</SelectTrigger>
<SelectContent>
<SelectItem value="ASM">Sales ASM</SelectItem>
<SelectItem value="DD-AM">DD Area Manager</SelectItem>
</SelectContent>
</Select>
</div>
<div> <div>
<Label>Select {asmRoleCode === 'ASM' ? 'ASM' : 'DD Area Manager'} User</Label> <Label>Select DD-AM User</Label>
<Select value={asmManagerId} onValueChange={setAsmManagerId}> <Select value={asmManagerId} onValueChange={setAsmManagerId}>
<SelectTrigger className="mt-2 text-slate-900"> <SelectTrigger className="mt-2 text-slate-900">
<SelectValue placeholder={`Select ${asmRoleCode === 'ASM' ? 'ASM' : 'DD Area Manager'}`} /> <SelectValue placeholder="Select DD-AM" />
</SelectTrigger> </SelectTrigger>
<SelectContent className="max-h-64"> <SelectContent className="max-h-64">
{filteredASMUsers.map(user => ( {filteredASMUsers.map(user => (
@ -242,7 +226,7 @@ export const ASMDialog: React.FC<ASMDialogProps> = ({
)} )}
<div className="border-t pt-4"> <div className="border-t pt-4">
<Label>Area Sales Manager <span className="text-red-500">*</span></Label> <Label>DD Area Manager <span className="text-red-500">*</span></Label>
<Select <Select
value={asmManagerId} value={asmManagerId}
onValueChange={(value) => { onValueChange={(value) => {
@ -262,7 +246,7 @@ export const ASMDialog: React.FC<ASMDialogProps> = ({
disabled={!!editingASMId} disabled={!!editingASMId}
> >
<SelectTrigger className="mt-2 w-full text-slate-900"> <SelectTrigger className="mt-2 w-full text-slate-900">
<SelectValue placeholder="Select ASM User" /> <SelectValue placeholder="Select DD-AM User" />
</SelectTrigger> </SelectTrigger>
<SelectContent className="max-h-60"> <SelectContent className="max-h-60">
{filteredASMUsers.length > 0 ? ( {filteredASMUsers.length > 0 ? (
@ -296,7 +280,7 @@ export const ASMDialog: React.FC<ASMDialogProps> = ({
<div className="flex gap-3 pt-4"> <div className="flex gap-3 pt-4">
<Button variant="outline" className="flex-1" onClick={() => onOpenChange(false)}>Cancel</Button> <Button variant="outline" className="flex-1" onClick={() => onOpenChange(false)}>Cancel</Button>
<Button className="flex-1 bg-amber-600 hover:bg-amber-700" onClick={onSave}>Save ASM</Button> <Button className="flex-1 bg-amber-600 hover:bg-amber-700" onClick={onSave}>Save DD-AM</Button>
</div> </div>
</div> </div>
</DialogContent> </DialogContent>

View File

@ -38,12 +38,12 @@ export const ASMManagement: React.FC<ASMManagementProps> = ({
<CardHeader> <CardHeader>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<CardTitle>Area Sales Managers (ASM)</CardTitle> <CardTitle>District Development Area Managers (DD-AM)</CardTitle>
<CardDescription>Manage ASMs across all regions and zones</CardDescription> <CardDescription>Manage DD-AM users across districts (multi-district)</CardDescription>
</div> </div>
<Button onClick={onAddASM} className="bg-amber-600 hover:bg-amber-700"> <Button onClick={onAddASM} className="bg-amber-600 hover:bg-amber-700">
<Plus className="w-4 h-4 mr-2" /> <Plus className="w-4 h-4 mr-2" />
Add ASM Add DD-AM
</Button> </Button>
</div> </div>
</CardHeader> </CardHeader>
@ -51,7 +51,7 @@ export const ASMManagement: React.FC<ASMManagementProps> = ({
<Table> <Table>
<TableHeader> <TableHeader>
<TableRow> <TableRow>
<TableHead>ASM Code</TableHead> <TableHead>DD-AM Code</TableHead>
<TableHead>Name</TableHead> <TableHead>Name</TableHead>
<TableHead>Zone</TableHead> <TableHead>Zone</TableHead>
<TableHead>Region</TableHead> <TableHead>Region</TableHead>

View File

@ -0,0 +1,232 @@
import React, { useEffect, useMemo, useState } from 'react';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from '@/components/ui/command';
import { Check, ChevronsUpDown } from 'lucide-react';
import { toast } from 'sonner';
import { masterService } from '@/services/master.service';
import { cn } from '@/components/ui/utils';
type DealerRow = {
dealerId: string;
dealerName: string;
legalName: string;
dealerCode: string;
status: string;
assignedAsm: null | {
id: string;
fullName: string;
email: string;
employeeId?: string;
};
assignedAt?: string | null;
};
type AsmUser = {
id: string;
fullName: string;
email: string;
employeeId?: string;
};
type AsmSearchSelectProps = {
value: string;
onChange: (value: string) => void;
asmUsers: AsmUser[];
className?: string;
};
const AsmSearchSelect: React.FC<AsmSearchSelectProps> = ({ value, onChange, asmUsers, className }) => {
const [open, setOpen] = useState(false);
const selectedAsm = asmUsers.find((u) => u.id === value);
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={open}
className={cn('w-full min-w-0 justify-between', className)}
>
<span className="truncate text-left">
{value === '__none__'
? 'Unassign'
: selectedAsm
? `${selectedAsm.fullName} (${selectedAsm.employeeId || selectedAsm.email})`
: 'Select ASM'}
</span>
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-[360px] max-w-[90vw] p-0">
<Command>
<CommandInput placeholder="Search ASM by name/email/id..." />
<CommandList className="max-h-64 overflow-y-auto custom-scrollbar-slim">
<CommandEmpty>No ASM found.</CommandEmpty>
<CommandGroup>
<CommandItem
value="Unassign __none__"
onSelect={() => {
onChange('__none__');
setOpen(false);
}}
>
<Check className={cn('mr-2 h-4 w-4', value === '__none__' ? 'opacity-100' : 'opacity-0')} />
Unassign
</CommandItem>
{asmUsers.map((asm) => (
<CommandItem
key={asm.id}
value={`${asm.fullName} ${asm.email} ${asm.employeeId || ''}`}
onSelect={() => {
onChange(asm.id);
setOpen(false);
}}
>
<Check className={cn('mr-2 h-4 w-4', value === asm.id ? 'opacity-100' : 'opacity-0')} />
{asm.fullName} ({asm.employeeId || asm.email})
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
);
};
export const DealerAsmAssignment: React.FC = () => {
const [loading, setLoading] = useState(false);
const [dealers, setDealers] = useState<DealerRow[]>([]);
const [asmUsers, setAsmUsers] = useState<AsmUser[]>([]);
const [draft, setDraft] = useState<Record<string, string>>({});
const fetchData = async () => {
try {
setLoading(true);
const res: any = await (masterService as any).getDealerAsmMappings();
if (res?.success) {
setDealers(res.data?.dealers || []);
setAsmUsers(res.data?.asmUsers || []);
}
} catch (error: any) {
toast.error(error?.response?.data?.message || 'Failed to load dealer ASM mappings');
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchData();
}, []);
const sortedDealers = useMemo(
() =>
[...dealers].sort((a, b) => {
const aActive = String(a.status || '').toLowerCase() === 'active';
const bActive = String(b.status || '').toLowerCase() === 'active';
if (aActive !== bActive) return aActive ? -1 : 1;
return String(a.dealerName || '').localeCompare(String(b.dealerName || ''));
}),
[dealers]
);
const saveMapping = async (dealerId: string) => {
const selectedAsm = draft[dealerId] || '';
try {
const res: any = await (masterService as any).saveDealerAsmMapping({
dealerId,
asmUserId: selectedAsm === '__none__' ? null : selectedAsm || null,
});
if (res?.success) {
toast.success(res.message || 'Dealer ASM mapping updated');
await fetchData();
} else {
toast.error(res?.message || 'Failed to save mapping');
}
} catch (error: any) {
toast.error(error?.response?.data?.message || 'Failed to save mapping');
}
};
return (
<Card>
<CardHeader>
<CardTitle>Dealer-Level ASM Assignment</CardTitle>
<CardDescription>
Assign Sales ASM to onboarded dealers. DD-AM remains district-level in the section above.
</CardDescription>
</CardHeader>
<CardContent>
{loading ? (
<p className="text-sm text-slate-500">Loading mappings...</p>
) : (
<Table className="w-full">
<TableHeader>
<TableRow>
<TableHead>Dealer</TableHead>
<TableHead>Dealer Code</TableHead>
<TableHead>Status</TableHead>
<TableHead>Current ASM</TableHead>
<TableHead>Assign ASM</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{sortedDealers.length === 0 && (
<TableRow>
<TableCell colSpan={5} className="text-center text-slate-500 py-8">
No dealers available for ASM mapping yet.
</TableCell>
</TableRow>
)}
{sortedDealers.map((dealer) => (
<TableRow key={dealer.dealerId}>
<TableCell>
<div className="flex flex-col">
<span className="font-medium">{dealer.dealerName}</span>
<span className="text-xs text-slate-500">{dealer.legalName}</span>
</div>
</TableCell>
<TableCell>{dealer.dealerCode || 'N/A'}</TableCell>
<TableCell>
<Badge variant={String(dealer.status || '').toLowerCase() === 'active' ? 'default' : 'secondary'}>
{dealer.status || 'Unknown'}
</Badge>
</TableCell>
<TableCell>
{dealer.assignedAsm ? (
<div className="flex flex-col min-w-0">
<span>{dealer.assignedAsm.fullName}</span>
<span className="text-xs text-slate-500 truncate">{dealer.assignedAsm.employeeId || dealer.assignedAsm.email}</span>
</div>
) : (
<span className="text-slate-400 text-sm">Unassigned</span>
)}
</TableCell>
<TableCell>
<div className="flex items-center gap-2 w-full">
<AsmSearchSelect
asmUsers={asmUsers}
value={draft[dealer.dealerId] ?? dealer.assignedAsm?.id ?? '__none__'}
onChange={(val) => setDraft((prev) => ({ ...prev, [dealer.dealerId]: val }))}
className="flex-1 min-w-[180px]"
/>
<Button size="sm" className="shrink-0" onClick={() => saveMapping(dealer.dealerId)}>
Assign
</Button>
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
)}
</CardContent>
</Card>
);
};

View File

@ -24,6 +24,7 @@ import { AddRoleDialog } from '@/features/master/components/AddRoleDialog';
import { EmailTemplates } from '@/features/master/components/EmailTemplates'; import { EmailTemplates } from '@/features/master/components/EmailTemplates';
import { LocationManagement } from '@/features/master/components/LocationManagement'; import { LocationManagement } from '@/features/master/components/LocationManagement';
import { ASMDialog } from '@/features/master/components/ASMDialog'; import { ASMDialog } from '@/features/master/components/ASMDialog';
import { DealerAsmAssignment } from '@/features/master/components/DealerAsmAssignment';
import { ZMDialog } from '@/features/master/components/ZMDialog'; import { ZMDialog } from '@/features/master/components/ZMDialog';
import { ZoneDialog } from '@/features/master/components/ZoneDialog'; import { ZoneDialog } from '@/features/master/components/ZoneDialog';
import { RegionDialog } from '@/features/master/components/RegionDialog'; import { RegionDialog } from '@/features/master/components/RegionDialog';
@ -65,7 +66,7 @@ export const MasterPage: React.FC = () => {
const [selectedASMRegion, setSelectedASMRegion] = useState(''); const [selectedASMRegion, setSelectedASMRegion] = useState('');
const [selectedASMStates, setSelectedASMStates] = useState<string[]>([]); const [selectedASMStates, setSelectedASMStates] = useState<string[]>([]);
const [selectedASMDistricts, setSelectedASMDistricts] = useState<string[]>([]); const [selectedASMDistricts, setSelectedASMDistricts] = useState<string[]>([]);
const [asmRoleCode, setAsmRoleCode] = useState<'ASM' | 'DD-AM'>('ASM'); const [asmRoleCode, setAsmRoleCode] = useState<'ASM' | 'DD-AM'>('DD-AM');
// ZM Management State // ZM Management State
const [showZMDialog, setShowZMDialog] = useState(false); const [showZMDialog, setShowZMDialog] = useState(false);
@ -156,7 +157,7 @@ export const MasterPage: React.FC = () => {
// Handlers // Handlers
const handleSaveASM = async () => { const handleSaveASM = async () => {
if (!asmManagerId) { if (!asmManagerId) {
toast.error('Please select an ASM user'); toast.error('Please select a DD-AM user');
return; return;
} }
try { try {
@ -168,7 +169,7 @@ export const MasterPage: React.FC = () => {
}; };
const res = await masterService.saveASM(payload) as any; const res = await masterService.saveASM(payload) as any;
if (res.success) { if (res.success) {
toast.success(`${asmRoleCode === 'ASM' ? 'ASM' : 'DD Area Manager'} ${editingASMId ? 'updated' : 'assigned'} successfully`); toast.success(`DD Area Manager ${editingASMId ? 'updated' : 'assigned'} successfully`);
setShowASMDialog(false); setShowASMDialog(false);
fetchInitialData(); fetchInitialData();
} else { } else {
@ -188,7 +189,7 @@ export const MasterPage: React.FC = () => {
setSelectedASMRegion(asm.regionId); setSelectedASMRegion(asm.regionId);
setSelectedASMStates(asm.stateNames || []); setSelectedASMStates(asm.stateNames || []);
setSelectedASMDistricts(asm.areasManaged?.map((a: any) => a.id) || []); setSelectedASMDistricts(asm.areasManaged?.map((a: any) => a.id) || []);
setAsmRoleCode(asm.roleCode === 'DD-AM' ? 'DD-AM' : 'ASM'); setAsmRoleCode('DD-AM');
setShowASMDialog(true); setShowASMDialog(true);
}; };
@ -500,9 +501,11 @@ export const MasterPage: React.FC = () => {
onEditZM={handleEditZM} onEditZM={handleEditZM}
onDeleteZM={() => toast.error('ZM deletion restricted')} /> onDeleteZM={() => toast.error('ZM deletion restricted')} />
<ASMManagement selectedZone={selectedZone} onAddASM={() => { setEditingASMId(null); setAsmManagerId(''); setSelectedASMZone(selectedZone === 'all' ? '' : selectedZone); setSelectedASMRegion(''); setSelectedASMStates([]); setSelectedASMDistricts([]); setShowASMDialog(true); }} <ASMManagement selectedZone={selectedZone} onAddASM={() => { setEditingASMId(null); setAsmManagerId(''); setSelectedASMZone(selectedZone === 'all' ? '' : selectedZone); setSelectedASMRegion(''); setSelectedASMStates([]); setSelectedASMDistricts([]); setAsmRoleCode('DD-AM'); setShowASMDialog(true); }}
onEditASM={handleEditASM} onDeleteASM={() => toast.error('ASM deletion restricted')} /> onEditASM={handleEditASM} onDeleteASM={() => toast.error('ASM deletion restricted')} />
<DealerAsmAssignment />
<UserManagementTable userAssignedData={users.length > 0 ? users : asms} /> <UserManagementTable userAssignedData={users.length > 0 ? users : asms} />
</TabsContent> </TabsContent>

View File

@ -30,6 +30,11 @@ interface ApplicationDetailsActionModalsProps {
handleReject: () => void; handleReject: () => void;
showScheduleModal: boolean; showScheduleModal: boolean;
setShowScheduleModal: (value: boolean) => void; setShowScheduleModal: (value: boolean) => void;
showCancelInterviewModal: boolean;
setShowCancelInterviewModal: (value: boolean) => void;
setInterviewIdToCancel: (value: string) => void;
isCancellingInterview: boolean;
handleConfirmCancelInterview: () => void;
interviewType: string; interviewType: string;
setInterviewType: (value: string) => void; setInterviewType: (value: string) => void;
interviewMode: string; interviewMode: string;
@ -89,6 +94,11 @@ export function ApplicationDetailsActionModals(props: ApplicationDetailsActionMo
handleReject, handleReject,
showScheduleModal, showScheduleModal,
setShowScheduleModal, setShowScheduleModal,
showCancelInterviewModal,
setShowCancelInterviewModal,
setInterviewIdToCancel,
isCancellingInterview,
handleConfirmCancelInterview,
interviewType, interviewType,
setInterviewType, setInterviewType,
interviewMode, interviewMode,
@ -125,6 +135,14 @@ export function ApplicationDetailsActionModals(props: ApplicationDetailsActionMo
handleUpdateArchitectureStatus, handleUpdateArchitectureStatus,
} = props; } = props;
const participantRoleLabel = (participant: any) =>
participant?.__stageRole ||
participant?.role?.roleName ||
participant?.role?.roleCode ||
participant?.roleCode ||
participant?.role ||
'Panelist';
return ( return (
<> <>
<Dialog open={showApproveModal} onOpenChange={setShowApproveModal}> <Dialog open={showApproveModal} onOpenChange={setShowApproveModal}>
@ -278,6 +296,7 @@ export function ApplicationDetailsActionModals(props: ApplicationDetailsActionMo
{scheduledInterviewParticipants.map((p) => ( {scheduledInterviewParticipants.map((p) => (
<div key={p.id} className="flex items-center gap-1 bg-secondary px-2 py-1 rounded text-sm" data-testid={`onboarding-schedule-participant-${p.id}`}> <div key={p.id} className="flex items-center gap-1 bg-secondary px-2 py-1 rounded text-sm" data-testid={`onboarding-schedule-participant-${p.id}`}>
<span>{p.fullName || p.name || 'Unknown'}</span> <span>{p.fullName || p.name || 'Unknown'}</span>
<span className="text-[11px] text-muted-foreground">({participantRoleLabel(p)})</span>
<button onClick={() => handleRemoveInterviewer(p.id)} className="text-muted-foreground hover:text-destructive" data-testid={`onboarding-schedule-remove-participant-${p.id}`}>×</button> <button onClick={() => handleRemoveInterviewer(p.id)} className="text-muted-foreground hover:text-destructive" data-testid={`onboarding-schedule-remove-participant-${p.id}`}>×</button>
</div> </div>
))} ))}
@ -293,6 +312,46 @@ export function ApplicationDetailsActionModals(props: ApplicationDetailsActionMo
</DialogContent> </DialogContent>
</Dialog> </Dialog>
<Dialog
open={showCancelInterviewModal}
onOpenChange={(open) => {
setShowCancelInterviewModal(open);
if (!open) setInterviewIdToCancel('');
}}
>
<DialogContent data-testid="onboarding-cancel-interview-modal">
<DialogHeader>
<DialogTitle>Cancel Interview</DialogTitle>
<DialogDescription>
Are you sure you want to cancel this interview?
</DialogDescription>
</DialogHeader>
<div className="flex gap-3">
<Button
variant="outline"
className="flex-1"
onClick={() => {
setShowCancelInterviewModal(false);
setInterviewIdToCancel('');
}}
disabled={isCancellingInterview}
data-testid="onboarding-cancel-interview-close"
>
No
</Button>
<Button
variant="destructive"
className="flex-1"
onClick={handleConfirmCancelInterview}
disabled={isCancellingInterview}
data-testid="onboarding-cancel-interview-confirm"
>
{isCancellingInterview ? 'Cancelling...' : 'Yes, Cancel'}
</Button>
</div>
</DialogContent>
</Dialog>
<Dialog open={showAssignArchitectureModal} onOpenChange={setShowAssignArchitectureModal}> <Dialog open={showAssignArchitectureModal} onOpenChange={setShowAssignArchitectureModal}>
<DialogContent data-testid="onboarding-architecture-assign-modal"> <DialogContent data-testid="onboarding-architecture-assign-modal">
<DialogHeader> <DialogHeader>

View File

@ -36,6 +36,8 @@ export function ApplicationDetailsExtendedModals(props: ApplicationDetailsExtend
handleKTMatrixChange, handleKTMatrixChange,
ktMatrixRemarks, ktMatrixRemarks,
setKtMatrixRemarks, setKtMatrixRemarks,
ktMatrixRecommendation,
setKtMatrixRecommendation,
calculateKTScore, calculateKTScore,
handleSubmitKTMatrix, handleSubmitKTMatrix,
isSubmittingKT, isSubmittingKT,
@ -43,15 +45,20 @@ export function ApplicationDetailsExtendedModals(props: ApplicationDetailsExtend
setShowLevel2FeedbackModal, setShowLevel2FeedbackModal,
level2Feedback, level2Feedback,
handleLevel2Change, handleLevel2Change,
level2Recommendation,
setLevel2Recommendation,
handleSubmitLevel2Feedback, handleSubmitLevel2Feedback,
isSubmittingLevel2, isSubmittingLevel2,
showFeedbackDetailsModal, showFeedbackDetailsModal,
setShowFeedbackDetailsModal, setShowFeedbackDetailsModal,
selectedEvaluationForView, selectedEvaluationForView,
selectedInterviewForFeedback,
showLevel3FeedbackModal, showLevel3FeedbackModal,
setShowLevel3FeedbackModal, setShowLevel3FeedbackModal,
level3Feedback, level3Feedback,
handleLevel3Change, handleLevel3Change,
level3Recommendation,
setLevel3Recommendation,
handleSubmitLevel3Feedback, handleSubmitLevel3Feedback,
isSubmittingLevel3, isSubmittingLevel3,
showDocumentsModal, showDocumentsModal,
@ -95,6 +102,11 @@ export function ApplicationDetailsExtendedModals(props: ApplicationDetailsExtend
handleUpdateFirmType, handleUpdateFirmType,
} = props; } = props;
const selectedInterviewDate = selectedInterviewForFeedback?.scheduleDate
? new Date(selectedInterviewForFeedback.scheduleDate).toISOString().split('T')[0]
: '';
const interviewerDisplayName = currentUser?.fullName || currentUser?.name || '';
return ( return (
<> <>
<Dialog open={showKTMatrixModal} onOpenChange={setShowKTMatrixModal}> <Dialog open={showKTMatrixModal} onOpenChange={setShowKTMatrixModal}>
@ -113,6 +125,11 @@ export function ApplicationDetailsExtendedModals(props: ApplicationDetailsExtend
</DialogHeader> </DialogHeader>
<div className="custom-scrollbar-slim min-h-0 flex-1 overflow-y-auto px-5 py-5"> <div className="custom-scrollbar-slim min-h-0 flex-1 overflow-y-auto px-5 py-5">
<div className="space-y-6"> <div className="space-y-6">
{ktCriteria.length === 0 && (
<div className="rounded-md border border-amber-200 bg-amber-50 px-3 py-2 text-sm text-amber-800">
KT Matrix configuration is not available. Configure it in Master &gt; Interview Configurations.
</div>
)}
{ktCriteria.map((criterion: any, idx: number) => ( {ktCriteria.map((criterion: any, idx: number) => (
<div key={criterion.name} className="space-y-2"> <div key={criterion.name} className="space-y-2">
<Label htmlFor={`kt-matrix-${idx}`} className="block text-sm font-medium leading-relaxed text-foreground"> <Label htmlFor={`kt-matrix-${idx}`} className="block text-sm font-medium leading-relaxed text-foreground">
@ -150,13 +167,26 @@ export function ApplicationDetailsExtendedModals(props: ApplicationDetailsExtend
data-testid="onboarding-kt-matrix-remarks-textarea" data-testid="onboarding-kt-matrix-remarks-textarea"
/> />
</div> </div>
<div className="space-y-2">
<Label className="text-sm font-medium">Recommendation <span className="text-red-500">*</span></Label>
<Select value={ktMatrixRecommendation} onValueChange={setKtMatrixRecommendation}>
<SelectTrigger data-testid="onboarding-kt-matrix-recommendation-select">
<SelectValue placeholder="Select recommendation" />
</SelectTrigger>
<SelectContent>
<SelectItem value="Approve">Approve</SelectItem>
<SelectItem value="Reject">Reject</SelectItem>
<SelectItem value="Hold">Hold</SelectItem>
</SelectContent>
</Select>
</div>
</div> </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"> <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" data-testid="onboarding-kt-matrix-total-score">{calculateKTScore()}</span><span className="text-muted-foreground"> / 100</span></p> <p className="text-sm text-muted-foreground">Weighted total <span className="font-semibold tabular-nums text-foreground" data-testid="onboarding-kt-matrix-total-score">{calculateKTScore()}</span><span className="text-muted-foreground"> / 100</span></p>
<div className="flex gap-2 sm:shrink-0"> <div className="flex gap-2 sm:shrink-0">
<Button variant="outline" onClick={() => setShowKTMatrixModal(false)} data-testid="onboarding-kt-matrix-cancel">Cancel</Button> <Button variant="outline" onClick={() => setShowKTMatrixModal(false)} data-testid="onboarding-kt-matrix-cancel">Cancel</Button>
<Button onClick={handleSubmitKTMatrix} disabled={isSubmittingKT || Object.keys(ktMatrixSelectedValues).length < ktCriteria.length} data-testid="onboarding-kt-matrix-submit">{isSubmittingKT ? 'Saving…' : 'Submit'}</Button> <Button onClick={handleSubmitKTMatrix} disabled={isSubmittingKT || ktCriteria.length === 0 || Object.keys(ktMatrixSelectedValues).length < ktCriteria.length} data-testid="onboarding-kt-matrix-submit">{isSubmittingKT ? 'Saving…' : 'Submit'}</Button>
</div> </div>
</div> </div>
</DialogContent> </DialogContent>
@ -169,8 +199,8 @@ export function ApplicationDetailsExtendedModals(props: ApplicationDetailsExtend
<DialogDescription>Provide detailed feedback from the Level 2 interview (DD Lead + ZBH evaluation).</DialogDescription> <DialogDescription>Provide detailed feedback from the Level 2 interview (DD Lead + ZBH evaluation).</DialogDescription>
</DialogHeader> </DialogHeader>
<div className="space-y-4"> <div className="space-y-4">
<div><Label>Interview Date <span className="text-red-500">*</span></Label><Input type="date" className="mt-2" value={level2Feedback.interviewDate} disabled /></div> <div><Label>Interview Date <span className="text-red-500">*</span></Label><Input type="date" className="mt-2" value={level2Feedback.interviewDate || selectedInterviewDate} disabled /></div>
<div><Label>Interviewer Name <span className="text-red-500">*</span></Label><Input placeholder="Enter your name" className="mt-2" value={level2Feedback.interviewerName} disabled /></div> <div><Label>Interviewer Name <span className="text-red-500">*</span></Label><Input placeholder="Enter your name" className="mt-2" value={level2Feedback.interviewerName || interviewerDisplayName} disabled /></div>
<div> <div>
<Label>Overall Performance Score <span className="text-red-500">*</span></Label> <Label>Overall Performance Score <span className="text-red-500">*</span></Label>
<Select value={level2Feedback.overallScore} onValueChange={(value) => handleLevel2Change('overallScore', value)}> <Select value={level2Feedback.overallScore} onValueChange={(value) => handleLevel2Change('overallScore', value)}>
@ -178,7 +208,25 @@ export function ApplicationDetailsExtendedModals(props: ApplicationDetailsExtend
<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> <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> </Select>
</div> </div>
<div>
<Label>Recommendation <span className="text-red-500">*</span></Label>
<Select value={level2Recommendation} onValueChange={setLevel2Recommendation}>
<SelectTrigger className="mt-2" data-testid="onboarding-level2-recommendation-select">
<SelectValue placeholder="Select recommendation" />
</SelectTrigger>
<SelectContent>
<SelectItem value="Approve">Approve</SelectItem>
<SelectItem value="Reject">Reject</SelectItem>
<SelectItem value="Hold">Hold</SelectItem>
</SelectContent>
</Select>
</div>
<Separator /> <Separator />
{l2Fields.length === 0 && (
<div className="rounded-md border border-amber-200 bg-amber-50 px-3 py-2 text-sm text-amber-800">
Level 2 feedback configuration is not available. Configure it in Master &gt; Interview Configurations.
</div>
)}
{(l2Fields || []).map((field: any, idx: number) => ( {(l2Fields || []).map((field: any, idx: number) => (
<div key={field.itemKey || idx}> <div key={field.itemKey || idx}>
<Label> <Label>
@ -209,7 +257,7 @@ export function ApplicationDetailsExtendedModals(props: ApplicationDetailsExtend
))} ))}
<div className="flex gap-3"> <div className="flex gap-3">
<Button variant="outline" className="flex-1" onClick={() => setShowLevel2FeedbackModal(false)} data-testid="onboarding-level2-feedback-cancel">Cancel</Button> <Button variant="outline" className="flex-1" onClick={() => setShowLevel2FeedbackModal(false)} data-testid="onboarding-level2-feedback-cancel">Cancel</Button>
<Button className="flex-1 bg-black hover:bg-zinc-800 text-white" onClick={handleSubmitLevel2Feedback} disabled={isSubmittingLevel2} data-testid="onboarding-level2-feedback-submit">{isSubmittingLevel2 ? 'Submitting...' : 'Submit Feedback'}</Button> <Button className="flex-1 bg-black hover:bg-zinc-800 text-white" onClick={handleSubmitLevel2Feedback} disabled={isSubmittingLevel2 || l2Fields.length === 0} data-testid="onboarding-level2-feedback-submit">{isSubmittingLevel2 ? 'Submitting...' : 'Submit Feedback'}</Button>
</div> </div>
</div> </div>
</DialogContent> </DialogContent>
@ -254,8 +302,8 @@ export function ApplicationDetailsExtendedModals(props: ApplicationDetailsExtend
<DialogDescription>Provide detailed feedback from the Level 3 interview (NBH + DD-Head evaluation).</DialogDescription> <DialogDescription>Provide detailed feedback from the Level 3 interview (NBH + DD-Head evaluation).</DialogDescription>
</DialogHeader> </DialogHeader>
<div className="space-y-4"> <div className="space-y-4">
<div><Label>Interview Date <span className="text-red-500">*</span></Label><Input type="date" className="mt-2" value={level3Feedback.interviewDate} disabled /></div> <div><Label>Interview Date <span className="text-red-500">*</span></Label><Input type="date" className="mt-2" value={level3Feedback.interviewDate || selectedInterviewDate} disabled /></div>
<div><Label>Interviewer Name <span className="text-red-500">*</span></Label><Input placeholder="Enter your name" className="mt-2" value={level3Feedback.interviewerName} disabled /></div> <div><Label>Interviewer Name <span className="text-red-500">*</span></Label><Input placeholder="Enter your name" className="mt-2" value={level3Feedback.interviewerName || interviewerDisplayName} disabled /></div>
<div> <div>
<Label>Overall Performance Score <span className="text-red-500">*</span></Label> <Label>Overall Performance Score <span className="text-red-500">*</span></Label>
<Select value={level3Feedback.overallScore} onValueChange={(value) => handleLevel3Change('overallScore', value)}> <Select value={level3Feedback.overallScore} onValueChange={(value) => handleLevel3Change('overallScore', value)}>
@ -263,7 +311,25 @@ export function ApplicationDetailsExtendedModals(props: ApplicationDetailsExtend
<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> <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> </Select>
</div> </div>
<div>
<Label>Recommendation <span className="text-red-500">*</span></Label>
<Select value={level3Recommendation} onValueChange={setLevel3Recommendation}>
<SelectTrigger className="mt-2" data-testid="onboarding-level3-recommendation-select">
<SelectValue placeholder="Select recommendation" />
</SelectTrigger>
<SelectContent>
<SelectItem value="Approve">Approve</SelectItem>
<SelectItem value="Reject">Reject</SelectItem>
<SelectItem value="Hold">Hold</SelectItem>
</SelectContent>
</Select>
</div>
<Separator /> <Separator />
{l3Fields.length === 0 && (
<div className="rounded-md border border-amber-200 bg-amber-50 px-3 py-2 text-sm text-amber-800">
Level 3 feedback configuration is not available. Configure it in Master &gt; Interview Configurations.
</div>
)}
{(l3Fields || []).map((field: any, idx: number) => ( {(l3Fields || []).map((field: any, idx: number) => (
<div key={field.itemKey || idx}> <div key={field.itemKey || idx}>
<Label> <Label>
@ -294,7 +360,7 @@ export function ApplicationDetailsExtendedModals(props: ApplicationDetailsExtend
))} ))}
<div className="flex gap-3"> <div className="flex gap-3">
<Button variant="outline" className="flex-1" onClick={() => setShowLevel3FeedbackModal(false)} data-testid="onboarding-level3-feedback-cancel">Cancel</Button> <Button variant="outline" className="flex-1" onClick={() => setShowLevel3FeedbackModal(false)} data-testid="onboarding-level3-feedback-cancel">Cancel</Button>
<Button className="flex-1 bg-black hover:bg-zinc-800 text-white" onClick={handleSubmitLevel3Feedback} disabled={isSubmittingLevel3} data-testid="onboarding-level3-feedback-submit">{isSubmittingLevel3 ? 'Submitting...' : 'Submit Feedback'}</Button> <Button className="flex-1 bg-black hover:bg-zinc-800 text-white" onClick={handleSubmitLevel3Feedback} disabled={isSubmittingLevel3 || l3Fields.length === 0} data-testid="onboarding-level3-feedback-submit">{isSubmittingLevel3 ? 'Submitting...' : 'Submit Feedback'}</Button>
</div> </div>
</div> </div>
</DialogContent> </DialogContent>

View File

@ -105,6 +105,22 @@ export function ApplicationDetailsTabs(props: ApplicationDetailsTabsProps) {
auditLogActionBadgeClass, auditLogActionBadgeClass,
} = props; } = props;
const normalizeRole = (value: unknown): string =>
String(value || '')
.trim()
.toLowerCase()
.replace(/[_\s-]+/g, ' ');
const participantHasAnyRole = (participant: any, expectedRoles: string[]) => {
const participantRoles = [
participant?.user?.role,
participant?.user?.roleCode,
participant?.metadata?.role,
].map(normalizeRole);
const normalizedExpected = expectedRoles.map(normalizeRole);
return participantRoles.some((role) => normalizedExpected.includes(role));
};
return ( return (
<Card data-testid="onboarding-details-tabs-container"> <Card data-testid="onboarding-details-tabs-container">
<Tabs value={activeTab} onValueChange={setActiveTab}> <Tabs value={activeTab} onValueChange={setActiveTab}>
@ -139,13 +155,38 @@ export function ApplicationDetailsTabs(props: ApplicationDetailsTabsProps) {
<div className="relative" data-testid="onboarding-progress-stages-container"> <div className="relative" data-testid="onboarding-progress-stages-container">
{(() => { {(() => {
const interviewRoleMap: Record<number, string[]> = {
1: ['DD-ZM', 'RBM'],
2: ['DD Lead', 'ZBH'],
3: ['NBH', 'DD Head'],
};
const stageRoleMap: Record<string, string[]> = {
LOI_APPROVAL: ['DD Head', 'NBH'],
LOA_APPROVAL: ['DD Head', 'NBH'],
};
const getApproverStatus = (stageCode: string | number) => { const getApproverStatus = (stageCode: string | number) => {
const stageParticipants = (application.participants || []).filter((p: any) => const stageParticipants = (application.participants || []).filter((p: any) => {
p.metadata?.stageCode === stageCode || const metadataMatch =
p.metadata?.allAssignments?.includes(stageCode) || p.metadata?.stageCode === stageCode ||
(typeof stageCode === 'number' && (p.metadata?.interviewLevel === stageCode || p.metadata?.allAssignments?.includes(stageCode))) || p.metadata?.allAssignments?.includes(stageCode) ||
(typeof stageCode === 'string' && !isNaN(Number(stageCode)) && (p.metadata?.interviewLevel === Number(stageCode) || p.metadata?.allAssignments?.includes(Number(stageCode)))) (typeof stageCode === 'number' &&
); (p.metadata?.interviewLevel === stageCode ||
p.metadata?.interviewLevel === String(stageCode) ||
p.metadata?.allAssignments?.includes(stageCode) ||
p.metadata?.allAssignments?.includes(String(stageCode)))) ||
(typeof stageCode === 'string' &&
!isNaN(Number(stageCode)) &&
(p.metadata?.interviewLevel === Number(stageCode) ||
p.metadata?.allAssignments?.includes(Number(stageCode))));
if (metadataMatch) return true;
if (typeof stageCode === 'number') {
return participantHasAnyRole(p, interviewRoleMap[stageCode] || []);
}
return participantHasAnyRole(p, stageRoleMap[stageCode] || []);
});
return stageParticipants.map((p: any) => { return stageParticipants.map((p: any) => {
const saCode = typeof stageCode === 'number' ? `INTERVIEW_LEVEL_${stageCode}` : stageCode; const saCode = typeof stageCode === 'number' ? `INTERVIEW_LEVEL_${stageCode}` : stageCode;
@ -155,8 +196,8 @@ export function ApplicationDetailsTabs(props: ApplicationDetailsTabsProps) {
); );
return { return {
name: p.user?.name || 'Unknown', name: p.user?.name || p.user?.fullName || 'Unknown',
role: p.user?.role || 'Reviewer', role: p.user?.role || p.user?.roleCode || p.metadata?.role || 'Reviewer',
status: approval ? (approval.decision === 'Approved' ? 'approved' : 'rejected') : 'pending' status: approval ? (approval.decision === 'Approved' ? 'approved' : 'rejected') : 'pending'
}; };
}); });
@ -278,11 +319,16 @@ export function ApplicationDetailsTabs(props: ApplicationDetailsTabsProps) {
const stageId = Number(stage.id); const stageId = Number(stage.id);
const expectedCount = expectedMap[stageId]; const expectedCount = expectedMap[stageId];
let actualCount = stage.evaluators?.length || 0; const stageCodeById: Record<number, string | number> = {
if (stageId === 3) { 3: 1, // shortlist depends on L1 evaluators
const l1Evaluators = (application.participants || []).filter((p: any) => p.metadata?.interviewLevel === 1 || p.metadata?.interviewLevel === '1'); 4: 1,
actualCount = l1Evaluators.length; 5: 2,
} 6: 3,
8: 'LOI_APPROVAL',
12: 'LOA_APPROVAL',
};
const mappedStageCode = stageCodeById[stageId];
const actualCount = mappedStageCode ? getApproverStatus(mappedStageCode).length : (stage.evaluators?.length || 0);
const isEligibleForWarning = stageId === 3 ? (stage.status === 'completed') : (stage.status !== 'pending'); const isEligibleForWarning = stageId === 3 ? (stage.status === 'completed') : (stage.status !== 'pending');

View File

@ -1,4 +1,4 @@
import { Dispatch, SetStateAction } from 'react'; import { Dispatch, SetStateAction, useCallback } from 'react';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { onboardingService } from '@/services/onboarding.service'; import { onboardingService } from '@/services/onboarding.service';
@ -42,6 +42,10 @@ interface UseApplicationDetailsAdminActionsParams {
setLoading: Dispatch<SetStateAction<boolean>>; setLoading: Dispatch<SetStateAction<boolean>>;
setIsScheduling: Dispatch<SetStateAction<boolean>>; setIsScheduling: Dispatch<SetStateAction<boolean>>;
setShowScheduleModal: Dispatch<SetStateAction<boolean>>; setShowScheduleModal: Dispatch<SetStateAction<boolean>>;
setShowCancelInterviewModal: Dispatch<SetStateAction<boolean>>;
interviewIdToCancel: string;
setInterviewIdToCancel: Dispatch<SetStateAction<string>>;
setIsCancellingInterview: Dispatch<SetStateAction<boolean>>;
setIsUploading: Dispatch<SetStateAction<boolean>>; setIsUploading: Dispatch<SetStateAction<boolean>>;
setShowUploadForm: Dispatch<SetStateAction<boolean>>; setShowUploadForm: Dispatch<SetStateAction<boolean>>;
setUploadFile: Dispatch<SetStateAction<File | null>>; setUploadFile: Dispatch<SetStateAction<File | null>>;
@ -100,6 +104,10 @@ export function useApplicationDetailsAdminActions(params: UseApplicationDetailsA
setLoading, setLoading,
setIsScheduling, setIsScheduling,
setShowScheduleModal, setShowScheduleModal,
setShowCancelInterviewModal,
interviewIdToCancel,
setInterviewIdToCancel,
setIsCancellingInterview,
setIsUploading, setIsUploading,
setShowUploadForm, setShowUploadForm,
setUploadFile, setUploadFile,
@ -131,7 +139,7 @@ export function useApplicationDetailsAdminActions(params: UseApplicationDetailsA
setScheduledInterviewParticipants(scheduledInterviewParticipants.filter((p) => p.id !== userId)); setScheduledInterviewParticipants(scheduledInterviewParticipants.filter((p) => p.id !== userId));
}; };
const fetchUsers = async (type?: string) => { const fetchUsers = useCallback(async (type?: string) => {
if (!currentUser || !['DD Admin', 'Super Admin', 'DD Lead', 'DD Head', 'NBH'].includes(currentUser.role)) return; if (!currentUser || !['DD Admin', 'Super Admin', 'DD Lead', 'DD Head', 'NBH'].includes(currentUser.role)) return;
try { try {
const reqParams: any = {}; const reqParams: any = {};
@ -141,34 +149,77 @@ export function useApplicationDetailsAdminActions(params: UseApplicationDetailsA
level2: ['DD Lead', 'ZBH'], level2: ['DD Lead', 'ZBH'],
level3: ['NBH', 'DD Head'], level3: ['NBH', 'DD Head'],
}; };
reqParams.roleCode = roleMapping[type]; // Keep stage roles as preferred default, but allow broader user pool
// so admins can add extra panelists for the same interview.
if (roleMapping[type]) {
reqParams.preferredRoleCode = roleMapping[type];
}
if (application) { if (application) {
reqParams.locationId = application.districtId || application.areaId || application.regionId || application.zoneId; reqParams.locationId = application.districtId || application.areaId || application.regionId || application.zoneId;
} }
} }
reqParams.isExternal = false; reqParams.isExternal = false;
const response = await onboardingService.getUsers(reqParams); const response = await onboardingService.getUsers(reqParams);
if (Array.isArray(response)) setUsers(response); const rawUsers = Array.isArray(response)
else if (response && Array.isArray(response.data)) setUsers(response.data); ? response
else if (response && Array.isArray(response.users)) setUsers(response.users); : response && Array.isArray(response.data)
else setUsers([]); ? response.data
: response && Array.isArray(response.users)
? response.users
: [];
// Exclude inactive users and keep deterministic sorting.
const activeUsers = rawUsers.filter((u: any) => (u.status || '').toLowerCase() !== 'inactive');
setUsers(activeUsers.sort((a: any, b: any) => String(a.fullName || a.name || '').localeCompare(String(b.fullName || b.name || ''))));
} catch { } catch {
setUsers([]); setUsers([]);
} }
}; }, [currentUser, application, setUsers]);
const prefillInterviewParticipants = () => { const prefillInterviewParticipants = useCallback(() => {
if (!showScheduleModal || !application) return; if (!showScheduleModal || !application) return;
const levelNum = parseInt(interviewType.replace('level', '')) || 1; const levelNum = parseInt(interviewType.replace('level', '')) || 1;
const requiredRolesByLevel: Record<number, string[]> = {
1: ['DD-ZM', 'RBM'],
2: ['DD Lead', 'ZBH'],
3: ['NBH', 'DD Head'],
};
const normalizeRole = (value: unknown) =>
String(value || '')
.trim()
.toLowerCase()
.replace(/[_\s-]+/g, ' ');
const expectedRoles = (requiredRolesByLevel[levelNum] || []).map(normalizeRole);
const deriveDisplayRole = (participant: any, user: any): string => {
const candidateRoles = [
participant?.metadata?.role,
user?.role?.roleName,
user?.role?.roleCode,
user?.roleCode,
user?.role,
].filter(Boolean);
const matched = candidateRoles.find((r: any) => expectedRoles.includes(normalizeRole(r)));
return String(matched || candidateRoles[0] || 'Panelist');
};
const preAssigned = (application?.participants || []) const preAssigned = (application?.participants || [])
.filter((p: any) => .filter((p: any) =>
p.metadata?.interviewLevel === levelNum || p.metadata?.interviewLevel === levelNum ||
p.metadata?.interviewLevel === String(levelNum) || p.metadata?.interviewLevel === String(levelNum) ||
p.metadata?.allAssignments?.includes(levelNum) || p.metadata?.allAssignments?.includes(levelNum) ||
p.metadata?.allAssignments?.includes(String(levelNum)) p.metadata?.allAssignments?.includes(String(levelNum)) ||
expectedRoles.includes(normalizeRole(p.user?.role)) ||
expectedRoles.includes(normalizeRole(p.user?.roleCode)) ||
expectedRoles.includes(normalizeRole(p.metadata?.role))
) )
.map((p: any) => p.user) .map((p: any) => {
.filter(Boolean); const user = p.user || {};
return {
...user,
__stageRole: deriveDisplayRole(p, user),
};
})
.filter((u: any) => !!u?.id);
if (preAssigned.length === 0) { if (preAssigned.length === 0) {
setScheduledInterviewParticipants([]); setScheduledInterviewParticipants([]);
return; return;
@ -182,7 +233,7 @@ export function useApplicationDetailsAdminActions(params: UseApplicationDetailsA
} }
}); });
setScheduledInterviewParticipants(unique); setScheduledInterviewParticipants(unique);
}; }, [showScheduleModal, application, interviewType, setScheduledInterviewParticipants]);
const handleScheduleInterview = async () => { const handleScheduleInterview = async () => {
if (!interviewDate) { if (!interviewDate) {
@ -211,13 +262,23 @@ export function useApplicationDetailsAdminActions(params: UseApplicationDetailsA
}; };
const handleCancelInterview = async (interviewId: string) => { const handleCancelInterview = async (interviewId: string) => {
if (!window.confirm('Are you sure you want to cancel this interview?')) return; setInterviewIdToCancel(interviewId);
setShowCancelInterviewModal(true);
};
const handleConfirmCancelInterview = async () => {
if (!interviewIdToCancel) return;
try { try {
await onboardingService.updateInterview(interviewId, { status: 'Cancelled' }); setIsCancellingInterview(true);
await onboardingService.updateInterview(interviewIdToCancel, { status: 'Cancelled' });
toast.success('Interview cancelled successfully'); toast.success('Interview cancelled successfully');
setShowCancelInterviewModal(false);
setInterviewIdToCancel('');
await fetchInterviews(); await fetchInterviews();
} catch { } catch {
toast.error('Failed to cancel interview'); toast.error('Failed to cancel interview');
} finally {
setIsCancellingInterview(false);
} }
}; };
@ -520,7 +581,7 @@ export function useApplicationDetailsAdminActions(params: UseApplicationDetailsA
} }
}; };
const maybeFetchUsersForModal = async () => { const maybeFetchUsersForModal = useCallback(async () => {
if (showScheduleModal && application) { if (showScheduleModal && application) {
await fetchUsers(interviewType); await fetchUsers(interviewType);
prefillInterviewParticipants(); prefillInterviewParticipants();
@ -529,7 +590,15 @@ export function useApplicationDetailsAdminActions(params: UseApplicationDetailsA
if ((showAssignArchitectureModal || showAssignModal) && application) { if ((showAssignArchitectureModal || showAssignModal) && application) {
await fetchUsers(); await fetchUsers();
} }
}; }, [
showScheduleModal,
showAssignArchitectureModal,
showAssignModal,
application,
interviewType,
fetchUsers,
prefillInterviewParticipants,
]);
return { return {
handleAddInterviewer, handleAddInterviewer,
@ -538,6 +607,7 @@ export function useApplicationDetailsAdminActions(params: UseApplicationDetailsA
maybeFetchUsersForModal, maybeFetchUsersForModal,
handleScheduleInterview, handleScheduleInterview,
handleCancelInterview, handleCancelInterview,
handleConfirmCancelInterview,
handleUpload, handleUpload,
handleApprove, handleApprove,
handleReject, handleReject,

View File

@ -1,4 +1,4 @@
import { useEffect, useState } from 'react'; import { useCallback, useEffect, useState } from 'react';
import { Application, ApplicationStatus } from '@/lib/mock-data'; import { Application, ApplicationStatus } from '@/lib/mock-data';
import { onboardingService } from '@/services/onboarding.service'; import { onboardingService } from '@/services/onboarding.service';
import { eorService } from '@/services/eor.service'; import { eorService } from '@/services/eor.service';
@ -20,16 +20,16 @@ export function useApplicationDetailsData({ applicationId }: UseApplicationDetai
const [deposits, setDeposits] = useState<any[]>([]); const [deposits, setDeposits] = useState<any[]>([]);
const [paymentConfigs, setPaymentConfigs] = useState<any>({}); const [paymentConfigs, setPaymentConfigs] = useState<any>({});
const refreshDocuments = async () => { const refreshDocuments = useCallback(async () => {
try { try {
const docs = await onboardingService.getDocuments(applicationId); const docs = await onboardingService.getDocuments(applicationId);
setDocuments(docs || []); setDocuments(docs || []);
} catch (error) { } catch (error) {
console.error('Failed to refresh documents:', error); console.error('Failed to refresh documents:', error);
} }
}; }, [applicationId]);
const fetchApplication = async (silent = false) => { const fetchApplication = useCallback(async (silent = false) => {
try { try {
if (!silent) setLoading(true); if (!silent) setLoading(true);
const data = await onboardingService.getApplicationById(applicationId); const data = await onboardingService.getApplicationById(applicationId);
@ -123,9 +123,9 @@ export function useApplicationDetailsData({ applicationId }: UseApplicationDetai
} finally { } finally {
setLoading(false); setLoading(false);
} }
}; }, [applicationId]);
const fetchEorData = async () => { const fetchEorData = useCallback(async () => {
if (!applicationId) return; if (!applicationId) return;
try { try {
const resp = await eorService.getChecklist(applicationId); const resp = await eorService.getChecklist(applicationId);
@ -133,7 +133,7 @@ export function useApplicationDetailsData({ applicationId }: UseApplicationDetai
} catch { } catch {
setEorData(null); setEorData(null);
} }
}; }, [applicationId]);
const getDeposit = (type: string) => deposits.find((d) => d.depositType === type); const getDeposit = (type: string) => deposits.find((d) => d.depositType === type);

View File

@ -1,7 +1,6 @@
import { toast } from 'sonner'; import { toast } from 'sonner';
import { Dispatch, SetStateAction } from 'react'; import { Dispatch, SetStateAction } from 'react';
import { onboardingService } from '@/services/onboarding.service'; import { onboardingService } from '@/services/onboarding.service';
import { KT_MATRIX_CRITERIA } from '@/features/onboarding/components/application-details/applicationDetails.shared';
import type { InterviewConfig } from './useInterviewConfigs'; import type { InterviewConfig } from './useInterviewConfigs';
interface UseApplicationDetailsFeedbackActionsParams { interface UseApplicationDetailsFeedbackActionsParams {
@ -10,16 +9,22 @@ interface UseApplicationDetailsFeedbackActionsParams {
setKtMatrixSelectedValues: Dispatch<SetStateAction<Record<string, string>>>; setKtMatrixSelectedValues: Dispatch<SetStateAction<Record<string, string>>>;
ktMatrixRemarks: string; ktMatrixRemarks: string;
setKtMatrixRemarks: Dispatch<SetStateAction<string>>; setKtMatrixRemarks: Dispatch<SetStateAction<string>>;
ktMatrixRecommendation: string;
setKtMatrixRecommendation: Dispatch<SetStateAction<string>>;
selectedInterviewForFeedback: any; selectedInterviewForFeedback: any;
interviews: any[]; interviews: any[];
setIsSubmittingKT: Dispatch<SetStateAction<boolean>>; setIsSubmittingKT: Dispatch<SetStateAction<boolean>>;
setShowKTMatrixModal: Dispatch<SetStateAction<boolean>>; setShowKTMatrixModal: Dispatch<SetStateAction<boolean>>;
level2Feedback: any; level2Feedback: any;
setLevel2Feedback: Dispatch<SetStateAction<any>>; setLevel2Feedback: Dispatch<SetStateAction<any>>;
level2Recommendation: string;
setLevel2Recommendation: Dispatch<SetStateAction<string>>;
setIsSubmittingLevel2: Dispatch<SetStateAction<boolean>>; setIsSubmittingLevel2: Dispatch<SetStateAction<boolean>>;
setShowLevel2FeedbackModal: Dispatch<SetStateAction<boolean>>; setShowLevel2FeedbackModal: Dispatch<SetStateAction<boolean>>;
level3Feedback: any; level3Feedback: any;
setLevel3Feedback: Dispatch<SetStateAction<any>>; setLevel3Feedback: Dispatch<SetStateAction<any>>;
level3Recommendation: string;
setLevel3Recommendation: Dispatch<SetStateAction<string>>;
setIsSubmittingLevel3: Dispatch<SetStateAction<boolean>>; setIsSubmittingLevel3: Dispatch<SetStateAction<boolean>>;
setShowLevel3FeedbackModal: Dispatch<SetStateAction<boolean>>; setShowLevel3FeedbackModal: Dispatch<SetStateAction<boolean>>;
currentUser: any; currentUser: any;
@ -64,16 +69,22 @@ export function useApplicationDetailsFeedbackActions({
setKtMatrixSelectedValues, setKtMatrixSelectedValues,
ktMatrixRemarks, ktMatrixRemarks,
setKtMatrixRemarks, setKtMatrixRemarks,
ktMatrixRecommendation,
setKtMatrixRecommendation,
selectedInterviewForFeedback, selectedInterviewForFeedback,
interviews, interviews,
setIsSubmittingKT, setIsSubmittingKT,
setShowKTMatrixModal, setShowKTMatrixModal,
level2Feedback, level2Feedback,
setLevel2Feedback, setLevel2Feedback,
level2Recommendation,
setLevel2Recommendation,
setIsSubmittingLevel2, setIsSubmittingLevel2,
setShowLevel2FeedbackModal, setShowLevel2FeedbackModal,
level3Feedback, level3Feedback,
setLevel3Feedback, setLevel3Feedback,
level3Recommendation,
setLevel3Recommendation,
setIsSubmittingLevel3, setIsSubmittingLevel3,
setShowLevel3FeedbackModal, setShowLevel3FeedbackModal,
currentUser, currentUser,
@ -83,6 +94,17 @@ export function useApplicationDetailsFeedbackActions({
level2Config, level2Config,
level3Config, level3Config,
}: UseApplicationDetailsFeedbackActionsParams) { }: UseApplicationDetailsFeedbackActionsParams) {
const mapRecommendationForFeedback = (value: string) => {
if (value === 'Approve') return 'Recommended';
if (value === 'Reject') return 'Not Recommended';
return 'Hold';
};
const mapRecommendationForDecision = (value: string) => {
if (value === 'Approve') return 'Approved';
if (value === 'Reject') return 'Rejected';
return null;
};
// Resolve active criteria/fields from config or fallback to hardcoded defaults // Resolve active criteria/fields from config or fallback to hardcoded defaults
const getKtCriteria = () => { const getKtCriteria = () => {
if (ktMatrixConfig?.items && ktMatrixConfig.items.length > 0) { if (ktMatrixConfig?.items && ktMatrixConfig.items.length > 0) {
@ -97,37 +119,21 @@ export function useApplicationDetailsFeedbackActions({
})), })),
})); }));
} }
return KT_MATRIX_CRITERIA; return [];
}; };
const getLevel2Fields = () => { const getLevel2Fields = () => {
if (level2Config?.items && level2Config.items.length > 0) { if (level2Config?.items && level2Config.items.length > 0) {
return level2Config.items; return level2Config.items;
} }
return [ return [];
{ itemKey: 'strategicVision', label: 'Strategic Vision', isRequired: true },
{ itemKey: 'managementCapabilities', label: 'Management Capabilities', isRequired: true },
{ itemKey: 'operationalUnderstanding', label: 'Operational Understanding', isRequired: true },
{ itemKey: 'keyStrengths', label: 'Key Strengths', isRequired: true },
{ itemKey: 'areasOfConcern', label: 'Areas of Concern', isRequired: true },
{ itemKey: 'additionalComments', label: 'Additional Comments', isRequired: false },
];
}; };
const getLevel3Fields = () => { const getLevel3Fields = () => {
if (level3Config?.items && level3Config.items.length > 0) { if (level3Config?.items && level3Config.items.length > 0) {
return level3Config.items; return level3Config.items;
} }
return [ return [];
{ itemKey: 'strategicVision', label: 'Business Vision & Strategy', isRequired: true },
{ itemKey: 'managementCapabilities', label: 'Leadership & Decision Making', isRequired: true },
{ itemKey: 'operationalUnderstanding', label: 'Operational & Financial Readiness', isRequired: true },
{ itemKey: 'brandAlignment', label: 'Brand Alignment', isRequired: true },
{ itemKey: 'keyStrengths', label: 'Key Strengths', isRequired: true },
{ itemKey: 'areasOfConcern', label: 'Areas of Concern', isRequired: true },
{ itemKey: 'executiveSummary', label: 'Executive Summary', isRequired: false },
{ itemKey: 'additionalComments', label: 'Additional Comments', isRequired: false },
];
}; };
const ktCriteria = getKtCriteria(); const ktCriteria = getKtCriteria();
@ -151,6 +157,10 @@ export function useApplicationDetailsFeedbackActions({
}; };
const handleSubmitKTMatrix = async () => { const handleSubmitKTMatrix = async () => {
if (ktCriteria.length === 0) {
toast.error('KT Matrix configuration is missing. Please configure it in Master > Interview Configurations.');
return;
}
if (Object.keys(ktMatrixScores).length < ktCriteria.length) { if (Object.keys(ktMatrixScores).length < ktCriteria.length) {
toast.warning('Please fill all fields in the KT Matrix'); toast.warning('Please fill all fields in the KT Matrix');
return; return;
@ -168,12 +178,32 @@ export function useApplicationDetailsFeedbackActions({
maxScore: c.maxScore || 10, maxScore: c.maxScore || 10,
weightage: c.weight || 0, weightage: c.weight || 0,
})); }));
await onboardingService.submitKTMatrix({ interviewId, criteriaScores, feedback: ktMatrixRemarks, recommendation: null }); await onboardingService.submitKTMatrix({
toast.success('KT Matrix submitted successfully'); interviewId,
criteriaScores,
feedback: ktMatrixRemarks,
recommendation: mapRecommendationForFeedback(ktMatrixRecommendation)
});
const decision = mapRecommendationForDecision(ktMatrixRecommendation);
if (decision) {
await onboardingService.updateInterviewDecision({
interviewId,
decision,
remarks: ktMatrixRemarks || `Level 1 ${decision.toLowerCase()} via KT Matrix`
});
}
toast.success(
decision
? `KT Matrix submitted and interview ${decision.toLowerCase()}`
: 'KT Matrix submitted and interview kept on hold'
);
setShowKTMatrixModal(false); setShowKTMatrixModal(false);
setKtMatrixScores({}); setKtMatrixScores({});
setKtMatrixSelectedValues({}); setKtMatrixSelectedValues({});
setKtMatrixRemarks(''); setKtMatrixRemarks('');
setKtMatrixRecommendation('Approve');
await fetchInterviews(); await fetchInterviews();
await fetchApplication(); await fetchApplication();
} catch { } catch {
@ -188,6 +218,10 @@ export function useApplicationDetailsFeedbackActions({
}; };
const handleSubmitLevel2Feedback = async () => { const handleSubmitLevel2Feedback = async () => {
if (l2Fields.length === 0) {
toast.error('Level 2 feedback configuration is missing. Please configure it in Master > Interview Configurations.');
return;
}
if (!level2Feedback.overallScore) { if (!level2Feedback.overallScore) {
toast.warning('Please provide an overall score.'); toast.warning('Please provide an overall score.');
return; return;
@ -202,10 +236,27 @@ export function useApplicationDetailsFeedbackActions({
const feedbackItems = l2Fields const feedbackItems = l2Fields
.map((f: any) => ({ type: f.label, comments: level2Feedback[f.itemKey] || '' })) .map((f: any) => ({ type: f.label, comments: level2Feedback[f.itemKey] || '' }))
.filter((item) => item.comments.trim() !== ''); .filter((item) => item.comments.trim() !== '');
await onboardingService.submitLevel2Feedback({ interviewId, overallScore: Number(level2Feedback.overallScore), feedbackItems }); await onboardingService.submitLevel2Feedback({
toast.success('Level 2 Feedback submitted successfully'); interviewId,
overallScore: Number(level2Feedback.overallScore),
feedbackItems,
recommendation: mapRecommendationForFeedback(level2Recommendation)
});
const decision = mapRecommendationForDecision(level2Recommendation);
const remarks = level2Feedback.additionalComments || 'Level 2 decision submitted via feedback modal';
if (decision) {
await onboardingService.updateInterviewDecision({ interviewId, decision, remarks });
}
toast.success(
decision
? `Level 2 feedback submitted and interview ${decision.toLowerCase()}`
: 'Level 2 feedback submitted and interview kept on hold'
);
setShowLevel2FeedbackModal(false); setShowLevel2FeedbackModal(false);
setLevel2Feedback(createInitialLevel2Feedback(currentUser)); setLevel2Feedback(createInitialLevel2Feedback(currentUser));
setLevel2Recommendation('Approve');
await fetchInterviews(); await fetchInterviews();
await fetchApplication(); await fetchApplication();
} catch { } catch {
@ -220,6 +271,10 @@ export function useApplicationDetailsFeedbackActions({
}; };
const handleSubmitLevel3Feedback = async () => { const handleSubmitLevel3Feedback = async () => {
if (l3Fields.length === 0) {
toast.error('Level 3 feedback configuration is missing. Please configure it in Master > Interview Configurations.');
return;
}
if (!level3Feedback.overallScore) { if (!level3Feedback.overallScore) {
toast.warning('Please provide an overall score.'); toast.warning('Please provide an overall score.');
return; return;
@ -234,10 +289,30 @@ export function useApplicationDetailsFeedbackActions({
const feedbackItems = l3Fields const feedbackItems = l3Fields
.map((f: any) => ({ type: f.label, comments: level3Feedback[f.itemKey] || '' })) .map((f: any) => ({ type: f.label, comments: level3Feedback[f.itemKey] || '' }))
.filter((item) => item.comments.trim() !== ''); .filter((item) => item.comments.trim() !== '');
await onboardingService.submitLevel2Feedback({ interviewId, overallScore: Number(level3Feedback.overallScore), feedbackItems }); await onboardingService.submitLevel2Feedback({
toast.success('Level 3 Feedback submitted successfully'); interviewId,
overallScore: Number(level3Feedback.overallScore),
feedbackItems,
recommendation: mapRecommendationForFeedback(level3Recommendation)
});
const decision = mapRecommendationForDecision(level3Recommendation);
const remarks =
level3Feedback.executiveSummary ||
level3Feedback.additionalComments ||
'Level 3 decision submitted via feedback modal';
if (decision) {
await onboardingService.updateInterviewDecision({ interviewId, decision, remarks });
}
toast.success(
decision
? `Level 3 feedback submitted and interview ${decision.toLowerCase()}`
: 'Level 3 feedback submitted and interview kept on hold'
);
setShowLevel3FeedbackModal(false); setShowLevel3FeedbackModal(false);
setLevel3Feedback(createInitialLevel3Feedback(currentUser)); setLevel3Feedback(createInitialLevel3Feedback(currentUser));
setLevel3Recommendation('Approve');
await fetchInterviews(); await fetchInterviews();
await fetchApplication(); await fetchApplication();
} catch { } catch {

View File

@ -1,4 +1,4 @@
import { Dispatch, SetStateAction } from 'react'; import { Dispatch, SetStateAction, useCallback } from 'react';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { onboardingService } from '@/services/onboarding.service'; import { onboardingService } from '@/services/onboarding.service';
@ -75,14 +75,14 @@ export function useApplicationDetailsLocalActions({
} }
}; };
const fetchFddAgencies = async () => { const fetchFddAgencies = useCallback(async () => {
try { try {
const agencies = await onboardingService.getUsers({ roleCode: 'FDD' }); const agencies = await onboardingService.getUsers({ roleCode: 'FDD' });
setFddAgencies(Array.isArray(agencies) ? agencies : []); setFddAgencies(Array.isArray(agencies) ? agencies : []);
} catch { } catch {
setFddAgencies([]); setFddAgencies([]);
} }
}; }, [setFddAgencies]);
const handleAssignAgency = async () => { const handleAssignAgency = async () => {
if (!selectedAgencyId) { if (!selectedAgencyId) {

View File

@ -80,10 +80,6 @@ export function useApplicationDetailsPermissions({
getDeposit('SECURITY_DEPOSIT')?.status !== 'Verified'; getDeposit('SECURITY_DEPOSIT')?.status !== 'Verified';
const isFinalState = application.status === 'Onboarded' || application.status === 'Rejected'; 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 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'); const ddHeadLoaApproved = application.stageApprovals?.some((a: any) => a.stageCode === 'LOA_APPROVAL' && a.actorRole === 'DD Head' && a.decision === 'Approved');
@ -105,11 +101,10 @@ export function useApplicationDetailsPermissions({
const canApproveReject = const canApproveReject =
!isFinalState && !isFinalState &&
!isDecisionMade && !isDecisionMade &&
((!!activeInterviewForUser && !!hasFeedbackForActive) || (isAdminRole &&
(isAdminRole && isAdministrativeStage &&
isAdministrativeStage && sequenceMet &&
sequenceMet && (!['EOR In Progress', 'Inauguration', 'Approved'].includes(application.status) || eorProgress === 100));
(!['EOR In Progress', 'Inauguration', 'Approved'].includes(application.status) || eorProgress === 100)));
return { return {
canApprove: canApproveReject && !isLoaLocked && !isSecurityDetailsLocked, canApprove: canApproveReject && !isLoaLocked && !isSecurityDetailsLocked,

View File

@ -15,6 +15,25 @@ export function useApplicationDetailsStageData({
eorData, eorData,
getDeposit, getDeposit,
}: UseApplicationDetailsStageDataParams) { }: UseApplicationDetailsStageDataParams) {
const normalizeRole = (value: unknown): string =>
String(value || '')
.trim()
.toLowerCase()
.replace(/[_\s-]+/g, ' ');
const hasAnyRole = (participant: any, expectedRoles: string[]) => {
const userRoles = [
participant?.user?.role,
participant?.user?.roleCode,
participant?.metadata?.role,
].map(normalizeRole);
const target = expectedRoles.map(normalizeRole);
return userRoles.some((r) => target.includes(r));
};
const participantLabel = (participant: any) =>
`${participant?.user?.fullName || participant?.user?.name || 'User'} (${participant?.user?.role || participant?.user?.roleCode || participant?.metadata?.role || participant?.participantType || 'participant'})`;
const isDocumentUploaded = (docType: string) => { const isDocumentUploaded = (docType: string) => {
return (documents || []).some((d) => d.documentType === docType); return (documents || []).some((d) => d.documentType === docType);
}; };
@ -47,26 +66,56 @@ export function useApplicationDetailsStageData({
{ {
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'), 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', 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})`))), evaluators: Array.from(new Set(
(application.participants || [])
.filter((p: any) =>
p.metadata?.interviewLevel === 1 ||
p.metadata?.interviewLevel === '1' ||
p.metadata?.allAssignments?.includes(1) ||
p.metadata?.allAssignments?.includes('1') ||
hasAnyRole(p, ['DD-ZM', 'RBM'])
)
.map(participantLabel)
)),
documentsUploaded: 1 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'), 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', 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})`))), evaluators: Array.from(new Set(
(application.participants || [])
.filter((p: any) =>
p.metadata?.interviewLevel === 2 ||
p.metadata?.interviewLevel === '2' ||
p.metadata?.allAssignments?.includes(2) ||
p.metadata?.allAssignments?.includes('2') ||
hasAnyRole(p, ['DD Lead', 'ZBH'])
)
.map(participantLabel)
)),
documentsUploaded: 1 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'), 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', 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})`))), evaluators: Array.from(new Set(
(application.participants || [])
.filter((p: any) =>
p.metadata?.interviewLevel === 3 ||
p.metadata?.interviewLevel === '3' ||
p.metadata?.allAssignments?.includes(3) ||
p.metadata?.allAssignments?.includes('3') ||
hasAnyRole(p, ['NBH', 'DD Head'])
)
.map(participantLabel)
)),
documentsUploaded: 2 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: 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'), 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', 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})`))), evaluators: Array.from(new Set((application.participants || []).filter((p: any) => p.metadata?.stageCode === 'LOI_APPROVAL' || p.metadata?.allAssignments?.includes('LOI_APPROVAL')).map(participantLabel))),
documentsUploaded: 1 documentsUploaded: 1
}, },
{ {
@ -105,7 +154,7 @@ export function useApplicationDetailsStageData({
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'), 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', isLocked: application.status === 'LOA Pending' && getDeposit('FIRST_FILL')?.status !== 'Verified',
lockMessage: 'First Fill (₹15L) must be verified by Finance before LOA Approval.', 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})`))), evaluators: Array.from(new Set((application.participants || []).filter((p: any) => p.metadata?.stageCode === 'LOA_APPROVAL' || p.metadata?.allAssignments?.includes('LOA_APPROVAL')).map(participantLabel))),
description: 'Letter of Authorization' 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: 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' },

View File

@ -17,13 +17,15 @@ export function useApplicationDetailsUIState({ initialTab = 'questionnaire' }: U
const [rejectionReason, setRejectionReason] = useState(''); const [rejectionReason, setRejectionReason] = useState('');
const [scheduledInterviewParticipants, setScheduledInterviewParticipants] = useState<any[]>([]); const [scheduledInterviewParticipants, setScheduledInterviewParticipants] = useState<any[]>([]);
const [showScheduleModal, setShowScheduleModal] = useState(false); const [showScheduleModal, setShowScheduleModal] = useState(false);
const [showCancelInterviewModal, setShowCancelInterviewModal] = useState(false);
const [interviewIdToCancel, setInterviewIdToCancel] = useState('');
const [showKTMatrixModal, setShowKTMatrixModal] = useState(false); const [showKTMatrixModal, setShowKTMatrixModal] = useState(false);
const [showLevel2FeedbackModal, setShowLevel2FeedbackModal] = useState(false); const [showLevel2FeedbackModal, setShowLevel2FeedbackModal] = useState(false);
const [showLevel3FeedbackModal, setShowLevel3FeedbackModal] = useState(false); const [showLevel3FeedbackModal, setShowLevel3FeedbackModal] = useState(false);
const [showDocumentsModal, setShowDocumentsModal] = useState(false); const [showDocumentsModal, setShowDocumentsModal] = useState(false);
const [showAssignModal, setShowAssignModal] = useState(false); const [showAssignModal, setShowAssignModal] = useState(false);
const [selectedStage, setSelectedStage] = useState<string | null>(null); const [selectedStage, setSelectedStage] = useState<string | null>(null);
const [interviewMode, setInterviewMode] = useState('physical'); const [interviewMode, setInterviewMode] = useState('virtual');
const [approvalRemark, setApprovalRemark] = useState(''); const [approvalRemark, setApprovalRemark] = useState('');
const [expandedBranches, setExpandedBranches] = useState<Record<string, boolean>>({}); const [expandedBranches, setExpandedBranches] = useState<Record<string, boolean>>({});
const [users, setUsers] = useState<any[]>([]); const [users, setUsers] = useState<any[]>([]);
@ -46,6 +48,7 @@ export function useApplicationDetailsUIState({ initialTab = 'questionnaire' }: U
const [isSavingStatutory, setIsSavingStatutory] = useState(false); const [isSavingStatutory, setIsSavingStatutory] = useState(false);
const [interviews, setInterviews] = useState<any[]>([]); const [interviews, setInterviews] = useState<any[]>([]);
const [isScheduling, setIsScheduling] = useState(false); const [isScheduling, setIsScheduling] = useState(false);
const [isCancellingInterview, setIsCancellingInterview] = useState(false);
const [showAssignArchitectureModal, setShowAssignArchitectureModal] = useState(false); const [showAssignArchitectureModal, setShowAssignArchitectureModal] = useState(false);
const [architectureLeadId, setArchitectureLeadId] = useState(''); const [architectureLeadId, setArchitectureLeadId] = useState('');
const [isAssigningArchitecture, setIsAssigningArchitecture] = useState(false); const [isAssigningArchitecture, setIsAssigningArchitecture] = useState(false);
@ -63,6 +66,7 @@ export function useApplicationDetailsUIState({ initialTab = 'questionnaire' }: U
const [ktMatrixScores, setKtMatrixScores] = useState<Record<string, number>>({}); const [ktMatrixScores, setKtMatrixScores] = useState<Record<string, number>>({});
const [ktMatrixSelectedValues, setKtMatrixSelectedValues] = useState<Record<string, string>>({}); const [ktMatrixSelectedValues, setKtMatrixSelectedValues] = useState<Record<string, string>>({});
const [ktMatrixRemarks, setKtMatrixRemarks] = useState<string>(''); const [ktMatrixRemarks, setKtMatrixRemarks] = useState<string>('');
const [ktMatrixRecommendation, setKtMatrixRecommendation] = useState<string>('Approve');
const [isSubmittingKT, setIsSubmittingKT] = useState(false); const [isSubmittingKT, setIsSubmittingKT] = useState(false);
const [selectedInterviewForFeedback, setSelectedInterviewForFeedback] = useState<any>(null); const [selectedInterviewForFeedback, setSelectedInterviewForFeedback] = useState<any>(null);
const [showFddFinalizeModal, setShowFddFinalizeModal] = useState(false); const [showFddFinalizeModal, setShowFddFinalizeModal] = useState(false);
@ -72,8 +76,10 @@ export function useApplicationDetailsUIState({ initialTab = 'questionnaire' }: U
const [isFinalizingFdd, setIsFinalizingFdd] = useState(false); const [isFinalizingFdd, setIsFinalizingFdd] = useState(false);
const [isFddFlagging, setIsFddFlagging] = useState(false); const [isFddFlagging, setIsFddFlagging] = useState(false);
const [level2Feedback, setLevel2Feedback] = useState<any>({}); const [level2Feedback, setLevel2Feedback] = useState<any>({});
const [level2Recommendation, setLevel2Recommendation] = useState<string>('Approve');
const [isSubmittingLevel2, setIsSubmittingLevel2] = useState(false); const [isSubmittingLevel2, setIsSubmittingLevel2] = useState(false);
const [level3Feedback, setLevel3Feedback] = useState<any>({}); const [level3Feedback, setLevel3Feedback] = useState<any>({});
const [level3Recommendation, setLevel3Recommendation] = useState<string>('Approve');
const [isSubmittingLevel3, setIsSubmittingLevel3] = useState(false); const [isSubmittingLevel3, setIsSubmittingLevel3] = useState(false);
const [selectedEvaluationForView, setSelectedEvaluationForView] = useState<any>(null); const [selectedEvaluationForView, setSelectedEvaluationForView] = useState<any>(null);
const [showFeedbackDetailsModal, setShowFeedbackDetailsModal] = useState(false); const [showFeedbackDetailsModal, setShowFeedbackDetailsModal] = useState(false);
@ -90,6 +96,8 @@ export function useApplicationDetailsUIState({ initialTab = 'questionnaire' }: U
rejectionReason, setRejectionReason, rejectionReason, setRejectionReason,
scheduledInterviewParticipants, setScheduledInterviewParticipants, scheduledInterviewParticipants, setScheduledInterviewParticipants,
showScheduleModal, setShowScheduleModal, showScheduleModal, setShowScheduleModal,
showCancelInterviewModal, setShowCancelInterviewModal,
interviewIdToCancel, setInterviewIdToCancel,
showKTMatrixModal, setShowKTMatrixModal, showKTMatrixModal, setShowKTMatrixModal,
showLevel2FeedbackModal, setShowLevel2FeedbackModal, showLevel2FeedbackModal, setShowLevel2FeedbackModal,
showLevel3FeedbackModal, setShowLevel3FeedbackModal, showLevel3FeedbackModal, setShowLevel3FeedbackModal,
@ -119,6 +127,7 @@ export function useApplicationDetailsUIState({ initialTab = 'questionnaire' }: U
isSavingStatutory, setIsSavingStatutory, isSavingStatutory, setIsSavingStatutory,
interviews, setInterviews, interviews, setInterviews,
isScheduling, setIsScheduling, isScheduling, setIsScheduling,
isCancellingInterview, setIsCancellingInterview,
showAssignArchitectureModal, setShowAssignArchitectureModal, showAssignArchitectureModal, setShowAssignArchitectureModal,
architectureLeadId, setArchitectureLeadId, architectureLeadId, setArchitectureLeadId,
isAssigningArchitecture, setIsAssigningArchitecture, isAssigningArchitecture, setIsAssigningArchitecture,
@ -136,6 +145,7 @@ export function useApplicationDetailsUIState({ initialTab = 'questionnaire' }: U
ktMatrixScores, setKtMatrixScores, ktMatrixScores, setKtMatrixScores,
ktMatrixSelectedValues, setKtMatrixSelectedValues, ktMatrixSelectedValues, setKtMatrixSelectedValues,
ktMatrixRemarks, setKtMatrixRemarks, ktMatrixRemarks, setKtMatrixRemarks,
ktMatrixRecommendation, setKtMatrixRecommendation,
isSubmittingKT, setIsSubmittingKT, isSubmittingKT, setIsSubmittingKT,
selectedInterviewForFeedback, setSelectedInterviewForFeedback, selectedInterviewForFeedback, setSelectedInterviewForFeedback,
showFddFinalizeModal, setShowFddFinalizeModal, showFddFinalizeModal, setShowFddFinalizeModal,
@ -145,8 +155,10 @@ export function useApplicationDetailsUIState({ initialTab = 'questionnaire' }: U
isFinalizingFdd, setIsFinalizingFdd, isFinalizingFdd, setIsFinalizingFdd,
isFddFlagging, setIsFddFlagging, isFddFlagging, setIsFddFlagging,
level2Feedback, setLevel2Feedback, level2Feedback, setLevel2Feedback,
level2Recommendation, setLevel2Recommendation,
isSubmittingLevel2, setIsSubmittingLevel2, isSubmittingLevel2, setIsSubmittingLevel2,
level3Feedback, setLevel3Feedback, level3Feedback, setLevel3Feedback,
level3Recommendation, setLevel3Recommendation,
isSubmittingLevel3, setIsSubmittingLevel3, isSubmittingLevel3, setIsSubmittingLevel3,
selectedEvaluationForView, setSelectedEvaluationForView, selectedEvaluationForView, setSelectedEvaluationForView,
showFeedbackDetailsModal, setShowFeedbackDetailsModal, showFeedbackDetailsModal, setShowFeedbackDetailsModal,

View File

@ -60,6 +60,8 @@ export const ApplicationDetails = () => {
rejectionReason, setRejectionReason, rejectionReason, setRejectionReason,
scheduledInterviewParticipants, setScheduledInterviewParticipants, scheduledInterviewParticipants, setScheduledInterviewParticipants,
showScheduleModal, setShowScheduleModal, showScheduleModal, setShowScheduleModal,
showCancelInterviewModal, setShowCancelInterviewModal,
interviewIdToCancel, setInterviewIdToCancel,
showKTMatrixModal, setShowKTMatrixModal, showKTMatrixModal, setShowKTMatrixModal,
showLevel2FeedbackModal, setShowLevel2FeedbackModal, showLevel2FeedbackModal, setShowLevel2FeedbackModal,
showLevel3FeedbackModal, setShowLevel3FeedbackModal, showLevel3FeedbackModal, setShowLevel3FeedbackModal,
@ -89,6 +91,7 @@ export const ApplicationDetails = () => {
isSavingStatutory, setIsSavingStatutory, isSavingStatutory, setIsSavingStatutory,
interviews, setInterviews, interviews, setInterviews,
isScheduling, setIsScheduling, isScheduling, setIsScheduling,
isCancellingInterview, setIsCancellingInterview,
showAssignArchitectureModal, setShowAssignArchitectureModal, showAssignArchitectureModal, setShowAssignArchitectureModal,
architectureLeadId, setArchitectureLeadId, architectureLeadId, setArchitectureLeadId,
isAssigningArchitecture, setIsAssigningArchitecture, isAssigningArchitecture, setIsAssigningArchitecture,
@ -106,6 +109,7 @@ export const ApplicationDetails = () => {
ktMatrixScores, setKtMatrixScores, ktMatrixScores, setKtMatrixScores,
ktMatrixSelectedValues, setKtMatrixSelectedValues, ktMatrixSelectedValues, setKtMatrixSelectedValues,
ktMatrixRemarks, setKtMatrixRemarks, ktMatrixRemarks, setKtMatrixRemarks,
ktMatrixRecommendation, setKtMatrixRecommendation,
isSubmittingKT, setIsSubmittingKT, isSubmittingKT, setIsSubmittingKT,
selectedInterviewForFeedback, setSelectedInterviewForFeedback, selectedInterviewForFeedback, setSelectedInterviewForFeedback,
showFddFinalizeModal, setShowFddFinalizeModal, showFddFinalizeModal, setShowFddFinalizeModal,
@ -115,8 +119,10 @@ export const ApplicationDetails = () => {
isFinalizingFdd, setIsFinalizingFdd, isFinalizingFdd, setIsFinalizingFdd,
isFddFlagging, setIsFddFlagging, isFddFlagging, setIsFddFlagging,
level2Feedback, setLevel2Feedback, level2Feedback, setLevel2Feedback,
level2Recommendation, setLevel2Recommendation,
isSubmittingLevel2, setIsSubmittingLevel2, isSubmittingLevel2, setIsSubmittingLevel2,
level3Feedback, setLevel3Feedback, level3Feedback, setLevel3Feedback,
level3Recommendation, setLevel3Recommendation,
isSubmittingLevel3, setIsSubmittingLevel3, isSubmittingLevel3, setIsSubmittingLevel3,
showFeedbackDetailsModal, setShowFeedbackDetailsModal, showFeedbackDetailsModal, setShowFeedbackDetailsModal,
selectedEvaluationForView, setSelectedEvaluationForView, selectedEvaluationForView, setSelectedEvaluationForView,
@ -220,16 +226,22 @@ export const ApplicationDetails = () => {
setKtMatrixSelectedValues, setKtMatrixSelectedValues,
ktMatrixRemarks, ktMatrixRemarks,
setKtMatrixRemarks, setKtMatrixRemarks,
ktMatrixRecommendation,
setKtMatrixRecommendation,
selectedInterviewForFeedback, selectedInterviewForFeedback,
interviews, interviews,
setIsSubmittingKT, setIsSubmittingKT,
setShowKTMatrixModal, setShowKTMatrixModal,
level2Feedback, level2Feedback,
setLevel2Feedback, setLevel2Feedback,
level2Recommendation,
setLevel2Recommendation,
setIsSubmittingLevel2, setIsSubmittingLevel2,
setShowLevel2FeedbackModal, setShowLevel2FeedbackModal,
level3Feedback, level3Feedback,
setLevel3Feedback, setLevel3Feedback,
level3Recommendation,
setLevel3Recommendation,
setIsSubmittingLevel3, setIsSubmittingLevel3,
setShowLevel3FeedbackModal, setShowLevel3FeedbackModal,
currentUser, currentUser,
@ -255,6 +267,7 @@ export const ApplicationDetails = () => {
maybeFetchUsersForModal, maybeFetchUsersForModal,
handleScheduleInterview, handleScheduleInterview,
handleCancelInterview, handleCancelInterview,
handleConfirmCancelInterview,
handleUpload, handleUpload,
handleApprove, handleApprove,
handleReject, handleReject,
@ -303,6 +316,10 @@ export const ApplicationDetails = () => {
setLoading, setLoading,
setIsScheduling, setIsScheduling,
setShowScheduleModal, setShowScheduleModal,
setShowCancelInterviewModal,
interviewIdToCancel,
setInterviewIdToCancel,
setIsCancellingInterview,
setIsUploading, setIsUploading,
setShowUploadForm, setShowUploadForm,
setUploadFile, setUploadFile,
@ -322,7 +339,7 @@ export const ApplicationDetails = () => {
useEffect(() => { useEffect(() => {
maybeFetchUsersForModal(); maybeFetchUsersForModal();
}, [showScheduleModal, showAssignArchitectureModal, showAssignModal, interviewType, application?.participants, maybeFetchUsersForModal]); }, [showScheduleModal, showAssignArchitectureModal, showAssignModal, interviewType, application?.id, maybeFetchUsersForModal]);
if (loading && !application) { if (loading && !application) {
return ( return (
@ -510,6 +527,11 @@ export const ApplicationDetails = () => {
handleReject={handleReject} handleReject={handleReject}
showScheduleModal={showScheduleModal} showScheduleModal={showScheduleModal}
setShowScheduleModal={setShowScheduleModal} setShowScheduleModal={setShowScheduleModal}
showCancelInterviewModal={showCancelInterviewModal}
setShowCancelInterviewModal={setShowCancelInterviewModal}
setInterviewIdToCancel={setInterviewIdToCancel}
isCancellingInterview={isCancellingInterview}
handleConfirmCancelInterview={handleConfirmCancelInterview}
interviewType={interviewType} interviewType={interviewType}
setInterviewType={setInterviewType} setInterviewType={setInterviewType}
interviewMode={interviewMode} interviewMode={interviewMode}
@ -557,6 +579,8 @@ export const ApplicationDetails = () => {
handleKTMatrixChange={handleKTMatrixChange} handleKTMatrixChange={handleKTMatrixChange}
ktMatrixRemarks={ktMatrixRemarks} ktMatrixRemarks={ktMatrixRemarks}
setKtMatrixRemarks={setKtMatrixRemarks} setKtMatrixRemarks={setKtMatrixRemarks}
ktMatrixRecommendation={ktMatrixRecommendation}
setKtMatrixRecommendation={setKtMatrixRecommendation}
calculateKTScore={calculateKTScore} calculateKTScore={calculateKTScore}
handleSubmitKTMatrix={handleSubmitKTMatrix} handleSubmitKTMatrix={handleSubmitKTMatrix}
isSubmittingKT={isSubmittingKT} isSubmittingKT={isSubmittingKT}
@ -564,15 +588,20 @@ export const ApplicationDetails = () => {
setShowLevel2FeedbackModal={setShowLevel2FeedbackModal} setShowLevel2FeedbackModal={setShowLevel2FeedbackModal}
level2Feedback={level2Feedback} level2Feedback={level2Feedback}
handleLevel2Change={handleLevel2Change} handleLevel2Change={handleLevel2Change}
level2Recommendation={level2Recommendation}
setLevel2Recommendation={setLevel2Recommendation}
handleSubmitLevel2Feedback={handleSubmitLevel2Feedback} handleSubmitLevel2Feedback={handleSubmitLevel2Feedback}
isSubmittingLevel2={isSubmittingLevel2} isSubmittingLevel2={isSubmittingLevel2}
showFeedbackDetailsModal={showFeedbackDetailsModal} showFeedbackDetailsModal={showFeedbackDetailsModal}
setShowFeedbackDetailsModal={setShowFeedbackDetailsModal} setShowFeedbackDetailsModal={setShowFeedbackDetailsModal}
selectedEvaluationForView={selectedEvaluationForView} selectedEvaluationForView={selectedEvaluationForView}
selectedInterviewForFeedback={selectedInterviewForFeedback}
showLevel3FeedbackModal={showLevel3FeedbackModal} showLevel3FeedbackModal={showLevel3FeedbackModal}
setShowLevel3FeedbackModal={setShowLevel3FeedbackModal} setShowLevel3FeedbackModal={setShowLevel3FeedbackModal}
level3Feedback={level3Feedback} level3Feedback={level3Feedback}
handleLevel3Change={handleLevel3Change} handleLevel3Change={handleLevel3Change}
level3Recommendation={level3Recommendation}
setLevel3Recommendation={setLevel3Recommendation}
handleSubmitLevel3Feedback={handleSubmitLevel3Feedback} handleSubmitLevel3Feedback={handleSubmitLevel3Feedback}
isSubmittingLevel3={isSubmittingLevel3} isSubmittingLevel3={isSubmittingLevel3}
showDocumentsModal={showDocumentsModal} showDocumentsModal={showDocumentsModal}

View File

@ -214,18 +214,9 @@ export function OpportunityRequestsPage({ onViewDetails }: OpportunityRequestsPa
const response = await onboardingService.shortlistApplications(selectedIds, [], shortlistRemark); const response = await onboardingService.shortlistApplications(selectedIds, [], shortlistRemark);
if (response && response.success) { if (response && response.success) {
// Update local state and show success only if API succeeded // Refresh data from server to ensure correct filtering and pagination
const updatedApplications = applicationsData.map(app => { await fetchApplications();
if (selectedIds.includes(app.id)) {
return {
...app,
ddLeadShortlisted: true
} as any;
}
return app;
});
setApplicationsData(updatedApplications);
setSelectedIds([]); setSelectedIds([]);
setShowShortlistModal(false); setShowShortlistModal(false);
setShortlistRemark(''); setShortlistRemark('');

View File

@ -5,7 +5,7 @@ import { toast } from 'sonner';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { RootState } from '../../store'; import { RootState } from '../../store';
import { import {
User, RefreshCw, HelpCircle, ArrowLeft, Bike, User, RefreshCw, HelpCircle, ArrowLeft,
Users, FileText, ChevronRight, Users, FileText, ChevronRight,
CheckCircle CheckCircle
} from 'lucide-react'; } from 'lucide-react';
@ -188,19 +188,12 @@ const PublicQuestionnairePage: React.FC = () => {
<div className="max-w-5xl mx-auto py-8 px-6"> <div className="max-w-5xl mx-auto py-8 px-6">
{/* Hero Section */} {/* Hero Section */}
<div className="bg-gradient-to-br from-slate-900 via-slate-800 to-amber-900 rounded-t-lg overflow-hidden shadow-xl"> <div className="bg-re-black rounded-t-lg overflow-hidden shadow-xl">
<div className="relative px-8 py-12"> <div className="relative px-8 py-12">
<div className="absolute inset-0 opacity-10 pointer-events-none">
<div className="absolute top-0 right-0 w-64 h-64 bg-amber-500 rounded-full blur-3xl"></div>
<div className="absolute bottom-0 left-0 w-64 h-64 bg-amber-600 rounded-full blur-3xl"></div>
</div>
<div className="relative z-10 text-center"> <div className="relative z-10 text-center">
<div className="inline-flex items-center justify-center mb-6"> <div className="flex items-center justify-center mb-7">
<div className="w-20 h-20 bg-amber-600 rounded-full flex items-center justify-center shadow-xl"> <img src="/assets/images/Re_Logo.png" alt="Royal Enfield" className="h-12 w-auto" />
<Bike className="w-10 h-10 text-white" />
</div>
</div> </div>
<h1 className="text-white text-3xl mb-3 font-serif tracking-wide">ROYAL ENFIELD</h1>
<div className="h-1 w-24 bg-amber-600 mx-auto mb-4"></div> <div className="h-1 w-24 bg-amber-600 mx-auto mb-4"></div>
<h2 className="text-amber-400 text-xl mb-4 font-light">Dealership Partner Application</h2> <h2 className="text-amber-400 text-xl mb-4 font-light">Dealership Partner Application</h2>
<p className="text-slate-300 max-w-2xl mx-auto leading-relaxed text-sm"> <p className="text-slate-300 max-w-2xl mx-auto leading-relaxed text-sm">

View File

@ -162,5 +162,13 @@ export const masterService = {
saveSystemConfig: async (data: any) => { saveSystemConfig: async (data: any) => {
const response = await API.saveSystemConfig(data); const response = await API.saveSystemConfig(data);
return response.data; return response.data;
},
getDealerAsmMappings: async () => {
const response = await (API as any).getDealerAsmMappings();
return response.data;
},
saveDealerAsmMapping: async (data: { dealerId: string; asmUserId?: string | null }) => {
const response = await (API as any).saveDealerAsmMapping(data);
return response.data;
} }
}; };

View File

@ -148,6 +148,16 @@
} }
} }
@layer utilities {
button.bg-amber-600 {
background-color: var(--color-re-red) !important;
}
button.hover\:bg-amber-700:hover {
background-color: var(--color-re-red) !important;
}
}
/* RE Branding Utilities */ /* RE Branding Utilities */
.re-heading { .re-heading {
font-family: 'Montserrat', sans-serif; font-family: 'Montserrat', sans-serif;