progress track enhnced tested upto eor step work flow service file added for deale onboarding

This commit is contained in:
laxmanhalaki 2026-04-02 01:42:51 +05:30
parent 7fa34dd3d6
commit 574e648618
4 changed files with 312 additions and 230 deletions

View File

@ -51,7 +51,7 @@ import { SocketProvider } from './context/SocketContext';
// Layout Component
const AppLayout = ({ onLogout, title }: { onLogout: () => void, title: string }) => {
return (
<div className="flex h-screen bg-slate-50">
<div className="flex h-screen bg-slate-50 overflow-hidden">
<Sidebar onLogout={onLogout} />
<div className="flex-1 flex flex-col overflow-hidden">
<Header title={title} onRefresh={() => window.location.reload()} />

View File

@ -322,6 +322,7 @@ export function ApplicationDetails() {
regionId: data.regionId,
areaId: data.areaId,
districtId: data.districtId,
stageApprovals: data.stageApprovals || [],
};
setApplication(mappedApp);
} catch (error) {
@ -978,7 +979,7 @@ export function ApplicationDetails() {
{
id: 9,
name: 'Security Details',
status: getStageStatus('Security Details', () => ['Payment Pending', 'Dealer Code Generation', 'Architecture Team Assigned', 'Architecture Document Upload', 'Architecture Team Completion', 'LOA Pending', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Onboarded'].includes(application.status) ? 'completed' : 'pending'),
status: getStageStatus('Security Details', () => ['LOI Issued', 'Statutory LOI Ack', 'Dealer Code Generation', 'Architecture Team Assigned', 'Architecture Document Upload', 'Architecture Team Completion', 'LOA Pending', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Onboarded'].includes(application.status) ? 'completed' : application.status === 'Payment Pending' ? 'active' : 'pending'),
date: application.securityDetailsDate,
description: 'Security verification',
documentsUploaded: 3
@ -986,7 +987,7 @@ export function ApplicationDetails() {
{
id: 10,
name: 'LOI Issue',
status: getStageStatus('LOI Issue', () => ['Statutory LOI Ack', 'Dealer Code Generation', 'Architecture Team Assigned', 'Architecture Document Upload', 'Architecture Team Completion', 'LOA Pending', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Onboarded'].includes(application.status) ? 'completed' : ['Payment Pending', 'LOI Issued'].includes(application.status) ? 'active' : 'pending'),
status: getStageStatus('LOI Issue', () => ['Statutory LOI Ack', 'Dealer Code Generation', 'Architecture Team Assigned', 'Architecture Document Upload', 'Architecture Team Completion', 'LOA Pending', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Onboarded'].includes(application.status) ? 'completed' : application.status === 'LOI Issued' ? 'active' : 'pending'),
date: application.loiIssueDate,
description: 'Letter of Intent issued',
documentsUploaded: 1
@ -1580,7 +1581,9 @@ export function ApplicationDetails() {
const currentStageCode = policyManagedStages[application.status];
const currentUserStageAction = application.stageApprovals?.find(
(a: any) => a.stageCode === currentStageCode && a.actorUserId === currentUser?.id
(a: any) =>
a.stageCode === currentStageCode &&
String(a.actorUserId) === String(currentUser?.id)
);
const hasMadeStageDecision = !!currentUserStageAction;
@ -1591,7 +1594,7 @@ export function ApplicationDetails() {
['Approved', 'Rejected', 'Selected'].includes(currentUserEvaluation?.recommendation || '');
// Final visibility flags
const isAdmin = currentUser && ['DD Admin', 'Super Admin', 'NBH', 'DD Lead', 'DD Head'].includes(currentUser.role);
const isAdmin = currentUser && ['DD Admin', 'Super Admin', 'NBH', 'DD Lead', 'DD Head', 'Finance'].includes(currentUser.role);
const isAdministrativeStage = [
'Level 3 Approved', 'FDD Verification',
'LOI In Progress', 'LOI Issued', 'Statutory LOI Ack',
@ -1817,48 +1820,124 @@ export function ApplicationDetails() {
</div>
<div className="relative">
{processStages.map((stage, index) => (
{(() => {
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))))
);
return stageParticipants.map((p: any) => {
const saCode = typeof stageCode === 'number' ? `INTERVIEW_LEVEL_${stageCode}` : stageCode;
const approval = (application.stageApprovals || []).find((sa: any) =>
sa.stageCode === saCode &&
String(sa.actorUserId) === String(p.userId)
);
return {
name: p.user?.name || 'Unknown',
role: p.user?.role || 'Reviewer',
status: approval ? (approval.decision === 'Approved' ? 'approved' : 'rejected') : 'pending'
};
});
};
const renderApprovers = (stageName: string) => {
const stageMapping: Record<string, string | number> = {
'1st Level Interview': 1,
'2nd Level Interview': 2,
'3rd Level Interview': 3,
'LOI Approval': 'LOI_APPROVAL',
'LOA': 'LOA_APPROVAL'
};
const stageCode = stageMapping[stageName];
if (!stageCode) return null;
const approvers = getApproverStatus(stageCode);
if (approvers.length === 0) return null;
return (
<div className="flex flex-wrap gap-2 mt-3">
{approvers.map((approver, i) => (
<div key={i} className="group relative flex items-center gap-1.5 bg-slate-50 border border-slate-200 rounded-full pl-1 pr-2.5 py-0.5 transition-all hover:bg-white hover:shadow-sm">
<div className={cn(
"w-6 h-6 rounded-full flex items-center justify-center text-[10px] font-bold text-white",
approver.status === 'approved' ? "bg-green-500" : approver.status === 'rejected' ? "bg-red-500" : "bg-slate-300"
)}>
{approver.name.split(' ').map(n => n[0]).join('').substring(0, 2).toUpperCase()}
</div>
<div className="flex flex-col">
<span className="text-[10px] font-medium text-slate-700 leading-none">{approver.name}</span>
<span className="text-[8px] text-slate-500 leading-none mt-0.5">{approver.role}</span>
</div>
{/* Status Dot Overlay */}
<div className={cn(
"absolute -top-0.5 -right-0.5 w-2 h-2 rounded-full border border-white",
approver.status === 'approved' ? "bg-green-500" : approver.status === 'rejected' ? "bg-red-500" : "bg-amber-400"
)} />
{/* Tooltip */}
<div className="absolute bottom-full left-1/2 -translate-x-1/2 mb-2 px-2 py-1 bg-slate-900 text-white text-[10px] rounded opacity-0 group-hover:opacity-100 pointer-events-none transition-opacity whitespace-nowrap z-50">
{approver.role}: {approver.status.toUpperCase()}
</div>
</div>
))}
</div>
);
};
return processStages.map((stage, index) => (
<div key={stage.id}>
<div className="flex gap-4 pb-8">
<div className="relative">
<div className={`w-10 h-10 rounded-full flex items-center justify-center border-2 ${stage.status === 'completed'
? 'bg-green-500 border-green-500'
<div className={`w-10 h-10 rounded-full flex items-center justify-center border-2 z-10 relative ${stage.status === 'completed'
? 'bg-green-500 border-green-500 text-white shadow-sm'
: stage.status === 'active'
? 'bg-amber-500 border-amber-500'
: 'bg-slate-200 border-slate-300'
? 'bg-amber-500 border-amber-500 text-white animate-pulse-subtle'
: 'bg-white border-slate-300 text-slate-400 shadow-none'
}`}>
{stage.isParallel ? (
<GitBranch className="w-5 h-5 text-white" />
<GitBranch className="w-5 h-5" />
) : (
<>
{stage.status === 'completed' && (
<CheckCircle className="w-5 h-5 text-white" />
)}
{stage.status === 'active' && (
{stage.status === 'completed' ? (
<Check className="w-5 h-5" />
) : stage.status === 'active' ? (
<Clock className="w-5 h-5 text-white" />
)}
{stage.status === 'pending' && (
<div className="w-3 h-3 bg-slate-400 rounded-full"></div>
) : (
<div className="w-3 h-3 bg-slate-300 rounded-full"></div>
)}
</>
)}
</div>
{index < processStages.length - 1 && !stage.isParallel && (
<div className={`absolute top-10 left-1/2 -translate-x-1/2 w-0.5 h-full ${stage.status === 'completed' ? 'bg-green-500' : 'bg-slate-300'
<div className={`absolute top-10 left-1/2 -translate-x-1/2 w-0.5 h-full z-0 ${stage.status === 'completed' ? 'bg-green-500/30' : 'bg-slate-200'
}`}></div>
)}
</div>
<div className="flex-1 pt-1">
<p className="text-slate-900">{stage.name}</p>
<p className={cn(
"font-bold transition-colors",
stage.status === 'completed' ? "text-green-700" : stage.status === 'active' ? "text-amber-700" : "text-slate-900"
)}>{stage.name}</p>
{stage.description && (
<p className="text-slate-600 text-sm mt-0.5">{stage.description}</p>
<p className="text-slate-600 text-sm mt-0.5 leading-relaxed">{stage.description}</p>
)}
{stage.evaluators && stage.evaluators.length > 0 ? (
<p className="text-amber-600 text-sm mt-0.5">
{renderApprovers(stage.name as string)}
{stage.evaluators && stage.evaluators.length > 0 && !['LOI Approval', 'LOA', '1st Level Interview', '2nd Level Interview', '3rd Level Interview'].includes(stage.name as string) && (
<p className="text-amber-600 text-xs mt-1.5 flex items-center gap-1 bg-amber-50 w-fit px-2 py-0.5 rounded border border-amber-100">
<User className="w-3 h-3" />
Evaluators: {stage.evaluators.join(' + ')}
</p>
) : (() => {
// Determine expected count for this stage
)}
{(() => {
const expectedMap: Record<number, number> = {
4: 2, // L1 Interview (ZM + RBM)
5: 2, // L2 Interview (ZBH + DD Lead)
@ -1897,7 +1976,7 @@ export function ApplicationDetails() {
}
return null;
})()}
{/* Stage Docs Link */}
{(() => {
const stageDocsCount = documents.filter(doc =>
doc.stage === stage.name ||
@ -1920,7 +1999,7 @@ export function ApplicationDetails() {
);
})()}
<p className="text-slate-500 mt-1">
<p className="text-slate-500 mt-1 text-xs">
{stage.status === 'completed' && stage.date && `Completed: ${new Date(stage.date).toLocaleDateString()}`}
{stage.status === 'active' && 'In Progress'}
{stage.status === 'pending' && 'Pending'}
@ -1928,7 +2007,6 @@ export function ApplicationDetails() {
</div>
</div>
{/* Parallel Branches */}
{stage.isParallel && stage.branches && (
<div className="ml-5 mb-8">
{stage.branches.map((branch, branchIndex) => {
@ -1938,7 +2016,6 @@ export function ApplicationDetails() {
return (
<div key={branchIndex} className="mb-6 last:mb-0">
{/* Branch Header - Clickable */}
<button
onClick={() => setExpandedBranches(prev => ({
...prev,
@ -1959,21 +2036,20 @@ export function ApplicationDetails() {
<GitBranch className={`w-4 h-4 ${branchColor === 'blue' ? 'text-blue-700' : 'text-green-700'}`} />
</div>
<div className="flex-1 text-left">
<p className={`${branchColor === 'blue' ? 'text-blue-900' : 'text-green-900'}`}>
<p className={`${branchColor === 'blue' ? 'text-blue-900' : 'text-green-900'} font-semibold`}>
{branch.name}
</p>
<p className={`text-sm ${branchColor === 'blue' ? 'text-blue-700' : 'text-green-700'}`}>
<p className={`text-xs ${branchColor === 'blue' ? 'text-blue-700' : 'text-green-700'}`}>
{branch.stages.length} steps
</p>
</div>
</button>
{/* Branch Content - Expandable */}
{isExpanded && (
<div className="mt-4 ml-8 border-l-2 border-slate-200 pl-6 space-y-6">
{branch.stages.map((branchStage) => (
<div key={branchStage.id} className="relative">
<div className="flex gap-4">
<div className="flex gap-4 text-xs">
<div className="relative">
<div className={`w-8 h-8 rounded-full flex items-center justify-center border-2 ${branchStage.status === 'completed'
? `${branchColor === 'blue' ? 'bg-blue-500 border-blue-500' : 'bg-green-500 border-green-500'}`
@ -1981,24 +2057,21 @@ export function ApplicationDetails() {
? 'bg-amber-500 border-amber-500'
: 'bg-slate-200 border-slate-300'
}`}>
{branchStage.status === 'completed' && (
<CheckCircle className="w-4 h-4 text-white" />
)}
{branchStage.status === 'active' && (
{branchStage.status === 'completed' ? (
<Check className="w-4 h-4 text-white" />
) : branchStage.status === 'active' ? (
<Clock className="w-4 h-4 text-white" />
)}
{branchStage.status === 'pending' && (
) : (
<div className="w-2 h-2 bg-slate-400 rounded-full"></div>
)}
</div>
</div>
<div className="flex-1">
<p className="text-slate-900">{branchStage.name}</p>
<p className="font-semibold text-slate-800">{branchStage.name}</p>
{branchStage.description && (
<p className="text-slate-600 text-sm mt-0.5">{branchStage.description}</p>
<p className="text-slate-500 text-xs mt-0.5">{branchStage.description}</p>
)}
{/* Branch Stage Docs Link */}
{(() => {
const branchDocsCount = documents.filter(doc =>
doc.documentType?.toLowerCase().includes(branchStage.name.toLowerCase().split(' ')[0]) ||
@ -2012,17 +2085,17 @@ export function ApplicationDetails() {
setSelectedStage(branchStage.name);
setShowDocumentsModal(true);
}}
className="text-xs font-medium text-blue-700 hover:text-blue-800 flex items-center gap-1 bg-blue-50 px-2 py-0.5 rounded border border-blue-200"
className="text-[10px] font-medium text-blue-700 hover:text-blue-800 flex items-center gap-1 bg-blue-50 px-1.5 py-0.5 rounded border border-blue-100"
>
<FileText className="w-3 h-3" />
{branchDocsCount > 0 ? `${branchDocsCount} Documents Uploaded` : 'Upload Document'}
<FileText className="w-2.5 h-2.5" />
{branchDocsCount > 0 ? `${branchDocsCount} Docs` : 'Upload'}
</button>
</div>
);
})()}
<p className="text-slate-500 text-sm mt-1">
{branchStage.status === 'completed' && branchStage.date && `Completed: ${new Date(branchStage.date).toLocaleDateString()}`}
{branchStage.status === 'active' && 'In Progress'}
<p className="text-slate-400 text-[10px] mt-1">
{branchStage.status === 'completed' && branchStage.date && `Done: ${new Date(branchStage.date).toLocaleDateString()}`}
{branchStage.status === 'active' && 'Evaluating'}
{branchStage.status === 'pending' && 'Pending'}
</p>
</div>
@ -2034,13 +2107,12 @@ export function ApplicationDetails() {
</div>
);
})}
{/* Connecting line to next stage */}
<div className="h-8 w-0.5 bg-slate-300 ml-5"></div>
<div className="h-8 w-0.5 bg-slate-300 ml-5 opacity-50"></div>
</div>
)}
</div>
))}
))
})()}
</div>
</TabsContent>
@ -2101,7 +2173,6 @@ export function ApplicationDetails() {
</div>
</TabsContent>
{/* Interviews Tab */}
{/* Interviews Tab */}
<TabsContent value="interviews" className="space-y-6">
<div>
@ -2672,14 +2743,25 @@ export function ApplicationDetails() {
)}
{/* Dedicated Onboarding Button - Appears ONLY when everything is ready (last step) */}
{isAdmin && ['Inauguration', 'EOR Complete', 'Approved', 'EOR In Progress', 'LOA Pending'].includes(application.status) && !application.dealer && (
{isAdmin && application.status === 'Inauguration' && !application.dealer && (
<div className="space-y-2">
{eorProgress < 100 && (
<Alert variant="destructive" className="bg-amber-50 border-amber-200 text-amber-800 py-2">
<AlertCircle className="h-4 w-4 text-amber-600" />
<AlertDescription className="text-xs">
EOR Checklist must be 100% complete before onboarding. (Current: {eorProgress.toFixed(0)}%)
</AlertDescription>
</Alert>
)}
<Button
className="w-full bg-green-600 hover:bg-green-700 font-bold shadow-lg shadow-green-100"
className="w-full bg-green-600 hover:bg-green-700 font-bold shadow-lg shadow-green-100 disabled:bg-slate-300 disabled:text-slate-500"
onClick={() => setShowOnboardModal(true)}
disabled={eorProgress < 100}
>
<CheckCircle className="w-4 h-4 mr-2" />
Onboard as Dealer (Final Step)
</Button>
</div>
)}
{/* Dealer Onboarded Status & Link */}

View File

@ -106,7 +106,7 @@ export function Sidebar({ onLogout }: SidebarProps) {
return (
<div
className={`bg-slate-900 text-white h-screen flex flex-col transition-all duration-300 ${collapsed ? 'w-20' : 'w-64'
className={`bg-slate-900 text-white h-screen flex flex-col transition-all duration-300 overflow-hidden ${collapsed ? 'w-20' : 'w-64'
}`}
>
{/* Header with Logo */}
@ -153,7 +153,7 @@ export function Sidebar({ onLogout }: SidebarProps) {
)}
{/* Menu Items */}
<nav className="flex-1 p-4 space-y-2">
<nav className="flex-1 p-4 space-y-2 overflow-y-auto custom-scrollbar">
{menuItems.map((item) => {
const Icon = item.icon;
const isActive = activeView === item.id;

View File

@ -190,7 +190,7 @@ html {
}
.custom-scrollbar::-webkit-scrollbar {
width: 5px;
width: 3px;
}
.custom-scrollbar::-webkit-scrollbar-track {