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:
parent
5fbf06d827
commit
5170ab6c5a
@ -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}`),
|
||||||
|
|||||||
@ -146,7 +146,20 @@ const QuestionnaireForm: React.FC<QuestionnaireFormProps> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
|
<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>
|
<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">
|
||||||
|
|||||||
@ -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">
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
232
src/features/master/components/DealerAsmAssignment.tsx
Normal file
232
src/features/master/components/DealerAsmAssignment.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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 > 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 > 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 > 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>
|
||||||
|
|||||||
@ -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) => {
|
||||||
|
const metadataMatch =
|
||||||
p.metadata?.stageCode === stageCode ||
|
p.metadata?.stageCode === stageCode ||
|
||||||
p.metadata?.allAssignments?.includes(stageCode) ||
|
p.metadata?.allAssignments?.includes(stageCode) ||
|
||||||
(typeof stageCode === 'number' && (p.metadata?.interviewLevel === stageCode || p.metadata?.allAssignments?.includes(stageCode))) ||
|
(typeof stageCode === 'number' &&
|
||||||
(typeof stageCode === 'string' && !isNaN(Number(stageCode)) && (p.metadata?.interviewLevel === Number(stageCode) || p.metadata?.allAssignments?.includes(Number(stageCode))))
|
(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');
|
||||||
|
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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);
|
||||||
|
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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' },
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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}
|
||||||
|
|||||||
@ -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('');
|
||||||
|
|||||||
@ -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">
|
||||||
|
|||||||
@ -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;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user