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
|
||||
getSystemConfigs: (params?: any) => client.get('/master/system-configs', params),
|
||||
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
|
||||
getEorChecklistForApplication: (applicationId: string) => client.get(`/eor/application/${applicationId}`),
|
||||
|
||||
@ -146,7 +146,20 @@ const QuestionnaireForm: React.FC<QuestionnaireFormProps> = ({
|
||||
|
||||
return (
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{Object.entries(sections).map(([sectionName, sectionQuestions]) => (
|
||||
<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 */}
|
||||
{currentUser && (
|
||||
<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" />
|
||||
</div>
|
||||
<div className="text-left">
|
||||
|
||||
@ -43,12 +43,9 @@ export const ASMDialog: React.FC<ASMDialogProps> = ({
|
||||
const { zones, regionalOffices } = useSelector((state: RootState) => state.master);
|
||||
|
||||
const filteredASMUsers = userAssignedData.filter(u => {
|
||||
const roles = u.allRoles || [];
|
||||
return roles.some((r: string) => {
|
||||
const roleStr = (r || '').toUpperCase();
|
||||
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');
|
||||
});
|
||||
const roles = (u.allRoles || []).map((r: string) => String(r || '').toUpperCase());
|
||||
const roleCode = String(u.roleCode || '').toUpperCase();
|
||||
return roles.includes('DD-AM') || roleCode === 'DD-AM';
|
||||
});
|
||||
|
||||
// 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}>
|
||||
<DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{editingASMId ? 'Edit' : 'Add'} Area Sales Manager</DialogTitle>
|
||||
<DialogDescription>Configure ASM details and assignment</DialogDescription>
|
||||
<DialogTitle>{editingASMId ? 'Edit' : 'Add'} DD Area Manager</DialogTitle>
|
||||
<DialogDescription>Configure DD-AM details and district assignment</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
@ -116,24 +113,11 @@ export const ASMDialog: React.FC<ASMDialogProps> = ({
|
||||
</Select>
|
||||
</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>
|
||||
<Label>Select {asmRoleCode === 'ASM' ? 'ASM' : 'DD Area Manager'} User</Label>
|
||||
<Label>Select DD-AM User</Label>
|
||||
<Select value={asmManagerId} onValueChange={setAsmManagerId}>
|
||||
<SelectTrigger className="mt-2 text-slate-900">
|
||||
<SelectValue placeholder={`Select ${asmRoleCode === 'ASM' ? 'ASM' : 'DD Area Manager'}`} />
|
||||
<SelectValue placeholder="Select DD-AM" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="max-h-64">
|
||||
{filteredASMUsers.map(user => (
|
||||
@ -242,7 +226,7 @@ export const ASMDialog: React.FC<ASMDialogProps> = ({
|
||||
)}
|
||||
|
||||
<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
|
||||
value={asmManagerId}
|
||||
onValueChange={(value) => {
|
||||
@ -262,7 +246,7 @@ export const ASMDialog: React.FC<ASMDialogProps> = ({
|
||||
disabled={!!editingASMId}
|
||||
>
|
||||
<SelectTrigger className="mt-2 w-full text-slate-900">
|
||||
<SelectValue placeholder="Select ASM User" />
|
||||
<SelectValue placeholder="Select DD-AM User" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="max-h-60">
|
||||
{filteredASMUsers.length > 0 ? (
|
||||
@ -296,7 +280,7 @@ export const ASMDialog: React.FC<ASMDialogProps> = ({
|
||||
|
||||
<div className="flex gap-3 pt-4">
|
||||
<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>
|
||||
</DialogContent>
|
||||
|
||||
@ -38,12 +38,12 @@ export const ASMManagement: React.FC<ASMManagementProps> = ({
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<CardTitle>Area Sales Managers (ASM)</CardTitle>
|
||||
<CardDescription>Manage ASMs across all regions and zones</CardDescription>
|
||||
<CardTitle>District Development Area Managers (DD-AM)</CardTitle>
|
||||
<CardDescription>Manage DD-AM users across districts (multi-district)</CardDescription>
|
||||
</div>
|
||||
<Button onClick={onAddASM} className="bg-amber-600 hover:bg-amber-700">
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
Add ASM
|
||||
Add DD-AM
|
||||
</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
@ -51,7 +51,7 @@ export const ASMManagement: React.FC<ASMManagementProps> = ({
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>ASM Code</TableHead>
|
||||
<TableHead>DD-AM Code</TableHead>
|
||||
<TableHead>Name</TableHead>
|
||||
<TableHead>Zone</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 { LocationManagement } from '@/features/master/components/LocationManagement';
|
||||
import { ASMDialog } from '@/features/master/components/ASMDialog';
|
||||
import { DealerAsmAssignment } from '@/features/master/components/DealerAsmAssignment';
|
||||
import { ZMDialog } from '@/features/master/components/ZMDialog';
|
||||
import { ZoneDialog } from '@/features/master/components/ZoneDialog';
|
||||
import { RegionDialog } from '@/features/master/components/RegionDialog';
|
||||
@ -65,7 +66,7 @@ export const MasterPage: React.FC = () => {
|
||||
const [selectedASMRegion, setSelectedASMRegion] = useState('');
|
||||
const [selectedASMStates, setSelectedASMStates] = 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
|
||||
const [showZMDialog, setShowZMDialog] = useState(false);
|
||||
@ -156,7 +157,7 @@ export const MasterPage: React.FC = () => {
|
||||
// Handlers
|
||||
const handleSaveASM = async () => {
|
||||
if (!asmManagerId) {
|
||||
toast.error('Please select an ASM user');
|
||||
toast.error('Please select a DD-AM user');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
@ -168,7 +169,7 @@ export const MasterPage: React.FC = () => {
|
||||
};
|
||||
const res = await masterService.saveASM(payload) as any;
|
||||
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);
|
||||
fetchInitialData();
|
||||
} else {
|
||||
@ -188,7 +189,7 @@ export const MasterPage: React.FC = () => {
|
||||
setSelectedASMRegion(asm.regionId);
|
||||
setSelectedASMStates(asm.stateNames || []);
|
||||
setSelectedASMDistricts(asm.areasManaged?.map((a: any) => a.id) || []);
|
||||
setAsmRoleCode(asm.roleCode === 'DD-AM' ? 'DD-AM' : 'ASM');
|
||||
setAsmRoleCode('DD-AM');
|
||||
setShowASMDialog(true);
|
||||
};
|
||||
|
||||
@ -500,9 +501,11 @@ export const MasterPage: React.FC = () => {
|
||||
onEditZM={handleEditZM}
|
||||
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')} />
|
||||
|
||||
<DealerAsmAssignment />
|
||||
|
||||
|
||||
<UserManagementTable userAssignedData={users.length > 0 ? users : asms} />
|
||||
</TabsContent>
|
||||
|
||||
@ -30,6 +30,11 @@ interface ApplicationDetailsActionModalsProps {
|
||||
handleReject: () => void;
|
||||
showScheduleModal: boolean;
|
||||
setShowScheduleModal: (value: boolean) => void;
|
||||
showCancelInterviewModal: boolean;
|
||||
setShowCancelInterviewModal: (value: boolean) => void;
|
||||
setInterviewIdToCancel: (value: string) => void;
|
||||
isCancellingInterview: boolean;
|
||||
handleConfirmCancelInterview: () => void;
|
||||
interviewType: string;
|
||||
setInterviewType: (value: string) => void;
|
||||
interviewMode: string;
|
||||
@ -89,6 +94,11 @@ export function ApplicationDetailsActionModals(props: ApplicationDetailsActionMo
|
||||
handleReject,
|
||||
showScheduleModal,
|
||||
setShowScheduleModal,
|
||||
showCancelInterviewModal,
|
||||
setShowCancelInterviewModal,
|
||||
setInterviewIdToCancel,
|
||||
isCancellingInterview,
|
||||
handleConfirmCancelInterview,
|
||||
interviewType,
|
||||
setInterviewType,
|
||||
interviewMode,
|
||||
@ -125,6 +135,14 @@ export function ApplicationDetailsActionModals(props: ApplicationDetailsActionMo
|
||||
handleUpdateArchitectureStatus,
|
||||
} = props;
|
||||
|
||||
const participantRoleLabel = (participant: any) =>
|
||||
participant?.__stageRole ||
|
||||
participant?.role?.roleName ||
|
||||
participant?.role?.roleCode ||
|
||||
participant?.roleCode ||
|
||||
participant?.role ||
|
||||
'Panelist';
|
||||
|
||||
return (
|
||||
<>
|
||||
<Dialog open={showApproveModal} onOpenChange={setShowApproveModal}>
|
||||
@ -278,6 +296,7 @@ export function ApplicationDetailsActionModals(props: ApplicationDetailsActionMo
|
||||
{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}`}>
|
||||
<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>
|
||||
</div>
|
||||
))}
|
||||
@ -293,6 +312,46 @@ export function ApplicationDetailsActionModals(props: ApplicationDetailsActionMo
|
||||
</DialogContent>
|
||||
</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}>
|
||||
<DialogContent data-testid="onboarding-architecture-assign-modal">
|
||||
<DialogHeader>
|
||||
|
||||
@ -36,6 +36,8 @@ export function ApplicationDetailsExtendedModals(props: ApplicationDetailsExtend
|
||||
handleKTMatrixChange,
|
||||
ktMatrixRemarks,
|
||||
setKtMatrixRemarks,
|
||||
ktMatrixRecommendation,
|
||||
setKtMatrixRecommendation,
|
||||
calculateKTScore,
|
||||
handleSubmitKTMatrix,
|
||||
isSubmittingKT,
|
||||
@ -43,15 +45,20 @@ export function ApplicationDetailsExtendedModals(props: ApplicationDetailsExtend
|
||||
setShowLevel2FeedbackModal,
|
||||
level2Feedback,
|
||||
handleLevel2Change,
|
||||
level2Recommendation,
|
||||
setLevel2Recommendation,
|
||||
handleSubmitLevel2Feedback,
|
||||
isSubmittingLevel2,
|
||||
showFeedbackDetailsModal,
|
||||
setShowFeedbackDetailsModal,
|
||||
selectedEvaluationForView,
|
||||
selectedInterviewForFeedback,
|
||||
showLevel3FeedbackModal,
|
||||
setShowLevel3FeedbackModal,
|
||||
level3Feedback,
|
||||
handleLevel3Change,
|
||||
level3Recommendation,
|
||||
setLevel3Recommendation,
|
||||
handleSubmitLevel3Feedback,
|
||||
isSubmittingLevel3,
|
||||
showDocumentsModal,
|
||||
@ -95,6 +102,11 @@ export function ApplicationDetailsExtendedModals(props: ApplicationDetailsExtend
|
||||
handleUpdateFirmType,
|
||||
} = props;
|
||||
|
||||
const selectedInterviewDate = selectedInterviewForFeedback?.scheduleDate
|
||||
? new Date(selectedInterviewForFeedback.scheduleDate).toISOString().split('T')[0]
|
||||
: '';
|
||||
const interviewerDisplayName = currentUser?.fullName || currentUser?.name || '';
|
||||
|
||||
return (
|
||||
<>
|
||||
<Dialog open={showKTMatrixModal} onOpenChange={setShowKTMatrixModal}>
|
||||
@ -113,6 +125,11 @@ export function ApplicationDetailsExtendedModals(props: ApplicationDetailsExtend
|
||||
</DialogHeader>
|
||||
<div className="custom-scrollbar-slim min-h-0 flex-1 overflow-y-auto px-5 py-5">
|
||||
<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) => (
|
||||
<div key={criterion.name} className="space-y-2">
|
||||
<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"
|
||||
/>
|
||||
</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 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>
|
||||
<div className="flex gap-2 sm:shrink-0">
|
||||
<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>
|
||||
</DialogContent>
|
||||
@ -169,8 +199,8 @@ export function ApplicationDetailsExtendedModals(props: ApplicationDetailsExtend
|
||||
<DialogDescription>Provide detailed feedback from the Level 2 interview (DD Lead + ZBH evaluation).</DialogDescription>
|
||||
</DialogHeader>
|
||||
<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>Interviewer Name <span className="text-red-500">*</span></Label><Input placeholder="Enter your name" className="mt-2" value={level2Feedback.interviewerName} 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 || interviewerDisplayName} disabled /></div>
|
||||
<div>
|
||||
<Label>Overall Performance Score <span className="text-red-500">*</span></Label>
|
||||
<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>
|
||||
</Select>
|
||||
</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 />
|
||||
{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) => (
|
||||
<div key={field.itemKey || idx}>
|
||||
<Label>
|
||||
@ -209,7 +257,7 @@ export function ApplicationDetailsExtendedModals(props: ApplicationDetailsExtend
|
||||
))}
|
||||
<div className="flex gap-3">
|
||||
<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>
|
||||
</DialogContent>
|
||||
@ -254,8 +302,8 @@ export function ApplicationDetailsExtendedModals(props: ApplicationDetailsExtend
|
||||
<DialogDescription>Provide detailed feedback from the Level 3 interview (NBH + DD-Head evaluation).</DialogDescription>
|
||||
</DialogHeader>
|
||||
<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>Interviewer Name <span className="text-red-500">*</span></Label><Input placeholder="Enter your name" className="mt-2" value={level3Feedback.interviewerName} 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 || interviewerDisplayName} disabled /></div>
|
||||
<div>
|
||||
<Label>Overall Performance Score <span className="text-red-500">*</span></Label>
|
||||
<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>
|
||||
</Select>
|
||||
</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 />
|
||||
{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) => (
|
||||
<div key={field.itemKey || idx}>
|
||||
<Label>
|
||||
@ -294,7 +360,7 @@ export function ApplicationDetailsExtendedModals(props: ApplicationDetailsExtend
|
||||
))}
|
||||
<div className="flex gap-3">
|
||||
<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>
|
||||
</DialogContent>
|
||||
|
||||
@ -105,6 +105,22 @@ export function ApplicationDetailsTabs(props: ApplicationDetailsTabsProps) {
|
||||
auditLogActionBadgeClass,
|
||||
} = 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 (
|
||||
<Card data-testid="onboarding-details-tabs-container">
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab}>
|
||||
@ -139,13 +155,38 @@ export function ApplicationDetailsTabs(props: ApplicationDetailsTabsProps) {
|
||||
|
||||
<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 stageParticipants = (application.participants || []).filter((p: any) =>
|
||||
const stageParticipants = (application.participants || []).filter((p: any) => {
|
||||
const metadataMatch =
|
||||
p.metadata?.stageCode === stageCode ||
|
||||
p.metadata?.allAssignments?.includes(stageCode) ||
|
||||
(typeof stageCode === 'number' && (p.metadata?.interviewLevel === stageCode || p.metadata?.allAssignments?.includes(stageCode))) ||
|
||||
(typeof stageCode === 'string' && !isNaN(Number(stageCode)) && (p.metadata?.interviewLevel === Number(stageCode) || p.metadata?.allAssignments?.includes(Number(stageCode))))
|
||||
);
|
||||
(typeof stageCode === 'number' &&
|
||||
(p.metadata?.interviewLevel === stageCode ||
|
||||
p.metadata?.interviewLevel === String(stageCode) ||
|
||||
p.metadata?.allAssignments?.includes(stageCode) ||
|
||||
p.metadata?.allAssignments?.includes(String(stageCode)))) ||
|
||||
(typeof stageCode === 'string' &&
|
||||
!isNaN(Number(stageCode)) &&
|
||||
(p.metadata?.interviewLevel === Number(stageCode) ||
|
||||
p.metadata?.allAssignments?.includes(Number(stageCode))));
|
||||
|
||||
if (metadataMatch) return true;
|
||||
if (typeof stageCode === 'number') {
|
||||
return participantHasAnyRole(p, interviewRoleMap[stageCode] || []);
|
||||
}
|
||||
return participantHasAnyRole(p, stageRoleMap[stageCode] || []);
|
||||
});
|
||||
|
||||
return stageParticipants.map((p: any) => {
|
||||
const saCode = typeof stageCode === 'number' ? `INTERVIEW_LEVEL_${stageCode}` : stageCode;
|
||||
@ -155,8 +196,8 @@ export function ApplicationDetailsTabs(props: ApplicationDetailsTabsProps) {
|
||||
);
|
||||
|
||||
return {
|
||||
name: p.user?.name || 'Unknown',
|
||||
role: p.user?.role || 'Reviewer',
|
||||
name: p.user?.name || p.user?.fullName || 'Unknown',
|
||||
role: p.user?.role || p.user?.roleCode || p.metadata?.role || 'Reviewer',
|
||||
status: approval ? (approval.decision === 'Approved' ? 'approved' : 'rejected') : 'pending'
|
||||
};
|
||||
});
|
||||
@ -278,11 +319,16 @@ export function ApplicationDetailsTabs(props: ApplicationDetailsTabsProps) {
|
||||
const stageId = Number(stage.id);
|
||||
const expectedCount = expectedMap[stageId];
|
||||
|
||||
let actualCount = stage.evaluators?.length || 0;
|
||||
if (stageId === 3) {
|
||||
const l1Evaluators = (application.participants || []).filter((p: any) => p.metadata?.interviewLevel === 1 || p.metadata?.interviewLevel === '1');
|
||||
actualCount = l1Evaluators.length;
|
||||
}
|
||||
const stageCodeById: Record<number, string | number> = {
|
||||
3: 1, // shortlist depends on L1 evaluators
|
||||
4: 1,
|
||||
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');
|
||||
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { Dispatch, SetStateAction } from 'react';
|
||||
import { Dispatch, SetStateAction, useCallback } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
import { onboardingService } from '@/services/onboarding.service';
|
||||
|
||||
@ -42,6 +42,10 @@ interface UseApplicationDetailsAdminActionsParams {
|
||||
setLoading: Dispatch<SetStateAction<boolean>>;
|
||||
setIsScheduling: 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>>;
|
||||
setShowUploadForm: Dispatch<SetStateAction<boolean>>;
|
||||
setUploadFile: Dispatch<SetStateAction<File | null>>;
|
||||
@ -100,6 +104,10 @@ export function useApplicationDetailsAdminActions(params: UseApplicationDetailsA
|
||||
setLoading,
|
||||
setIsScheduling,
|
||||
setShowScheduleModal,
|
||||
setShowCancelInterviewModal,
|
||||
interviewIdToCancel,
|
||||
setInterviewIdToCancel,
|
||||
setIsCancellingInterview,
|
||||
setIsUploading,
|
||||
setShowUploadForm,
|
||||
setUploadFile,
|
||||
@ -131,7 +139,7 @@ export function useApplicationDetailsAdminActions(params: UseApplicationDetailsA
|
||||
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;
|
||||
try {
|
||||
const reqParams: any = {};
|
||||
@ -141,34 +149,77 @@ export function useApplicationDetailsAdminActions(params: UseApplicationDetailsA
|
||||
level2: ['DD Lead', 'ZBH'],
|
||||
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) {
|
||||
reqParams.locationId = application.districtId || application.areaId || application.regionId || application.zoneId;
|
||||
}
|
||||
}
|
||||
reqParams.isExternal = false;
|
||||
const response = await onboardingService.getUsers(reqParams);
|
||||
if (Array.isArray(response)) setUsers(response);
|
||||
else if (response && Array.isArray(response.data)) setUsers(response.data);
|
||||
else if (response && Array.isArray(response.users)) setUsers(response.users);
|
||||
else setUsers([]);
|
||||
const rawUsers = Array.isArray(response)
|
||||
? response
|
||||
: response && Array.isArray(response.data)
|
||||
? 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 {
|
||||
setUsers([]);
|
||||
}
|
||||
};
|
||||
}, [currentUser, application, setUsers]);
|
||||
|
||||
const prefillInterviewParticipants = () => {
|
||||
const prefillInterviewParticipants = useCallback(() => {
|
||||
if (!showScheduleModal || !application) return;
|
||||
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 || [])
|
||||
.filter((p: any) =>
|
||||
p.metadata?.interviewLevel === levelNum ||
|
||||
p.metadata?.interviewLevel === String(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)
|
||||
.filter(Boolean);
|
||||
.map((p: any) => {
|
||||
const user = p.user || {};
|
||||
return {
|
||||
...user,
|
||||
__stageRole: deriveDisplayRole(p, user),
|
||||
};
|
||||
})
|
||||
.filter((u: any) => !!u?.id);
|
||||
if (preAssigned.length === 0) {
|
||||
setScheduledInterviewParticipants([]);
|
||||
return;
|
||||
@ -182,7 +233,7 @@ export function useApplicationDetailsAdminActions(params: UseApplicationDetailsA
|
||||
}
|
||||
});
|
||||
setScheduledInterviewParticipants(unique);
|
||||
};
|
||||
}, [showScheduleModal, application, interviewType, setScheduledInterviewParticipants]);
|
||||
|
||||
const handleScheduleInterview = async () => {
|
||||
if (!interviewDate) {
|
||||
@ -211,13 +262,23 @@ export function useApplicationDetailsAdminActions(params: UseApplicationDetailsA
|
||||
};
|
||||
|
||||
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 {
|
||||
await onboardingService.updateInterview(interviewId, { status: 'Cancelled' });
|
||||
setIsCancellingInterview(true);
|
||||
await onboardingService.updateInterview(interviewIdToCancel, { status: 'Cancelled' });
|
||||
toast.success('Interview cancelled successfully');
|
||||
setShowCancelInterviewModal(false);
|
||||
setInterviewIdToCancel('');
|
||||
await fetchInterviews();
|
||||
} catch {
|
||||
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) {
|
||||
await fetchUsers(interviewType);
|
||||
prefillInterviewParticipants();
|
||||
@ -529,7 +590,15 @@ export function useApplicationDetailsAdminActions(params: UseApplicationDetailsA
|
||||
if ((showAssignArchitectureModal || showAssignModal) && application) {
|
||||
await fetchUsers();
|
||||
}
|
||||
};
|
||||
}, [
|
||||
showScheduleModal,
|
||||
showAssignArchitectureModal,
|
||||
showAssignModal,
|
||||
application,
|
||||
interviewType,
|
||||
fetchUsers,
|
||||
prefillInterviewParticipants,
|
||||
]);
|
||||
|
||||
return {
|
||||
handleAddInterviewer,
|
||||
@ -538,6 +607,7 @@ export function useApplicationDetailsAdminActions(params: UseApplicationDetailsA
|
||||
maybeFetchUsersForModal,
|
||||
handleScheduleInterview,
|
||||
handleCancelInterview,
|
||||
handleConfirmCancelInterview,
|
||||
handleUpload,
|
||||
handleApprove,
|
||||
handleReject,
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { Application, ApplicationStatus } from '@/lib/mock-data';
|
||||
import { onboardingService } from '@/services/onboarding.service';
|
||||
import { eorService } from '@/services/eor.service';
|
||||
@ -20,16 +20,16 @@ export function useApplicationDetailsData({ applicationId }: UseApplicationDetai
|
||||
const [deposits, setDeposits] = useState<any[]>([]);
|
||||
const [paymentConfigs, setPaymentConfigs] = useState<any>({});
|
||||
|
||||
const refreshDocuments = async () => {
|
||||
const refreshDocuments = useCallback(async () => {
|
||||
try {
|
||||
const docs = await onboardingService.getDocuments(applicationId);
|
||||
setDocuments(docs || []);
|
||||
} catch (error) {
|
||||
console.error('Failed to refresh documents:', error);
|
||||
}
|
||||
};
|
||||
}, [applicationId]);
|
||||
|
||||
const fetchApplication = async (silent = false) => {
|
||||
const fetchApplication = useCallback(async (silent = false) => {
|
||||
try {
|
||||
if (!silent) setLoading(true);
|
||||
const data = await onboardingService.getApplicationById(applicationId);
|
||||
@ -123,9 +123,9 @@ export function useApplicationDetailsData({ applicationId }: UseApplicationDetai
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
}, [applicationId]);
|
||||
|
||||
const fetchEorData = async () => {
|
||||
const fetchEorData = useCallback(async () => {
|
||||
if (!applicationId) return;
|
||||
try {
|
||||
const resp = await eorService.getChecklist(applicationId);
|
||||
@ -133,7 +133,7 @@ export function useApplicationDetailsData({ applicationId }: UseApplicationDetai
|
||||
} catch {
|
||||
setEorData(null);
|
||||
}
|
||||
};
|
||||
}, [applicationId]);
|
||||
|
||||
const getDeposit = (type: string) => deposits.find((d) => d.depositType === type);
|
||||
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
import { toast } from 'sonner';
|
||||
import { Dispatch, SetStateAction } from 'react';
|
||||
import { onboardingService } from '@/services/onboarding.service';
|
||||
import { KT_MATRIX_CRITERIA } from '@/features/onboarding/components/application-details/applicationDetails.shared';
|
||||
import type { InterviewConfig } from './useInterviewConfigs';
|
||||
|
||||
interface UseApplicationDetailsFeedbackActionsParams {
|
||||
@ -10,16 +9,22 @@ interface UseApplicationDetailsFeedbackActionsParams {
|
||||
setKtMatrixSelectedValues: Dispatch<SetStateAction<Record<string, string>>>;
|
||||
ktMatrixRemarks: string;
|
||||
setKtMatrixRemarks: Dispatch<SetStateAction<string>>;
|
||||
ktMatrixRecommendation: string;
|
||||
setKtMatrixRecommendation: Dispatch<SetStateAction<string>>;
|
||||
selectedInterviewForFeedback: any;
|
||||
interviews: any[];
|
||||
setIsSubmittingKT: Dispatch<SetStateAction<boolean>>;
|
||||
setShowKTMatrixModal: Dispatch<SetStateAction<boolean>>;
|
||||
level2Feedback: any;
|
||||
setLevel2Feedback: Dispatch<SetStateAction<any>>;
|
||||
level2Recommendation: string;
|
||||
setLevel2Recommendation: Dispatch<SetStateAction<string>>;
|
||||
setIsSubmittingLevel2: Dispatch<SetStateAction<boolean>>;
|
||||
setShowLevel2FeedbackModal: Dispatch<SetStateAction<boolean>>;
|
||||
level3Feedback: any;
|
||||
setLevel3Feedback: Dispatch<SetStateAction<any>>;
|
||||
level3Recommendation: string;
|
||||
setLevel3Recommendation: Dispatch<SetStateAction<string>>;
|
||||
setIsSubmittingLevel3: Dispatch<SetStateAction<boolean>>;
|
||||
setShowLevel3FeedbackModal: Dispatch<SetStateAction<boolean>>;
|
||||
currentUser: any;
|
||||
@ -64,16 +69,22 @@ export function useApplicationDetailsFeedbackActions({
|
||||
setKtMatrixSelectedValues,
|
||||
ktMatrixRemarks,
|
||||
setKtMatrixRemarks,
|
||||
ktMatrixRecommendation,
|
||||
setKtMatrixRecommendation,
|
||||
selectedInterviewForFeedback,
|
||||
interviews,
|
||||
setIsSubmittingKT,
|
||||
setShowKTMatrixModal,
|
||||
level2Feedback,
|
||||
setLevel2Feedback,
|
||||
level2Recommendation,
|
||||
setLevel2Recommendation,
|
||||
setIsSubmittingLevel2,
|
||||
setShowLevel2FeedbackModal,
|
||||
level3Feedback,
|
||||
setLevel3Feedback,
|
||||
level3Recommendation,
|
||||
setLevel3Recommendation,
|
||||
setIsSubmittingLevel3,
|
||||
setShowLevel3FeedbackModal,
|
||||
currentUser,
|
||||
@ -83,6 +94,17 @@ export function useApplicationDetailsFeedbackActions({
|
||||
level2Config,
|
||||
level3Config,
|
||||
}: 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
|
||||
const getKtCriteria = () => {
|
||||
if (ktMatrixConfig?.items && ktMatrixConfig.items.length > 0) {
|
||||
@ -97,37 +119,21 @@ export function useApplicationDetailsFeedbackActions({
|
||||
})),
|
||||
}));
|
||||
}
|
||||
return KT_MATRIX_CRITERIA;
|
||||
return [];
|
||||
};
|
||||
|
||||
const getLevel2Fields = () => {
|
||||
if (level2Config?.items && level2Config.items.length > 0) {
|
||||
return level2Config.items;
|
||||
}
|
||||
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 },
|
||||
];
|
||||
return [];
|
||||
};
|
||||
|
||||
const getLevel3Fields = () => {
|
||||
if (level3Config?.items && level3Config.items.length > 0) {
|
||||
return level3Config.items;
|
||||
}
|
||||
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 },
|
||||
];
|
||||
return [];
|
||||
};
|
||||
|
||||
const ktCriteria = getKtCriteria();
|
||||
@ -151,6 +157,10 @@ export function useApplicationDetailsFeedbackActions({
|
||||
};
|
||||
|
||||
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) {
|
||||
toast.warning('Please fill all fields in the KT Matrix');
|
||||
return;
|
||||
@ -168,12 +178,32 @@ export function useApplicationDetailsFeedbackActions({
|
||||
maxScore: c.maxScore || 10,
|
||||
weightage: c.weight || 0,
|
||||
}));
|
||||
await onboardingService.submitKTMatrix({ interviewId, criteriaScores, feedback: ktMatrixRemarks, recommendation: null });
|
||||
toast.success('KT Matrix submitted successfully');
|
||||
await onboardingService.submitKTMatrix({
|
||||
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);
|
||||
setKtMatrixScores({});
|
||||
setKtMatrixSelectedValues({});
|
||||
setKtMatrixRemarks('');
|
||||
setKtMatrixRecommendation('Approve');
|
||||
await fetchInterviews();
|
||||
await fetchApplication();
|
||||
} catch {
|
||||
@ -188,6 +218,10 @@ export function useApplicationDetailsFeedbackActions({
|
||||
};
|
||||
|
||||
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) {
|
||||
toast.warning('Please provide an overall score.');
|
||||
return;
|
||||
@ -202,10 +236,27 @@ export function useApplicationDetailsFeedbackActions({
|
||||
const feedbackItems = l2Fields
|
||||
.map((f: any) => ({ type: f.label, comments: level2Feedback[f.itemKey] || '' }))
|
||||
.filter((item) => item.comments.trim() !== '');
|
||||
await onboardingService.submitLevel2Feedback({ interviewId, overallScore: Number(level2Feedback.overallScore), feedbackItems });
|
||||
toast.success('Level 2 Feedback submitted successfully');
|
||||
await onboardingService.submitLevel2Feedback({
|
||||
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);
|
||||
setLevel2Feedback(createInitialLevel2Feedback(currentUser));
|
||||
setLevel2Recommendation('Approve');
|
||||
await fetchInterviews();
|
||||
await fetchApplication();
|
||||
} catch {
|
||||
@ -220,6 +271,10 @@ export function useApplicationDetailsFeedbackActions({
|
||||
};
|
||||
|
||||
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) {
|
||||
toast.warning('Please provide an overall score.');
|
||||
return;
|
||||
@ -234,10 +289,30 @@ export function useApplicationDetailsFeedbackActions({
|
||||
const feedbackItems = l3Fields
|
||||
.map((f: any) => ({ type: f.label, comments: level3Feedback[f.itemKey] || '' }))
|
||||
.filter((item) => item.comments.trim() !== '');
|
||||
await onboardingService.submitLevel2Feedback({ interviewId, overallScore: Number(level3Feedback.overallScore), feedbackItems });
|
||||
toast.success('Level 3 Feedback submitted successfully');
|
||||
await onboardingService.submitLevel2Feedback({
|
||||
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);
|
||||
setLevel3Feedback(createInitialLevel3Feedback(currentUser));
|
||||
setLevel3Recommendation('Approve');
|
||||
await fetchInterviews();
|
||||
await fetchApplication();
|
||||
} catch {
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { Dispatch, SetStateAction } from 'react';
|
||||
import { Dispatch, SetStateAction, useCallback } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
import { onboardingService } from '@/services/onboarding.service';
|
||||
|
||||
@ -75,14 +75,14 @@ export function useApplicationDetailsLocalActions({
|
||||
}
|
||||
};
|
||||
|
||||
const fetchFddAgencies = async () => {
|
||||
const fetchFddAgencies = useCallback(async () => {
|
||||
try {
|
||||
const agencies = await onboardingService.getUsers({ roleCode: 'FDD' });
|
||||
setFddAgencies(Array.isArray(agencies) ? agencies : []);
|
||||
} catch {
|
||||
setFddAgencies([]);
|
||||
}
|
||||
};
|
||||
}, [setFddAgencies]);
|
||||
|
||||
const handleAssignAgency = async () => {
|
||||
if (!selectedAgencyId) {
|
||||
|
||||
@ -80,10 +80,6 @@ export function useApplicationDetailsPermissions({
|
||||
getDeposit('SECURITY_DEPOSIT')?.status !== 'Verified';
|
||||
const isFinalState = application.status === 'Onboarded' || application.status === 'Rejected';
|
||||
|
||||
const hasFeedbackForActive = !!(activeInterviewForUser || lastInterviewForUser)?.evaluations?.find(
|
||||
(e: any) => e.evaluatorId === currentUser?.id
|
||||
);
|
||||
|
||||
const ddHeadApproved = application.stageApprovals?.some((a: any) => a.stageCode === 'LOI_APPROVAL' && a.actorRole === 'DD Head' && a.decision === 'Approved');
|
||||
const ddHeadLoaApproved = application.stageApprovals?.some((a: any) => a.stageCode === 'LOA_APPROVAL' && a.actorRole === 'DD Head' && a.decision === 'Approved');
|
||||
|
||||
@ -105,11 +101,10 @@ export function useApplicationDetailsPermissions({
|
||||
const canApproveReject =
|
||||
!isFinalState &&
|
||||
!isDecisionMade &&
|
||||
((!!activeInterviewForUser && !!hasFeedbackForActive) ||
|
||||
(isAdminRole &&
|
||||
isAdministrativeStage &&
|
||||
sequenceMet &&
|
||||
(!['EOR In Progress', 'Inauguration', 'Approved'].includes(application.status) || eorProgress === 100)));
|
||||
(!['EOR In Progress', 'Inauguration', 'Approved'].includes(application.status) || eorProgress === 100));
|
||||
|
||||
return {
|
||||
canApprove: canApproveReject && !isLoaLocked && !isSecurityDetailsLocked,
|
||||
|
||||
@ -15,6 +15,25 @@ export function useApplicationDetailsStageData({
|
||||
eorData,
|
||||
getDeposit,
|
||||
}: 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) => {
|
||||
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'),
|
||||
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
|
||||
},
|
||||
{
|
||||
id: 5, name: '2nd Level Interview', status: getStageStatus('2nd Level Interview', () => ['Level 2 Approved', 'Level 2 Recommended', 'Level 3 Interview Pending', 'Level 3 Approved', 'FDD Verification', 'LOI In Progress', 'Payment Pending', 'LOI Issued', 'Statutory LOI Ack', 'Dealer Code Generation', 'Architecture Work', 'Statutory Work', 'LOA Pending', 'LOA Issued', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Onboarded'].includes(application.status) ? 'completed' : (application.status === 'Level 2 Interview Pending' && isInterviewScheduled(2)) ? 'active' : 'pending'),
|
||||
date: application.level2InterviewDate, description: 'DD Lead + ZBH evaluation',
|
||||
evaluators: Array.from(new Set((application.participants || []).filter((p: any) => p.metadata?.interviewLevel === 2 || p.metadata?.interviewLevel === '2' || p.metadata?.allAssignments?.includes(2)).map((p: any) => `${p.user?.name} (${p.user?.role})`))),
|
||||
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
|
||||
},
|
||||
{
|
||||
id: 6, name: '3rd Level Interview', status: getStageStatus('3rd Level Interview', () => ['Level 3 Approved', 'FDD Verification', 'LOI In Progress', 'Payment Pending', 'LOI Issued', 'Statutory LOI Ack', 'Dealer Code Generation', 'Architecture Work', 'Statutory Work', 'LOA Pending', 'LOA Issued', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Onboarded'].includes(application.status) ? 'completed' : (application.status === 'Level 3 Interview Pending' && isInterviewScheduled(3)) ? 'active' : 'pending'),
|
||||
date: application.level3InterviewDate, description: 'NBH + DD Head evaluation',
|
||||
evaluators: Array.from(new Set((application.participants || []).filter((p: any) => p.metadata?.interviewLevel === 3 || p.metadata?.interviewLevel === '3' || p.metadata?.allAssignments?.includes(3)).map((p: any) => `${p.user?.name} (${p.user?.role})`))),
|
||||
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
|
||||
},
|
||||
{ id: 7, name: 'FDD', status: getStageStatus('FDD', () => ['LOI In Progress', 'Payment Pending', 'LOI Issued', 'Statutory LOI Ack', 'Dealer Code Generation', 'Architecture Work', 'Statutory Work', 'LOA Pending', 'LOA Issued', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Onboarded'].includes(application.status) ? 'completed' : application.status === 'FDD Verification' ? 'active' : 'pending'), date: application.fddDate, description: 'Financial Due Diligence', documentsUploaded: 5 },
|
||||
{
|
||||
id: 8, name: 'LOI Approval', status: getStageStatus('LOI Approval', () => ['Security Details', 'Payment Pending', 'LOI Issued', 'Statutory LOI Ack', 'Dealer Code Generation', 'Architecture Work', 'Statutory Work', 'LOA Pending', 'LOA Issued', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Onboarded'].includes(application.status) ? 'completed' : application.status === 'LOI In Progress' ? 'active' : 'pending'),
|
||||
date: application.loiApprovalDate, description: 'Letter of Intent approval',
|
||||
evaluators: Array.from(new Set((application.participants || []).filter((p: any) => p.metadata?.stageCode === 'LOI_APPROVAL' || p.metadata?.allAssignments?.includes('LOI_APPROVAL')).map((p: any) => `${p.user?.name} (${p.user?.role})`))),
|
||||
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
|
||||
},
|
||||
{
|
||||
@ -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'),
|
||||
isLocked: application.status === 'LOA Pending' && getDeposit('FIRST_FILL')?.status !== 'Verified',
|
||||
lockMessage: 'First Fill (₹15L) must be verified by Finance before LOA Approval.',
|
||||
evaluators: Array.from(new Set((application.participants || []).filter((p: any) => p.metadata?.stageCode === 'LOA_APPROVAL' || p.metadata?.allAssignments?.includes('LOA_APPROVAL')).map((p: any) => `${p.user?.name} (${p.user?.role})`))),
|
||||
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'
|
||||
},
|
||||
{ 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 [scheduledInterviewParticipants, setScheduledInterviewParticipants] = useState<any[]>([]);
|
||||
const [showScheduleModal, setShowScheduleModal] = useState(false);
|
||||
const [showCancelInterviewModal, setShowCancelInterviewModal] = useState(false);
|
||||
const [interviewIdToCancel, setInterviewIdToCancel] = useState('');
|
||||
const [showKTMatrixModal, setShowKTMatrixModal] = useState(false);
|
||||
const [showLevel2FeedbackModal, setShowLevel2FeedbackModal] = useState(false);
|
||||
const [showLevel3FeedbackModal, setShowLevel3FeedbackModal] = useState(false);
|
||||
const [showDocumentsModal, setShowDocumentsModal] = useState(false);
|
||||
const [showAssignModal, setShowAssignModal] = useState(false);
|
||||
const [selectedStage, setSelectedStage] = useState<string | null>(null);
|
||||
const [interviewMode, setInterviewMode] = useState('physical');
|
||||
const [interviewMode, setInterviewMode] = useState('virtual');
|
||||
const [approvalRemark, setApprovalRemark] = useState('');
|
||||
const [expandedBranches, setExpandedBranches] = useState<Record<string, boolean>>({});
|
||||
const [users, setUsers] = useState<any[]>([]);
|
||||
@ -46,6 +48,7 @@ export function useApplicationDetailsUIState({ initialTab = 'questionnaire' }: U
|
||||
const [isSavingStatutory, setIsSavingStatutory] = useState(false);
|
||||
const [interviews, setInterviews] = useState<any[]>([]);
|
||||
const [isScheduling, setIsScheduling] = useState(false);
|
||||
const [isCancellingInterview, setIsCancellingInterview] = useState(false);
|
||||
const [showAssignArchitectureModal, setShowAssignArchitectureModal] = useState(false);
|
||||
const [architectureLeadId, setArchitectureLeadId] = useState('');
|
||||
const [isAssigningArchitecture, setIsAssigningArchitecture] = useState(false);
|
||||
@ -63,6 +66,7 @@ export function useApplicationDetailsUIState({ initialTab = 'questionnaire' }: U
|
||||
const [ktMatrixScores, setKtMatrixScores] = useState<Record<string, number>>({});
|
||||
const [ktMatrixSelectedValues, setKtMatrixSelectedValues] = useState<Record<string, string>>({});
|
||||
const [ktMatrixRemarks, setKtMatrixRemarks] = useState<string>('');
|
||||
const [ktMatrixRecommendation, setKtMatrixRecommendation] = useState<string>('Approve');
|
||||
const [isSubmittingKT, setIsSubmittingKT] = useState(false);
|
||||
const [selectedInterviewForFeedback, setSelectedInterviewForFeedback] = useState<any>(null);
|
||||
const [showFddFinalizeModal, setShowFddFinalizeModal] = useState(false);
|
||||
@ -72,8 +76,10 @@ export function useApplicationDetailsUIState({ initialTab = 'questionnaire' }: U
|
||||
const [isFinalizingFdd, setIsFinalizingFdd] = useState(false);
|
||||
const [isFddFlagging, setIsFddFlagging] = useState(false);
|
||||
const [level2Feedback, setLevel2Feedback] = useState<any>({});
|
||||
const [level2Recommendation, setLevel2Recommendation] = useState<string>('Approve');
|
||||
const [isSubmittingLevel2, setIsSubmittingLevel2] = useState(false);
|
||||
const [level3Feedback, setLevel3Feedback] = useState<any>({});
|
||||
const [level3Recommendation, setLevel3Recommendation] = useState<string>('Approve');
|
||||
const [isSubmittingLevel3, setIsSubmittingLevel3] = useState(false);
|
||||
const [selectedEvaluationForView, setSelectedEvaluationForView] = useState<any>(null);
|
||||
const [showFeedbackDetailsModal, setShowFeedbackDetailsModal] = useState(false);
|
||||
@ -90,6 +96,8 @@ export function useApplicationDetailsUIState({ initialTab = 'questionnaire' }: U
|
||||
rejectionReason, setRejectionReason,
|
||||
scheduledInterviewParticipants, setScheduledInterviewParticipants,
|
||||
showScheduleModal, setShowScheduleModal,
|
||||
showCancelInterviewModal, setShowCancelInterviewModal,
|
||||
interviewIdToCancel, setInterviewIdToCancel,
|
||||
showKTMatrixModal, setShowKTMatrixModal,
|
||||
showLevel2FeedbackModal, setShowLevel2FeedbackModal,
|
||||
showLevel3FeedbackModal, setShowLevel3FeedbackModal,
|
||||
@ -119,6 +127,7 @@ export function useApplicationDetailsUIState({ initialTab = 'questionnaire' }: U
|
||||
isSavingStatutory, setIsSavingStatutory,
|
||||
interviews, setInterviews,
|
||||
isScheduling, setIsScheduling,
|
||||
isCancellingInterview, setIsCancellingInterview,
|
||||
showAssignArchitectureModal, setShowAssignArchitectureModal,
|
||||
architectureLeadId, setArchitectureLeadId,
|
||||
isAssigningArchitecture, setIsAssigningArchitecture,
|
||||
@ -136,6 +145,7 @@ export function useApplicationDetailsUIState({ initialTab = 'questionnaire' }: U
|
||||
ktMatrixScores, setKtMatrixScores,
|
||||
ktMatrixSelectedValues, setKtMatrixSelectedValues,
|
||||
ktMatrixRemarks, setKtMatrixRemarks,
|
||||
ktMatrixRecommendation, setKtMatrixRecommendation,
|
||||
isSubmittingKT, setIsSubmittingKT,
|
||||
selectedInterviewForFeedback, setSelectedInterviewForFeedback,
|
||||
showFddFinalizeModal, setShowFddFinalizeModal,
|
||||
@ -145,8 +155,10 @@ export function useApplicationDetailsUIState({ initialTab = 'questionnaire' }: U
|
||||
isFinalizingFdd, setIsFinalizingFdd,
|
||||
isFddFlagging, setIsFddFlagging,
|
||||
level2Feedback, setLevel2Feedback,
|
||||
level2Recommendation, setLevel2Recommendation,
|
||||
isSubmittingLevel2, setIsSubmittingLevel2,
|
||||
level3Feedback, setLevel3Feedback,
|
||||
level3Recommendation, setLevel3Recommendation,
|
||||
isSubmittingLevel3, setIsSubmittingLevel3,
|
||||
selectedEvaluationForView, setSelectedEvaluationForView,
|
||||
showFeedbackDetailsModal, setShowFeedbackDetailsModal,
|
||||
|
||||
@ -60,6 +60,8 @@ export const ApplicationDetails = () => {
|
||||
rejectionReason, setRejectionReason,
|
||||
scheduledInterviewParticipants, setScheduledInterviewParticipants,
|
||||
showScheduleModal, setShowScheduleModal,
|
||||
showCancelInterviewModal, setShowCancelInterviewModal,
|
||||
interviewIdToCancel, setInterviewIdToCancel,
|
||||
showKTMatrixModal, setShowKTMatrixModal,
|
||||
showLevel2FeedbackModal, setShowLevel2FeedbackModal,
|
||||
showLevel3FeedbackModal, setShowLevel3FeedbackModal,
|
||||
@ -89,6 +91,7 @@ export const ApplicationDetails = () => {
|
||||
isSavingStatutory, setIsSavingStatutory,
|
||||
interviews, setInterviews,
|
||||
isScheduling, setIsScheduling,
|
||||
isCancellingInterview, setIsCancellingInterview,
|
||||
showAssignArchitectureModal, setShowAssignArchitectureModal,
|
||||
architectureLeadId, setArchitectureLeadId,
|
||||
isAssigningArchitecture, setIsAssigningArchitecture,
|
||||
@ -106,6 +109,7 @@ export const ApplicationDetails = () => {
|
||||
ktMatrixScores, setKtMatrixScores,
|
||||
ktMatrixSelectedValues, setKtMatrixSelectedValues,
|
||||
ktMatrixRemarks, setKtMatrixRemarks,
|
||||
ktMatrixRecommendation, setKtMatrixRecommendation,
|
||||
isSubmittingKT, setIsSubmittingKT,
|
||||
selectedInterviewForFeedback, setSelectedInterviewForFeedback,
|
||||
showFddFinalizeModal, setShowFddFinalizeModal,
|
||||
@ -115,8 +119,10 @@ export const ApplicationDetails = () => {
|
||||
isFinalizingFdd, setIsFinalizingFdd,
|
||||
isFddFlagging, setIsFddFlagging,
|
||||
level2Feedback, setLevel2Feedback,
|
||||
level2Recommendation, setLevel2Recommendation,
|
||||
isSubmittingLevel2, setIsSubmittingLevel2,
|
||||
level3Feedback, setLevel3Feedback,
|
||||
level3Recommendation, setLevel3Recommendation,
|
||||
isSubmittingLevel3, setIsSubmittingLevel3,
|
||||
showFeedbackDetailsModal, setShowFeedbackDetailsModal,
|
||||
selectedEvaluationForView, setSelectedEvaluationForView,
|
||||
@ -220,16 +226,22 @@ export const ApplicationDetails = () => {
|
||||
setKtMatrixSelectedValues,
|
||||
ktMatrixRemarks,
|
||||
setKtMatrixRemarks,
|
||||
ktMatrixRecommendation,
|
||||
setKtMatrixRecommendation,
|
||||
selectedInterviewForFeedback,
|
||||
interviews,
|
||||
setIsSubmittingKT,
|
||||
setShowKTMatrixModal,
|
||||
level2Feedback,
|
||||
setLevel2Feedback,
|
||||
level2Recommendation,
|
||||
setLevel2Recommendation,
|
||||
setIsSubmittingLevel2,
|
||||
setShowLevel2FeedbackModal,
|
||||
level3Feedback,
|
||||
setLevel3Feedback,
|
||||
level3Recommendation,
|
||||
setLevel3Recommendation,
|
||||
setIsSubmittingLevel3,
|
||||
setShowLevel3FeedbackModal,
|
||||
currentUser,
|
||||
@ -255,6 +267,7 @@ export const ApplicationDetails = () => {
|
||||
maybeFetchUsersForModal,
|
||||
handleScheduleInterview,
|
||||
handleCancelInterview,
|
||||
handleConfirmCancelInterview,
|
||||
handleUpload,
|
||||
handleApprove,
|
||||
handleReject,
|
||||
@ -303,6 +316,10 @@ export const ApplicationDetails = () => {
|
||||
setLoading,
|
||||
setIsScheduling,
|
||||
setShowScheduleModal,
|
||||
setShowCancelInterviewModal,
|
||||
interviewIdToCancel,
|
||||
setInterviewIdToCancel,
|
||||
setIsCancellingInterview,
|
||||
setIsUploading,
|
||||
setShowUploadForm,
|
||||
setUploadFile,
|
||||
@ -322,7 +339,7 @@ export const ApplicationDetails = () => {
|
||||
|
||||
useEffect(() => {
|
||||
maybeFetchUsersForModal();
|
||||
}, [showScheduleModal, showAssignArchitectureModal, showAssignModal, interviewType, application?.participants, maybeFetchUsersForModal]);
|
||||
}, [showScheduleModal, showAssignArchitectureModal, showAssignModal, interviewType, application?.id, maybeFetchUsersForModal]);
|
||||
|
||||
if (loading && !application) {
|
||||
return (
|
||||
@ -510,6 +527,11 @@ export const ApplicationDetails = () => {
|
||||
handleReject={handleReject}
|
||||
showScheduleModal={showScheduleModal}
|
||||
setShowScheduleModal={setShowScheduleModal}
|
||||
showCancelInterviewModal={showCancelInterviewModal}
|
||||
setShowCancelInterviewModal={setShowCancelInterviewModal}
|
||||
setInterviewIdToCancel={setInterviewIdToCancel}
|
||||
isCancellingInterview={isCancellingInterview}
|
||||
handleConfirmCancelInterview={handleConfirmCancelInterview}
|
||||
interviewType={interviewType}
|
||||
setInterviewType={setInterviewType}
|
||||
interviewMode={interviewMode}
|
||||
@ -557,6 +579,8 @@ export const ApplicationDetails = () => {
|
||||
handleKTMatrixChange={handleKTMatrixChange}
|
||||
ktMatrixRemarks={ktMatrixRemarks}
|
||||
setKtMatrixRemarks={setKtMatrixRemarks}
|
||||
ktMatrixRecommendation={ktMatrixRecommendation}
|
||||
setKtMatrixRecommendation={setKtMatrixRecommendation}
|
||||
calculateKTScore={calculateKTScore}
|
||||
handleSubmitKTMatrix={handleSubmitKTMatrix}
|
||||
isSubmittingKT={isSubmittingKT}
|
||||
@ -564,15 +588,20 @@ export const ApplicationDetails = () => {
|
||||
setShowLevel2FeedbackModal={setShowLevel2FeedbackModal}
|
||||
level2Feedback={level2Feedback}
|
||||
handleLevel2Change={handleLevel2Change}
|
||||
level2Recommendation={level2Recommendation}
|
||||
setLevel2Recommendation={setLevel2Recommendation}
|
||||
handleSubmitLevel2Feedback={handleSubmitLevel2Feedback}
|
||||
isSubmittingLevel2={isSubmittingLevel2}
|
||||
showFeedbackDetailsModal={showFeedbackDetailsModal}
|
||||
setShowFeedbackDetailsModal={setShowFeedbackDetailsModal}
|
||||
selectedEvaluationForView={selectedEvaluationForView}
|
||||
selectedInterviewForFeedback={selectedInterviewForFeedback}
|
||||
showLevel3FeedbackModal={showLevel3FeedbackModal}
|
||||
setShowLevel3FeedbackModal={setShowLevel3FeedbackModal}
|
||||
level3Feedback={level3Feedback}
|
||||
handleLevel3Change={handleLevel3Change}
|
||||
level3Recommendation={level3Recommendation}
|
||||
setLevel3Recommendation={setLevel3Recommendation}
|
||||
handleSubmitLevel3Feedback={handleSubmitLevel3Feedback}
|
||||
isSubmittingLevel3={isSubmittingLevel3}
|
||||
showDocumentsModal={showDocumentsModal}
|
||||
|
||||
@ -214,18 +214,9 @@ export function OpportunityRequestsPage({ onViewDetails }: OpportunityRequestsPa
|
||||
const response = await onboardingService.shortlistApplications(selectedIds, [], shortlistRemark);
|
||||
|
||||
if (response && response.success) {
|
||||
// Update local state and show success only if API succeeded
|
||||
const updatedApplications = applicationsData.map(app => {
|
||||
if (selectedIds.includes(app.id)) {
|
||||
return {
|
||||
...app,
|
||||
ddLeadShortlisted: true
|
||||
} as any;
|
||||
}
|
||||
return app;
|
||||
});
|
||||
// Refresh data from server to ensure correct filtering and pagination
|
||||
await fetchApplications();
|
||||
|
||||
setApplicationsData(updatedApplications);
|
||||
setSelectedIds([]);
|
||||
setShowShortlistModal(false);
|
||||
setShortlistRemark('');
|
||||
|
||||
@ -5,7 +5,7 @@ import { toast } from 'sonner';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { RootState } from '../../store';
|
||||
import {
|
||||
User, RefreshCw, HelpCircle, ArrowLeft, Bike,
|
||||
User, RefreshCw, HelpCircle, ArrowLeft,
|
||||
Users, FileText, ChevronRight,
|
||||
CheckCircle
|
||||
} from 'lucide-react';
|
||||
@ -188,19 +188,12 @@ const PublicQuestionnairePage: React.FC = () => {
|
||||
|
||||
<div className="max-w-5xl mx-auto py-8 px-6">
|
||||
{/* 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="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="inline-flex items-center justify-center mb-6">
|
||||
<div className="w-20 h-20 bg-amber-600 rounded-full flex items-center justify-center shadow-xl">
|
||||
<Bike className="w-10 h-10 text-white" />
|
||||
<div className="flex items-center justify-center mb-7">
|
||||
<img src="/assets/images/Re_Logo.png" alt="Royal Enfield" className="h-12 w-auto" />
|
||||
</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>
|
||||
<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">
|
||||
|
||||
@ -162,5 +162,13 @@ export const masterService = {
|
||||
saveSystemConfig: async (data: any) => {
|
||||
const response = await API.saveSystemConfig(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-heading {
|
||||
font-family: 'Montserrat', sans-serif;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user