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

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

View File

@ -222,6 +222,8 @@ export const API = {
// System Configs
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}`),

View File

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

View File

@ -108,7 +108,7 @@ export function Header({ title, onRefresh }: HeaderProps) {
{/* Current User Info */}
{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">

View File

@ -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>

View File

@ -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>

View File

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

View File

@ -24,6 +24,7 @@ import { AddRoleDialog } from '@/features/master/components/AddRoleDialog';
import { EmailTemplates } from '@/features/master/components/EmailTemplates';
import { 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>

View File

@ -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>

View File

@ -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 &gt; 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 &gt; 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 &gt; 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>

View File

@ -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) =>
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))))
);
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?.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');

View File

@ -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,

View File

@ -1,4 +1,4 @@
import { useEffect, useState } from 'react';
import { useCallback, useEffect, useState } from 'react';
import { Application, ApplicationStatus } from '@/lib/mock-data';
import { 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);

View File

@ -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 {

View File

@ -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) {

View File

@ -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)));
(isAdminRole &&
isAdministrativeStage &&
sequenceMet &&
(!['EOR In Progress', 'Inauguration', 'Approved'].includes(application.status) || eorProgress === 100));
return {
canApprove: canApproveReject && !isLoaLocked && !isSecurityDetailsLocked,

View File

@ -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' },

View File

@ -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,

View File

@ -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}

View File

@ -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('');

View File

@ -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>
<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>
<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">

View File

@ -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;
}
};

View File

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