Compare commits

...

16 Commits

Author SHA1 Message Date
05211fe90a typo chnge errors and Loi Document request mail templatd added 2026-05-26 12:34:32 +05:30
dc49fa9065 dispatch feature approve restriction and some few bugs covered from tracker 2026-05-25 22:53:58 +05:30
06116af31a theme have color enhanced and smtp setup done 2026-05-20 20:16:50 +05:30
faa29a7511 started removing document type dependecy and f&F screeen bug changes fixed 2026-05-19 21:26:47 +05:30
61deac775c few de demo bugs and sla tracker implemeted alog with sla monitor screen 2026-05-18 21:10:22 +05:30
81d4dd493f bugs coverd 2026-05-15 20:20:15 +05:30
1340f44485 few more bugs fixed and dealer ide ui alignmen and visibibity chabges done F& F made manuall trigger 2026-05-14 14:39:31 +05:30
ec70f1d3f1 system log table added and feew bugs coverd from the tracker 2026-05-13 20:45:33 +05:30
201bfa6a41 new mail templates added for edge scenerios 2026-05-12 19:59:47 +05:30
c23593bb11 contitutional and relocation changes done based on document alignment 2026-05-06 10:45:55 +05:30
b357dbdcbb stage names modified and calendar addd in opportunity requests 2026-05-04 13:26:51 +05:30
2f82699572 notif
ication service enhanced even more detailed way added more templates documentented i splitted based on modulewise joint approval added for resignation flow, upload ppt document with new docment type add for DD Lead user
2026-04-30 18:52:17 +05:30
95032cf2a7 added joint approval in resignation for RBM DD-ZM approval stage 2026-04-29 16:37:06 +05:30
352c656a9e theme color changed to red i multiple places 2026-04-28 20:42:15 +05:30
2a289e433c stage transition bug resolved 2026-04-28 13:48:05 +05:30
f3927b4686 progress track isue for interview level 1 fixed 2026-04-28 08:17:23 +05:30
112 changed files with 6804 additions and 2754 deletions

1
package-lock.json generated
View File

@ -12222,7 +12222,6 @@
"version": "2.3.2", "version": "2.3.2",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
"dev": true,
"hasInstallScript": true, "hasInstallScript": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,

View File

@ -47,6 +47,7 @@ import { DealerRelocationPage } from '@/features/relocation/pages/DealerRelocati
import QuestionnaireBuilder from '@/components/admin/QuestionnaireBuilder'; import QuestionnaireBuilder from '@/components/admin/QuestionnaireBuilder';
import QuestionnaireList from '@/components/admin/QuestionnaireList'; import QuestionnaireList from '@/components/admin/QuestionnaireList';
import InterviewConfigManagement from '@/features/master/components/InterviewConfigManagement'; import InterviewConfigManagement from '@/features/master/components/InterviewConfigManagement';
import { SystemLogsPage } from '@/features/admin/pages/SystemLogsPage';
import { WorkNotesPage } from '@/features/onboarding/pages/WorkNotesPage'; import { WorkNotesPage } from '@/features/onboarding/pages/WorkNotesPage';
import { NotificationsPage } from '@/pages/NotificationsPage'; import { NotificationsPage } from '@/pages/NotificationsPage';
import { Toaster } from '@/components/ui/sonner'; import { Toaster } from '@/components/ui/sonner';
@ -56,12 +57,20 @@ import { API } from '@/api/API';
import { SocketProvider } from '@/context/SocketContext'; import { SocketProvider } from '@/context/SocketContext';
// Layout Component // Layout Component
const AppLayout = ({ onLogout, title }: { onLogout: () => void, title: string }) => { const AppLayout = ({
onLogout,
title,
subtitle,
}: {
onLogout: () => void;
title: string;
subtitle: string;
}) => {
return ( return (
<div className="flex h-screen bg-slate-50 overflow-hidden"> <div className="flex h-screen bg-slate-50 overflow-hidden">
<Sidebar onLogout={onLogout} /> <Sidebar onLogout={onLogout} />
<div className="flex-1 flex flex-col overflow-hidden"> <div className="flex-1 flex flex-col overflow-hidden">
<Header title={title} onRefresh={() => window.location.reload()} /> <Header title={title} subtitle={subtitle} onRefresh={() => window.location.reload()} />
<main className="flex-1 overflow-y-auto p-6"> <main className="flex-1 overflow-y-auto p-6">
<Outlet /> <Outlet />
</main> </main>
@ -77,11 +86,15 @@ export default function App() {
const navigate = useNavigate(); const navigate = useNavigate();
const location = useLocation(); const location = useLocation();
const currentRole = currentUser?.role || currentUser?.roleCode || ''; const currentRole = currentUser?.role || currentUser?.roleCode || '';
const normalizedRole = String(currentRole).trim().toLowerCase(); const hasRole = (roles: string[]) => {
const hasRole = (roles: string[]) => roles.map((r) => r.toLowerCase()).includes(normalizedRole); const normalizedTargetRoles = roles.map((r) => r.toLowerCase());
const resignationRoles = ['DD Admin', 'ASM', 'RBM', 'DD Lead', 'ZBH', 'NBH', 'Legal', 'Legal Admin', 'Super Admin']; const userRole = String(currentUser?.role || '').toLowerCase();
const terminationRoles = ['ASM', 'RBM', 'ZBH', 'DD Lead', 'DD Head', 'NBH', 'Legal Admin', 'Legal', 'DD Admin', 'CCO', 'CEO', 'Super Admin']; const userRoleCode = String(currentUser?.roleCode || '').toLowerCase();
const fnfRoles = ['DD Admin', 'DD Lead', 'NBH', 'Finance', 'Finance Admin', 'Super Admin']; return normalizedTargetRoles.includes(userRole) || normalizedTargetRoles.includes(userRoleCode);
};
const resignationRoles = ['DD Admin', 'DD_ADMIN', 'ASM', 'RBM', 'DD-ZM', 'DD_ZM', 'DD Lead', 'DD_LEAD', 'DD Head', 'DD_HEAD', 'ZBH', 'NBH', 'Legal', 'Legal Admin', 'LEGAL_ADMIN', 'Super Admin', 'SUPER_ADMIN'];
const terminationRoles = ['ASM', 'RBM', 'DD-ZM', 'DD_ZM', 'ZBH', 'DD Lead', 'DD_LEAD', 'DD Head', 'DD_HEAD', 'NBH', 'Legal Admin', 'LEGAL_ADMIN', 'Legal', 'DD Admin', 'DD_ADMIN', 'CCO', 'CEO', 'Super Admin', 'SUPER_ADMIN'];
const fnfRoles = ['DD Admin', 'DD_ADMIN', 'DD-ZM', 'DD_ZM', 'DD Lead', 'DD_LEAD', 'DD Head', 'DD_HEAD', 'NBH', 'Finance', 'Finance Admin', 'FINANCE_ADMIN', 'Super Admin', 'SUPER_ADMIN'];
const financeRoles = ['Finance', 'Finance Admin']; const financeRoles = ['Finance', 'Finance Admin'];
useEffect(() => { useEffect(() => {
@ -134,8 +147,18 @@ export default function App() {
// Helper to determine page title based on path // Helper to determine page title based on path
const getPageTitle = (pathname: string) => { const getPageTitle = (pathname: string) => {
if (pathname.startsWith('/applications/') && pathname.length > 14) return 'Application Details'; if (pathname.startsWith('/applications/') && pathname.length > 14) return 'Application Details';
if (pathname.includes('/resignation/') && pathname.length > 13) return 'Resignation Details'; if (pathname.startsWith('/resignation/') && !pathname.startsWith('/dealer-resignation')) return 'Resignation Details';
// ... Add more dynamic title logic as needed if (pathname.startsWith('/dealer-resignation/')) return 'Resignation Request Details';
if (pathname.startsWith('/termination/')) return 'Termination Details';
if (pathname.startsWith('/fnf/')) return 'F&F Request Details';
if (pathname.startsWith('/constitutional-change/')) return 'Constitutional Change Details';
if (pathname.startsWith('/relocation-requests/')) return 'Relocation Request Details';
if (pathname.startsWith('/finance-onboarding/')) return 'Payment Details';
if (pathname.startsWith('/finance-audit/')) return 'Finance Audit';
if (pathname.startsWith('/finance-fnf/')) return 'F&F Settlement Details';
if (pathname.startsWith('/fdd-details/')) return 'FDD Audit Workspace';
if (pathname.startsWith('/questionnaire-builder/')) return 'Questionnaire Builder';
if (pathname.startsWith('/worknotes/')) return 'Work Notes';
const titles: Record<string, string> = { const titles: Record<string, string> = {
'/dashboard': 'Dashboard', '/dashboard': 'Dashboard',
'/applications': 'Dealership Requests', '/applications': 'Dealership Requests',
@ -161,11 +184,155 @@ export default function App() {
'/approval-policies': 'Approval Policies', '/approval-policies': 'Approval Policies',
'/fdd-dashboard': 'FDD Dashboard', '/fdd-dashboard': 'FDD Dashboard',
'/fdd-details': 'Audit Workspace', '/fdd-details': 'Audit Workspace',
'/questions': 'Questionnaires',
'/questionnaires': 'Questionnaire Templates',
'/interview-configs': 'Interview Configuration',
'/system-logs': 'System Logs',
'/sla-configurations': 'SLA Matrix',
'/notifications': 'Notifications', '/notifications': 'Notifications',
}; };
return titles[pathname] || 'Dashboard'; return titles[pathname] || 'Dashboard';
}; };
/** Short context line under the main header title — varies by route (and sometimes role). */
const getPageSubtitle = (pathname: string, role: string) => {
const rl = (role || '').toLowerCase();
const isDealerRole = rl === 'dealer' || rl.includes('dealer');
const isFinanceRole = rl.includes('finance');
if (pathname.startsWith('/worknotes/')) {
return 'Collaborative notes and clarifications linked to this onboarding or offboarding record.';
}
if (pathname.startsWith('/applications/') && pathname !== '/applications') {
return 'Review stages, documents, interviews, and decisions for this single dealership onboarding application.';
}
if (pathname === '/applications') {
return 'Search, filter, and open dealership applications your role can work on.';
}
if (pathname === '/all-applications') {
return 'Cross-team view of every dealership application in the pipeline.';
}
if (pathname === '/opportunity-requests') {
return 'Applications tied to an opportunity location for DD prioritisation.';
}
if (pathname === '/non-opportunities') {
return 'Applications without a mapped opportunity; track for future reference or follow-up.';
}
if (pathname === '/dashboard') {
if (isDealerRole) {
return 'Your home for outlet actions: constitutional change (how your business is legally registered), relocation, and resignation requests.';
}
if (isFinanceRole) {
return 'Payment verification, finance audits, and F&F settlement work for dealership accounts.';
}
return 'Operational snapshot for dealership onboarding: workloads, alerts, and shortcuts for your role.';
}
if (pathname.startsWith('/dealer-resignation/')) {
return 'Read-only summary of the resignation you submitted for this outlet.';
}
if (pathname === '/dealer-resignation') {
return 'Start a new outlet resignation or open a request you already submitted.';
}
if (pathname === '/dealer-constitutional') {
return 'Constitutional change updates your outlets registered legal structure (for example sole proprietorship to private limited). Submit one request per outlet; Royal Enfield teams review documents and approve before records change.';
}
if (pathname === '/dealer-relocation') {
return 'Request a move of your dealership to a new address or territory, and track requests in progress.';
}
if (pathname.startsWith('/constitutional-change/')) {
return 'Review evidence, comments, and workflow for this constitutional change case.';
}
if (pathname === '/constitutional-change') {
return 'Process dealer requests to change registered legal constitution, supporting documents, and approvals.';
}
if (pathname.startsWith('/relocation-requests/')) {
return 'Assess feasibility, documents, and approvals for this relocation request.';
}
if (pathname === '/relocation-requests') {
return 'Manage dealer relocation proposals: new sites, handovers, and compliance checks.';
}
if (pathname.startsWith('/resignation/') && !pathname.startsWith('/dealer-resignation')) {
return 'HR workflow: clearances, handovers, and settlement steps for this resignation.';
}
if (pathname === '/resignation') {
return 'Queue of dealership resignation cases across your authorised outlets.';
}
if (pathname.startsWith('/termination/')) {
return 'Contractual exit details, evidence, and approvals for this termination case.';
}
if (pathname === '/termination') {
return 'Monitor dealership terminations, disputes, and mandated approvals.';
}
if (pathname.startsWith('/fnf/')) {
return 'Line items, deductions, and payout status for this full & final settlement.';
}
if (pathname === '/fnf') {
return 'Track F&F batches from clearance through finance payout.';
}
if (pathname.startsWith('/finance-onboarding/')) {
return 'Payment schedule, proofs, and audit notes for this onboarding application.';
}
if (pathname === '/finance-onboarding') {
return 'Validate Security Deposit, first fills, and related onboarding payments.';
}
if (pathname.startsWith('/finance-audit/')) {
return 'Finance audit trail and checklist for this application.';
}
if (pathname.startsWith('/finance-fnf/')) {
return 'Settlement calculations and release steps for this F&F record.';
}
if (pathname === '/finance-fnf') {
return 'Finance queue for F&F approvals and disbursements.';
}
if (pathname.startsWith('/fdd-details/')) {
return 'Field Development Director audit workspace for this application.';
}
if (pathname === '/fdd-dashboard') {
return 'FDD workload: audits due, flags raised, and follow-up actions.';
}
if (pathname.startsWith('/questionnaire-builder/') || pathname === '/questionnaire-builder') {
return 'Author and publish questionnaire versions used in dealership assessments.';
}
if (pathname === '/questionnaires' || pathname === '/questions') {
return 'List of published questionnaire templates and versions.';
}
if (pathname === '/master') {
return 'Hierarchy, geography, templates, and reference data shared across onboarding.';
}
if (pathname === '/users') {
return 'Create and maintain internal users, roles, and access for this portal.';
}
if (pathname === '/approval-policies') {
return 'Configure who must approve each onboarding stage or document type.';
}
if (pathname === '/sla-configurations') {
return 'Define turnaround targets and escalations for onboarding milestones.';
}
if (pathname === '/interview-configs') {
return 'Set up interview templates, panels, and scoring used during selection.';
}
if (pathname === '/system-logs') {
return 'Immutable record of configuration and administrative actions for compliance.';
}
if (pathname === '/notifications') {
return 'System and workflow alerts for your account.';
}
if (pathname === '/tasks') {
return 'Tasks assigned to you (placeholder module).';
}
if (pathname === '/reports') {
return 'Analytics and exports for onboarding performance (placeholder module).';
}
if (pathname === '/settings') {
return 'Profile, notifications, and security preferences for your account.';
}
const title = getPageTitle(pathname);
return title === 'Dashboard'
? 'Operational snapshot for dealership onboarding: workloads, alerts, and shortcuts for your role.'
: `You are viewing: ${title}.`;
};
if (loading) { if (loading) {
return ( return (
<div className="flex items-center justify-center h-screen bg-slate-50"> <div className="flex items-center justify-center h-screen bg-slate-50">
@ -210,7 +377,11 @@ export default function App() {
{/* Internal & Dealer Routes - EXCLUDES Prospective Dealers */} {/* Internal & Dealer Routes - EXCLUDES Prospective Dealers */}
<Route element={ <Route element={
<RoleGuard excludeRoles={['Prospective Dealer']} redirectTo="/prospective-dashboard"> <RoleGuard excludeRoles={['Prospective Dealer']} redirectTo="/prospective-dashboard">
<AppLayout onLogout={handleLogout} title={getPageTitle(location.pathname)} /> <AppLayout
onLogout={handleLogout}
title={getPageTitle(location.pathname)}
subtitle={getPageSubtitle(location.pathname, currentRole)}
/>
</RoleGuard> </RoleGuard>
}> }>
<Route path="/" element={<Navigate to="/dashboard" replace />} /> <Route path="/" element={<Navigate to="/dashboard" replace />} />
@ -271,6 +442,11 @@ export default function App() {
? <InterviewConfigManagement /> ? <InterviewConfigManagement />
: <Navigate to="/dashboard" /> : <Navigate to="/dashboard" />
} /> } />
<Route path="/system-logs" element={
hasRole(['Super Admin'])
? <SystemLogsPage />
: <Navigate to="/dashboard" />
} />
{/* HR/Finance Modules (Simplified for brevity, following pattern) */} {/* HR/Finance Modules (Simplified for brevity, following pattern) */}
<Route path="/resignation" element={ <Route path="/resignation" element={
@ -330,10 +506,10 @@ export default function App() {
} /> } />
<Route path="/constitutional-change" element={<ConstitutionalChangePage currentUser={currentUser} onViewDetails={(id: string) => navigate(`/constitutional-change/${id}`)} />} /> <Route path="/constitutional-change" element={<ConstitutionalChangePage currentUser={currentUser} onViewDetails={(id: string) => navigate(`/constitutional-change/${id}`)} />} />
<Route path="/constitutional-change/:id" element={<ConstitutionalChangeDetails requestId={window.location.pathname.split('/').pop() || ''} onBack={() => navigate('/constitutional-change')} currentUser={currentUser} />} /> <Route path="/constitutional-change/:id" element={<ConstitutionalChangeDetails requestId={window.location.pathname.split('/').pop() || ''} onBack={() => navigate(hasRole(['Dealer']) ? '/dealer-constitutional' : '/constitutional-change')} currentUser={currentUser} />} />
<Route path="/relocation-requests" element={<RelocationRequestPage currentUser={currentUser} onViewDetails={(id: string) => navigate(`/relocation-requests/${id}`)} />} /> <Route path="/relocation-requests" element={<RelocationRequestPage currentUser={currentUser} onViewDetails={(id: string) => navigate(`/relocation-requests/${id}`)} />} />
<Route path="/relocation-requests/:id" element={<RelocationRequestDetails requestId={window.location.pathname.split('/').pop() || ''} onBack={() => navigate('/relocation-requests')} currentUser={currentUser} />} /> <Route path="/relocation-requests/:id" element={<RelocationRequestDetails requestId={window.location.pathname.split('/').pop() || ''} onBack={() => navigate(hasRole(['Dealer']) ? '/dealer-relocation' : '/relocation-requests')} currentUser={currentUser} />} />
{/* Dealer Routes */} {/* Dealer Routes */}
<Route path="/dealer-resignation" element={<DealerResignationPage currentUser={currentUser} onViewDetails={(id: string) => navigate(`/dealer-resignation/${id}`)} />} /> <Route path="/dealer-resignation" element={<DealerResignationPage currentUser={currentUser} onViewDetails={(id: string) => navigate(`/dealer-resignation/${id}`)} />} />

View File

@ -43,6 +43,7 @@ export const API = {
exportApplicationResponses: (params: { applicationIds: string }) => client.get('/onboarding/applications/export-responses', params), exportApplicationResponses: (params: { applicationIds: string }) => client.get('/onboarding/applications/export-responses', params),
getApplications: (params?: any) => client.get('/onboarding/applications', params), getApplications: (params?: any) => client.get('/onboarding/applications', params),
shortlistApplications: (data: any) => client.post('/onboarding/applications/shortlist', data), shortlistApplications: (data: any) => client.post('/onboarding/applications/shortlist', data),
sendBulkReminders: (data: { applicationIds: string[] }) => client.post('/onboarding/applications/reminders', data),
getApplicationById: (id: string) => client.get(`/onboarding/applications/${id}`), getApplicationById: (id: string) => client.get(`/onboarding/applications/${id}`),
updateApplication: (id: string, data: any) => client.put(`/onboarding/applications/${id}`, data), updateApplication: (id: string, data: any) => client.put(`/onboarding/applications/${id}`, data),
getLatestQuestionnaire: () => client.get('/questionnaire/latest'), getLatestQuestionnaire: () => client.get('/questionnaire/latest'),
@ -53,6 +54,10 @@ export const API = {
assignArchitectureTeam: (applicationId: string, assignedTo: string) => client.post(`/onboarding/applications/${applicationId}/assign-architecture`, { assignedTo }), assignArchitectureTeam: (applicationId: string, assignedTo: string) => client.post(`/onboarding/applications/${applicationId}/assign-architecture`, { assignedTo }),
updateArchitectureStatus: (applicationId: string, status: string, remarks?: string) => client.post(`/onboarding/applications/${applicationId}/architecture-status`, { status, remarks }), updateArchitectureStatus: (applicationId: string, status: string, remarks?: string) => client.post(`/onboarding/applications/${applicationId}/architecture-status`, { status, remarks }),
generateDealerCodes: (applicationId: string) => client.post(`/onboarding/applications/${applicationId}/generate-codes`), generateDealerCodes: (applicationId: string) => client.post(`/onboarding/applications/${applicationId}/generate-codes`),
requestProspectDocuments: (
applicationId: string,
data: { documentTypes: string[]; dueDays?: number; customMessage?: string }
) => client.post(`/onboarding/applications/${applicationId}/request-documents`, data),
updateApplicationStatus: (id: string, data: any) => client.put(`/onboarding/applications/${id}/status`, data), updateApplicationStatus: (id: string, data: any) => client.put(`/onboarding/applications/${id}/status`, data),
convertToOpportunity: (id: string, data?: any) => client.post(`/onboarding/applications/${id}/convert-to-opportunity`, data), convertToOpportunity: (id: string, data?: any) => client.post(`/onboarding/applications/${id}/convert-to-opportunity`, data),
bulkConvertToOpportunity: (data: any) => client.post('/onboarding/applications/bulk-convert-to-opportunity', data), bulkConvertToOpportunity: (data: any) => client.post('/onboarding/applications/bulk-convert-to-opportunity', data),
@ -157,11 +162,17 @@ export const API = {
getResignations: (params?: any) => client.get('/resignation', params), getResignations: (params?: any) => client.get('/resignation', params),
createResignation: (data: any) => client.post('/resignation', data), createResignation: (data: any) => client.post('/resignation', data),
approveResignation: (id: string, data?: any) => client.post(`/resignation/${id}/approve`, data), approveResignation: (id: string, data?: any) => client.post(`/resignation/${id}/approve`, data),
dispatchResignation: (id: string, data?: any) => client.post(`/resignation/${id}/dispatch`, data),
rejectResignation: (id: string, data: any) => client.post(`/resignation/${id}/reject`, data), rejectResignation: (id: string, data: any) => client.post(`/resignation/${id}/reject`, data),
withdrawResignation: (id: string, reason: string) => client.post(`/resignation/${id}/withdraw`, { reason }), withdrawResignation: (id: string, reason: string) => client.post(`/resignation/${id}/withdraw`, { reason }),
getTerminations: (params?: any) => client.get('/termination', params), getTerminations: (params?: any) => client.get('/termination', params),
createTermination: (data: any) => client.post('/termination', data), createTermination: (data: any) => {
const isFormData = typeof FormData !== 'undefined' && data instanceof FormData;
return client.post('/termination', data, isFormData ? {
headers: { 'Content-Type': 'multipart/form-data' }
} : undefined);
},
updateTermination: (id: string, data: any) => client.post(`/termination/${id}/status`, data), updateTermination: (id: string, data: any) => client.post(`/termination/${id}/status`, data),
getOnboardingPayments: () => client.get('/settlement/onboarding'), getOnboardingPayments: () => client.get('/settlement/onboarding'),
@ -206,11 +217,26 @@ export const API = {
updateConstitutionalChange: (id: string, action: ConstitutionalChangeAction, data?: { comments?: string; remarks?: string }) => updateConstitutionalChange: (id: string, action: ConstitutionalChangeAction, data?: { comments?: string; remarks?: string }) =>
client.post(`/constitutional-change/${id}/action`, { action, ...data }), client.post(`/constitutional-change/${id}/action`, { action, ...data }),
uploadConstitutionalDocuments: (id: string, documents: any[]) => client.post(`/constitutional-change/${id}/documents`, { documents }), uploadConstitutionalDocuments: (id: string, documents: any[]) => client.post(`/constitutional-change/${id}/documents`, { documents }),
verifyConstitutionalDocument: (id: string, documentId: string) =>
client.post(`/constitutional-change/${id}/documents/${documentId}/verify`),
rejectConstitutionalDocument: (id: string, documentId: string, data?: { remarks?: string }) =>
client.post(`/constitutional-change/${id}/documents/${documentId}/reject`, data || {}),
// SLA // SLA
getSlaConfigs: () => client.get('/master/sla-configs'), getSlaConfigs: () => client.get('/master/sla-configs'),
saveSlaConfig: (data: any) => client.post('/master/sla-configs', data), saveSlaConfig: (data: any) => client.post('/master/sla-configs', data),
initializeDefaultSlas: () => client.post('/master/sla-configs/initialize'), initializeDefaultSlas: () => client.post('/master/sla-configs/initialize'),
getSlaOperationsDashboard: (params?: { module?: string; breachedOnly?: boolean; mineOnly?: boolean }) =>
client.get('/sla/operations/dashboard', params),
postSlaBatchStatus: (data: { items: Array<{ entityType: string; entityId: string }> }) =>
client.post('/sla/status/batch', data),
getQuestionnaireReminderSettings: () => client.get('/sla/settings/questionnaire-reminder'),
updateQuestionnaireReminderSettings: (data: {
enabled?: boolean;
firstAfterDays?: number;
intervalDays?: number;
maxCount?: number;
}) => client.put('/sla/settings/questionnaire-reminder', data),
// Interview Configs // Interview Configs
getInterviewConfigs: (params?: any) => client.get('/master/interview-configs', params), getInterviewConfigs: (params?: any) => client.get('/master/interview-configs', params),
@ -238,6 +264,21 @@ export const API = {
getFddAssignment: (applicationId: string) => client.get(`/fdd/${applicationId}`), getFddAssignment: (applicationId: string) => client.get(`/fdd/${applicationId}`),
assignFddAgency: (data: any) => client.post('/fdd/assign', data), assignFddAgency: (data: any) => client.post('/fdd/assign', data),
flagNonResponsive: (data: any) => client.post('/flag', data), flagNonResponsive: (data: any) => client.post('/flag', data),
// System Audit Logs (segregated `system_audit_logs` table — Super Admin only)
getSystemAuditLogs: (params?: {
module?: string;
entityType?: string;
entityId?: string;
action?: string;
userId?: string;
search?: string;
dateFrom?: string;
dateTo?: string;
page?: number;
limit?: number;
}) => client.get('/audit/system-logs', params),
getSystemAuditSummary: () => client.get('/audit/system-summary'),
}; };
export default API; export default API;

View File

@ -175,7 +175,7 @@ export function ApprovalPoliciesPage() {
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<h1 className="text-2xl font-bold text-slate-900 flex items-center gap-2"> <h1 className="text-2xl font-bold text-slate-900 flex items-center gap-2">
<Settings2 className="w-6 h-6 text-amber-600" /> <Settings2 className="w-6 h-6 text-re-red" />
Approval Policies Approval Policies
</h1> </h1>
<p className="text-slate-500">Configure stage-level approvers, mode, and minimum approvals.</p> <p className="text-slate-500">Configure stage-level approvers, mode, and minimum approvals.</p>
@ -185,7 +185,7 @@ export function ApprovalPoliciesPage() {
<RefreshCw className={`w-4 h-4 mr-2 ${loading ? 'animate-spin' : ''}`} /> <RefreshCw className={`w-4 h-4 mr-2 ${loading ? 'animate-spin' : ''}`} />
Refresh Refresh
</Button> </Button>
<Button className="bg-amber-600 hover:bg-amber-700" onClick={openCreateModal}> <Button className="bg-re-red hover:bg-re-red-hover" onClick={openCreateModal}>
<Plus className="w-4 h-4 mr-2" /> <Plus className="w-4 h-4 mr-2" />
Add New Policy Add New Policy
</Button> </Button>
@ -197,7 +197,7 @@ export function ApprovalPoliciesPage() {
<CardTitle className="text-lg font-semibold text-slate-800">Configured Stages</CardTitle> <CardTitle className="text-lg font-semibold text-slate-800">Configured Stages</CardTitle>
</CardHeader> </CardHeader>
<CardContent className="p-0"> <CardContent className="p-0">
<div className="overflow-x-auto"> <div className="overflow-x-auto custom-scrollbar-x-slim">
<Table> <Table>
<TableHeader className="bg-slate-50/50"> <TableHeader className="bg-slate-50/50">
<TableRow> <TableRow>
@ -244,7 +244,7 @@ export function ApprovalPoliciesPage() {
<Button <Button
size="sm" size="sm"
variant="ghost" variant="ghost"
className="text-amber-600 hover:text-amber-700 hover:bg-amber-50 h-8 px-2" className="text-re-red hover:text-re-red-hover hover:bg-red-50 h-8 px-2"
onClick={() => openEditModal(policy)} onClick={() => openEditModal(policy)}
> >
<Edit2 className="w-4 h-4 mr-1.5" /> <Edit2 className="w-4 h-4 mr-1.5" />
@ -264,7 +264,7 @@ export function ApprovalPoliciesPage() {
<DialogContent className="sm:max-w-[480px] overflow-visible"> <DialogContent className="sm:max-w-[480px] overflow-visible">
<DialogHeader className="gap-1 pb-2 border-b"> <DialogHeader className="gap-1 pb-2 border-b">
<DialogTitle className="text-base flex items-center gap-2"> <DialogTitle className="text-base flex items-center gap-2">
{isEditMode ? <Edit2 className="w-4 h-4 text-amber-600" /> : <Plus className="w-4 h-4 text-amber-600" />} {isEditMode ? <Edit2 className="w-4 h-4 text-re-red" /> : <Plus className="w-4 h-4 text-re-red" />}
{isEditMode ? 'Edit Policy' : 'Create New Policy'} {isEditMode ? 'Edit Policy' : 'Create New Policy'}
</DialogTitle> </DialogTitle>
<DialogDescription className="text-[11px]"> <DialogDescription className="text-[11px]">
@ -307,7 +307,7 @@ export function ApprovalPoliciesPage() {
))} ))}
<SelectGroup> <SelectGroup>
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<SelectItem value="CUSTOM" className="text-xs font-semibold text-amber-600 italic"> <SelectItem value="CUSTOM" className="text-xs font-semibold text-re-red italic">
+ Enter Custom Stage Code + Enter Custom Stage Code
</SelectItem> </SelectItem>
</SelectGroup> </SelectGroup>
@ -376,10 +376,10 @@ export function ApprovalPoliciesPage() {
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<Button variant="outline" size="sm" className="w-full justify-between h-8 text-[11px] text-slate-500 font-normal border-slate-200"> <Button variant="outline" size="sm" className="w-full justify-between h-8 text-[11px] text-slate-500 font-normal border-slate-200">
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1.5">
<Plus className="w-3 h-3 text-amber-600" /> <Plus className="w-3 h-3 text-re-red" />
<span>Add Roles...</span> <span>Add Roles...</span>
</div> </div>
<Badge variant="secondary" className="bg-amber-50 text-amber-700 border-transparent text-[9px] px-1 h-4"> <Badge variant="secondary" className="bg-red-50 text-re-red-hover border-transparent text-[9px] px-1 h-4">
{draft.requiredRoles.length} {draft.requiredRoles.length}
</Badge> </Badge>
</Button> </Button>
@ -445,7 +445,7 @@ export function ApprovalPoliciesPage() {
<Button variant="outline" size="sm" className="text-xs h-8" onClick={() => setIsModalOpen(false)}> <Button variant="outline" size="sm" className="text-xs h-8" onClick={() => setIsModalOpen(false)}>
Cancel Cancel
</Button> </Button>
<Button className="bg-amber-600 hover:bg-amber-700 h-8 text-xs font-semibold" onClick={savePolicy}> <Button className="bg-re-red hover:bg-re-red-hover h-8 text-xs font-semibold" onClick={savePolicy}>
<Save className="w-3 h-3 mr-1.5" /> <Save className="w-3 h-3 mr-1.5" />
{isEditMode ? 'Save Changes' : 'Create Policy'} {isEditMode ? 'Save Changes' : 'Create Policy'}
</Button> </Button>

View File

@ -204,7 +204,7 @@ const QuestionnaireBuilder: React.FC = () => {
if (fetching) { if (fetching) {
return ( return (
<div className="flex justify-center p-12"> <div className="flex justify-center p-12">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-amber-600"></div> <div className="animate-spin rounded-full h-12 w-12 border-b-2 border-re-red"></div>
</div> </div>
); );
} }
@ -230,7 +230,7 @@ const QuestionnaireBuilder: React.FC = () => {
</div> </div>
<div className="flex flex-wrap items-center gap-4 w-full md:w-auto"> <div className="flex flex-wrap items-center gap-4 w-full md:w-auto">
<div className={`flex items-center gap-2 px-3 py-2 rounded-lg border ${totalWeight === 100 ? 'bg-green-50 border-green-200 text-green-700' : 'bg-amber-50 border-amber-200 text-amber-700'}`}> <div className={`flex items-center gap-2 px-3 py-2 rounded-lg border ${totalWeight === 100 ? 'bg-green-50 border-green-200 text-green-700' : 'bg-red-50 border-red-200 text-re-red-hover'}`}>
{totalWeight !== 100 && <AlertCircle size={16} />} {totalWeight !== 100 && <AlertCircle size={16} />}
<span className="text-sm font-semibold">Total Score: {totalWeight}/100</span> <span className="text-sm font-semibold">Total Score: {totalWeight}/100</span>
</div> </div>
@ -240,13 +240,13 @@ const QuestionnaireBuilder: React.FC = () => {
type="text" type="text"
value={version} value={version}
onChange={(e) => setVersion(e.target.value)} onChange={(e) => setVersion(e.target.value)}
className="border border-slate-300 p-2 rounded-lg w-full md:w-48 text-sm focus:ring-2 focus:ring-amber-500 outline-none" className="border border-slate-300 p-2 rounded-lg w-full md:w-48 text-sm focus:ring-2 focus:ring-red-500 outline-none"
placeholder="Version Name (e.g. v2.0)" placeholder="Version Name (e.g. v2.0)"
/> />
<button <button
onClick={handleSave} onClick={handleSave}
disabled={loading} disabled={loading}
className="bg-amber-600 text-white px-6 py-2 rounded-lg flex items-center gap-2 hover:bg-amber-700 disabled:bg-slate-300 disabled:cursor-not-allowed transition shadow-sm font-medium whitespace-nowrap" className="bg-re-red text-white px-6 py-2 rounded-lg flex items-center gap-2 hover:bg-re-red-hover disabled:bg-slate-300 disabled:cursor-not-allowed transition shadow-sm font-medium whitespace-nowrap"
> >
<Save size={18} /> {loading ? 'Saving...' : 'Publish'} <Save size={18} /> {loading ? 'Saving...' : 'Publish'}
</button> </button>
@ -271,7 +271,7 @@ const QuestionnaireBuilder: React.FC = () => {
type="text" type="text"
value={q.questionText} value={q.questionText}
onChange={(e) => updateQuestion(index, 'questionText', e.target.value)} onChange={(e) => updateQuestion(index, 'questionText', e.target.value)}
className="w-full border border-slate-300 p-2.5 rounded-lg focus:ring-2 focus:ring-amber-500 outline-none transition-shadow" className="w-full border border-slate-300 p-2.5 rounded-lg focus:ring-2 focus:ring-red-500 outline-none transition-shadow"
placeholder="Enter your question here..." placeholder="Enter your question here..."
/> />
</div> </div>
@ -281,7 +281,7 @@ const QuestionnaireBuilder: React.FC = () => {
<select <select
value={q.sectionName} value={q.sectionName}
onChange={(e) => updateQuestion(index, 'sectionName', e.target.value)} onChange={(e) => updateQuestion(index, 'sectionName', e.target.value)}
className="w-full border border-slate-300 p-2.5 rounded-lg focus:ring-2 focus:ring-amber-500 outline-none bg-white" className="w-full border border-slate-300 p-2.5 rounded-lg focus:ring-2 focus:ring-red-500 outline-none bg-white"
> >
{SECTIONS.map(s => <option key={s} value={s}>{s}</option>)} {SECTIONS.map(s => <option key={s} value={s}>{s}</option>)}
</select> </select>
@ -292,16 +292,16 @@ const QuestionnaireBuilder: React.FC = () => {
<select <select
value={q.inputType} value={q.inputType}
onChange={(e) => updateQuestion(index, 'inputType', e.target.value as any)} onChange={(e) => updateQuestion(index, 'inputType', e.target.value as any)}
className="w-full border border-slate-300 p-2.5 rounded-lg focus:ring-2 focus:ring-amber-500 outline-none bg-white" className="w-full border border-slate-300 p-2.5 rounded-lg focus:ring-2 focus:ring-red-500 outline-none bg-white"
> >
<option value="text">Text Input</option> <option value="text">One Liner</option>
<option value="email">Email Address</option> <option value="email">Email Address</option>
<option value="textarea">Long Text (Textarea)</option> <option value="textarea">Paragraph</option>
<option value="number">Numeric</option> <option value="number">Numeric</option>
<option value="file">File Upload</option> <option value="file">File Upload</option>
<option value="yesno">Yes / No</option> <option value="yesno">Yes / No</option>
<option value="select">Multiple Choice (Dropdown)</option> <option value="select">Options (Dropdown)</option>
<option value="radio">Multiple Choice (Radio)</option> <option value="radio">Options (Radio)</option>
</select> </select>
</div> </div>
@ -313,17 +313,17 @@ const QuestionnaireBuilder: React.FC = () => {
type="number" type="number"
value={isNaN(q.weight) ? 0 : q.weight} value={isNaN(q.weight) ? 0 : q.weight}
onChange={(e) => updateQuestion(index, 'weight', parseFloat(e.target.value) || 0)} onChange={(e) => updateQuestion(index, 'weight', parseFloat(e.target.value) || 0)}
className="w-full border border-slate-300 p-2.5 rounded-lg focus:ring-2 focus:ring-amber-500 outline-none pl-3 pr-8" className="w-full border border-slate-300 p-2.5 rounded-lg focus:ring-2 focus:ring-red-500 outline-none pl-3 pr-8"
title="Weightage" title="Weightage"
/> />
<span className="absolute right-3 top-1/2 -translate-y-1/2 text-slate-400 text-xs font-bold">%</span> <span className="absolute right-3 top-1/2 -translate-y-1/2 text-slate-400 text-xs font-bold">%</span>
</div> </div>
</div> </div>
{/* <div className="flex items-center gap-2 px-3 py-2.5 bg-white border border-slate-200 rounded-lg cursor-pointer hover:border-amber-300 transition-colors" {/* <div className="flex items-center gap-2 px-3 py-2.5 bg-white border border-slate-200 rounded-lg cursor-pointer hover:border-red-300 transition-colors"
onClick={() => updateQuestion(index, 'isMandatory', !q.isMandatory)} onClick={() => updateQuestion(index, 'isMandatory', !q.isMandatory)}
> >
<div className={`w-4 h-4 rounded border flex items-center justify-center transition-colors ${q.isMandatory ? 'bg-amber-600 border-amber-600' : 'border-slate-300'}`}> <div className={`w-4 h-4 rounded border flex items-center justify-center transition-colors ${q.isMandatory ? 'bg-re-red border-re-red' : 'border-slate-300'}`}>
{q.isMandatory && <div className="w-2 h-2 bg-white rounded-sm" />} {q.isMandatory && <div className="w-2 h-2 bg-white rounded-sm" />}
</div> </div>
<span className="text-xs font-medium text-slate-600 select-none">Req.</span> <span className="text-xs font-medium text-slate-600 select-none">Req.</span>
@ -345,7 +345,7 @@ const QuestionnaireBuilder: React.FC = () => {
type="text" type="text"
value={opt.text} value={opt.text}
onChange={(e) => updateOption(index, optIndex, 'text', e.target.value)} onChange={(e) => updateOption(index, optIndex, 'text', e.target.value)}
className="flex-1 border border-slate-300 p-2 rounded-md text-sm focus:ring-1 focus:ring-amber-500 outline-none" className="flex-1 border border-slate-300 p-2 rounded-md text-sm focus:ring-1 focus:ring-red-500 outline-none"
placeholder={`Option ${optIndex + 1}`} placeholder={`Option ${optIndex + 1}`}
/> />
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
@ -356,7 +356,7 @@ const QuestionnaireBuilder: React.FC = () => {
max={isNaN(q.weight) ? 0 : q.weight} max={isNaN(q.weight) ? 0 : q.weight}
min={0} min={0}
onChange={(e) => updateOption(index, optIndex, 'score', e.target.value)} onChange={(e) => updateOption(index, optIndex, 'score', e.target.value)}
className={`w-20 border ${(opt.score > q.weight) || isNaN(opt.score) ? 'border-red-500 text-red-600' : 'border-slate-300'} p-2 rounded-md text-sm focus:ring-1 focus:ring-amber-500 outline-none`} className={`w-20 border ${(opt.score > q.weight) || isNaN(opt.score) ? 'border-red-500 text-red-600' : 'border-slate-300'} p-2 rounded-md text-sm focus:ring-1 focus:ring-red-500 outline-none`}
/> />
</div> </div>
<button <button
@ -371,7 +371,7 @@ const QuestionnaireBuilder: React.FC = () => {
</div> </div>
<button <button
onClick={() => addOption(index)} onClick={() => addOption(index)}
className="mt-3 text-sm flex items-center gap-1 text-amber-600 hover:text-amber-700 font-medium" className="mt-3 text-sm flex items-center gap-1 text-re-red hover:text-re-red-hover font-medium"
> >
<Plus size={16} /> Add Option <Plus size={16} /> Add Option
</button> </button>
@ -392,7 +392,7 @@ const QuestionnaireBuilder: React.FC = () => {
<button <button
onClick={addQuestion} onClick={addQuestion}
className="mt-8 w-full border-2 border-dashed border-slate-300 p-4 rounded-xl text-slate-500 hover:border-amber-500 hover:text-amber-600 hover:bg-amber-50/30 flex justify-center items-center gap-2 transition-all font-medium" className="mt-8 w-full border-2 border-dashed border-slate-300 p-4 rounded-xl text-slate-500 hover:border-red-500 hover:text-re-red hover:bg-red-50/30 flex justify-center items-center gap-2 transition-all font-medium"
> >
<Plus size={20} /> Add Another Question <Plus size={20} /> Add Another Question
</button> </button>

View File

@ -47,7 +47,7 @@ const QuestionnaireList: React.FC = () => {
</div> </div>
<button <button
onClick={() => navigate('/questionnaire-builder')} onClick={() => navigate('/questionnaire-builder')}
className="bg-amber-600 text-white px-4 py-2 rounded-lg flex items-center gap-2 hover:bg-amber-700 transition" className="bg-re-red text-white px-4 py-2 rounded-lg flex items-center gap-2 hover:bg-re-red-hover transition"
> >
<Plus size={20} /> Create New Version <Plus size={20} /> Create New Version
</button> </button>
@ -55,14 +55,14 @@ const QuestionnaireList: React.FC = () => {
{loading ? ( {loading ? (
<div className="flex justify-center p-12"> <div className="flex justify-center p-12">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-amber-600"></div> <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-re-red"></div>
</div> </div>
) : versions.length === 0 ? ( ) : versions.length === 0 ? (
<div className="text-center p-12 bg-white rounded-lg shadow-sm border border-slate-200"> <div className="text-center p-12 bg-white rounded-lg shadow-sm border border-slate-200">
<p className="text-slate-500 mb-4">No questionnaire versions found.</p> <p className="text-slate-500 mb-4">No questionnaire versions found.</p>
<button <button
onClick={() => navigate('/questionnaire-builder')} onClick={() => navigate('/questionnaire-builder')}
className="text-amber-600 font-medium hover:underline" className="text-re-red font-medium hover:underline"
> >
Create your first version Create your first version
</button> </button>
@ -102,7 +102,7 @@ const QuestionnaireList: React.FC = () => {
<td className="p-4 text-right"> <td className="p-4 text-right">
<button <button
onClick={() => navigate(`/questionnaire-builder/${v.id}`)} onClick={() => navigate(`/questionnaire-builder/${v.id}`)}
className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded text-sm font-medium text-slate-600 hover:text-amber-600 hover:bg-amber-50 transition border border-slate-200 hover:border-amber-200" className="inline-flex items-center gap-1.5 px-3 py-1.5 rounded text-sm font-medium text-slate-600 hover:text-re-red hover:bg-red-50 transition border border-slate-200 hover:border-red-200"
> >
<Edit2 size={14} /> Edit / Clone <Edit2 size={14} /> Edit / Clone
</button> </button>

View File

@ -219,7 +219,7 @@ export function UserManagementPage() {
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4"> <div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
<div> <div>
<h1 className="text-2xl font-bold text-slate-900 flex items-center gap-2"> <h1 className="text-2xl font-bold text-slate-900 flex items-center gap-2">
<Users className="w-6 h-6 text-amber-600" /> <Users className="w-6 h-6 text-re-red" />
User Management User Management
</h1> </h1>
<p className="text-slate-500">Manage system users, roles, and access permissions.</p> <p className="text-slate-500">Manage system users, roles, and access permissions.</p>
@ -232,7 +232,7 @@ export function UserManagementPage() {
zoneId: '', regionId: '', stateId: '', districtId: '' zoneId: '', regionId: '', stateId: '', districtId: ''
}); setShowUserModal(true); }); setShowUserModal(true);
}} }}
className="bg-amber-600 hover:bg-amber-700 text-white shrink-0" className="bg-re-red hover:bg-re-red-hover text-white shrink-0"
> >
<UserPlus className="w-4 h-4 mr-2" /> <UserPlus className="w-4 h-4 mr-2" />
Add New User Add New User
@ -318,7 +318,7 @@ export function UserManagementPage() {
<TableRow key={user.id} className="hover:bg-slate-50/50"> <TableRow key={user.id} className="hover:bg-slate-50/50">
<TableCell> <TableCell>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-full bg-amber-100 text-amber-700 flex items-center justify-center font-bold"> <div className="w-10 h-10 rounded-full bg-red-100 text-re-red-hover flex items-center justify-center font-bold">
{user.fullName?.charAt(0) || user.email?.charAt(0)} {user.fullName?.charAt(0) || user.email?.charAt(0)}
</div> </div>
<div> <div>
@ -384,7 +384,7 @@ export function UserManagementPage() {
<TableCell className="text-right"> <TableCell className="text-right">
<div className="flex justify-end gap-2"> <div className="flex justify-end gap-2">
<Button variant="ghost" size="icon" onClick={() => handleEditUser(user)}> <Button variant="ghost" size="icon" onClick={() => handleEditUser(user)}>
<Edit2 className="w-4 h-4 text-slate-400 hover:text-amber-600" /> <Edit2 className="w-4 h-4 text-slate-400 hover:text-re-red" />
</Button> </Button>
<Button variant="ghost" size="icon"> <Button variant="ghost" size="icon">
<Trash2 className="w-4 h-4 text-slate-400 hover:text-red-600" /> <Trash2 className="w-4 h-4 text-slate-400 hover:text-red-600" />
@ -569,7 +569,7 @@ export function UserManagementPage() {
<DialogFooter className="bg-slate-50 -mx-6 -mb-6 p-4 rounded-b-lg"> <DialogFooter className="bg-slate-50 -mx-6 -mb-6 p-4 rounded-b-lg">
<Button variant="outline" onClick={() => setShowUserModal(false)}>Cancel</Button> <Button variant="outline" onClick={() => setShowUserModal(false)}>Cancel</Button>
<Button className="bg-amber-600 hover:bg-amber-700 text-white" onClick={handleSubmit}> <Button className="bg-re-red hover:bg-re-red-hover text-white" onClick={handleSubmit}>
{editingUser ? 'Save Changes' : 'Create User'} {editingUser ? 'Save Changes' : 'Create User'}
</Button> </Button>
</DialogFooter> </DialogFooter>

View File

@ -198,8 +198,8 @@ const QuestionnaireForm: React.FC<QuestionnaireFormProps> = ({
file:mr-4 file:py-2 file:px-4 file:mr-4 file:py-2 file:px-4
file:rounded-full file:border-0 file:rounded-full file:border-0
file:text-sm file:font-semibold file:text-sm file:font-semibold
file:bg-amber-50 file:text-amber-700 file:bg-red-50 file:text-re-red
hover:file:bg-amber-100" hover:file:bg-red-100"
onChange={(e) => handleFileChange(q.id, e)} onChange={(e) => handleFileChange(q.id, e)}
disabled={readOnly} disabled={readOnly}
/> />
@ -240,7 +240,7 @@ const QuestionnaireForm: React.FC<QuestionnaireFormProps> = ({
checked={responses[q.id] === val} checked={responses[q.id] === val}
onChange={() => handleInputChange(q.id, val)} onChange={() => handleInputChange(q.id, val)}
disabled={readOnly} disabled={readOnly}
className="text-amber-600 focus:ring-amber-500 w-4 h-4" className="text-re-red focus:ring-re-red w-4 h-4"
/> />
<span className="text-gray-700">{val}</span> <span className="text-gray-700">{val}</span>
</label> </label>

View File

@ -18,10 +18,12 @@ import { formatDistanceToNow } from 'date-fns';
interface HeaderProps { interface HeaderProps {
title: string; title: string;
/** Context line under the title; changes per route in App layout. */
subtitle: string;
onRefresh?: () => void; onRefresh?: () => void;
} }
export function Header({ title, onRefresh }: HeaderProps) { export function Header({ title, subtitle, onRefresh }: HeaderProps) {
const { user: currentUser } = useSelector((state: RootState) => state.auth); const { user: currentUser } = useSelector((state: RootState) => state.auth);
const { socket } = useSocket(); const { socket } = useSocket();
const [notifications, setNotifications] = useState<Notification[]>([]); const [notifications, setNotifications] = useState<Notification[]>([]);
@ -101,7 +103,7 @@ export function Header({ title, onRefresh }: HeaderProps) {
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<h1 className="text-slate-900">{title}</h1> <h1 className="text-slate-900">{title}</h1>
<p className="text-slate-600">Manage and track dealership applications</p> <p className="text-slate-600 text-sm leading-snug max-w-3xl">{subtitle}</p>
</div> </div>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">

View File

@ -14,7 +14,8 @@ import {
RefreshCcw, RefreshCcw,
MapPin, MapPin,
ClipboardList, ClipboardList,
ListChecks ListChecks,
Activity
} from 'lucide-react'; } from 'lucide-react';
import { useState, useRef, useCallback, useEffect } from 'react'; import { useState, useRef, useCallback, useEffect } from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
@ -46,13 +47,16 @@ export function Sidebar({ onLogout }: SidebarProps) {
const [flyout, setFlyout] = useState<FlyoutState | null>(null); const [flyout, setFlyout] = useState<FlyoutState | null>(null);
const hoverTimeout = useRef<ReturnType<typeof setTimeout> | null>(null); const hoverTimeout = useRef<ReturnType<typeof setTimeout> | null>(null);
const currentRole = currentUser?.role || currentUser?.roleCode || ''; const hasRole = (roles: string[]) => {
const normalizedRole = String(currentRole).trim().toLowerCase(); const normalizedTargetRoles = roles.map((r) => r.toLowerCase());
const hasRole = (roles: string[]) => roles.map((r) => r.toLowerCase()).includes(normalizedRole); const userRole = String(currentUser?.role || '').toLowerCase();
const userRoleCode = String(currentUser?.roleCode || '').toLowerCase();
return normalizedTargetRoles.includes(userRole) || normalizedTargetRoles.includes(userRoleCode);
};
const resignationRoles = ['DD Admin', 'ASM', 'RBM', 'DD Lead', 'ZBH', 'NBH', 'Legal', 'Legal Admin', 'Super Admin']; const resignationRoles = ['DD Admin', 'DD_ADMIN', 'ASM', 'RBM', 'DD-ZM', 'DD_ZM', 'DD Lead', 'DD_LEAD', 'DD Head', 'DD_HEAD', 'ZBH', 'NBH', 'Legal', 'Legal Admin', 'LEGAL_ADMIN', 'Super Admin', 'SUPER_ADMIN'];
const terminationRoles = ['ASM', 'RBM', 'ZBH', 'DD Lead', 'DD Head', 'NBH', 'Legal Admin', 'Legal', 'DD Admin', 'CCO', 'CEO', 'Super Admin']; const terminationRoles = ['ASM', 'RBM', 'DD-ZM', 'DD_ZM', 'ZBH', 'DD Lead', 'DD_LEAD', 'DD Head', 'DD_HEAD', 'NBH', 'Legal Admin', 'LEGAL_ADMIN', 'Legal', 'DD Admin', 'DD_ADMIN', 'CCO', 'CEO', 'Super Admin', 'SUPER_ADMIN'];
const fnfRoles = ['DD Admin', 'DD Lead', 'NBH', 'Finance', 'Finance Admin', 'Super Admin']; const fnfRoles = ['DD Admin', 'DD_ADMIN', 'DD-ZM', 'DD_ZM', 'DD Lead', 'DD_LEAD', 'DD Head', 'DD_HEAD', 'NBH', 'Finance', 'Finance Admin', 'FINANCE_ADMIN', 'Super Admin', 'SUPER_ADMIN'];
const canSeeResignation = hasRole(resignationRoles); const canSeeResignation = hasRole(resignationRoles);
const canSeeTermination = hasRole(terminationRoles); const canSeeTermination = hasRole(terminationRoles);
const canSeeFnF = hasRole(fnfRoles); const canSeeFnF = hasRole(fnfRoles);
@ -111,6 +115,7 @@ export function Sidebar({ onLogout }: SidebarProps) {
menuItems.push({ id: 'users', label: 'User Management', icon: Users }); menuItems.push({ id: 'users', label: 'User Management', icon: Users });
menuItems.push({ id: 'questionnaires', label: 'Questionnaire Templates', icon: ClipboardList }); menuItems.push({ id: 'questionnaires', label: 'Questionnaire Templates', icon: ClipboardList });
menuItems.push({ id: 'interview-configs', label: 'Interview Configs', icon: ListChecks }); menuItems.push({ id: 'interview-configs', label: 'Interview Configs', icon: ListChecks });
menuItems.push({ id: 'system-logs', label: 'System Logs', icon: Activity });
} }
const handleSearch = (e: React.FormEvent) => { const handleSearch = (e: React.FormEvent) => {
@ -175,7 +180,7 @@ export function Sidebar({ onLogout }: SidebarProps) {
<div className="flex flex-col min-w-0"> <div className="flex flex-col min-w-0">
<img src="/assets/images/Re_Logo.png" alt="Royal Enfield" className="h-6 w-auto" /> <img src="/assets/images/Re_Logo.png" alt="Royal Enfield" className="h-6 w-auto" />
<span className="text-[10px] uppercase tracking-[0.2em] font-bold text-slate-400 mt-1 whitespace-nowrap"> <span className="text-[10px] uppercase tracking-[0.2em] font-bold text-slate-400 mt-1 whitespace-nowrap">
Dealer Onboarding Dealer Network
</span> </span>
</div> </div>
<button <button

View File

@ -65,6 +65,20 @@ export function ApplicationFormPage({ onAdminLogin }: ApplicationFormPageProps)
} }
}; };
const isEmailValid = (email: string) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
const isMobileValid = (mobile: string) => /^[0-9]{10}$/.test(mobile);
const isPincodeValid = (pincode: string) => /^[0-9]{6}$/.test(pincode);
const isFormValid = Boolean(
formData.country && formData.stateId && formData.districtId && formData.name &&
formData.interestedCity && formData.email && formData.pincode && formData.mobile &&
formData.ownRoyalEnfield && formData.age && formData.education &&
formData.companyName && formData.source && formData.existingDealer &&
formData.description && formData.address && formData.acceptTerms &&
otpVerified && isEmailValid(formData.email) && isMobileValid(formData.mobile) && isPincodeValid(formData.pincode) &&
(formData.ownRoyalEnfield === 'no' || (formData.ownRoyalEnfield === 'yes' && formData.royalEnfieldModel))
);
const handleVerifyMobile = () => { const handleVerifyMobile = () => {
if (!formData.mobile || formData.mobile.length < 10) { if (!formData.mobile || formData.mobile.length < 10) {
toast.error('Please enter a valid mobile number'); toast.error('Please enter a valid mobile number');
@ -93,6 +107,11 @@ export function ApplicationFormPage({ onAdminLogin }: ApplicationFormPageProps)
return; return;
} }
if (formData.ownRoyalEnfield === 'yes' && !formData.royalEnfieldModel) {
toast.error('Please select your motorcycle model');
return;
}
if (!formData.acceptTerms) { if (!formData.acceptTerms) {
toast.error('Please accept the terms and conditions'); toast.error('Please accept the terms and conditions');
return; return;
@ -145,10 +164,10 @@ export function ApplicationFormPage({ onAdminLogin }: ApplicationFormPageProps)
}; };
const reModels = [ const reModels = [
"Classic 650", "Scram 440", "Goan Classic 350", "Bear 650", "Guerrilla 450", "Continental GT", "Interceptor 650", "Himalayan", "Classic 350",
"Shotgun 650", "Himalayan 450", "Bullet 350", "Super Meteor 650", "Hunter 350", "Classic 500", "Thunderbird 350", "Thunderbird 500", "Thunderbird X 350",
"Scram 411", "Meteor 350", "Interceptor INT 650", "Continental GT 650", "Thunderbird X 500", "Bullet 350", "Bullet 500", "Bullet ES",
"Classic 350", "Other Royal Enfield motorcycle" "Bullet Trials 350", "Bullet Trials 500", "Other Royal Enfield motorcycle"
]; ];
const sourceOptions = [ const sourceOptions = [
@ -257,24 +276,35 @@ export function ApplicationFormPage({ onAdminLogin }: ApplicationFormPageProps)
onChange={(e) => setFormData({...formData, interestedCity: e.target.value})} onChange={(e) => setFormData({...formData, interestedCity: e.target.value})}
/> />
<Input <Input
type="email"
placeholder="Email Id*" placeholder="Email Id*"
className="h-[44px] border-[#cccccc] rounded-none px-4 text-[14px] focus-visible:ring-1 focus-visible:ring-black placeholder:text-[#999999]" className="h-[44px] border-[#cccccc] rounded-none px-4 text-[14px] focus-visible:ring-1 focus-visible:ring-black placeholder:text-[#999999]"
value={formData.email} value={formData.email}
onChange={(e) => setFormData({...formData, email: e.target.value})} onChange={(e) => setFormData({...formData, email: e.target.value})}
/> />
<Input <Input
type="text"
maxLength={6}
placeholder="Pincode*" placeholder="Pincode*"
className="h-[44px] border-[#cccccc] rounded-none px-4 text-[14px] focus-visible:ring-1 focus-visible:ring-black placeholder:text-[#999999]" className="h-[44px] border-[#cccccc] rounded-none px-4 text-[14px] focus-visible:ring-1 focus-visible:ring-black placeholder:text-[#999999]"
value={formData.pincode} value={formData.pincode}
onChange={(e) => setFormData({...formData, pincode: e.target.value})} onChange={(e) => {
const val = e.target.value.replace(/\D/g, '');
setFormData({...formData, pincode: val});
}}
/> />
<div className="relative"> <div className="relative">
<Input <Input
type="text"
maxLength={10}
placeholder="Mobile No.*" placeholder="Mobile No.*"
className="h-[44px] border-[#cccccc] rounded-none px-4 text-[14px] focus-visible:ring-1 focus-visible:ring-black placeholder:text-[#999999]" className="h-[44px] border-[#cccccc] rounded-none px-4 text-[14px] focus-visible:ring-1 focus-visible:ring-black placeholder:text-[#999999]"
value={formData.mobile} value={formData.mobile}
onChange={(e) => setFormData({...formData, mobile: e.target.value})} onChange={(e) => {
const val = e.target.value.replace(/\D/g, '');
setFormData({...formData, mobile: val});
}}
/> />
{!otpVerified ? ( {!otpVerified ? (
<button <button
@ -301,7 +331,13 @@ export function ApplicationFormPage({ onAdminLogin }: ApplicationFormPageProps)
type="radio" type="radio"
className="hidden" className="hidden"
checked={formData.ownRoyalEnfield === val} checked={formData.ownRoyalEnfield === val}
onChange={() => setFormData({...formData, ownRoyalEnfield: val})} onChange={() => {
setFormData({
...formData,
ownRoyalEnfield: val,
royalEnfieldModel: val === 'no' ? '' : formData.royalEnfieldModel
});
}}
/> />
<span className="text-[14px] capitalize">{val}</span> <span className="text-[14px] capitalize">{val}</span>
</label> </label>
@ -316,18 +352,19 @@ export function ApplicationFormPage({ onAdminLogin }: ApplicationFormPageProps)
onChange={(e) => setFormData({...formData, age: e.target.value})} onChange={(e) => setFormData({...formData, age: e.target.value})}
/> />
<div className="relative"> {formData.ownRoyalEnfield === 'yes' && (
<select <div className="relative">
className="w-full h-[44px] px-4 border border-[#cccccc] appearance-none bg-white text-[14px] outline-none disabled:bg-slate-50" <select
value={formData.royalEnfieldModel} className="w-full h-[44px] px-4 border border-[#cccccc] appearance-none bg-white text-[14px] outline-none"
disabled={formData.ownRoyalEnfield !== 'yes'} value={formData.royalEnfieldModel}
onChange={(e) => setFormData({...formData, royalEnfieldModel: e.target.value})} onChange={(e) => setFormData({...formData, royalEnfieldModel: e.target.value})}
> >
<option value="">Motorcycle Owned</option> <option value="">Select Motorcycle*</option>
{reModels.map(m => <option key={m} value={m}>{m}</option>)} {reModels.map(m => <option key={m} value={m}>{m}</option>)}
</select> </select>
<ChevronDown className="absolute right-4 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-500 pointer-events-none" /> <ChevronDown className="absolute right-4 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-500 pointer-events-none" />
</div> </div>
)}
<Input <Input
placeholder="Education Qualification*" placeholder="Education Qualification*"
@ -406,14 +443,15 @@ export function ApplicationFormPage({ onAdminLogin }: ApplicationFormPageProps)
onCheckedChange={(checked) => setFormData({...formData, acceptTerms: checked as boolean})} onCheckedChange={(checked) => setFormData({...formData, acceptTerms: checked as boolean})}
/> />
<label htmlFor="terms" className="text-[14px] font-medium cursor-pointer"> <label htmlFor="terms" className="text-[14px] font-medium cursor-pointer">
I accept the <b>terms and conditions</b> as well as <b>privacy policy</b>. I accept the <b>terms and conditions</b> as well as <b>privacy policy</b>.<span className="text-red-500">*</span>
</label> </label>
</div> </div>
</div> </div>
<button <button
type="submit" type="submit"
className="h-12 px-10 bg-black text-white flex items-center gap-3 hover:bg-slate-900 transition-colors" disabled={!isFormValid}
className="h-12 px-10 bg-black text-white flex items-center gap-3 hover:bg-slate-900 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
> >
<span className="font-bold uppercase tracking-wider text-[14px]">Submit</span> <span className="font-bold uppercase tracking-wider text-[14px]">Submit</span>
<ChevronRight className="w-4 h-4" /> <ChevronRight className="w-4 h-4" />

View File

@ -0,0 +1,33 @@
import { Badge } from '@/components/ui/badge';
import { SlaBucket, SlaStatusSnapshot } from '@/services/sla.service';
const BUCKET_CLASS: Record<SlaBucket, string> = {
healthy: 'bg-emerald-100 text-emerald-800 border-emerald-200',
warning: 'bg-red-50 text-red-800 border-red-200',
critical: 'bg-orange-100 text-orange-800 border-orange-200',
breached: 'bg-red-100 text-red-800 border-red-200'
};
const BUCKET_LABEL: Record<SlaBucket, string> = {
healthy: 'On track',
warning: 'Due soon',
critical: 'At risk',
breached: 'Breached'
};
export function SlaBadge({ status, compact }: { status: SlaStatusSnapshot | null | undefined; compact?: boolean }) {
if (!status) return null;
const bucket = status.isPaused ? 'warning' : status.bucket;
const label = status.isPaused ? 'Paused' : BUCKET_LABEL[status.bucket];
return (
<Badge
variant="outline"
className={`text-[10px] font-semibold ${BUCKET_CLASS[bucket]} ${compact ? 'px-1.5' : ''}`}
title={`${status.stageName} · ${status.remainingLabel} (${status.percentUsed}% of TAT)`}
>
{compact ? label : `${label} · ${status.remainingLabel}`}
</Badge>
);
}

View File

@ -38,8 +38,8 @@ export const DocumentPreviewModal: React.FC<DocumentPreviewModalProps> = ({
<> <>
<div className="flex items-center justify-between p-4 border-b bg-slate-50"> <div className="flex items-center justify-between p-4 border-b bg-slate-50">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-lg bg-amber-100 flex items-center justify-center border border-amber-200"> <div className="w-10 h-10 rounded-lg bg-red-50 flex items-center justify-center border border-red-200">
<Eye className="w-5 h-5 text-amber-600" /> <Eye className="w-5 h-5 text-re-red" />
</div> </div>
<div> <div>
<DialogTitle className="text-sm font-bold text-slate-900 leading-none mb-1"> <DialogTitle className="text-sm font-bold text-slate-900 leading-none mb-1">

View File

@ -7,9 +7,12 @@ import { cn } from "./utils";
function Progress({ function Progress({
className, className,
indicatorClassName,
value, value,
...props ...props
}: React.ComponentProps<typeof ProgressPrimitive.Root>) { }: React.ComponentProps<typeof ProgressPrimitive.Root> & {
indicatorClassName?: string;
}) {
return ( return (
<ProgressPrimitive.Root <ProgressPrimitive.Root
data-slot="progress" data-slot="progress"
@ -21,7 +24,7 @@ function Progress({
> >
<ProgressPrimitive.Indicator <ProgressPrimitive.Indicator
data-slot="progress-indicator" data-slot="progress-indicator"
className="bg-primary h-full w-full flex-1 transition-all" className={cn("bg-primary h-full w-full flex-1 transition-all", indicatorClassName)}
style={{ transform: `translateX(-${100 - (value || 0)}%)` }} style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
/> />
</ProgressPrimitive.Root> </ProgressPrimitive.Root>

View File

@ -42,14 +42,18 @@ function ScrollBar({
orientation === "vertical" && orientation === "vertical" &&
"h-full w-2.5 border-l border-l-transparent", "h-full w-2.5 border-l border-l-transparent",
orientation === "horizontal" && orientation === "horizontal" &&
"h-2.5 flex-col border-t border-t-transparent", "h-1 flex-col border-t border-t-transparent",
className, className,
)} )}
{...props} {...props}
> >
<ScrollAreaPrimitive.ScrollAreaThumb <ScrollAreaPrimitive.ScrollAreaThumb
data-slot="scroll-area-thumb" data-slot="scroll-area-thumb"
className="bg-border relative flex-1 rounded-full" className={cn(
"relative flex-1 rounded-full",
orientation === "vertical" && "bg-border",
orientation === "horizontal" && "bg-slate-200/70"
)}
/> />
</ScrollAreaPrimitive.ScrollAreaScrollbar> </ScrollAreaPrimitive.ScrollAreaScrollbar>
); );

View File

@ -8,7 +8,7 @@ function Table({ className, ...props }: React.ComponentProps<"table">) {
return ( return (
<div <div
data-slot="table-container" data-slot="table-container"
className="relative w-full overflow-x-auto" className="relative w-full overflow-x-auto custom-scrollbar-x-slim"
> >
<table <table
data-slot="table" data-slot="table"

View File

@ -0,0 +1,612 @@
import { useCallback, useEffect, useMemo, useState } from 'react';
import {
Activity,
RefreshCw,
Search,
ChevronLeft,
ChevronRight,
Eye,
User as UserIcon,
Filter
} from 'lucide-react';
import { toast } from 'sonner';
import { API } from '@/api/API';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Input } from '@/components/ui/input';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue
} from '@/components/ui/select';
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle
} from '@/components/ui/dialog';
type SystemAuditLog = {
id: string;
module: string;
moduleLabel: string;
action: string;
actionLabel: string;
entityType: string;
entityId: string | null;
entityLabel: string | null;
description: string;
actor: {
id: string | null;
name: string;
role: string | null;
email: string | null;
};
oldData: any;
newData: any;
metadata: any;
ipAddress: string | null;
userAgent: string | null;
timestamp: string;
};
type Summary = {
totalEntries: number;
byModule: { module: string; moduleLabel: string; total: number }[];
lastActivity: SystemAuditLog | null;
};
const MODULE_OPTIONS = [
{ value: '__all__', label: 'All Modules' },
{ value: 'QUESTIONNAIRE', label: 'Questionnaire' },
{ value: 'INTERVIEW_CONFIG', label: 'Interview Configuration' },
{ value: 'SYSTEM_CONFIG', label: 'System Configuration' },
{ value: 'SLA_CONFIG', label: 'SLA Configuration' },
{ value: 'EMAIL_TEMPLATE', label: 'Email Template' },
{ value: 'MASTER_HIERARCHY', label: 'Master Hierarchy' },
{ value: 'ROLE_ASSIGNMENT', label: 'Role Assignment' },
{ value: 'USER_ADMIN', label: 'User Administration' },
{ value: 'DEALER_MAPPING', label: 'Dealer Mapping' }
];
const ACTION_OPTIONS = [
{ value: '__all__', label: 'All Actions' },
{ value: 'CREATED', label: 'Created' },
{ value: 'UPDATED', label: 'Updated' },
{ value: 'DELETED', label: 'Deleted' },
{ value: 'ACTIVATED', label: 'Activated' },
{ value: 'DEACTIVATED', label: 'Deactivated' },
{ value: 'INITIALIZED', label: 'Initialized' },
{ value: 'SUBMITTED', label: 'Submitted' },
{ value: 'ASSIGNED', label: 'Assigned' },
{ value: 'UNASSIGNED', label: 'Unassigned' },
{ value: 'REORDERED', label: 'Reordered' }
];
const ACTION_BADGE_CLASS: Record<string, string> = {
CREATED: 'bg-emerald-100 text-emerald-700 border-emerald-200',
UPDATED: 'bg-blue-100 text-blue-700 border-blue-200',
DELETED: 'bg-rose-100 text-rose-700 border-rose-200',
ACTIVATED: 'bg-emerald-100 text-emerald-700 border-emerald-200',
DEACTIVATED: 'bg-slate-200 text-slate-700 border-slate-300',
INITIALIZED: 'bg-red-50 text-re-red-hover border-red-200',
SUBMITTED: 'bg-indigo-100 text-indigo-700 border-indigo-200',
ASSIGNED: 'bg-violet-100 text-violet-700 border-violet-200',
UNASSIGNED: 'bg-slate-200 text-slate-700 border-slate-300',
REORDERED: 'bg-sky-100 text-sky-700 border-sky-200'
};
const formatTimestamp = (ts: string) => {
try {
const d = new Date(ts);
return d.toLocaleString('en-IN', {
day: '2-digit',
month: 'short',
year: 'numeric',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
});
} catch {
return ts;
}
};
const JsonBlock = ({ value }: { value: any }) => {
if (value === null || value === undefined) {
return <p className="text-xs italic text-slate-400"></p>;
}
return (
<pre className="text-xs bg-slate-900 text-slate-100 rounded-lg p-3 overflow-auto max-h-72 whitespace-pre-wrap break-words">
{JSON.stringify(value, null, 2)}
</pre>
);
};
export const SystemLogsPage: React.FC = () => {
const [logs, setLogs] = useState<SystemAuditLog[]>([]);
const [summary, setSummary] = useState<Summary | null>(null);
const [loading, setLoading] = useState(false);
const [page, setPage] = useState(1);
const [limit] = useState(25);
const [totalPages, setTotalPages] = useState(1);
const [totalEntries, setTotalEntries] = useState(0);
const [moduleFilter, setModuleFilter] = useState<string>('__all__');
const [actionFilter, setActionFilter] = useState<string>('__all__');
const [search, setSearch] = useState('');
const [dateFrom, setDateFrom] = useState('');
const [dateTo, setDateTo] = useState('');
const [appliedSearch, setAppliedSearch] = useState('');
const [selected, setSelected] = useState<SystemAuditLog | null>(null);
const queryParams = useMemo(
() => ({
module: moduleFilter !== '__all__' ? moduleFilter : undefined,
action: actionFilter !== '__all__' ? actionFilter : undefined,
search: appliedSearch || undefined,
dateFrom: dateFrom || undefined,
dateTo: dateTo || undefined,
page,
limit
}),
[moduleFilter, actionFilter, appliedSearch, dateFrom, dateTo, page, limit]
);
const fetchLogs = useCallback(async () => {
setLoading(true);
try {
const res = (await API.getSystemAuditLogs(queryParams)) as any;
const body = res?.data;
if (res?.ok && body?.success) {
setLogs(body.data || []);
setTotalPages(body.pagination?.totalPages || 1);
setTotalEntries(body.pagination?.total || 0);
} else {
toast.error(body?.message || 'Unable to load system logs');
setLogs([]);
}
} catch (err) {
console.error('[SystemLogsPage] fetchLogs error:', err);
toast.error('Failed to load system logs');
setLogs([]);
} finally {
setLoading(false);
}
}, [queryParams]);
const fetchSummary = useCallback(async () => {
try {
const res = (await API.getSystemAuditSummary()) as any;
const body = res?.data;
if (res?.ok && body?.success) {
setSummary(body.data || null);
}
} catch (err) {
console.error('[SystemLogsPage] fetchSummary error:', err);
}
}, []);
useEffect(() => {
fetchLogs();
}, [fetchLogs]);
useEffect(() => {
fetchSummary();
}, [fetchSummary]);
const applySearch = () => {
setPage(1);
setAppliedSearch(search.trim());
};
const clearFilters = () => {
setModuleFilter('__all__');
setActionFilter('__all__');
setSearch('');
setAppliedSearch('');
setDateFrom('');
setDateTo('');
setPage(1);
};
return (
<div className="space-y-6 max-w-7xl mx-auto">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-3">
<div>
<h1 className="text-2xl font-bold text-slate-900 flex items-center gap-2">
<Activity className="w-6 h-6 text-re-red" />
System Activity Logs
</h1>
<p className="text-slate-500">
Configuration-level changes across questionnaires, interview configs, master hierarchy, system / SLA configs, and role assignments.
</p>
</div>
<Button
variant="outline"
onClick={() => {
fetchLogs();
fetchSummary();
}}
disabled={loading}
>
<RefreshCw className={`w-4 h-4 mr-2 ${loading ? 'animate-spin' : ''}`} />
Refresh
</Button>
</div>
{/* Summary */}
<div className="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-5 gap-3">
<Card className="border-slate-200">
<CardHeader className="pb-2">
<CardTitle className="text-xs font-bold text-slate-500 uppercase tracking-wider">
Total Events
</CardTitle>
</CardHeader>
<CardContent>
<p className="text-2xl font-bold text-slate-900">{summary?.totalEntries ?? '—'}</p>
<p className="text-[11px] text-slate-400 mt-1">Lifetime</p>
</CardContent>
</Card>
{(summary?.byModule || []).slice(0, 4).map((row) => (
<Card key={row.module} className="border-slate-200">
<CardHeader className="pb-2">
<CardTitle className="text-xs font-bold text-slate-500 uppercase tracking-wider">
{row.moduleLabel}
</CardTitle>
</CardHeader>
<CardContent>
<p className="text-2xl font-bold text-slate-900">{row.total}</p>
<button
onClick={() => {
setModuleFilter(row.module);
setPage(1);
}}
className="text-[11px] text-re-red hover:underline mt-1"
>
View module
</button>
</CardContent>
</Card>
))}
</div>
{/* Filters */}
<Card className="border-slate-200">
<CardContent className="pt-6">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-6 gap-3 items-end">
<div className="lg:col-span-2">
<label className="text-xs font-semibold text-slate-600 mb-1 block flex items-center gap-1">
<Search className="w-3 h-3" /> Search
</label>
<div className="flex gap-2">
<Input
placeholder="Entity name, description, or actor…"
value={search}
onChange={(e) => setSearch(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter') applySearch();
}}
/>
<Button variant="outline" onClick={applySearch}>
Go
</Button>
</div>
</div>
<div>
<label className="text-xs font-semibold text-slate-600 mb-1 block">Module</label>
<Select
value={moduleFilter}
onValueChange={(v) => {
setModuleFilter(v);
setPage(1);
}}
>
<SelectTrigger>
<SelectValue placeholder="All Modules" />
</SelectTrigger>
<SelectContent>
{MODULE_OPTIONS.map((m) => (
<SelectItem key={m.value} value={m.value}>
{m.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div>
<label className="text-xs font-semibold text-slate-600 mb-1 block">Action</label>
<Select
value={actionFilter}
onValueChange={(v) => {
setActionFilter(v);
setPage(1);
}}
>
<SelectTrigger>
<SelectValue placeholder="All Actions" />
</SelectTrigger>
<SelectContent>
{ACTION_OPTIONS.map((a) => (
<SelectItem key={a.value} value={a.value}>
{a.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div>
<label className="text-xs font-semibold text-slate-600 mb-1 block">From</label>
<Input
type="date"
value={dateFrom}
onChange={(e) => {
setDateFrom(e.target.value);
setPage(1);
}}
/>
</div>
<div>
<label className="text-xs font-semibold text-slate-600 mb-1 block">To</label>
<Input
type="date"
value={dateTo}
onChange={(e) => {
setDateTo(e.target.value);
setPage(1);
}}
/>
</div>
</div>
<div className="flex items-center justify-between mt-4">
<div className="text-xs text-slate-500 flex items-center gap-2">
<Filter className="w-3 h-3" />
Showing {logs.length} of {totalEntries} matching event(s)
</div>
<Button variant="ghost" size="sm" onClick={clearFilters}>
Reset filters
</Button>
</div>
</CardContent>
</Card>
{/* Logs table */}
<Card className="border-slate-200 overflow-hidden">
<div className="overflow-x-auto custom-scrollbar-x-slim">
<table className="w-full text-sm">
<thead className="bg-slate-50 text-slate-600">
<tr className="text-left">
<th className="px-4 py-3 font-semibold">When</th>
<th className="px-4 py-3 font-semibold">Module</th>
<th className="px-4 py-3 font-semibold">Action</th>
<th className="px-4 py-3 font-semibold">Entity</th>
<th className="px-4 py-3 font-semibold">Actor</th>
<th className="px-4 py-3 font-semibold">Description</th>
<th className="px-4 py-3 font-semibold w-10"></th>
</tr>
</thead>
<tbody className="divide-y divide-slate-100">
{loading && (
<tr>
<td colSpan={7} className="px-4 py-10 text-center text-slate-400">
<RefreshCw className="w-5 h-5 inline-block mr-2 animate-spin" />
Loading system logs
</td>
</tr>
)}
{!loading && logs.length === 0 && (
<tr>
<td colSpan={7} className="px-4 py-10 text-center text-slate-400">
No events match the current filters.
</td>
</tr>
)}
{!loading &&
logs.map((log) => (
<tr
key={log.id}
className="hover:bg-slate-50/70 cursor-pointer"
onClick={() => setSelected(log)}
>
<td className="px-4 py-3 whitespace-nowrap text-slate-700">
{formatTimestamp(log.timestamp)}
</td>
<td className="px-4 py-3 whitespace-nowrap">
<Badge variant="outline" className="border-slate-300 text-slate-700">
{log.moduleLabel}
</Badge>
</td>
<td className="px-4 py-3 whitespace-nowrap">
<Badge
variant="outline"
className={
ACTION_BADGE_CLASS[log.action] ||
'bg-slate-100 text-slate-700 border-slate-200'
}
>
{log.actionLabel}
</Badge>
</td>
<td className="px-4 py-3 text-slate-800">
<div className="font-medium truncate max-w-[260px]" title={log.entityLabel || ''}>
{log.entityLabel || `${log.entityType}`}
</div>
{log.entityId && (
<div className="text-[10px] font-mono text-slate-400 truncate max-w-[260px]">
{log.entityId}
</div>
)}
</td>
<td className="px-4 py-3 whitespace-nowrap">
<div className="flex items-center gap-2">
<div className="w-7 h-7 rounded-full bg-slate-100 text-slate-600 flex items-center justify-center text-xs font-semibold">
{(log.actor?.name || 'S').charAt(0).toUpperCase()}
</div>
<div>
<div className="text-slate-800 leading-tight">{log.actor?.name || 'System'}</div>
{log.actor?.role && (
<div className="text-[10px] text-slate-500 uppercase tracking-wider">
{log.actor.role}
</div>
)}
</div>
</div>
</td>
<td className="px-4 py-3 text-slate-600">
<div className="truncate max-w-[360px]" title={log.description}>
{log.description}
</div>
</td>
<td className="px-4 py-3">
<Button
size="icon"
variant="ghost"
className="h-8 w-8"
onClick={(e) => {
e.stopPropagation();
setSelected(log);
}}
title="View details"
>
<Eye className="w-4 h-4 text-slate-500" />
</Button>
</td>
</tr>
))}
</tbody>
</table>
</div>
{/* Pagination */}
<div className="flex items-center justify-between px-4 py-3 border-t border-slate-100 bg-slate-50/50">
<div className="text-xs text-slate-500">
Page {page} of {totalPages}
</div>
<div className="flex items-center gap-2">
<Button
size="sm"
variant="outline"
disabled={loading || page <= 1}
onClick={() => setPage((p) => Math.max(1, p - 1))}
>
<ChevronLeft className="w-4 h-4 mr-1" />
Prev
</Button>
<Button
size="sm"
variant="outline"
disabled={loading || page >= totalPages}
onClick={() => setPage((p) => Math.min(totalPages, p + 1))}
>
Next
<ChevronRight className="w-4 h-4 ml-1" />
</Button>
</div>
</div>
</Card>
{/* Detail dialog */}
<Dialog open={!!selected} onOpenChange={(open) => !open && setSelected(null)}>
<DialogContent className="max-w-3xl max-h-[85vh] overflow-y-auto">
{selected && (
<>
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<Activity className="w-5 h-5 text-re-red" />
{selected.moduleLabel} · {selected.actionLabel}
</DialogTitle>
<DialogDescription className="text-slate-600">
{selected.description}
</DialogDescription>
</DialogHeader>
<div className="grid grid-cols-2 gap-4 text-sm mt-4">
<div>
<p className="text-xs font-bold text-slate-500 uppercase tracking-wider mb-1">
Entity
</p>
<p className="text-slate-800">{selected.entityLabel || selected.entityType}</p>
{selected.entityId && (
<p className="text-[11px] font-mono text-slate-400 mt-0.5">
{selected.entityId}
</p>
)}
</div>
<div>
<p className="text-xs font-bold text-slate-500 uppercase tracking-wider mb-1">
Actor
</p>
<p className="text-slate-800 flex items-center gap-2">
<UserIcon className="w-4 h-4 text-slate-400" />
{selected.actor?.name || 'System'}
</p>
{selected.actor?.role && (
<p className="text-[11px] text-slate-500 uppercase tracking-wider mt-0.5">
{selected.actor.role}
</p>
)}
{selected.actor?.email && (
<p className="text-[11px] text-slate-500 mt-0.5">
{selected.actor.email}
</p>
)}
</div>
<div>
<p className="text-xs font-bold text-slate-500 uppercase tracking-wider mb-1">
When
</p>
<p className="text-slate-800">{formatTimestamp(selected.timestamp)}</p>
</div>
<div>
<p className="text-xs font-bold text-slate-500 uppercase tracking-wider mb-1">
Source
</p>
<p className="text-slate-800">{selected.ipAddress || '—'}</p>
<p className="text-[11px] text-slate-500 truncate" title={selected.userAgent || ''}>
{selected.userAgent || ''}
</p>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mt-4">
<div>
<p className="text-xs font-bold text-slate-500 uppercase tracking-wider mb-2">
Previous values
</p>
<JsonBlock value={selected.oldData} />
</div>
<div>
<p className="text-xs font-bold text-slate-500 uppercase tracking-wider mb-2">
New values
</p>
<JsonBlock value={selected.newData} />
</div>
</div>
{selected.metadata && (
<div className="mt-4">
<p className="text-xs font-bold text-slate-500 uppercase tracking-wider mb-2">
Metadata
</p>
<JsonBlock value={selected.metadata} />
</div>
)}
</>
)}
</DialogContent>
</Dialog>
</div>
);
};
export default SystemLogsPage;

View File

@ -1,5 +1,5 @@
import { useState } from 'react'; import { useState } from 'react';
import { useNavigate } from 'react-router-dom'; import { useLocation, useNavigate } from 'react-router-dom';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label'; import { Label } from '@/components/ui/label';
@ -11,6 +11,7 @@ import { setCredentials } from '@/store/slices/authSlice';
export function ProspectiveLoginPage() { export function ProspectiveLoginPage() {
const navigate = useNavigate(); const navigate = useNavigate();
const routerLocation = useLocation();
const dispatch = useDispatch(); const dispatch = useDispatch();
const [step, setStep] = useState<'PHONE' | 'OTP'>('PHONE'); const [step, setStep] = useState<'PHONE' | 'OTP'>('PHONE');
const [phone, setPhone] = useState(''); const [phone, setPhone] = useState('');
@ -18,6 +19,20 @@ export function ProspectiveLoginPage() {
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(''); const [error, setError] = useState('');
// Honour an optional `?next=...` deep-link sent by applicant emails. We allow only
// internal paths under `/prospective-dashboard/...` to prevent open-redirect abuse.
const resolveRedirectTarget = (): string => {
const raw = new URLSearchParams(routerLocation.search).get('next');
if (!raw) return '/prospective-dashboard';
try {
const decoded = decodeURIComponent(raw);
if (decoded.startsWith('/prospective-dashboard')) return decoded;
} catch {
// fall through to default
}
return '/prospective-dashboard';
};
const handleSendOtp = async (e: React.FormEvent) => { const handleSendOtp = async (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
if (!phone || phone.length < 10) { if (!phone || phone.length < 10) {
@ -76,7 +91,7 @@ export function ProspectiveLoginPage() {
localStorage.setItem('token', token); localStorage.setItem('token', token);
toast.success('Logged in successfully!'); toast.success('Logged in successfully!');
navigate('/prospective-dashboard'); navigate(resolveRedirectTarget());
} else { } else {
const errorMessage = response.data?.message || 'Invalid OTP'; const errorMessage = response.data?.message || 'Invalid OTP';
setError(errorMessage); setError(errorMessage);
@ -94,13 +109,13 @@ export function ProspectiveLoginPage() {
return ( return (
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-slate-900 via-slate-800 to-slate-900 p-4 overflow-y-auto"> <div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-slate-900 via-slate-800 to-slate-900 p-4 overflow-y-auto">
<div className="absolute inset-0 overflow-hidden pointer-events-none"> <div className="absolute inset-0 overflow-hidden pointer-events-none">
<div className="absolute -top-40 -right-40 w-80 h-80 bg-amber-600/10 rounded-full blur-3xl"></div> <div className="absolute -top-40 -right-40 w-80 h-80 bg-re-red/10 rounded-full blur-3xl"></div>
<div className="absolute -bottom-40 -left-40 w-80 h-80 bg-amber-600/10 rounded-full blur-3xl"></div> <div className="absolute -bottom-40 -left-40 w-80 h-80 bg-re-red/10 rounded-full blur-3xl"></div>
</div> </div>
<div className="relative w-full max-w-md"> <div className="relative w-full max-w-md">
<div className="text-center mb-8"> <div className="text-center mb-8">
<div className="inline-flex items-center justify-center w-20 h-20 bg-amber-600 rounded-full mb-4"> <div className="inline-flex items-center justify-center w-20 h-20 bg-re-red rounded-full mb-4">
<svg viewBox="0 0 24 24" className="w-12 h-12 text-white" fill="currentColor"> <svg viewBox="0 0 24 24" className="w-12 h-12 text-white" fill="currentColor">
<path d="M12 2L4 6v6c0 5.5 3.8 10.7 8 12 4.2-1.3 8-6.5 8-12V6l-8-4zm0 2.2l6 3v4.8c0 4.5-3.1 8.7-6 10-2.9-1.3-6-5.5-6-10V7.2l6-3z"></path> <path d="M12 2L4 6v6c0 5.5 3.8 10.7 8 12 4.2-1.3 8-6.5 8-12V6l-8-4zm0 2.2l6 3v4.8c0 4.5-3.1 8.7-6 10-2.9-1.3-6-5.5-6-10V7.2l6-3z"></path>
<circle cx="12" cy="12" r="3"></circle> <circle cx="12" cy="12" r="3"></circle>
@ -122,8 +137,8 @@ export function ProspectiveLoginPage() {
</Button> </Button>
<div className="flex items-center gap-3 mb-2"> <div className="flex items-center gap-3 mb-2">
<div className="p-2 bg-amber-100 rounded-lg"> <div className="p-2 bg-red-50 rounded-lg">
<Smartphone className="w-6 h-6 text-amber-600" /> <Smartphone className="w-6 h-6 text-re-red" />
</div> </div>
<div> <div>
<h2 className="text-slate-900 text-lg font-semibold">Dealer Login</h2> <h2 className="text-slate-900 text-lg font-semibold">Dealer Login</h2>
@ -158,7 +173,7 @@ export function ProspectiveLoginPage() {
<Button <Button
type="submit" type="submit"
className="w-full bg-amber-600 hover:bg-amber-700 h-9" className="w-full bg-re-red hover:bg-re-red-hover h-9"
disabled={isLoading || phone.length < 10} disabled={isLoading || phone.length < 10}
> >
{isLoading ? 'Sending...' : 'Send OTP'} {isLoading ? 'Sending...' : 'Send OTP'}
@ -198,7 +213,7 @@ export function ProspectiveLoginPage() {
<Button <Button
type="submit" type="submit"
className="w-full bg-amber-600 hover:bg-amber-700 h-9" className="w-full bg-re-red hover:bg-re-red-hover h-9"
disabled={isLoading || otp.length < 6} disabled={isLoading || otp.length < 6}
> >
{isLoading ? 'Verifying...' : 'Verify OTP'} {isLoading ? 'Verifying...' : 'Verify OTP'}
@ -207,7 +222,7 @@ export function ProspectiveLoginPage() {
<div className="text-center text-sm"> <div className="text-center text-sm">
<button <button
type="button" type="button"
className="text-amber-600 hover:text-amber-700 font-medium" className="text-re-red hover:text-re-red-hover font-medium"
onClick={() => setStep('PHONE')} onClick={() => setStep('PHONE')}
> >
Change Phone Number Change Phone Number
@ -215,7 +230,7 @@ export function ProspectiveLoginPage() {
<span className="mx-2 text-slate-400">|</span> <span className="mx-2 text-slate-400">|</span>
<button <button
type="button" type="button"
className="text-amber-600 hover:text-amber-700 font-medium" className="text-re-red hover:text-re-red-hover font-medium"
onClick={handleSendOtp} onClick={handleSendOtp}
disabled={isLoading} disabled={isLoading}
> >

View File

@ -1,6 +1,7 @@
import { render, screen, waitFor } from "@testing-library/react" import { render, screen, waitFor } from "@testing-library/react"
import userEvent from "@testing-library/user-event" import userEvent from "@testing-library/user-event"
import { ConstitutionalChangePage } from "../pages/ConstitutionalChangePage" import { ConstitutionalChangePage } from "../pages/ConstitutionalChangePage"
import { toast } from "sonner"
jest.mock("sonner", () => ({ jest.mock("sonner", () => ({
toast: { toast: {
@ -138,4 +139,54 @@ describe("ConstitutionalChangePage", () => {
expect(screen.getByText(/^Dealer \*$/i)).toBeInTheDocument() expect(screen.getByText(/^Dealer \*$/i)).toBeInTheDocument()
}) })
it("shows backend duplicate-open message on create conflict", async () => {
const user = userEvent.setup()
const { API } = await import("@/api/API")
;(API.getDealers as jest.Mock).mockResolvedValueOnce({
data: {
success: true,
data: [
{
user: { id: "dealer-user-1" },
constitutionType: "Proprietorship",
businessName: "Dealer A",
legalName: "Dealer A Pvt",
dealerCode: { dealerCode: "DLR-1" },
},
],
},
})
;(API.createConstitutionalChange as jest.Mock).mockRejectedValueOnce({
response: {
data: {
message:
"Open constitutional request CCR-1 already exists at ASM Review. Complete it before creating a new one.",
},
},
})
setup()
await user.click(screen.getByRole("button", { name: /new request/i }))
await screen.findByRole("heading", {
name: /create constitutional change request/i,
})
await user.click(screen.getByRole("combobox", { name: /dealer/i }))
await user.click(await screen.findByText(/DLR-1 — Dealer A/i))
await user.click(screen.getByRole("combobox", { name: /proposed constitution/i }))
await user.click(await screen.findByText(/^Partnership$/i))
const reasonField = screen.getByLabelText(/reason for constitutional change/i)
await user.type(reasonField, "Need to onboard new partner")
await user.click(screen.getByRole("button", { name: /submit request/i }))
await waitFor(() => {
expect(toast.error).toHaveBeenCalledWith(
expect.stringContaining("Open constitutional request CCR-1 already exists")
)
})
})
}) })

View File

@ -12,9 +12,19 @@ import { useState, useEffect, useMemo } from 'react';
import { User as UserType } from '@/lib/mock-data'; import { User as UserType } from '@/lib/mock-data';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { API } from '@/api/API'; import { API } from '@/api/API';
import { SlaBadge } from '@/components/sla/SlaBadge';
import { useSlaBatchStatus } from '@/hooks/useSlaBatchStatus';
import { OFFBOARDING_ACTIONS } from '@/lib/offboarding-actions'; import { OFFBOARDING_ACTIONS } from '@/lib/offboarding-actions';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { formatDateTime } from '@/components/ui/utils'; import { formatDateTime } from '@/components/ui/utils';
import {
getCurrentStageBadgeClass,
getOffboardingRequestStatusBadgeClass,
getStatusLabelBadgeClass,
getStatusProgressBarClass,
isOffboardingTerminalNegative,
WORKFLOW_IN_PROGRESS_ACCENT,
} from '@/lib/offboardingDisplay';
interface ConstitutionalChangeDetailsProps { interface ConstitutionalChangeDetailsProps {
requestId: string; requestId: string;
@ -44,7 +54,7 @@ const formatStageRole = (role: string) =>
// Document requirements mapping (same as in ConstitutionalChangePage) — SRS §12.2.4 by target constitution // Document requirements mapping (same as in ConstitutionalChangePage) — SRS §12.2.4 by target constitution
const documentRequirements: Record<string, number[]> = { const documentRequirements: Record<string, number[]> = {
'Partnership': [1, 2, 3, 4, 8, 9, 10, 16], 'Partnership': [1, 2, 3, 4, 8, 9, 10, 16],
'LLP': [1, 2, 3, 7, 8, 9, 10, 11, 16], 'LLP': [1, 2, 3, 7, 8, 10, 11, 16],
'Private Limited': [1, 2, 3, 5, 6, 7, 8, 10, 16], 'Private Limited': [1, 2, 3, 5, 6, 7, 8, 10, 16],
'Pvt Ltd': [1, 2, 3, 5, 6, 7, 8, 10, 16], 'Pvt Ltd': [1, 2, 3, 5, 6, 7, 8, 10, 16],
'Proprietorship': [1, 2, 3, 10, 16] 'Proprietorship': [1, 2, 3, 10, 16]
@ -86,19 +96,12 @@ const getTypeColor = (type: string) => {
} }
}; };
const getStatusColor = (status: string) => { const getStatusColor = (status: string) => getStatusLabelBadgeClass(status);
const s = String(status || '');
if (s === 'Completed' || s === 'Verified' || s === 'APPROVED' || s === 'COMPLETED' || s === 'CREATED' || /^DOCUMENT/i.test(s)) { const getDocChecklistUploadButtonClass = (isRejected: boolean) =>
return 'bg-green-100 text-green-700 border-green-300'; isRejected
} ? 'h-8 px-2 text-red-700 hover:bg-red-50 hover:text-red-800'
if (s.includes('Revoked') || s === 'REVOKED') return 'bg-orange-100 text-orange-800 border-orange-300'; : 'h-8 px-2 text-slate-700 hover:bg-slate-50';
if (s.includes('Rejected') || s === 'REJECTED') return 'bg-red-100 text-red-700 border-red-300';
if (s === 'SENT BACK' || s.includes('Review') || s.includes('Pending') || s === 'In Progress' || s === 'Submitted') {
return 'bg-yellow-100 text-yellow-700 border-yellow-300';
}
if (s === 'UPDATED') return 'bg-slate-100 text-slate-700 border-slate-300';
return 'bg-slate-100 text-slate-700 border-slate-300';
};
/** Audit rows were stored as UPDATED for approvals; avoid treating "UPDATED" as pending via substring "update". */ /** Audit rows were stored as UPDATED for approvals; avoid treating "UPDATED" as pending via substring "update". */
const getConstitutionalHistoryPresentation = (entry: any) => { const getConstitutionalHistoryPresentation = (entry: any) => {
@ -145,12 +148,25 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }:
const [isUploadDialogOpen, setIsUploadDialogOpen] = useState(false); const [isUploadDialogOpen, setIsUploadDialogOpen] = useState(false);
const [selectedDocType, setSelectedDocType] = useState<number | null>(null); const [selectedDocType, setSelectedDocType] = useState<number | null>(null);
const [uploadFile, setUploadFile] = useState<File | null>(null); const [uploadFile, setUploadFile] = useState<File | null>(null);
// True when the dialog was opened from a checklist row -> doc type is implicit,
// so we hide the dropdown and show the doc name as a read-only badge instead.
const [docTypeLocked, setDocTypeLocked] = useState(false);
const [activeMainTab, setActiveMainTab] = useState('workflow'); const [activeMainTab, setActiveMainTab] = useState('workflow');
const [activeDocumentTab, setActiveDocumentTab] = useState('required'); const [activeDocumentTab, setActiveDocumentTab] = useState('required');
const [request, setRequest] = useState<any>(null); const [request, setRequest] = useState<any>(null);
// The URL slug (`requestId` prop) is the human-readable code such as
// `CC-2026-MAY-00002`, but `sla_tracking.entityId` is a UUID column.
// Wait until the request has loaded and feed its UUID `id` to the SLA hook.
const slaEntityId: string = request?.id || '';
const { get: getSla } = useSlaBatchStatus(
slaEntityId ? [{ entityType: 'constitutional', entityId: slaEntityId }] : [],
Boolean(slaEntityId)
);
const [auditLogs, setAuditLogs] = useState<any[]>([]); const [auditLogs, setAuditLogs] = useState<any[]>([]);
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const [isActionLoading, setIsActionLoading] = useState(false); const [isActionLoading, setIsActionLoading] = useState(false);
/** Set when POST /action returns 4xx (apisauce does not throw — must check response.ok). */
const [actionDialogError, setActionDialogError] = useState<string | null>(null);
const [isUploadingDoc, setIsUploadingDoc] = useState(false); const [isUploadingDoc, setIsUploadingDoc] = useState(false);
const [rejectDocDialogOpen, setRejectDocDialogOpen] = useState(false); const [rejectDocDialogOpen, setRejectDocDialogOpen] = useState(false);
const [rejectDocIndex, setRejectDocIndex] = useState<number | null>(null); const [rejectDocIndex, setRejectDocIndex] = useState<number | null>(null);
@ -222,7 +238,7 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }:
if (isLoading) { if (isLoading) {
return ( return (
<div className="flex flex-col items-center justify-center min-h-[400px] space-y-4"> <div className="flex flex-col items-center justify-center min-h-[400px] space-y-4">
<Loader2 className="w-8 h-8 text-amber-600 animate-spin" /> <Loader2 className="w-8 h-8 text-re-red animate-spin" />
<p className="text-slate-600">Loading request details...</p> <p className="text-slate-600">Loading request details...</p>
</div> </div>
); );
@ -301,9 +317,9 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }:
(String(request.status || '') === 'Completed' && !['Rejected', 'Revoked'].includes(String(request.currentStage || ''))); (String(request.status || '') === 'Completed' && !['Rejected', 'Revoked'].includes(String(request.currentStage || '')));
/** SRS §12.2 — closed failure states: do not show misleading step progress. */ /** SRS §12.2 — closed failure states: do not show misleading step progress. */
const workflowTerminalNegative = const workflowTerminalNegative = isOffboardingTerminalNegative(request.status, request.currentStage);
['Rejected', 'Revoked'].includes(String(request.status || '')) || const statusProgressBarClass = getStatusProgressBarClass(request.status, request.currentStage);
['Rejected', 'Revoked'].includes(String(request.currentStage || '')); const requestStatusBadgeClass = getOffboardingRequestStatusBadgeClass(request.status, request.currentStage);
const getLatestStageTimelineEntry = (stageName: string) => { const getLatestStageTimelineEntry = (stageName: string) => {
const aliases: Record<string, string[]> = { const aliases: Record<string, string[]> = {
@ -415,6 +431,7 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }:
const handleAction = (type: 'approve' | 'reject' | 'sendBack' | 'revoke') => { const handleAction = (type: 'approve' | 'reject' | 'sendBack' | 'revoke') => {
setActionType(type); setActionType(type);
setActionDialogError(null);
setIsActionDialogOpen(true); setIsActionDialogOpen(true);
}; };
@ -436,6 +453,7 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }:
try { try {
setIsActionLoading(true); setIsActionLoading(true);
setActionDialogError(null);
const actionPayload = const actionPayload =
actionType === 'approve' actionType === 'approve'
? OFFBOARDING_ACTIONS.APPROVE ? OFFBOARDING_ACTIONS.APPROVE
@ -448,7 +466,9 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }:
comments comments
}) as any; }) as any;
if (response.data.success) { const payload = response?.data as { success?: boolean; message?: string } | undefined;
/** apisauce returns { ok: false } on 4xx without throwing — must branch on this. */
if (response?.ok && payload?.success) {
const actionText = const actionText =
actionType === 'approve' ? 'approved' : actionType === 'approve' ? 'approved' :
actionType === 'reject' ? 'rejected' : actionType === 'reject' ? 'rejected' :
@ -457,12 +477,26 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }:
toast.success(`Request ${actionText} successfully`); toast.success(`Request ${actionText} successfully`);
setIsActionDialogOpen(false); setIsActionDialogOpen(false);
setComments(''); setComments('');
setActionDialogError(null);
await fetchRequestDetails(); await fetchRequestDetails();
return;
} }
const message =
payload?.message ||
(response as any)?.data?.error ||
'Failed to submit action';
setActionDialogError(message);
const docGate = /mandatory documents/i.test(message);
toast.error(message, { duration: docGate ? 14000 : 8000 });
} catch (error) { } catch (error) {
console.error('Submit action error:', error); console.error('Submit action error:', error);
const message = (error as any)?.response?.data?.message || 'Failed to submit action'; const message =
toast.error(message); (error as any)?.response?.data?.message ||
(error as any)?.message ||
'Failed to submit action';
setActionDialogError(message);
toast.error(message, { duration: /mandatory documents/i.test(message) ? 14000 : 8000 });
} finally { } finally {
setIsActionLoading(false); setIsActionLoading(false);
} }
@ -483,6 +517,7 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }:
? existingDocs.findIndex((d: any) => Number(d?.docNumber) === selectedDocType) ? existingDocs.findIndex((d: any) => Number(d?.docNumber) === selectedDocType)
: -1; : -1;
const payloadDoc = { const payloadDoc = {
id: globalThis.crypto?.randomUUID?.() ?? `doc-${Date.now()}-${selectedDocType}`,
docNumber: selectedDocType, docNumber: selectedDocType,
name: documentNames[selectedDocType] || 'Other', name: documentNames[selectedDocType] || 'Other',
fileName: uploadFile.name, fileName: uploadFile.name,
@ -512,22 +547,19 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }:
} }
}; };
const resolveDocumentId = (doc: any, index: number) =>
doc?.id != null ? String(doc.id) : String(index);
const handleVerifyDocument = async (targetDoc: any, targetIndex: number) => { const handleVerifyDocument = async (targetDoc: any, targetIndex: number) => {
try { try {
const existingDocs = Array.isArray(request.documents) ? [...request.documents] : []; const documentId = resolveDocumentId(targetDoc, targetIndex);
const updatedDocs = existingDocs.map((doc: any, index: number) => { const response = await API.verifyConstitutionalDocument(requestId, documentId) as any;
const isTargetByIndex = index === targetIndex;
const isTargetByDocNumber = targetDoc.docNumber && doc.docNumber === targetDoc.docNumber;
if (!(isTargetByIndex || isTargetByDocNumber)) return doc;
return { ...doc, status: 'Verified', verifiedOn: new Date().toISOString(), verifiedBy: currentUser?.name || 'System' };
});
const response = await API.uploadConstitutionalDocuments(requestId, updatedDocs) as any;
if (response.data?.success) { if (response.data?.success) {
toast.success('Document verified successfully'); toast.success('Document verified successfully');
fetchRequestDetails(); await fetchRequestDetails({ silent: true });
if (request?.id) await fetchAuditLogs(request.id);
} else { } else {
toast.error('Failed to verify document'); toast.error(response.data?.message || 'Failed to verify document');
} }
} catch (error) { } catch (error) {
console.error('Verify document error:', error); console.error('Verify document error:', error);
@ -537,31 +569,29 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }:
const submitRejectDocument = async () => { const submitRejectDocument = async () => {
if (rejectDocIndex == null || !String(rejectDocReason).trim()) { if (rejectDocIndex == null || !String(rejectDocReason).trim()) {
toast.error('Please enter a rejection reason (SRS document verification).'); toast.error('Please enter a rejection reason.');
return;
}
const targetDoc = (request?.documents || [])[rejectDocIndex];
if (!targetDoc) {
toast.error('Document not found');
return; return;
} }
try { try {
setIsRejectingDoc(true); setIsRejectingDoc(true);
const existingDocs = Array.isArray(request.documents) ? [...request.documents] : []; const documentId = resolveDocumentId(targetDoc, rejectDocIndex);
const updatedDocs = existingDocs.map((doc: any, index: number) => { const response = await API.rejectConstitutionalDocument(requestId, documentId, {
if (index !== rejectDocIndex) return doc; remarks: rejectDocReason.trim()
return { }) as any;
...doc,
status: 'Rejected',
rejectedOn: new Date().toISOString(),
rejectedBy: (currentUser as any)?.fullName || 'System',
rejectionReason: rejectDocReason.trim()
};
});
const response = await API.uploadConstitutionalDocuments(requestId, updatedDocs) as any;
if (response.data?.success) { if (response.data?.success) {
toast.success('Document marked as rejected'); toast.success('Document marked as rejected');
setRejectDocDialogOpen(false); setRejectDocDialogOpen(false);
setRejectDocIndex(null); setRejectDocIndex(null);
setRejectDocReason(''); setRejectDocReason('');
await fetchRequestDetails({ silent: true }); await fetchRequestDetails({ silent: true });
if (request?.id) await fetchAuditLogs(request.id);
} else { } else {
toast.error('Failed to reject document'); toast.error(response.data?.message || 'Failed to reject document');
} }
} catch (error) { } catch (error) {
console.error('Reject document error:', error); console.error('Reject document error:', error);
@ -591,9 +621,10 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }:
</p> </p>
</div> </div>
</div> </div>
<Badge className={getStatusColor(request.status)}> <Badge className={requestStatusBadgeClass}>
{request.status} {request.status}
</Badge> </Badge>
<SlaBadge status={getSla('constitutional', slaEntityId)} />
</div> </div>
{/* Request Overview */} {/* Request Overview */}
@ -625,7 +656,12 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }:
<p className="text-slate-600 text-sm mb-1">Request Information</p> <p className="text-slate-600 text-sm mb-1">Request Information</p>
<p className="text-slate-900 text-sm">Submitted: {formatDateTime(request.createdAt)}</p> <p className="text-slate-900 text-sm">Submitted: {formatDateTime(request.createdAt)}</p>
<p className="text-slate-600 text-sm">By: {request.dealer?.fullName || 'Dealer'}</p> <p className="text-slate-600 text-sm">By: {request.dealer?.fullName || 'Dealer'}</p>
<p className="text-slate-900 text-sm mt-2">Current Stage: {request.currentStage}</p> <div className="mt-2 flex flex-wrap items-center gap-2">
<span className="text-slate-600 text-sm">Current Stage:</span>
<Badge className={getCurrentStageBadgeClass(request.currentStage, request.status)}>
{request.currentStage}
</Badge>
</div>
</div> </div>
</div> </div>
@ -681,7 +717,7 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }:
<Card> <Card>
<Tabs value={activeMainTab} onValueChange={setActiveMainTab} className="w-full"> <Tabs value={activeMainTab} onValueChange={setActiveMainTab} className="w-full">
<CardHeader className="pb-4"> <CardHeader className="pb-4">
<div className="overflow-x-auto -mx-6 px-6"> <div className="overflow-x-auto custom-scrollbar-x-slim -mx-6 px-6">
<TabsList className="w-max min-w-full justify-start"> <TabsList className="w-max min-w-full justify-start">
<TabsTrigger value="workflow">Workflow Progress</TabsTrigger> <TabsTrigger value="workflow">Workflow Progress</TabsTrigger>
<TabsTrigger value="documents">Documents</TabsTrigger> <TabsTrigger value="documents">Documents</TabsTrigger>
@ -692,16 +728,18 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }:
<CardContent> <CardContent>
{/* Workflow Progress Tab */} {/* Workflow Progress Tab */}
<TabsContent value="workflow" className="mt-0"> <TabsContent value="workflow" className="mt-0 status-progress-ui">
{/* Progress Bar */} {/* Progress Bar */}
<div className="mb-8"> <div className="mb-8">
<div className="flex items-center justify-between mb-2"> <div className="flex items-center justify-between mb-2">
<span className="text-slate-900">Overall Progress</span> <span className="text-slate-900">Overall Progress</span>
<span className="text-slate-600">{request.progressPercentage}%</span> <Badge className={`${statusProgressBarClass} text-white border-transparent hover:opacity-90`}>
{request.progressPercentage}% Complete
</Badge>
</div> </div>
<div className="h-3 bg-slate-200 rounded-full overflow-hidden"> <div className="h-3 bg-slate-200 rounded-full overflow-hidden">
<div <div
className="h-full bg-amber-600 transition-all duration-500" className={`h-full transition-all duration-500 ${statusProgressBarClass}`}
style={{ width: `${request.progressPercentage}%` }} style={{ width: `${request.progressPercentage}%` }}
/> />
</div> </div>
@ -760,13 +798,13 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }:
{/* Status Icon */} {/* Status Icon */}
<div className="flex flex-col items-center"> <div className="flex flex-col items-center">
<div className={`w-10 h-10 rounded-full flex items-center justify-center ${isCompleted ? 'bg-green-100' : <div className={`w-10 h-10 rounded-full flex items-center justify-center ${isCompleted ? 'bg-green-100' :
isCurrent ? 'bg-amber-100' : isCurrent ? WORKFLOW_IN_PROGRESS_ACCENT.icon :
'bg-slate-100' 'bg-slate-100'
}`}> }`}>
{isCompleted ? ( {isCompleted ? (
<CheckCircle2 className="w-5 h-5 text-green-600" /> <CheckCircle2 className="w-5 h-5 text-green-600" />
) : isCurrent ? ( ) : isCurrent ? (
<Clock className="w-5 h-5 text-amber-600" /> <Clock className="w-5 h-5 text-re-red" />
) : ( ) : (
<AlertCircle className="w-5 h-5 text-slate-400" /> <AlertCircle className="w-5 h-5 text-slate-400" />
)} )}
@ -778,20 +816,19 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }:
</div> </div>
{/* Stage Info */} {/* Stage Info */}
<div className={`flex-1 pb-8 ${isCurrent ? 'bg-amber-50 -ml-4 pl-4 pr-4 py-3 rounded-lg border border-amber-200' : ''}`}> <div className={`flex-1 pb-8 ${isCurrent ? WORKFLOW_IN_PROGRESS_ACCENT.panel : ''}`}>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<h4 className={`${isCurrent ? 'text-amber-900' : 'text-slate-900'}`}> <h4 className={`${isCurrent ? WORKFLOW_IN_PROGRESS_ACCENT.title : 'text-slate-900'}`}>
{formatStageLabel(stage.name)} {formatStageLabel(stage.name)}
</h4> </h4>
<p className={`text-sm ${isCurrent ? 'text-amber-700' : 'text-slate-600'}`}> <p className={`text-sm ${isCurrent ? WORKFLOW_IN_PROGRESS_ACCENT.subtitle : 'text-slate-600'}`}>
{`Responsible: ${formatStageRole(stage.role)}`} {`Responsible: ${formatStageRole(stage.role)}`}
</p> </p>
</div> </div>
<Badge className={ <Badge className={
isCompleted ? 'bg-green-100 text-green-700 border-green-300' : isCompleted ? 'bg-green-100 text-green-700 border-green-300' :
isCurrent ? 'bg-amber-100 text-amber-700 border-amber-300' : isCurrent ? WORKFLOW_IN_PROGRESS_ACCENT.stageBadge :
'bg-slate-100 text-slate-500 border-slate-300' 'bg-slate-100 text-slate-500 border-slate-300'
}> }>
{isCompleted ? 'Completed' : isCurrent ? 'In Progress' : 'Pending'} {isCompleted ? 'Completed' : isCurrent ? 'In Progress' : 'Pending'}
@ -823,7 +860,7 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }:
DD-ZM: {ddZmApproval?.approvedByUserId ? 'Approved' : 'Pending'} DD-ZM: {ddZmApproval?.approvedByUserId ? 'Approved' : 'Pending'}
</Badge> </Badge>
{currentRoleApproval?.approvedByUserId && ( {currentRoleApproval?.approvedByUserId && (
<Badge className="bg-blue-100 text-blue-700 border-blue-300"> <Badge className="bg-red-50 text-re-red-hover border-red-200">
Approved by you Approved by you
</Badge> </Badge>
)} )}
@ -863,9 +900,26 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }:
<div className="space-y-4"> <div className="space-y-4">
<div className="flex items-center justify-between gap-4"> <div className="flex items-center justify-between gap-4">
<h4 className="text-slate-900">Document Checklist</h4> <h4 className="text-slate-900">Document Checklist</h4>
<Dialog open={isUploadDialogOpen} onOpenChange={setIsUploadDialogOpen}> <Dialog
open={isUploadDialogOpen}
onOpenChange={(open) => {
setIsUploadDialogOpen(open);
if (!open) {
setDocTypeLocked(false);
setUploadFile(null);
}
}}
>
<DialogTrigger asChild> <DialogTrigger asChild>
<Button size="sm" className="bg-amber-600 hover:bg-amber-700"> <Button
size="sm"
className="bg-re-red hover:bg-re-red-hover"
onClick={() => {
setDocTypeLocked(false);
setSelectedDocType(null);
setUploadFile(null);
}}
>
<Upload className="w-4 h-4 mr-2" /> <Upload className="w-4 h-4 mr-2" />
Upload Document Upload Document
</Button> </Button>
@ -874,29 +928,42 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }:
<DialogHeader> <DialogHeader>
<DialogTitle>Upload Document</DialogTitle> <DialogTitle>Upload Document</DialogTitle>
<DialogDescription> <DialogDescription>
Select the document type and upload the file {docTypeLocked
? 'Pick a file for the selected document.'
: 'Select the document type and upload the file.'}
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<div className="space-y-4"> <div className="space-y-4">
<div> {docTypeLocked && selectedDocType != null ? (
<Label>Document Type</Label> <div>
<select <Label>Document</Label>
className="mt-1 flex h-10 w-full rounded-md border border-slate-200 bg-white px-3 py-2 text-sm ring-offset-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-amber-500 focus-visible:ring-offset-2" <div className="mt-1 flex items-center gap-2 bg-red-50 border border-red-200 rounded-md px-3 h-10">
value={selectedDocType != null ? String(selectedDocType) : ''} <Badge className="bg-re-red text-white border-transparent">
onChange={(e) => { {documentNames[selectedDocType] || `Document ${selectedDocType}`}
const v = e.target.value; </Badge>
setSelectedDocType(v ? Number(v) : null); </div>
}} </div>
> ) : (
<option value="">Select document type</option> <div>
{uploadDocumentTypeOptions.map((docNum) => ( <Label>Document Type</Label>
<option key={docNum} value={String(docNum)}> <select
{docNum !== OTHER_DOCUMENT_DOC_NUMBER && isDocTypeUploaded(docNum) ? '✓ ' : ''} className="mt-1 flex h-10 w-full rounded-md border border-slate-200 bg-white px-3 py-2 text-sm ring-offset-white focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-re-red focus-visible:ring-offset-2"
{documentNames[docNum] || `Document ${docNum}`} value={selectedDocType != null ? String(selectedDocType) : ''}
</option> onChange={(e) => {
))} const v = e.target.value;
</select> setSelectedDocType(v ? Number(v) : null);
</div> }}
>
<option value="">Select document type</option>
{uploadDocumentTypeOptions.map((docNum) => (
<option key={docNum} value={String(docNum)}>
{docNum !== OTHER_DOCUMENT_DOC_NUMBER && isDocTypeUploaded(docNum) ? '✓ ' : ''}
{documentNames[docNum] || `Document ${docNum}`}
</option>
))}
</select>
</div>
)}
<div> <div>
<Label>Upload File</Label> <Label>Upload File</Label>
<Input type="file" className="mt-1" onChange={(e) => setUploadFile(e.target.files?.[0] || null)} /> <Input type="file" className="mt-1" onChange={(e) => setUploadFile(e.target.files?.[0] || null)} />
@ -907,7 +974,7 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }:
Cancel Cancel
</Button> </Button>
<Button <Button
className="bg-amber-600 hover:bg-amber-700" className="bg-re-red hover:bg-re-red-hover"
onClick={handleUploadDocument} onClick={handleUploadDocument}
disabled={isUploadingDoc} disabled={isUploadingDoc}
> >
@ -944,20 +1011,40 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }:
{uploaded && ( {uploaded && (
<p className={isRejected ? 'text-red-700 text-sm' : ok ? 'text-green-700 text-sm' : 'text-slate-600 text-sm'}> <p className={isRejected ? 'text-red-700 text-sm' : ok ? 'text-green-700 text-sm' : 'text-slate-600 text-sm'}>
{uploaded.fileName || uploaded.name} {uploaded.fileName || uploaded.name}
{isRejected && uploaded.rejectionReason ? `${uploaded.rejectionReason}` : ''} {isRejected && (uploaded.rejectionReason || uploaded.rejectionRemarks)
? `${uploaded.rejectionReason || uploaded.rejectionRemarks}`
: ''}
</p> </p>
)} )}
</div> </div>
</div> </div>
{uploaded ? ( <div className="flex items-center gap-2">
<Badge className={getStatusColor(uploaded.status)}> {uploaded ? (
{uploaded.status} <Badge className={getStatusColor(uploaded.status)}>
</Badge> {uploaded.status}
) : ( </Badge>
<Badge className="bg-slate-100 text-slate-600 border-slate-300"> ) : (
Not Uploaded <Badge className="bg-slate-100 text-slate-600 border-slate-300">
</Badge> Not Uploaded
)} </Badge>
)}
{!ok && (
<Button
size="sm"
variant="ghost"
className={getDocChecklistUploadButtonClass(!!isRejected)}
onClick={() => {
setSelectedDocType(docNum);
setUploadFile(null);
setDocTypeLocked(true);
setIsUploadDialogOpen(true);
}}
>
<Upload className="w-3.5 h-3.5 mr-1" />
{isRejected ? 'Re-upload' : 'Upload'}
</Button>
)}
</div>
</div> </div>
); );
})} })}
@ -984,7 +1071,10 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }:
</TableHeader> </TableHeader>
<TableBody> <TableBody>
{(request.documents || []).map((doc: any, index: number) => ( {(request.documents || []).map((doc: any, index: number) => (
<TableRow key={index}> <TableRow
key={index}
className={String(doc.status) === 'Rejected' ? 'bg-red-50/80' : undefined}
>
<TableCell className="text-slate-900"> <TableCell className="text-slate-900">
{doc.docNumber ? documentNames[doc.docNumber] : doc.name} {doc.docNumber ? documentNames[doc.docNumber] : doc.name}
</TableCell> </TableCell>
@ -1088,7 +1178,7 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }:
<div key={entry.id || index} className="flex items-start gap-4 pb-4 border-b border-slate-200 last:border-0"> <div key={entry.id || index} className="flex items-start gap-4 pb-4 border-b border-slate-200 last:border-0">
<div className={`w-10 h-10 rounded-full flex items-center justify-center flex-shrink-0 ${pres.variant === 'success' ? 'bg-green-100' : <div className={`w-10 h-10 rounded-full flex items-center justify-center flex-shrink-0 ${pres.variant === 'success' ? 'bg-green-100' :
pres.variant === 'danger' ? 'bg-red-100' : pres.variant === 'danger' ? 'bg-red-100' :
pres.variant === 'pending' ? 'bg-amber-100' : pres.variant === 'pending' ? 'bg-red-50' :
'bg-slate-100' 'bg-slate-100'
}`}> }`}>
{pres.variant === 'success' ? ( {pres.variant === 'success' ? (
@ -1096,7 +1186,7 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }:
) : pres.variant === 'danger' ? ( ) : pres.variant === 'danger' ? (
<AlertCircle className="w-5 h-5 text-red-600" /> <AlertCircle className="w-5 h-5 text-red-600" />
) : pres.variant === 'pending' ? ( ) : pres.variant === 'pending' ? (
<Clock className="w-5 h-5 text-amber-600" /> <Clock className="w-5 h-5 text-re-red" />
) : ( ) : (
<Clock className="w-5 h-5 text-slate-500" /> <Clock className="w-5 h-5 text-slate-500" />
)} )}
@ -1145,7 +1235,21 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }:
<CardContent className="space-y-4"> <CardContent className="space-y-4">
<div> <div>
<p className="text-slate-600 text-sm">Current Stage</p> <p className="text-slate-600 text-sm">Current Stage</p>
<p className="text-slate-900">{request.currentStage}</p> <Badge className={getCurrentStageBadgeClass(request.currentStage, request.status)}>
{request.currentStage}
</Badge>
</div>
<div>
<p className="text-slate-600 text-sm">Progress</p>
<div className="flex items-center gap-2 mt-2">
<div className="flex-1 h-2 bg-slate-200 rounded-full overflow-hidden">
<div
className={`h-full transition-all duration-300 ${statusProgressBarClass}`}
style={{ width: `${request.progressPercentage ?? 0}%` }}
/>
</div>
<span className="text-slate-900">{request.progressPercentage ?? 0}%</span>
</div>
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
@ -1201,7 +1305,7 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }:
{permissions.canSendBack && ( {permissions.canSendBack && (
<Button <Button
variant="outline" variant="outline"
className="w-full border-amber-300 text-amber-900 hover:bg-amber-50" className="w-full border-red-300 text-red-900 hover:bg-red-50"
onClick={() => handleAction('sendBack')} onClick={() => handleAction('sendBack')}
disabled={isActionLoading} disabled={isActionLoading}
> >
@ -1217,7 +1321,7 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }:
{permissions.canRevoke && ( {permissions.canRevoke && (
<Button <Button
variant="outline" variant="outline"
className="w-full border-orange-300 text-orange-900 hover:bg-orange-50" className="w-full border-red-300 text-re-red-hover hover:bg-red-50"
onClick={() => handleAction('revoke')} onClick={() => handleAction('revoke')}
disabled={isActionLoading} disabled={isActionLoading}
> >
@ -1241,7 +1345,7 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }:
<div className="border-t border-slate-200 pt-3 mt-3"> <div className="border-t border-slate-200 pt-3 mt-3">
<Button <Button
variant="outline" variant="outline"
className="w-full border-blue-700 text-blue-800 hover:bg-blue-50" className="w-full border-re-red text-re-red hover:bg-red-50"
onClick={() => navigate(`/worknotes/constitutional/${request?.id || requestId}`, { onClick={() => navigate(`/worknotes/constitutional/${request?.id || requestId}`, {
state: { state: {
requestType: 'constitutional', requestType: 'constitutional',
@ -1261,7 +1365,13 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }:
</div> </div>
{/* Action Dialog */} {/* Action Dialog */}
<Dialog open={isActionDialogOpen} onOpenChange={setIsActionDialogOpen}> <Dialog
open={isActionDialogOpen}
onOpenChange={(open) => {
setIsActionDialogOpen(open);
if (!open) setActionDialogError(null);
}}
>
<DialogContent> <DialogContent>
<DialogHeader> <DialogHeader>
<DialogTitle> <DialogTitle>
@ -1278,6 +1388,24 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }:
</DialogHeader> </DialogHeader>
<form onSubmit={handleSubmitAction} className="space-y-4"> <form onSubmit={handleSubmitAction} className="space-y-4">
{actionDialogError && (
<div
role="alert"
className="rounded-md border border-red-200 bg-red-50 p-3 text-sm text-red-900 flex gap-2"
>
<AlertCircle className="w-5 h-5 shrink-0 text-red-600" aria-hidden />
<div className="min-w-0">
<p className="font-medium">This action was not completed</p>
<p className="mt-1 whitespace-pre-wrap break-words">{actionDialogError}</p>
{/mandatory documents/i.test(actionDialogError) && (
<p className="mt-2 text-red-800">
Use the <strong>Documents</strong> tab to upload every required file for this constitution
type, then approve again.
</p>
)}
</div>
</div>
)}
<div> <div>
<Label htmlFor="comments"> <Label htmlFor="comments">
{actionType === 'sendBack' || actionType === 'revoke' ? 'Remarks (required) *' : 'Comments *'} {actionType === 'sendBack' || actionType === 'revoke' ? 'Remarks (required) *' : 'Comments *'}
@ -1304,9 +1432,9 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }:
type="submit" type="submit"
className={ className={
actionType === 'approve' ? 'bg-green-600 hover:bg-green-700' : actionType === 'approve' ? 'bg-green-600 hover:bg-green-700' :
actionType === 'reject' ? 'bg-red-600 hover:bg-red-700' : actionType === 'reject' ? 'bg-re-red hover:bg-re-red-hover' :
actionType === 'sendBack' ? 'bg-amber-600 hover:bg-amber-700' : actionType === 'sendBack' ? 'bg-re-red hover:bg-re-red-hover' :
'bg-orange-600 hover:bg-orange-700' 'bg-re-red hover:bg-re-red-hover'
} }
disabled={isActionLoading} disabled={isActionLoading}
> >
@ -1332,7 +1460,7 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }:
<DialogHeader> <DialogHeader>
<DialogTitle>Reject document</DialogTitle> <DialogTitle>Reject document</DialogTitle>
<DialogDescription> <DialogDescription>
Per SRS relocation-style verification states, mark this upload as Rejected and provide a reason for the dealer. Mark this upload as rejected and provide a reason. The action is recorded in the audit trail.
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<div className="space-y-3"> <div className="space-y-3">

View File

@ -12,7 +12,14 @@ import { useState, useEffect } from 'react';
import { User as UserType } from '@/lib/mock-data'; import { User as UserType } from '@/lib/mock-data';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { API } from '@/api/API'; import { API } from '@/api/API';
import { SlaBadge } from '@/components/sla/SlaBadge';
import { useSlaBatchStatus } from '@/hooks/useSlaBatchStatus';
import { formatDateTime } from '@/components/ui/utils'; import { formatDateTime } from '@/components/ui/utils';
import {
getCurrentStageBadgeClass,
getRequestStatusBadgeClass,
getStatusProgressBarClass,
} from '@/lib/statusProgressTheme';
import { normalizeDealerProfileConstitution } from '@/lib/constitutional-change'; import { normalizeDealerProfileConstitution } from '@/lib/constitutional-change';
import { import {
Pagination, Pagination,
@ -32,7 +39,7 @@ interface ConstitutionalChangePageProps {
// Document requirements mapping (keys = DB `changeType` ENUM values) // Document requirements mapping (keys = DB `changeType` ENUM values)
const documentRequirements: Record<string, number[]> = { const documentRequirements: Record<string, number[]> = {
'Partnership': [1, 2, 3, 4, 8, 9, 10, 16], 'Partnership': [1, 2, 3, 4, 8, 9, 10, 16],
'LLP': [1, 2, 3, 7, 8, 9, 10, 16], 'LLP': [1, 2, 3, 7, 8, 10, 11, 16],
'Private Limited': [1, 2, 3, 5, 6, 7, 8, 10, 16], 'Private Limited': [1, 2, 3, 5, 6, 7, 8, 10, 16],
'Proprietorship': [1, 2, 3, 10, 16] 'Proprietorship': [1, 2, 3, 10, 16]
}; };
@ -60,23 +67,16 @@ const documentNames: Record<number, string> = {
[OTHER_DOCUMENT_DOC_NUMBER]: 'Other' [OTHER_DOCUMENT_DOC_NUMBER]: 'Other'
}; };
const getStatusColor = (status: string) => { const getStatusColor = (status: string, currentStage?: string) =>
if (status === 'Completed') return 'bg-green-100 text-green-700 border-green-300'; getRequestStatusBadgeClass(status, currentStage);
if (status.includes('Review') || status.includes('Pending')) return 'bg-yellow-100 text-yellow-700 border-yellow-300';
if (status.includes('Rejected')) return 'bg-red-100 text-red-700 border-red-300';
if (status.includes('Collection')) return 'bg-blue-100 text-blue-700 border-blue-300';
return 'bg-slate-100 text-slate-700 border-slate-300';
};
const getTypeColor = (type: string) => { const getTypeColor = (type: string) => {
switch(type) { switch(type) {
case 'Proprietorship': return 'bg-purple-100 text-purple-700 border-purple-300'; case 'Proprietorship': return 'bg-purple-100 text-purple-700 border-purple-300';
case 'Partnership': return 'bg-blue-100 text-blue-700 border-blue-300'; case 'Partnership': return 'bg-blue-100 text-blue-700 border-blue-300';
case 'LLP': case 'LLP':
case 'LLP Conversion':
return 'bg-indigo-100 text-indigo-700 border-indigo-300'; return 'bg-indigo-100 text-indigo-700 border-indigo-300';
case 'Private Limited': case 'Private Limited':
case 'Pvt Ltd':
return 'bg-cyan-100 text-cyan-700 border-cyan-300'; return 'bg-cyan-100 text-cyan-700 border-cyan-300';
default: return 'bg-slate-100 text-slate-700 border-slate-300'; default: return 'bg-slate-100 text-slate-700 border-slate-300';
} }
@ -100,6 +100,12 @@ export function ConstitutionalChangePage({ onViewDetails }: ConstitutionalChange
const [paginationMeta, setPaginationMeta] = useState<any>(null); const [paginationMeta, setPaginationMeta] = useState<any>(null);
const [activeTab, setActiveTab] = useState('all'); const [activeTab, setActiveTab] = useState('all');
const itemsPerPage = 10; const itemsPerPage = 10;
const slaItems = requests.map((r: any) => ({
entityType: 'constitutional',
entityId: r.id || r.requestId
}));
const { get: getSla } = useSlaBatchStatus(slaItems, requests.length > 0);
const [isSubmitting, setIsSubmitting] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false);
const [dialogDataLoading, setDialogDataLoading] = useState(false); const [dialogDataLoading, setDialogDataLoading] = useState(false);
@ -289,7 +295,7 @@ export function ConstitutionalChangePage({ onViewDetails }: ConstitutionalChange
title: 'Submitted / Review', title: 'Submitted / Review',
value: paginationMeta?.stats?.pending || 0, value: paginationMeta?.stats?.pending || 0,
icon: Calendar, icon: Calendar,
color: 'bg-yellow-500', color: 'bg-re-red',
}, },
{ {
title: 'Completed', title: 'Completed',
@ -310,7 +316,7 @@ export function ConstitutionalChangePage({ onViewDetails }: ConstitutionalChange
{/* Loading Overlay */} {/* Loading Overlay */}
{isLoading && ( {isLoading && (
<div className="fixed inset-0 bg-slate-900/20 backdrop-blur-sm z-50 flex items-center justify-center"> <div className="fixed inset-0 bg-slate-900/20 backdrop-blur-sm z-50 flex items-center justify-center">
<Loader2 className="w-8 h-8 text-amber-600 animate-spin" /> <Loader2 className="w-8 h-8 text-re-red animate-spin" />
</div> </div>
)} )}
@ -325,7 +331,7 @@ export function ConstitutionalChangePage({ onViewDetails }: ConstitutionalChange
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}> <Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
<DialogTrigger asChild> <DialogTrigger asChild>
<Button className="bg-amber-600 hover:bg-amber-700"> <Button className="bg-re-red hover:bg-re-red-hover">
<Plus className="w-4 h-4 mr-2" /> <Plus className="w-4 h-4 mr-2" />
New Request New Request
</Button> </Button>
@ -486,7 +492,7 @@ export function ConstitutionalChangePage({ onViewDetails }: ConstitutionalChange
</Button> </Button>
<Button <Button
type="submit" type="submit"
className="bg-amber-600 hover:bg-amber-700" className="bg-re-red hover:bg-re-red-hover"
disabled={ disabled={
!dealerData || !dealerData ||
!targetType || !targetType ||
@ -598,15 +604,18 @@ export function ConstitutionalChangePage({ onViewDetails }: ConstitutionalChange
</div> </div>
</TableCell> </TableCell>
<TableCell> <TableCell>
<Badge variant="outline" className="border-slate-300 text-slate-700"> <div className="flex flex-wrap items-center gap-1">
{request.currentStage} <Badge className={getCurrentStageBadgeClass(request.currentStage, request.status)}>
</Badge> {request.currentStage}
</Badge>
<SlaBadge status={getSla('constitutional', request.id || request.requestId)} compact />
</div>
</TableCell> </TableCell>
<TableCell> <TableCell>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="flex-1 h-2 bg-slate-200 rounded-full overflow-hidden"> <div className="flex-1 h-2 bg-slate-200 rounded-full overflow-hidden">
<div <div
className="h-full bg-amber-600 transition-all duration-300" className={`h-full transition-all duration-300 ${getStatusProgressBarClass(request.status, request.currentStage)}`}
style={{ width: `${request.progressPercentage || 0}%` }} style={{ width: `${request.progressPercentage || 0}%` }}
/> />
</div> </div>
@ -676,12 +685,15 @@ export function ConstitutionalChangePage({ onViewDetails }: ConstitutionalChange
</div> </div>
</TableCell> </TableCell>
<TableCell> <TableCell>
<Badge variant="outline" className="border-slate-300 text-slate-700"> <div className="flex flex-wrap items-center gap-1">
{request.currentStage} <Badge className={getCurrentStageBadgeClass(request.currentStage, request.status)}>
</Badge> {request.currentStage}
</Badge>
<SlaBadge status={getSla('constitutional', request.id || request.requestId)} compact />
</div>
</TableCell> </TableCell>
<TableCell> <TableCell>
<Badge className={getStatusColor(request.status)}> <Badge className={getStatusColor(request.status, request.currentStage)}>
{request.status} {request.status}
</Badge> </Badge>
</TableCell> </TableCell>
@ -754,7 +766,7 @@ export function ConstitutionalChangePage({ onViewDetails }: ConstitutionalChange
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="flex-1 h-2 bg-slate-200 rounded-full overflow-hidden"> <div className="flex-1 h-2 bg-slate-200 rounded-full overflow-hidden">
<div <div
className="h-full bg-amber-600 transition-all duration-300" className={`h-full transition-all duration-300 ${getStatusProgressBarClass(request.status, request.currentStage)}`}
style={{ width: `${request.progressPercentage || 0}%` }} style={{ width: `${request.progressPercentage || 0}%` }}
/> />
</div> </div>
@ -762,9 +774,12 @@ export function ConstitutionalChangePage({ onViewDetails }: ConstitutionalChange
</div> </div>
</TableCell> </TableCell>
<TableCell> <TableCell>
<Badge variant="outline" className="border-slate-300 text-slate-700"> <div className="flex flex-wrap items-center gap-1">
{request.currentStage} <Badge className={getCurrentStageBadgeClass(request.currentStage, request.status)}>
</Badge> {request.currentStage}
</Badge>
<SlaBadge status={getSla('constitutional', request.id || request.requestId)} compact />
</div>
</TableCell> </TableCell>
<TableCell> <TableCell>
<Button <Button
@ -831,7 +846,7 @@ export function ConstitutionalChangePage({ onViewDetails }: ConstitutionalChange
</div> </div>
</TableCell> </TableCell>
<TableCell> <TableCell>
<Badge className={getStatusColor(request.status)}> <Badge className={getStatusColor(request.status, request.currentStage)}>
{request.status} {request.status}
</Badge> </Badge>
</TableCell> </TableCell>

View File

@ -15,18 +15,15 @@ import { dealerService } from '@/services/dealer.service';
import { formatDateTime } from '@/components/ui/utils'; import { formatDateTime } from '@/components/ui/utils';
import { API } from '@/api/API'; import { API } from '@/api/API';
import { normalizeDealerProfileConstitution } from '@/lib/constitutional-change'; import { normalizeDealerProfileConstitution } from '@/lib/constitutional-change';
import { getRequestStatusBadgeClass, getStatusProgressBarClass } from '@/lib/statusProgressTheme';
interface DealerConstitutionalChangePageProps { interface DealerConstitutionalChangePageProps {
currentUser?: UserType | null; currentUser?: UserType | null;
onViewDetails?: (id: string) => void; onViewDetails?: (id: string) => void;
} }
const getStatusColor = (status: string) => { const getStatusColor = (status: string, currentStage?: string) =>
if (status === 'Completed') return 'bg-green-100 text-green-700 border-green-300'; getRequestStatusBadgeClass(status, currentStage);
if (status.includes('Review') || status.includes('Pending')) return 'bg-yellow-100 text-yellow-700 border-yellow-300';
if (status.includes('Rejected')) return 'bg-red-100 text-red-700 border-red-300';
return 'bg-slate-100 text-slate-700 border-slate-300';
};
export function DealerConstitutionalChangePage({ onViewDetails }: DealerConstitutionalChangePageProps) { export function DealerConstitutionalChangePage({ onViewDetails }: DealerConstitutionalChangePageProps) {
const [isDialogOpen, setIsDialogOpen] = useState(false); const [isDialogOpen, setIsDialogOpen] = useState(false);
@ -122,13 +119,13 @@ export function DealerConstitutionalChangePage({ onViewDetails }: DealerConstitu
title: 'Total Requests', title: 'Total Requests',
value: requests.length, value: requests.length,
icon: RefreshCcw, icon: RefreshCcw,
color: 'bg-blue-500', color: 'bg-re-red',
}, },
{ {
title: 'Pending', title: 'Pending',
value: requests.filter(r => r.status !== 'Completed' && r.status !== 'Rejected').length, value: requests.filter(r => r.status !== 'Completed' && r.status !== 'Rejected').length,
icon: Calendar, icon: Calendar,
color: 'bg-yellow-500', color: 'bg-re-red',
}, },
{ {
title: 'Completed', title: 'Completed',
@ -143,7 +140,7 @@ export function DealerConstitutionalChangePage({ onViewDetails }: DealerConstitu
{/* Loading Overlay */} {/* Loading Overlay */}
{loading && ( {loading && (
<div className="min-h-[400px] flex items-center justify-center"> <div className="min-h-[400px] flex items-center justify-center">
<Loader2 className="w-8 h-8 text-blue-600 animate-spin" /> <Loader2 className="w-8 h-8 text-re-red animate-spin" />
</div> </div>
)} )}
@ -160,7 +157,7 @@ export function DealerConstitutionalChangePage({ onViewDetails }: DealerConstitu
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}> <Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
<DialogTrigger asChild> <DialogTrigger asChild>
<Button className="bg-blue-600 hover:bg-blue-700"> <Button className="bg-re-red hover:bg-re-red-hover">
<Plus className="w-4 h-4 mr-2" /> <Plus className="w-4 h-4 mr-2" />
New Constitutional Change New Constitutional Change
</Button> </Button>
@ -265,17 +262,18 @@ export function DealerConstitutionalChangePage({ onViewDetails }: DealerConstitu
)} )}
{/* Document Requirements */} {/* Document Requirements */}
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4"> <div className="bg-red-50 border border-red-200 rounded-lg p-4">
<h4 className="text-blue-900 mb-2">Documents Required (to be uploaded later)</h4> <h4 className="text-re-red-hover mb-2">Documents Required (to be uploaded later)</h4>
<ul className="text-blue-800 text-sm space-y-1"> <ul className="text-re-red-hover text-sm space-y-1">
<li> GST Registration Certificate</li> <li> GST Registration Certificate</li>
<li> Firm PAN Copy</li> <li> Firm PAN Copy</li>
<li> Partnership Deed (if applicable)</li> <li> Self-attested KYC documents</li>
<li> LLP Agreement (if applicable)</li> <li> Business Purchase Agreement (BPA)</li>
<li> Certificate of Incorporation (if applicable)</li> <li> Partnership Agreement / Firm Registration (if target is Partnership)</li>
<li> MOA & AOA (if applicable)</li> <li> LLP Agreement / COI (if target is LLP)</li>
<li> Board Resolution</li> <li> MOA, AOA, COI (if target is Private Limited)</li>
<li> Aadhaar & PAN of all partners/directors</li> <li> Cancelled Cheque</li>
<li> Declaration / Authorization Letter</li>
</ul> </ul>
</div> </div>
@ -287,9 +285,9 @@ export function DealerConstitutionalChangePage({ onViewDetails }: DealerConstitu
> >
Cancel Cancel
</Button> </Button>
<Button <Button
type="submit" type="submit"
className="bg-blue-600 hover:bg-blue-700" className="bg-re-red hover:bg-re-red-hover"
disabled={submitting} disabled={submitting}
> >
{submitting ? ( {submitting ? (
@ -365,7 +363,7 @@ export function DealerConstitutionalChangePage({ onViewDetails }: DealerConstitu
<Badge variant="outline">{request.currentConstitution}</Badge> <Badge variant="outline">{request.currentConstitution}</Badge>
</TableCell> </TableCell>
<TableCell> <TableCell>
<Badge className="bg-blue-100 text-blue-700 border-blue-300"> <Badge className="bg-red-50 text-re-red-hover border-red-200">
{request.changeType} {request.changeType}
</Badge> </Badge>
</TableCell> </TableCell>
@ -381,7 +379,7 @@ export function DealerConstitutionalChangePage({ onViewDetails }: DealerConstitu
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="flex-1 bg-slate-200 rounded-full h-2 min-w-[60px]"> <div className="flex-1 bg-slate-200 rounded-full h-2 min-w-[60px]">
<div <div
className="bg-blue-600 h-2 rounded-full" className={`h-2 rounded-full transition-all duration-300 ${getStatusProgressBarClass(request.status, request.currentStage)}`}
style={{ width: `${request.progressPercentage || 0}%` }} style={{ width: `${request.progressPercentage || 0}%` }}
/> />
</div> </div>

View File

@ -42,7 +42,7 @@ export function Dashboard({ onNavigate }: DashboardProps) {
title: 'Level 1 Pending', title: 'Level 1 Pending',
value: dashboardStats.level1Pending, value: dashboardStats.level1Pending,
icon: Clock, icon: Clock,
color: 'bg-amber-500', color: 'bg-red-500',
trend: { value: 3, isPositive: false }, trend: { value: 3, isPositive: false },
filter: 'Level 1 Pending' filter: 'Level 1 Pending'
}, },
@ -210,7 +210,7 @@ export function Dashboard({ onNavigate }: DashboardProps) {
<div className="w-24 text-slate-700">{item.location}</div> <div className="w-24 text-slate-700">{item.location}</div>
<div className="flex-1 bg-slate-200 rounded-full h-8 relative overflow-hidden"> <div className="flex-1 bg-slate-200 rounded-full h-8 relative overflow-hidden">
<div <div
className="bg-amber-600 h-full rounded-full transition-all flex items-center justify-end px-3" className="bg-re-red h-full rounded-full transition-all flex items-center justify-end px-3"
style={{ width: `${(item.count / maxLocationCount) * 100}%` }} style={{ width: `${(item.count / maxLocationCount) * 100}%` }}
> >
<span className="text-white">{item.count}</span> <span className="text-white">{item.count}</span>
@ -237,7 +237,7 @@ export function Dashboard({ onNavigate }: DashboardProps) {
className="flex items-start gap-4 p-3 hover:bg-slate-50 rounded-lg cursor-pointer transition-colors" className="flex items-start gap-4 p-3 hover:bg-slate-50 rounded-lg cursor-pointer transition-colors"
onClick={() => onNavigate('applications')} onClick={() => onNavigate('applications')}
> >
<div className="w-10 h-10 bg-amber-100 rounded-full flex items-center justify-center flex-shrink-0"> <div className="w-10 h-10 bg-red-50 rounded-full flex items-center justify-center flex-shrink-0">
{activity.action === 'Approved' && <CheckCircle className="w-5 h-5 text-green-600" />} {activity.action === 'Approved' && <CheckCircle className="w-5 h-5 text-green-600" />}
{activity.action === 'Interview Scheduled' && <Clock className="w-5 h-5 text-blue-600" />} {activity.action === 'Interview Scheduled' && <Clock className="w-5 h-5 text-blue-600" />}
{activity.action === 'Document Uploaded' && <FileText className="w-5 h-5 text-purple-600" />} {activity.action === 'Document Uploaded' && <FileText className="w-5 h-5 text-purple-600" />}

View File

@ -32,7 +32,7 @@ export function DealerDashboard({ currentUser, onNavigate }: DealerDashboardProp
if (loading) { if (loading) {
return ( return (
<div className="flex flex-col items-center justify-center min-h-[400px]"> <div className="flex flex-col items-center justify-center min-h-[400px]">
<Loader2 className="w-10 h-10 text-amber-600 animate-spin mb-4" /> <Loader2 className="w-10 h-10 text-re-red animate-spin mb-4" />
<p className="text-slate-600">Loading your dashboard...</p> <p className="text-slate-600">Loading your dashboard...</p>
</div> </div>
); );
@ -58,7 +58,7 @@ export function DealerDashboard({ currentUser, onNavigate }: DealerDashboardProp
title: 'Relocation Requests', title: 'Relocation Requests',
value: statsSummary.relocation, value: statsSummary.relocation,
icon: MapPin, icon: MapPin,
color: 'bg-amber-500', color: 'bg-re-red',
change: 'Active Requests', change: 'Active Requests',
onClick: () => onNavigate('dealer-relocation') onClick: () => onNavigate('dealer-relocation')
}, },
@ -96,8 +96,8 @@ export function DealerDashboard({ currentUser, onNavigate }: DealerDashboardProp
title: 'Request Relocation', title: 'Request Relocation',
description: 'Move dealership to new location', description: 'Move dealership to new location',
icon: MapPin, icon: MapPin,
color: 'bg-amber-50 hover:bg-amber-100 border-amber-200', color: 'bg-red-50 hover:bg-red-100 border-red-200',
textColor: 'text-amber-700', textColor: 'text-re-red',
onClick: () => onNavigate('dealer-relocation') onClick: () => onNavigate('dealer-relocation')
}, },
]; ];
@ -105,14 +105,14 @@ export function DealerDashboard({ currentUser, onNavigate }: DealerDashboardProp
return ( return (
<div className="space-y-6"> <div className="space-y-6">
{/* Welcome Section */} {/* Welcome Section */}
<div className="bg-gradient-to-r from-amber-500 to-amber-600 rounded-lg p-6 text-white"> <div className="rounded-lg bg-gradient-to-r from-re-red to-re-red-hover p-6 text-white">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<h1 className="text-white mb-2">Welcome back, {profile.name || currentUser?.name}!</h1> <h1 className="text-white mb-2">Welcome back, {profile.name || currentUser?.name}!</h1>
<p className="text-amber-100"> <p className="text-white/90">
Dealer Code: {profile.dealerCode} {profile.businessName} Dealer Code: {profile.dealerCode} {profile.businessName}
</p> </p>
<p className="text-amber-100 text-sm mt-1"> <p className="text-white/90 text-sm mt-1">
{primaryOutlet.name} {primaryOutlet.location} {primaryOutlet.name} {primaryOutlet.location}
</p> </p>
</div> </div>
@ -225,21 +225,21 @@ export function DealerDashboard({ currentUser, onNavigate }: DealerDashboardProp
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle className="flex items-center gap-2"> <CardTitle className="flex items-center gap-2">
<Clock className="w-5 h-5 text-amber-600" /> <Clock className="w-5 h-5 text-re-red" />
Important Reminders Important Reminders
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="space-y-3"> <div className="space-y-3">
<div className="flex items-start gap-2"> <div className="flex items-start gap-2">
<AlertCircle className="w-4 h-4 text-amber-600 mt-0.5" /> <AlertCircle className="w-4 h-4 text-re-red mt-0.5" />
<div> <div>
<p className="text-slate-900 text-sm">GST Filing Due</p> <p className="text-slate-900 text-sm">GST Filing Due</p>
<p className="text-slate-600 text-xs">Due by Jan 15, 2026</p> <p className="text-slate-600 text-xs">Due by Jan 15, 2026</p>
</div> </div>
</div> </div>
<div className="flex items-start gap-2"> <div className="flex items-start gap-2">
<AlertCircle className="w-4 h-4 text-amber-600 mt-0.5" /> <AlertCircle className="w-4 h-4 text-re-red mt-0.5" />
<div> <div>
<p className="text-slate-900 text-sm">Inventory Audit Scheduled</p> <p className="text-slate-900 text-sm">Inventory Audit Scheduled</p>
<p className="text-slate-600 text-xs">Jan 20, 2026</p> <p className="text-slate-600 text-xs">Jan 20, 2026</p>

View File

@ -147,7 +147,7 @@ export function FDDDashboardPage() {
</p> </p>
</div> </div>
) : ( ) : (
<div className="overflow-x-auto"> <div className="overflow-x-auto custom-scrollbar-x-slim">
<table className="w-full text-left border-collapse"> <table className="w-full text-left border-collapse">
<thead className="bg-slate-50 border-b border-slate-200"> <thead className="bg-slate-50 border-b border-slate-200">
<tr> <tr>
@ -184,7 +184,7 @@ export function FDDDashboardPage() {
<td className="px-6 py-4"> <td className="px-6 py-4">
<Badge className={`px-3 py-1 rounded-full text-[10px] uppercase font-bold tracking-wider ${ <Badge className={`px-3 py-1 rounded-full text-[10px] uppercase font-bold tracking-wider ${
app.overallStatus === 'Completed' ? 'bg-green-100 text-green-700' : app.overallStatus === 'Completed' ? 'bg-green-100 text-green-700' :
'bg-amber-100 text-amber-700' 'bg-red-50 text-re-red-hover'
}`}> }`}>
{app.overallStatus === 'Active' ? 'FDD Pending' : app.overallStatus} {app.overallStatus === 'Active' ? 'FDD Pending' : app.overallStatus}
</Badge> </Badge>

View File

@ -52,14 +52,14 @@ export function FinanceDashboard({ onNavigate, onViewPaymentDetails, onViewAudit
const apps = response.data || []; const apps = response.data || [];
// Derive Onboarding Payments from Application + SecurityDeposit (Standardized nomenclature) // Derive Onboarding Payments from Application + SecurityDeposit (Standardized nomenclature)
// This ensures applications in "Payment Pending" / "Security Details" are visible // This ensures applications in "Payment Pending" / "Security Deposit" are visible
// even if no payment record has been manually initialized. // even if no payment record has been manually initialized.
const consolidatedPayments: any[] = []; const consolidatedPayments: any[] = [];
apps.forEach((app: any) => { apps.forEach((app: any) => {
const s = app.overallStatus || app.status; const s = app.overallStatus || app.status;
const isPaymentStage = [ const isPaymentStage = [
'Payment Pending', 'Security Details', 'LOI In Progress', 'LOI Issued', 'Payment Pending', 'Security Deposit', 'Security Details', 'LOI In Progress', 'LOI Issued',
'LOA Pending', 'Dealer Code Generation', 'LOA_APPROVAL' 'LOA Pending', 'Dealer Code Generation', 'LOA_APPROVAL'
].includes(s); ].includes(s);
const deposits = app.securityDeposits || []; const deposits = app.securityDeposits || [];
@ -80,7 +80,7 @@ export function FinanceDashboard({ onNavigate, onViewPaymentDetails, onViewAudit
}); });
}); });
} else if (isPaymentStage) { } else if (isPaymentStage) {
if (['Payment Pending', 'Security Details', 'LOI In Progress'].includes(s)) { if (['Payment Pending', 'Security Deposit', 'Security Details', 'LOI In Progress'].includes(s)) {
// Virtual pending record for Security Deposit (5L) // Virtual pending record for Security Deposit (5L)
consolidatedPayments.push({ consolidatedPayments.push({
id: `virtual-${app.id}-sd`, id: `virtual-${app.id}-sd`,
@ -214,7 +214,7 @@ export function FinanceDashboard({ onNavigate, onViewPaymentDetails, onViewAudit
if (loading) { if (loading) {
return ( return (
<div className="flex items-center justify-center p-20 text-amber-600"> <div className="flex items-center justify-center p-20 text-re-red">
<Clock className="w-8 h-8 animate-spin mr-3" /> <Clock className="w-8 h-8 animate-spin mr-3" />
<span>Loading Finance Data...</span> <span>Loading Finance Data...</span>
</div> </div>
@ -234,7 +234,7 @@ export function FinanceDashboard({ onNavigate, onViewPaymentDetails, onViewAudit
{/* Stats Cards */} {/* Stats Cards */}
<div className="grid grid-cols-1 md:grid-cols-5 gap-3"> <div className="grid grid-cols-1 md:grid-cols-5 gap-3">
<Card <Card
className="cursor-pointer hover:shadow-lg transition-shadow border-amber-200 bg-amber-50/20" className="cursor-pointer hover:shadow-lg transition-shadow border-red-200 bg-red-50/20"
onClick={() => { onClick={() => {
if (pendingAudits.length > 0 && onViewAuditDetails) { if (pendingAudits.length > 0 && onViewAuditDetails) {
onViewAuditDetails(pendingAudits[0].applicationId || pendingAudits[0].id); onViewAuditDetails(pendingAudits[0].applicationId || pendingAudits[0].id);
@ -244,12 +244,12 @@ export function FinanceDashboard({ onNavigate, onViewPaymentDetails, onViewAudit
}} }}
> >
<CardHeader className="pb-3"> <CardHeader className="pb-3">
<CardDescription className="text-amber-600 font-bold">Pending Audits</CardDescription> <CardDescription className="text-re-red font-bold">Pending Audits</CardDescription>
<CardTitle className="text-3xl text-amber-600">{pendingAudits.length}</CardTitle> <CardTitle className="text-3xl text-re-red">{pendingAudits.length}</CardTitle>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<p className="text-slate-600 text-xs font-medium">FDD Sign-offs</p> <p className="text-slate-600 text-xs font-medium">FDD Sign-offs</p>
<Button variant="link" className="p-0 h-auto text-amber-600 mt-2 text-xs"> <Button variant="link" className="p-0 h-auto text-re-red mt-2 text-xs">
Review Now Review Now
</Button> </Button>
</CardContent> </CardContent>
@ -265,7 +265,7 @@ export function FinanceDashboard({ onNavigate, onViewPaymentDetails, onViewAudit
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<p className="text-slate-600">Onboarding Payments</p> <p className="text-slate-600">Onboarding Payments</p>
<Button variant="link" className="p-0 h-auto text-amber-600 mt-2"> <Button variant="link" className="p-0 h-auto text-re-red mt-2">
View All View All
</Button> </Button>
</CardContent> </CardContent>
@ -281,7 +281,7 @@ export function FinanceDashboard({ onNavigate, onViewPaymentDetails, onViewAudit
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<p className="text-slate-600">Total Validated</p> <p className="text-slate-600">Total Validated</p>
<Button variant="link" className="p-0 h-auto text-amber-600 mt-2"> <Button variant="link" className="p-0 h-auto text-re-red mt-2">
View All View All
</Button> </Button>
</CardContent> </CardContent>
@ -297,7 +297,7 @@ export function FinanceDashboard({ onNavigate, onViewPaymentDetails, onViewAudit
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<p className="text-slate-600">Offboarding Cases</p> <p className="text-slate-600">Offboarding Cases</p>
<Button variant="link" className="p-0 h-auto text-amber-600 mt-2"> <Button variant="link" className="p-0 h-auto text-re-red mt-2">
View All View All
</Button> </Button>
</CardContent> </CardContent>
@ -313,7 +313,7 @@ export function FinanceDashboard({ onNavigate, onViewPaymentDetails, onViewAudit
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<p className="text-slate-600">Settlements Done</p> <p className="text-slate-600">Settlements Done</p>
<Button variant="link" className="p-0 h-auto text-amber-600 mt-2"> <Button variant="link" className="p-0 h-auto text-re-red mt-2">
View All View All
</Button> </Button>
</CardContent> </CardContent>
@ -381,7 +381,7 @@ export function FinanceDashboard({ onNavigate, onViewPaymentDetails, onViewAudit
</div> </div>
<div> <div>
<p className="text-slate-600">Amount</p> <p className="text-slate-600">Amount</p>
<p className="text-amber-700 font-bold"> <p className="text-re-red-hover font-bold">
{parseFloat(app.amount).toLocaleString('en-IN')} {parseFloat(app.amount).toLocaleString('en-IN')}
</p> </p>
</div> </div>
@ -535,7 +535,7 @@ export function FinanceDashboard({ onNavigate, onViewPaymentDetails, onViewAudit
</div> </div>
<div> <div>
<p className="text-slate-600 font-medium text-[10px] uppercase tracking-wider mb-1">Dealer Code</p> <p className="text-slate-600 font-medium text-[10px] uppercase tracking-wider mb-1">Dealer Code</p>
<p className="font-mono text-xs font-bold text-blue-600">{fnf.outlet?.code || 'N/A'}</p> <p className="font-mono text-xs font-bold text-re-red">{fnf.outlet?.code || 'N/A'}</p>
</div> </div>
<div> <div>
<p className="text-slate-600 font-medium text-[10px] uppercase tracking-wider mb-1">Location</p> <p className="text-slate-600 font-medium text-[10px] uppercase tracking-wider mb-1">Location</p>
@ -564,7 +564,7 @@ export function FinanceDashboard({ onNavigate, onViewPaymentDetails, onViewAudit
</Button> </Button>
<Button <Button
size="sm" size="sm"
className="bg-blue-600 hover:bg-blue-700" className="bg-re-red hover:bg-re-red-hover"
onClick={() => { onClick={() => {
setSelectedFnF(fnf); setSelectedFnF(fnf);
setLineItems([]); setLineItems([]);
@ -709,7 +709,7 @@ export function FinanceDashboard({ onNavigate, onViewPaymentDetails, onViewAudit
<Button <Button
size="sm" size="sm"
onClick={handleAddLineItem} onClick={handleAddLineItem}
className="w-full bg-amber-600 hover:bg-amber-700" className="w-full bg-re-red hover:bg-re-red-hover"
> >
<Plus className="w-4 h-4" /> <Plus className="w-4 h-4" />
</Button> </Button>
@ -837,7 +837,7 @@ export function FinanceDashboard({ onNavigate, onViewPaymentDetails, onViewAudit
</div> </div>
<div> <div>
<p className="text-xs text-slate-500 uppercase tracking-wider mb-1">Current Status</p> <p className="text-xs text-slate-500 uppercase tracking-wider mb-1">Current Status</p>
<Badge variant="outline" className="bg-amber-100 text-amber-700 border-amber-200"> <Badge variant="outline" className="bg-red-50 text-re-red-hover border-red-200">
{selectedFnF.status} {selectedFnF.status}
</Badge> </Badge>
</div> </div>
@ -845,10 +845,10 @@ export function FinanceDashboard({ onNavigate, onViewPaymentDetails, onViewAudit
</Card> </Card>
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
<Card className="border-blue-100"> <Card className="border-red-100">
<CardHeader className="bg-blue-50/50 pb-2"> <CardHeader className="bg-red-50/50 pb-2">
<CardTitle className="text-sm font-bold flex items-center gap-2"> <CardTitle className="text-sm font-bold flex items-center gap-2">
<TrendingUp className="w-4 h-4 text-blue-600" /> <TrendingUp className="w-4 h-4 text-re-red" />
Receivables Check Receivables Check
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
@ -861,7 +861,7 @@ export function FinanceDashboard({ onNavigate, onViewPaymentDetails, onViewAudit
<span className="text-slate-500">Other Payable Credits</span> <span className="text-slate-500">Other Payable Credits</span>
<span className="font-bold text-slate-900">0</span> <span className="font-bold text-slate-900">0</span>
</div> </div>
<div className="border-t pt-2 flex justify-between font-bold text-blue-700"> <div className="border-t pt-2 flex justify-between font-bold text-re-red-hover">
<span>Total Payables</span> <span>Total Payables</span>
<span>{parseFloat(selectedFnF.totalPayables || 0).toLocaleString('en-IN')}</span> <span>{parseFloat(selectedFnF.totalPayables || 0).toLocaleString('en-IN')}</span>
</div> </div>
@ -909,7 +909,7 @@ export function FinanceDashboard({ onNavigate, onViewPaymentDetails, onViewAudit
</div> </div>
</div> </div>
</div> </div>
<Button className="bg-blue-600 hover:bg-blue-500 px-6 font-bold shadow-md transition-all active:scale-95"> <Button className="bg-re-red hover:bg-re-red-hover px-6 font-bold shadow-md transition-all active:scale-95">
Generate PDF Summary Generate PDF Summary
</Button> </Button>
</div> </div>

View File

@ -8,18 +8,18 @@ import {
RefreshCw, RefreshCw,
} from 'lucide-react'; } from 'lucide-react';
import { useDispatch, useSelector } from 'react-redux'; import { useDispatch, useSelector } from 'react-redux';
import { useNavigate, Routes, Route, useParams } from 'react-router-dom'; import { useNavigate, Routes, Route, useParams, useLocation } from 'react-router-dom';
import { RootState } from '@/store'; import { RootState } from '@/store';
import { logout } from '@/store/slices/authSlice'; import { logout } from '@/store/slices/authSlice';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { API } from '@/api/API'; import { API } from '@/api/API';
import { formatDateTime } from '@/components/ui/utils'; import { formatDateTime } from '@/components/ui/utils';
import { Badge } from '@/components/ui/badge';
import { ProspectiveApplicationDetails } from '@/features/onboarding/pages/ProspectiveApplicationDetails'; import { ProspectiveApplicationDetails } from '@/features/onboarding/pages/ProspectiveApplicationDetails';
export function ProspectiveDashboardPage() { export function ProspectiveDashboardPage() {
const dispatch = useDispatch(); const dispatch = useDispatch();
const navigate = useNavigate(); const navigate = useNavigate();
const location = useLocation();
const { user } = useSelector((state: RootState) => state.auth); const { user } = useSelector((state: RootState) => state.auth);
const [collapsed, setCollapsed] = useState(false); const [collapsed, setCollapsed] = useState(false);
const [activeTab, setActiveTab] = useState('applicant'); const [activeTab, setActiveTab] = useState('applicant');
@ -35,22 +35,46 @@ export function ProspectiveDashboardPage() {
{/* Sidebar */} {/* Sidebar */}
<div className={`bg-slate-900 text-white h-screen flex flex-col transition-all duration-300 ${collapsed ? 'w-20' : 'w-64'}`}> <div className={`bg-slate-900 text-white h-screen flex flex-col transition-all duration-300 ${collapsed ? 'w-20' : 'w-64'}`}>
<div className="p-4 border-b border-slate-800"> <div className="p-4 border-b border-slate-800">
<div className="flex items-center justify-between"> {!collapsed ? (
{!collapsed && ( <div className="space-y-3">
<div className="flex items-center gap-2"> <div className="flex justify-end">
<div className="w-10 h-10 bg-amber-600 rounded-lg flex items-center justify-center"> <button
<FileText className="w-6 h-6 text-white" /> onClick={() => setCollapsed(true)}
</div> className="p-1 hover:bg-slate-800 rounded transition-colors"
<span className="text-amber-600 font-bold">Applicant Portal</span> title="Collapse sidebar"
>
<ChevronLeft className="w-5 h-5" />
</button>
</div> </div>
)} <div className="w-full">
<button <img
onClick={() => setCollapsed(!collapsed)} src="/assets/images/Re_Logo.png"
className="p-1 hover:bg-slate-800 rounded transition-colors" alt="Royal Enfield"
> className="mx-auto block h-auto w-full max-h-14 object-contain"
{collapsed ? <ChevronRight className="w-5 h-5" /> : <ChevronLeft className="w-5 h-5" />} />
</button> </div>
</div> <p className="text-center text-[10px] font-bold uppercase tracking-[0.2em] text-slate-400">
Applicant Portal
</p>
</div>
) : (
<div className="flex flex-col items-center gap-3">
<div className="w-full">
<img
src="/assets/images/Re_Logo.png"
alt="Royal Enfield"
className="block h-auto w-full max-h-10 object-contain"
/>
</div>
<button
onClick={() => setCollapsed(false)}
className="p-1 hover:bg-slate-800 rounded transition-colors"
title="Expand sidebar"
>
<ChevronRight className="w-5 h-5" />
</button>
</div>
)}
</div> </div>
<nav className="flex-1 p-4 space-y-2"> <nav className="flex-1 p-4 space-y-2">
@ -60,7 +84,7 @@ export function ProspectiveDashboardPage() {
setActiveTab('applicant'); setActiveTab('applicant');
navigate('/prospective-dashboard'); navigate('/prospective-dashboard');
}} }}
className={`w-full flex items-center gap-3 px-4 py-3 rounded-lg transition-colors ${activeTab === 'applicant' ? 'bg-amber-600 text-white' : 'text-slate-300 hover:bg-slate-800 hover:text-white'}`} className={`w-full flex items-center gap-3 px-4 py-3 rounded-lg transition-colors ${activeTab === 'applicant' ? 'bg-re-red text-white hover:bg-re-red-hover' : 'text-slate-300 hover:bg-slate-800 hover:text-white'}`}
> >
<FileText className="w-5 h-5 flex-shrink-0" /> <FileText className="w-5 h-5 flex-shrink-0" />
{!collapsed && <span className="flex-1 text-left">My Applications</span>} {!collapsed && <span className="flex-1 text-left">My Applications</span>}
@ -72,7 +96,7 @@ export function ProspectiveDashboardPage() {
{!collapsed && ( {!collapsed && (
<div className="px-4 py-2 bg-slate-800 rounded-lg mb-2"> <div className="px-4 py-2 bg-slate-800 rounded-lg mb-2">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="w-10 h-10 bg-amber-600 rounded-full flex items-center justify-center text-white"> <div className="w-10 h-10 bg-re-red rounded-full flex items-center justify-center text-white ring-2 ring-white/20">
<span className="font-bold">{user?.name?.charAt(0) || 'A'}</span> <span className="font-bold">{user?.name?.charAt(0) || 'A'}</span>
</div> </div>
<div className="flex-1 min-w-0"> <div className="flex-1 min-w-0">
@ -97,12 +121,20 @@ export function ProspectiveDashboardPage() {
<header className="bg-white border-b border-slate-200 px-6 py-4"> <header className="bg-white border-b border-slate-200 px-6 py-4">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<h1 className="text-slate-900 text-xl font-semibold">Applicant Management</h1> <h1 className="text-slate-900 text-xl font-semibold">
<p className="text-slate-600 text-sm">Manage and track dealership applications</p> {location.pathname.includes('/application/')
? 'Application details'
: 'Applicant management'}
</h1>
<p className="text-slate-600 text-sm max-w-2xl leading-snug">
{location.pathname.includes('/application/')
? 'Review and update statutory information and required documents for this application. Progress is coordinated by Royal Enfield after you submit.'
: 'Start or continue your dealership application, upload documents, and use links we email you (for example the questionnaire).'}
</p>
</div> </div>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="flex items-center gap-3 px-3 py-2 bg-slate-100 rounded-lg"> <div className="flex items-center gap-3 px-3 py-2 bg-slate-100 rounded-lg">
<div className="w-8 h-8 bg-amber-600 rounded-full flex items-center justify-center"> <div className="w-8 h-8 bg-re-red rounded-full flex items-center justify-center ring-2 ring-re-red/20">
<User className="w-4 h-4 text-white" /> <User className="w-4 h-4 text-white" />
</div> </div>
<div className="text-left"> <div className="text-left">
@ -175,40 +207,22 @@ function ProspectiveApplicationList() {
{applications.map((app) => ( {applications.map((app) => (
<div <div
key={app.id} key={app.id}
className="bg-white rounded-2xl border border-slate-200 p-6 shadow-sm hover:shadow-md hover:border-amber-500 cursor-pointer transition-all group" className="bg-white rounded-2xl border border-slate-200 p-6 shadow-sm hover:shadow-md hover:border-re-red cursor-pointer transition-all group"
onClick={() => navigate(`/prospective-dashboard/application/${app.id}`)} onClick={() => navigate(`/prospective-dashboard/application/${app.id}`)}
> >
<div className="flex justify-between items-start mb-4"> <div className="mb-4">
<div className="w-12 h-12 bg-amber-50 rounded-xl flex items-center justify-center group-hover:bg-amber-600 transition-colors"> <div className="w-12 h-12 bg-red-50 rounded-xl flex items-center justify-center group-hover:bg-re-red transition-colors">
<FileText className="w-6 h-6 text-amber-600 group-hover:text-white" /> <FileText className="w-6 h-6 text-re-red group-hover:text-white" />
</div> </div>
<Badge className={`px-4 py-1.5 rounded-xl text-[10px] uppercase font-bold ${app.overallStatus === 'Completed' ? 'bg-green-100 text-green-700' :
app.overallStatus === 'Rejected' ? 'bg-red-100 text-red-700' :
'bg-amber-100 text-amber-700'}`}>
{app.overallStatus || 'Active'}
</Badge>
</div> </div>
<h3 className="text-xl font-bold text-slate-900 mb-1 truncate">{app.applicationId}</h3> <h3 className="text-xl font-bold text-slate-900 mb-1 truncate">{app.applicationId}</h3>
<p className="text-slate-500 text-sm mb-4 font-medium">{app.city}, {app.state}</p> <p className="text-slate-500 text-sm mb-4 font-medium">{app.city}, {app.state}</p>
<div className="space-y-4 pt-6 border-t border-slate-100"> <div className="space-y-4 pt-6 border-t border-slate-100">
<div className="flex justify-between items-center">
<span className="text-xs text-slate-500 font-medium">Current Stage</span>
<span className="text-xs font-bold text-slate-900 bg-slate-100 px-3 py-1 rounded-lg">{app.currentStage || 'Initial'}</span>
</div>
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<span className="text-xs text-slate-500 font-medium">Applied</span> <span className="text-xs text-slate-500 font-medium">Applied</span>
<span className="text-xs font-bold text-slate-600">{formatDateTime(app.createdAt)}</span> <span className="text-xs font-bold text-slate-600">{formatDateTime(app.createdAt)}</span>
</div> </div>
<div className="mt-6">
<div className="flex justify-between items-center mb-1">
<span className="text-[10px] font-bold text-slate-500 uppercase tracking-wider">Progress</span>
<span className="text-xs font-bold text-amber-600">{app.progressPercentage || 0}%</span>
</div>
<div className="w-full bg-slate-100 rounded-full h-2">
<div className="bg-amber-500 h-2 rounded-full transition-all duration-1000" style={{ width: `${app.progressPercentage || 0}%` }}></div>
</div>
</div>
</div> </div>
</div> </div>
))} ))}

View File

@ -33,8 +33,10 @@ import {
Trash2, Trash2,
Save, Save,
Paperclip, Paperclip,
FileDown FileDown,
MessageSquare
} from 'lucide-react'; } from 'lucide-react';
import { useNavigate } from 'react-router-dom';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { DocumentPreviewModal } from '@/components/ui/DocumentPreviewModal'; import { DocumentPreviewModal } from '@/components/ui/DocumentPreviewModal';
import { formatDateTime } from '@/lib/dateUtils'; import { formatDateTime } from '@/lib/dateUtils';
@ -80,6 +82,7 @@ interface FinancialLineItem {
} }
export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPageProps) { export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPageProps) {
const navigate = useNavigate();
const [fnfCase, setFnfCase] = useState<any>(null); const [fnfCase, setFnfCase] = useState<any>(null);
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [activeTab, setActiveTab] = useState('overview'); const [activeTab, setActiveTab] = useState('overview');
@ -784,7 +787,7 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
if (loading) { if (loading) {
return ( return (
<div className="flex items-center justify-center p-12"> <div className="flex items-center justify-center p-12">
<Loader2 className="w-8 h-8 animate-spin text-amber-600" /> <Loader2 className="w-8 h-8 animate-spin text-re-red" />
</div> </div>
); );
} }
@ -801,22 +804,39 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
return ( return (
<div className="space-y-6"> <div className="space-y-6">
{/* Header */} {/* Header */}
<div className="flex items-center gap-4"> <div className="flex items-center justify-between gap-4">
<Button variant="outline" size="icon" onClick={onBack}> <div className="flex items-center gap-4">
<ArrowLeft className="w-4 h-4" /> <Button variant="outline" size="icon" onClick={onBack}>
</Button> <ArrowLeft className="w-4 h-4" />
<div> </Button>
<h1 className="text-3xl mb-1">F&F Settlement Review</h1> <div>
<p className="text-slate-600">Full & Final Settlement for {fnfCase.dealerName}</p> <h1 className="text-3xl mb-1">F&F Settlement Review</h1>
<p className="text-slate-600">Full & Final Settlement for {fnfCase.dealerName}</p>
</div>
</div> </div>
<Button
variant="outline"
onClick={() =>
navigate(`/worknotes/fnf/${fnfId}`, {
state: {
applicationName: fnfCase.dealerName || 'F&F Settlement',
registrationNumber: fnfCase.caseNumber || fnfCase.settlementId || '',
participants: fnfCase.participants || []
}
})
}
>
<MessageSquare className="w-4 h-4 mr-2" />
View Work Notes
</Button>
</div> </div>
{/* Status Banner */} {/* Status Banner */}
<Card className="border-amber-200 bg-amber-50"> <Card className="border-red-200 bg-red-50">
<CardContent className="pt-6"> <CardContent className="pt-6">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="size-12 shrink-0 aspect-square rounded-full bg-amber-100 flex items-center justify-center"> <div className="size-12 shrink-0 aspect-square rounded-full bg-red-50 flex items-center justify-center">
<IndianRupee className="w-5 h-5" /> <IndianRupee className="w-5 h-5" />
</div> </div>
<div> <div>
@ -825,7 +845,7 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
</div> </div>
</div> </div>
<div className="flex gap-2"> <div className="flex gap-2">
<Badge className="bg-amber-600"> <Badge className="bg-re-red">
{fnfCase.status} {fnfCase.status}
</Badge> </Badge>
<Badge variant={fnfCase.terminationType === 'Resignation' ? 'default' : 'secondary'}> <Badge variant={fnfCase.terminationType === 'Resignation' ? 'default' : 'secondary'}>
@ -932,7 +952,7 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
</div> </div>
<div> <div>
<Label className="text-slate-500">Status</Label> <Label className="text-slate-500">Status</Label>
<Badge className="bg-amber-600"> <Badge className="bg-re-red">
{fnfCase.status} {fnfCase.status}
</Badge> </Badge>
</div> </div>
@ -994,9 +1014,9 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
<span className="text-slate-900">Total Receivables (from Dealer)</span> <span className="text-slate-900">Total Receivables (from Dealer)</span>
<span className="text-red-700 text-lg">- {settlement.receivables.toLocaleString('en-IN')}</span> <span className="text-red-700 text-lg">- {settlement.receivables.toLocaleString('en-IN')}</span>
</div> </div>
<div className="flex justify-between items-center p-3 bg-amber-50 rounded-lg"> <div className="flex justify-between items-center p-3 bg-red-50 rounded-lg">
<span className="text-slate-900">Total Deductions</span> <span className="text-slate-900">Total Deductions</span>
<span className="text-amber-700 text-lg">- {settlement.deductions.toLocaleString('en-IN')}</span> <span className="text-re-red-hover text-lg">- {settlement.deductions.toLocaleString('en-IN')}</span>
</div> </div>
</div> </div>
@ -1030,8 +1050,8 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
</div> </div>
</div> </div>
<div className="flex items-start gap-3 p-4 bg-blue-50 border border-amber-200 rounded-lg"> <div className="flex items-start gap-3 p-4 bg-red-50 border border-red-200 rounded-lg">
<AlertCircle className="w-5 h-5 text-amber-600 mt-0.5" /> <AlertCircle className="w-5 h-5 text-re-red mt-0.5" />
<div> <div>
<p className="text-sm text-slate-900 mb-1">Calculation Formula</p> <p className="text-sm text-slate-900 mb-1">Calculation Formula</p>
<p className="text-sm text-slate-600"> <p className="text-sm text-slate-600">
@ -1045,7 +1065,7 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
</TabsContent> </TabsContent>
<TabsContent value="financial" className="space-y-4"> <TabsContent value="financial" className="space-y-4">
<Card className="border-blue-200 bg-blue-50"> <Card className="border-red-200 bg-red-50">
<CardHeader> <CardHeader>
<CardTitle className="text-base">Department Claim vs Finance Validation</CardTitle> <CardTitle className="text-base">Department Claim vs Finance Validation</CardTitle>
<CardDescription> <CardDescription>
@ -1401,12 +1421,12 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
</Card> </Card>
{/* Deductions - Editable */} {/* Deductions - Editable */}
<Card className="border-amber-200 bg-amber-50"> <Card className="border-red-200 bg-red-50">
<CardHeader> <CardHeader>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<CardTitle className="text-base flex items-center gap-2"> <CardTitle className="text-base flex items-center gap-2">
<AlertCircle className="w-5 h-5 text-amber-600" /> <AlertCircle className="w-5 h-5 text-re-red" />
Deductions (Editable) Deductions (Editable)
</CardTitle> </CardTitle>
<CardDescription>Add or modify pending claims and deductions</CardDescription> <CardDescription>Add or modify pending claims and deductions</CardDescription>
@ -1512,7 +1532,7 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
</Table> </Table>
{/* Add New Deduction */} {/* Add New Deduction */}
<div className="border-t border-amber-300 pt-4 space-y-3"> <div className="border-t border-red-300 pt-4 space-y-3">
<p className="text-sm text-slate-700">Add New Deduction Item:</p> <p className="text-sm text-slate-700">Add New Deduction Item:</p>
<div className="grid grid-cols-12 gap-2"> <div className="grid grid-cols-12 gap-2">
<Select <Select
@ -1541,17 +1561,17 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
onChange={(e) => setNewDeduction({ ...newDeduction, amount: e.target.value })} onChange={(e) => setNewDeduction({ ...newDeduction, amount: e.target.value })}
className="col-span-3" className="col-span-3"
/> />
<Button onClick={handleAddDeduction} className="col-span-1 bg-amber-600 hover:bg-amber-700"> <Button onClick={handleAddDeduction} className="col-span-1 bg-re-red hover:bg-re-red-hover">
<Plus className="w-4 h-4" /> <Plus className="w-4 h-4" />
</Button> </Button>
</div> </div>
</div> </div>
{/* Total */} {/* Total */}
<div className="pt-3 border-t-2 border-amber-400"> <div className="pt-3 border-t-2 border-red-300">
<div className="flex justify-between items-center"> <div className="flex justify-between items-center">
<span className="text-slate-900">Total Deductions</span> <span className="text-slate-900">Total Deductions</span>
<span className="text-amber-700 text-xl"> <span className="text-re-red-hover text-xl">
{settlement.deductions.toLocaleString('en-IN')} {settlement.deductions.toLocaleString('en-IN')}
</span> </span>
</div> </div>
@ -1560,10 +1580,10 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
</Card> </Card>
{/* Final Settlement Summary */} {/* Final Settlement Summary */}
<Card className="border-2 border-blue-300 bg-blue-50"> <Card className="border-2 border-red-300 bg-red-50">
<CardHeader> <CardHeader>
<CardTitle className="flex items-center gap-2"> <CardTitle className="flex items-center gap-2">
<CheckCircle className="w-5 h-5 text-amber-600" /> <CheckCircle className="w-5 h-5 text-re-red" />
Final Settlement Summary Final Settlement Summary
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
@ -1579,11 +1599,11 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
</div> </div>
<div className="flex justify-between items-center p-3 bg-white rounded-lg"> <div className="flex justify-between items-center p-3 bg-white rounded-lg">
<span className="text-slate-900">Total Deductions</span> <span className="text-slate-900">Total Deductions</span>
<span className="text-amber-700 text-lg">- {settlement.deductions.toLocaleString('en-IN')}</span> <span className="text-re-red-hover text-lg">- {settlement.deductions.toLocaleString('en-IN')}</span>
</div> </div>
</div> </div>
<div className="h-px bg-blue-300"></div> <div className="h-px bg-red-300"></div>
<div className={`p-4 rounded-lg border-2 ${ <div className={`p-4 rounded-lg border-2 ${
settlement.settlementType === 'Payable to Dealer' settlement.settlementType === 'Payable to Dealer'
@ -1613,8 +1633,8 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
</div> </div>
</div> </div>
<div className="flex items-start gap-3 p-4 bg-white border border-amber-200 rounded-lg"> <div className="flex items-start gap-3 p-4 bg-white border border-red-200 rounded-lg">
<AlertCircle className="w-5 h-5 text-amber-600 mt-0.5" /> <AlertCircle className="w-5 h-5 text-re-red mt-0.5" />
<div> <div>
<p className="text-sm text-slate-900 mb-1">Calculation Formula</p> <p className="text-sm text-slate-900 mb-1">Calculation Formula</p>
<p className="text-sm text-slate-600"> <p className="text-sm text-slate-600">
@ -1758,7 +1778,7 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
filePath: dept.supportingDocument, filePath: dept.supportingDocument,
documentType: 'Departmental Clearance Proof' documentType: 'Departmental Clearance Proof'
})} })}
className="flex items-center gap-1 text-[10px] text-amber-600 hover:underline" className="flex items-center gap-1 text-[10px] text-re-red hover:underline"
> >
<Paperclip className="w-3 h-3" /> <Paperclip className="w-3 h-3" />
View Proof View Proof
@ -1774,10 +1794,10 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
</Card> </Card>
{/* Important Notes */} {/* Important Notes */}
<Card className="bg-blue-50 border-amber-200"> <Card className="bg-red-50 border-red-200">
<CardContent className="pt-6"> <CardContent className="pt-6">
<div className="flex items-start gap-3"> <div className="flex items-start gap-3">
<AlertCircle className="w-5 h-5 text-amber-600 mt-0.5" /> <AlertCircle className="w-5 h-5 text-re-red mt-0.5" />
<div> <div>
<p className="text-sm text-slate-900 mb-1">Department Response Guidelines</p> <p className="text-sm text-slate-900 mb-1">Department Response Guidelines</p>
<ul className="text-sm text-slate-700 space-y-1"> <ul className="text-sm text-slate-700 space-y-1">
@ -1820,7 +1840,7 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
filePath: doc.url, filePath: doc.url,
documentType: doc.type documentType: doc.type
})} })}
className="text-amber-600 hover:text-amber-700 text-[10px] font-semibold flex items-center gap-1" className="text-re-red hover:text-re-red-hover text-[10px] font-semibold flex items-center gap-1"
> >
<Paperclip className="w-3 h-3" /> PREVIEW <Paperclip className="w-3 h-3" /> PREVIEW
</button> </button>
@ -1873,7 +1893,7 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<div className="space-y-4"> <div className="space-y-4">
<div className="border-2 border-dashed border-slate-300 rounded-lg p-8 text-center hover:border-amber-400 hover:bg-amber-50 transition-colors"> <div className="border-2 border-dashed border-slate-300 rounded-lg p-8 text-center hover:border-red-300 hover:bg-red-50 transition-colors">
<Upload className="w-8 h-8 text-slate-400 mx-auto mb-2" /> <Upload className="w-8 h-8 text-slate-400 mx-auto mb-2" />
<p className="text-slate-600 mb-2">Click to upload or drag and drop</p> <p className="text-slate-600 mb-2">Click to upload or drag and drop</p>
<p className="text-sm text-slate-500">PDF, DOC, DOCX, PNG, JPG, XLSX (max 10MB)</p> <p className="text-sm text-slate-500">PDF, DOC, DOCX, PNG, JPG, XLSX (max 10MB)</p>
@ -1910,7 +1930,7 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
</div> </div>
<Button <Button
size="sm" size="sm"
className="bg-amber-600" className="bg-re-red"
onClick={() => { onClick={() => {
setEditingBank(null); setEditingBank(null);
setIsBankModalOpen(true); setIsBankModalOpen(true);
@ -1924,9 +1944,9 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{bankDetails.length > 0 ? ( {bankDetails.length > 0 ? (
bankDetails.map((bank: any) => ( bankDetails.map((bank: any) => (
<Card key={bank.id} className={`relative ${bank.isPrimary ? 'border-amber-500 bg-blue-50/30' : ''}`}> <Card key={bank.id} className={`relative ${bank.isPrimary ? 'border-re-red bg-red-50/30' : ''}`}>
{bank.isPrimary && ( {bank.isPrimary && (
<div className="absolute top-0 right-0 p-1 bg-amber-600 text-white text-[10px] uppercase font-bold px-2 rounded-bl"> <div className="absolute top-0 right-0 p-1 bg-re-red text-white text-[10px] uppercase font-bold px-2 rounded-bl">
Primary Primary
</div> </div>
)} )}
@ -1955,7 +1975,7 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
<Button <Button
variant="ghost" variant="ghost"
size="sm" size="sm"
className="h-7 text-[11px] text-amber-600" className="h-7 text-[11px] text-re-red"
onClick={() => { onClick={() => {
setEditingBank(bank); setEditingBank(bank);
setIsBankModalOpen(true); setIsBankModalOpen(true);
@ -2056,7 +2076,7 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
{/* Settlement Checklist */} {/* Settlement Checklist */}
<div className="bg-slate-50 border border-slate-200 rounded-lg p-4 mb-4"> <div className="bg-slate-50 border border-slate-200 rounded-lg p-4 mb-4">
<p className="text-sm font-bold text-slate-900 mb-3 flex items-center gap-2"> <p className="text-sm font-bold text-slate-900 mb-3 flex items-center gap-2">
<CheckCircle className="w-4 h-4 text-amber-600" /> <CheckCircle className="w-4 h-4 text-re-red" />
Compliance Checklist Compliance Checklist
</p> </p>
<div className="space-y-3"> <div className="space-y-3">
@ -2067,7 +2087,7 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
id={`check-${item.id}`} id={`check-${item.id}`}
checked={checklist.includes(item.id)} checked={checklist.includes(item.id)}
onChange={() => toggleChecklist(item.id)} onChange={() => toggleChecklist(item.id)}
className="w-4 h-4 mt-1 rounded border-slate-300 text-amber-600 focus:ring-amber-500" className="w-4 h-4 mt-1 rounded border-slate-300 text-re-red focus:ring-re-red"
/> />
<label htmlFor={`check-${item.id}`} className="text-sm text-slate-700 leading-tight"> <label htmlFor={`check-${item.id}`} className="text-sm text-slate-700 leading-tight">
{item.label} {item.label}
@ -2146,7 +2166,7 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
}} }}
/> />
{parseFloat(settlementDetails.adjustments) !== 0 && ( {parseFloat(settlementDetails.adjustments) !== 0 && (
<p className="text-sm text-amber-600 mt-1 flex items-center gap-1"> <p className="text-sm text-re-red mt-1 flex items-center gap-1">
<AlertCircle className="w-3 h-3" /> <AlertCircle className="w-3 h-3" />
Adjusted amount: {settlementDetails.settlementAmount} Adjusted amount: {settlementDetails.settlementAmount}
</p> </p>
@ -2197,7 +2217,7 @@ export function FinanceFnFDetailsPage({ fnfId, onBack }: FinanceFnFDetailsPagePr
<Button <Button
variant="outline" variant="outline"
className="w-full border-blue-300 text-amber-600 hover:bg-blue-50" className="w-full border-red-300 text-re-red hover:bg-red-50"
onClick={handleRequestClarification} onClick={handleRequestClarification}
disabled={submitting} disabled={submitting}
> >

View File

@ -150,7 +150,7 @@ export function FinanceFnFPage({ onViewFnFDetails }: FinanceFnFPageProps = {}) {
if (loading) { if (loading) {
return ( return (
<div className="flex items-center justify-center p-12"> <div className="flex items-center justify-center p-12">
<Loader2 className="w-8 h-8 animate-spin text-amber-600" /> <Loader2 className="w-8 h-8 animate-spin text-re-red" />
</div> </div>
); );
} }
@ -172,7 +172,7 @@ export function FinanceFnFPage({ onViewFnFDetails }: FinanceFnFPageProps = {}) {
<CardContent> <CardContent>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="text-slate-900 text-2xl">{pendingCount}</div> <div className="text-slate-900 text-2xl">{pendingCount}</div>
<Calculator className="w-8 h-8 text-amber-600" /> <Calculator className="w-8 h-8 text-re-red" />
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
@ -196,7 +196,7 @@ export function FinanceFnFPage({ onViewFnFDetails }: FinanceFnFPageProps = {}) {
<CardContent> <CardContent>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="text-slate-900 text-2xl">{displaySettlements.length}</div> <div className="text-slate-900 text-2xl">{displaySettlements.length}</div>
<FileText className="w-8 h-8 text-blue-600" /> <FileText className="w-8 h-8 text-re-red" />
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
@ -224,21 +224,21 @@ export function FinanceFnFPage({ onViewFnFDetails }: FinanceFnFPageProps = {}) {
<Button <Button
variant={filterStatus === 'all' ? 'default' : 'outline'} variant={filterStatus === 'all' ? 'default' : 'outline'}
onClick={() => setFilterStatus('all')} onClick={() => setFilterStatus('all')}
className={filterStatus === 'all' ? 'bg-amber-600 hover:bg-amber-700' : ''} className={filterStatus === 'all' ? 'bg-re-red hover:bg-re-red-hover' : ''}
> >
All Cases ({displaySettlements.length}) All Cases ({displaySettlements.length})
</Button> </Button>
<Button <Button
variant={filterStatus === 'pending' ? 'default' : 'outline'} variant={filterStatus === 'pending' ? 'default' : 'outline'}
onClick={() => setFilterStatus('pending')} onClick={() => setFilterStatus('pending')}
className={filterStatus === 'pending' ? 'bg-amber-600 hover:bg-amber-700' : ''} className={filterStatus === 'pending' ? 'bg-re-red hover:bg-re-red-hover' : ''}
> >
Pending Review ({pendingCount}) Pending Review ({pendingCount})
</Button> </Button>
<Button <Button
variant={filterStatus === 'approved' ? 'default' : 'outline'} variant={filterStatus === 'approved' ? 'default' : 'outline'}
onClick={() => setFilterStatus('approved')} onClick={() => setFilterStatus('approved')}
className={filterStatus === 'approved' ? 'bg-amber-600 hover:bg-amber-700' : ''} className={filterStatus === 'approved' ? 'bg-re-red hover:bg-re-red-hover' : ''}
> >
Approved ({approvedCount}) Approved ({approvedCount})
</Button> </Button>
@ -314,7 +314,7 @@ export function FinanceFnFPage({ onViewFnFDetails }: FinanceFnFPageProps = {}) {
<TableCell> <TableCell>
<Badge <Badge
variant={fnfCase.status === 'Settlement Approved' ? 'default' : 'secondary'} variant={fnfCase.status === 'Settlement Approved' ? 'default' : 'secondary'}
className={fnfCase.status === 'Settlement Approved' ? 'bg-green-600 text-white' : 'bg-amber-600 text-white'} className={fnfCase.status === 'Settlement Approved' ? 'bg-green-600 text-white' : 'bg-re-red text-white'}
> >
{fnfCase.status} {fnfCase.status}
</Badge> </Badge>
@ -323,7 +323,7 @@ export function FinanceFnFPage({ onViewFnFDetails }: FinanceFnFPageProps = {}) {
<Button <Button
size="sm" size="sm"
variant={fnfCase.status === 'Pending Finance Review' ? 'default' : 'outline'} variant={fnfCase.status === 'Pending Finance Review' ? 'default' : 'outline'}
className={fnfCase.status === 'Pending Finance Review' ? 'bg-amber-600 hover:bg-amber-700' : ''} className={fnfCase.status === 'Pending Finance Review' ? 'bg-re-red hover:bg-re-red-hover' : ''}
onClick={() => handleViewDetails(fnfCase)} onClick={() => handleViewDetails(fnfCase)}
> >
<FileText className="w-4 h-4 mr-2" /> <FileText className="w-4 h-4 mr-2" />
@ -442,10 +442,10 @@ export function FinanceFnFPage({ onViewFnFDetails }: FinanceFnFPageProps = {}) {
</div> </div>
{/* Deductions */} {/* Deductions */}
<Card className="border-amber-200 bg-amber-50"> <Card className="border-red-200 bg-red-50">
<CardHeader> <CardHeader>
<CardTitle className="text-base flex items-center gap-2"> <CardTitle className="text-base flex items-center gap-2">
<AlertCircle className="w-5 h-5 text-amber-600" /> <AlertCircle className="w-5 h-5 text-re-red" />
Deductions Deductions
</CardTitle> </CardTitle>
<CardDescription>Pending claims and deductions</CardDescription> <CardDescription>Pending claims and deductions</CardDescription>
@ -479,9 +479,9 @@ export function FinanceFnFPage({ onViewFnFDetails }: FinanceFnFPageProps = {}) {
<span className="text-slate-900">Total Receivables (from Dealer)</span> <span className="text-slate-900">Total Receivables (from Dealer)</span>
<span className="text-red-600 text-lg">- {settlement.receivables.toLocaleString()}</span> <span className="text-red-600 text-lg">- {settlement.receivables.toLocaleString()}</span>
</div> </div>
<div className="flex justify-between items-center p-3 bg-amber-50 rounded"> <div className="flex justify-between items-center p-3 bg-red-50 rounded">
<span className="text-slate-900">Total Deductions</span> <span className="text-slate-900">Total Deductions</span>
<span className="text-amber-600 text-lg">- {settlement.deductions.toLocaleString()}</span> <span className="text-re-red text-lg">- {settlement.deductions.toLocaleString()}</span>
</div> </div>
</div> </div>
@ -513,8 +513,8 @@ export function FinanceFnFPage({ onViewFnFDetails }: FinanceFnFPageProps = {}) {
</div> </div>
</div> </div>
<div className="flex items-start gap-3 p-4 bg-blue-50 border border-blue-200 rounded-lg"> <div className="flex items-start gap-3 p-4 bg-red-50 border border-red-200 rounded-lg">
<AlertCircle className="w-5 h-5 text-blue-600 mt-0.5" /> <AlertCircle className="w-5 h-5 text-re-red mt-0.5" />
<div> <div>
<p className="text-sm text-slate-900 mb-1">Calculation Formula</p> <p className="text-sm text-slate-900 mb-1">Calculation Formula</p>
<p className="text-sm text-slate-600"> <p className="text-sm text-slate-600">
@ -695,7 +695,7 @@ export function FinanceFnFPage({ onViewFnFDetails }: FinanceFnFPageProps = {}) {
<p className="text-sm text-slate-500">Status</p> <p className="text-sm text-slate-500">Status</p>
<Badge <Badge
variant={selectedCase.status === 'Settlement Approved' ? 'default' : 'secondary'} variant={selectedCase.status === 'Settlement Approved' ? 'default' : 'secondary'}
className={selectedCase.status === 'Settlement Approved' ? 'bg-green-600 text-white' : 'bg-amber-600 text-white'} className={selectedCase.status === 'Settlement Approved' ? 'bg-green-600 text-white' : 'bg-re-red text-white'}
> >
{selectedCase.status} {selectedCase.status}
</Badge> </Badge>
@ -793,10 +793,10 @@ export function FinanceFnFPage({ onViewFnFDetails }: FinanceFnFPageProps = {}) {
</Card> </Card>
{/* Deductions */} {/* Deductions */}
<Card className="border-amber-200 bg-amber-50"> <Card className="border-red-200 bg-red-50">
<CardHeader> <CardHeader>
<CardTitle className="text-base flex items-center gap-2"> <CardTitle className="text-base flex items-center gap-2">
<AlertCircle className="w-5 h-5 text-amber-600" /> <AlertCircle className="w-5 h-5 text-re-red" />
Deductions Deductions
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
@ -850,7 +850,7 @@ export function FinanceFnFPage({ onViewFnFDetails }: FinanceFnFPageProps = {}) {
</Button> </Button>
{selectedCase?.status === 'Pending Finance Review' && ( {selectedCase?.status === 'Pending Finance Review' && (
<Button <Button
className="bg-amber-600 hover:bg-amber-700" className="bg-re-red hover:bg-re-red-hover"
onClick={() => { onClick={() => {
setShowDetailsDialog(false); setShowDetailsDialog(false);
handleReviewCase(selectedCase); handleReviewCase(selectedCase);

View File

@ -146,7 +146,7 @@ export function FinancePaymentDetailsPage({ applicationId, onBack }: FinancePaym
if (loading) { if (loading) {
return ( return (
<div className="flex items-center justify-center p-20"> <div className="flex items-center justify-center p-20">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-amber-600"></div> <div className="animate-spin rounded-full h-12 w-12 border-b-2 border-re-red"></div>
</div> </div>
); );
} }
@ -168,7 +168,7 @@ export function FinancePaymentDetailsPage({ applicationId, onBack }: FinancePaym
<Button <Button
size="sm" size="sm"
variant={activeType === 'SECURITY_DEPOSIT' ? 'default' : 'outline'} variant={activeType === 'SECURITY_DEPOSIT' ? 'default' : 'outline'}
className={activeType === 'SECURITY_DEPOSIT' ? 'bg-amber-600 hover:bg-amber-700' : ''} className={activeType === 'SECURITY_DEPOSIT' ? 'bg-re-red hover:bg-re-red-hover' : ''}
onClick={() => setActiveType('SECURITY_DEPOSIT')} onClick={() => setActiveType('SECURITY_DEPOSIT')}
> >
Security Deposit Security Deposit
@ -176,7 +176,7 @@ export function FinancePaymentDetailsPage({ applicationId, onBack }: FinancePaym
<Button <Button
size="sm" size="sm"
variant={activeType === 'FIRST_FILL' ? 'default' : 'outline'} variant={activeType === 'FIRST_FILL' ? 'default' : 'outline'}
className={activeType === 'FIRST_FILL' ? 'bg-amber-600 hover:bg-amber-700' : ''} className={activeType === 'FIRST_FILL' ? 'bg-re-red hover:bg-re-red-hover' : ''}
onClick={() => setActiveType('FIRST_FILL')} onClick={() => setActiveType('FIRST_FILL')}
> >
First Fill First Fill
@ -190,7 +190,7 @@ export function FinancePaymentDetailsPage({ applicationId, onBack }: FinancePaym
"border", "border",
activeDeposit?.status === 'Verified' ? "border-green-200 bg-green-50" : activeDeposit?.status === 'Verified' ? "border-green-200 bg-green-50" :
activeDeposit?.status === 'Rejected' ? "border-red-200 bg-red-50" : activeDeposit?.status === 'Rejected' ? "border-red-200 bg-red-50" :
"border-amber-200 bg-amber-50" "border-red-200 bg-red-50"
)}> )}>
<CardContent className="pt-6"> <CardContent className="pt-6">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
@ -199,13 +199,13 @@ export function FinancePaymentDetailsPage({ applicationId, onBack }: FinancePaym
"w-12 h-12 rounded-full flex items-center justify-center", "w-12 h-12 rounded-full flex items-center justify-center",
activeDeposit?.status === 'Verified' ? "bg-green-100" : activeDeposit?.status === 'Verified' ? "bg-green-100" :
activeDeposit?.status === 'Rejected' ? "bg-red-100" : activeDeposit?.status === 'Rejected' ? "bg-red-100" :
"bg-amber-100" "bg-red-50"
)}> )}>
<IndianRupee className={cn( <IndianRupee className={cn(
"w-6 h-6", "w-6 h-6",
activeDeposit?.status === 'Verified' ? "text-green-600" : activeDeposit?.status === 'Verified' ? "text-green-600" :
activeDeposit?.status === 'Rejected' ? "text-red-600" : activeDeposit?.status === 'Rejected' ? "text-red-600" :
"text-amber-600" "text-re-red"
)} /> )} />
</div> </div>
<div> <div>
@ -224,7 +224,7 @@ export function FinancePaymentDetailsPage({ applicationId, onBack }: FinancePaym
<Badge className={cn( <Badge className={cn(
activeDeposit?.status === 'Verified' ? "bg-green-600" : activeDeposit?.status === 'Verified' ? "bg-green-600" :
activeDeposit?.status === 'Rejected' ? "bg-red-600" : activeDeposit?.status === 'Rejected' ? "bg-red-600" :
"bg-amber-600 text-white" "bg-re-red text-white"
)}> )}>
{activeDeposit?.status || 'No Record'} {activeDeposit?.status || 'No Record'}
</Badge> </Badge>
@ -237,7 +237,7 @@ export function FinancePaymentDetailsPage({ applicationId, onBack }: FinancePaym
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle className="flex items-center gap-2 text-xl"> <CardTitle className="flex items-center gap-2 text-xl">
<User className="w-5 h-5 text-amber-600" /> <User className="w-5 h-5 text-re-red" />
Applicant Information Applicant Information
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
@ -265,7 +265,7 @@ export function FinancePaymentDetailsPage({ applicationId, onBack }: FinancePaym
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle className="flex items-center gap-2 text-xl"> <CardTitle className="flex items-center gap-2 text-xl">
<CreditCard className="w-5 h-5 text-amber-600" /> <CreditCard className="w-5 h-5 text-re-red" />
Deposit Tracking Deposit Tracking
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
@ -273,7 +273,7 @@ export function FinancePaymentDetailsPage({ applicationId, onBack }: FinancePaym
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
<div className="p-4 bg-slate-50 rounded-lg border border-slate-200"> <div className="p-4 bg-slate-50 rounded-lg border border-slate-200">
<Label className="text-slate-500 block mb-1">Expected Amount</Label> <Label className="text-slate-500 block mb-1">Expected Amount</Label>
<p className="text-2xl font-bold text-amber-900"> <p className="text-2xl font-bold text-red-900">
{(activeType === 'SECURITY_DEPOSIT' {(activeType === 'SECURITY_DEPOSIT'
? (configs.SECURITY_DEPOSIT?.amount || 500000) ? (configs.SECURITY_DEPOSIT?.amount || 500000)
: (configs.FIRST_FILL?.amount || 1500000) : (configs.FIRST_FILL?.amount || 1500000)
@ -282,12 +282,12 @@ export function FinancePaymentDetailsPage({ applicationId, onBack }: FinancePaym
</div> </div>
<div className={cn( <div className={cn(
"p-4 rounded-lg border", "p-4 rounded-lg border",
activeDeposit?.status === 'Verified' ? "bg-green-50 border-green-200" : "bg-blue-50 border-blue-200" activeDeposit?.status === 'Verified' ? "bg-green-50 border-green-200" : "bg-red-50 border-red-200"
)}> )}>
<Label className="text-slate-500 block mb-1">Receipt Status</Label> <Label className="text-slate-500 block mb-1">Receipt Status</Label>
<p className={cn( <p className={cn(
"text-2xl font-bold", "text-2xl font-bold",
activeDeposit?.status === 'Verified' ? "text-green-700" : "text-blue-700" activeDeposit?.status === 'Verified' ? "text-green-700" : "text-re-red-hover"
)}> )}>
{activeDeposit?.status || 'Not Started'} {activeDeposit?.status || 'Not Started'}
</p> </p>
@ -312,7 +312,7 @@ export function FinancePaymentDetailsPage({ applicationId, onBack }: FinancePaym
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle className="flex items-center gap-2 text-xl"> <CardTitle className="flex items-center gap-2 text-xl">
<FileText className="w-5 h-5 text-amber-600" /> <FileText className="w-5 h-5 text-re-red" />
Verification Evidence Verification Evidence
</CardTitle> </CardTitle>
<CardDescription>Documents uploaded by the applicant for payment proof</CardDescription> <CardDescription>Documents uploaded by the applicant for payment proof</CardDescription>
@ -342,7 +342,7 @@ export function FinancePaymentDetailsPage({ applicationId, onBack }: FinancePaym
<Button <Button
variant="ghost" variant="ghost"
size="sm" size="sm"
className="text-amber-600 hover:text-amber-700 hover:bg-amber-50" className="text-re-red hover:text-re-red-hover hover:bg-red-50"
onClick={() => { onClick={() => {
setPreviewDoc(doc); setPreviewDoc(doc);
setShowPreviewModal(true); setShowPreviewModal(true);
@ -364,10 +364,10 @@ export function FinancePaymentDetailsPage({ applicationId, onBack }: FinancePaym
</div> </div>
<div className="space-y-6"> <div className="space-y-6">
<Card className="border-amber-100 shadow-sm"> <Card className="border-red-100 shadow-sm">
<CardHeader className="bg-amber-50/50"> <CardHeader className="bg-red-50/50">
<CardTitle className="flex items-center gap-2 text-lg"> <CardTitle className="flex items-center gap-2 text-lg">
<Wallet className="w-5 h-5 text-amber-600" /> <Wallet className="w-5 h-5 text-re-red" />
Finance Action Finance Action
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
@ -431,7 +431,7 @@ export function FinancePaymentDetailsPage({ applicationId, onBack }: FinancePaym
<Button <Button
className={cn( className={cn(
"w-full transition-all duration-200", "w-full transition-all duration-200",
activeDeposit?.status === 'Verified' ? "bg-green-600 hover:bg-green-600 opacity-90" : "bg-amber-600 hover:bg-amber-700" activeDeposit?.status === 'Verified' ? "bg-green-600 hover:bg-green-600 opacity-90" : "bg-re-red hover:bg-re-red-hover"
)} )}
onClick={handleApprovePayment} onClick={handleApprovePayment}
disabled={isSubmitting || activeDeposit?.status === 'Verified'} disabled={isSubmitting || activeDeposit?.status === 'Verified'}
@ -461,7 +461,7 @@ export function FinancePaymentDetailsPage({ applicationId, onBack }: FinancePaym
<Card className="bg-slate-900 text-white border-none shadow-xl"> <Card className="bg-slate-900 text-white border-none shadow-xl">
<CardHeader> <CardHeader>
<CardTitle className="text-base font-medium flex items-center gap-2"> <CardTitle className="text-base font-medium flex items-center gap-2">
<Clock className="w-4 h-4 text-amber-400" /> <Clock className="w-4 h-4 text-re-red" />
Next Steps Next Steps
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>

View File

@ -10,6 +10,7 @@ import {
CheckCircle2, CheckCircle2,
AlertCircle, AlertCircle,
Loader2, Loader2,
Upload,
} from "lucide-react"; } from "lucide-react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { import {
@ -40,7 +41,7 @@ import {
TableRow, TableRow,
} from "@/components/ui/table"; } from "@/components/ui/table";
import { Progress } from "@/components/ui/progress"; import { Progress } from "@/components/ui/progress";
import { useState, useEffect } from "react"; import { useState, useEffect, useRef } from "react";
import { toast } from "sonner"; import { toast } from "sonner";
import { API } from "@/api/API"; import { API } from "@/api/API";
@ -86,6 +87,12 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
type: "Receivable", type: "Receivable",
}); });
const [clearanceFile, setClearanceFile] = useState<File | null>(null); const [clearanceFile, setClearanceFile] = useState<File | null>(null);
const [isUploadingDocs, setIsUploadingDocs] = useState(false);
const documentInputRef = useRef<HTMLInputElement>(null);
const [uploadDialogOpen, setUploadDialogOpen] = useState(false);
const [uploadDocName, setUploadDocName] = useState("");
const [uploadDocType, setUploadDocType] = useState("");
const [uploadFile, setUploadFile] = useState<File | null>(null);
useEffect(() => { useEffect(() => {
@ -300,6 +307,16 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
status: "Finance", status: "Finance",
url: doc.filePath, url: doc.filePath,
})), })),
...(s.clearanceDocuments || [])
.filter((d: any) => d?.supportingDocument)
.map((d: any, idx: number) => ({
id: d.id || `fnf-doc-${idx}`,
name: d.name || (d.supportingDocument || "").split("/").pop(),
type: d.documentType || d.department || "F&F Document",
uploadDate: d.clearedAt ? formatDateTime(d.clearedAt) : "-",
status: "Attached",
url: d.supportingDocument,
})),
], ],
participants: s.participants || [] participants: s.participants || []
}; };
@ -396,7 +413,7 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
if (loading) { if (loading) {
return ( return (
<div className="flex items-center justify-center p-12"> <div className="flex items-center justify-center p-12">
<Loader2 className="w-8 h-8 animate-spin text-amber-600" /> <Loader2 className="w-8 h-8 animate-spin text-re-red" />
</div> </div>
); );
} }
@ -423,9 +440,13 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
const fnfAge = calculateAge(fnfCase.submittedOn); const fnfAge = calculateAge(fnfCase.submittedOn);
const currentUserRole = String(currentUser?.role || "").toLowerCase();
const canViewDocuments =
currentUserRole.includes("super admin") || currentUserRole.includes("dd admin");
const canRespondToDepartment = (dept: any) => { const canRespondToDepartment = (dept: any) => {
if (!fnfCase || !dept) return false; if (!fnfCase || !dept) return false;
const role = String(currentUser?.role || "").toLowerCase(); const role = currentUserRole;
if (!role) return false; if (!role) return false;
// 1. If any user (including Admin) has already responded, hide the button to prevent double-submission // 1. If any user (including Admin) has already responded, hide the button to prevent double-submission
@ -486,6 +507,61 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
} }
}; };
const handlePickDocumentFile = (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0] || null;
setUploadFile(file);
};
const resetUploadDialog = () => {
setUploadDialogOpen(false);
setUploadDocName("");
setUploadDocType("");
setUploadFile(null);
if (documentInputRef.current) documentInputRef.current.value = "";
};
const handleSubmitDocumentUpload = async () => {
const trimmedName = uploadDocName.trim();
if (!trimmedName) {
toast.error("Please enter a document name.");
return;
}
if (!uploadFile) {
toast.error("Please choose a file to upload.");
return;
}
if (!fnfId) {
toast.error("Cannot upload — settlement id is missing.");
return;
}
setIsUploadingDocs(true);
try {
const formData = new FormData();
formData.append("file", uploadFile);
formData.append("documentName", trimmedName);
if (uploadDocType.trim()) {
formData.append("documentType", uploadDocType.trim());
}
const response: any = await API.uploadFnFDocument(fnfId, formData);
if (response.data?.success) {
toast.success("Document uploaded successfully");
resetUploadDialog();
fetchFnFDetails(false);
} else {
toast.error(response.data?.message || "Failed to upload document");
}
} catch (error: any) {
console.error("Upload F&F document error:", error);
toast.error(
error?.response?.data?.message || "Failed to upload document",
);
} finally {
setIsUploadingDocs(false);
}
};
const handleSendToStakeholders = () => { const handleSendToStakeholders = () => {
toast.success("Notifications sent to all 16 departments"); toast.success("Notifications sent to all 16 departments");
setSendStakeholdersDialog(false); setSendStakeholdersDialog(false);
@ -494,7 +570,7 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
const getStatusColor = (status: string) => { const getStatusColor = (status: string) => {
switch (status) { switch (status) {
case "New": case "New":
return "bg-amber-100 text-blue-700 border-blue-300"; return "bg-red-50 text-re-red-hover border-red-300";
case "In Progress": case "In Progress":
return "bg-yellow-100 text-yellow-700 border-yellow-300"; return "bg-yellow-100 text-yellow-700 border-yellow-300";
case "Under Review": case "Under Review":
@ -599,7 +675,7 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
<Badge <Badge
className={ className={
fnfCase.requestType === "Resignation" fnfCase.requestType === "Resignation"
? "bg-amber-100 text-amber-700 border-amber-300" ? "bg-red-50 text-re-red-hover border-red-300"
: "bg-red-100 text-red-700 border-red-300" : "bg-red-100 text-red-700 border-red-300"
} }
> >
@ -626,7 +702,7 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
{/* {canSendToStakeholders && fnfCase.status === "New" && ( {/* {canSendToStakeholders && fnfCase.status === "New" && (
<Button <Button
className="bg-amber-600 hover:bg-blue-700" className="bg-re-red hover:bg-re-red-hover"
onClick={() => setSendStakeholdersDialog(true)} onClick={() => setSendStakeholdersDialog(true)}
> >
<Send className="w-4 h-4 mr-2" /> <Send className="w-4 h-4 mr-2" />
@ -691,9 +767,11 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
<TabsTrigger value="progress">Progress</TabsTrigger> <TabsTrigger value="progress">Progress</TabsTrigger>
<TabsTrigger value="details">Case Details</TabsTrigger> <TabsTrigger value="details">Case Details</TabsTrigger>
<TabsTrigger value="departments">Department Responses</TabsTrigger> <TabsTrigger value="departments">Department Responses</TabsTrigger>
<TabsTrigger value="financial">Financial Summary</TabsTrigger> {canViewDocuments && (
<TabsTrigger value="documents">Documents</TabsTrigger> <TabsTrigger value="documents">Documents</TabsTrigger>
<TabsTrigger value="bank">Bank Details</TabsTrigger> )}
{/* Bank Details tab hidden temporarily */}
{/* <TabsTrigger value="bank">Bank Details</TabsTrigger> */}
<TabsTrigger value="audit">Audit Trail</TabsTrigger> <TabsTrigger value="audit">Audit Trail</TabsTrigger>
</TabsList> </TabsList>
@ -775,7 +853,7 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
) )
? "bg-green-100 border-green-600" ? "bg-green-100 border-green-600"
: responsesReceived > 0 : responsesReceived > 0
? "bg-amber-100 border-amber-600" ? "bg-red-50 border-re-red"
: "bg-slate-100 border-slate-300" : "bg-slate-100 border-slate-300"
}`} }`}
> >
@ -785,7 +863,7 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
) ? ( ) ? (
<Check className="w-6 h-6 text-green-600" /> <Check className="w-6 h-6 text-green-600" />
) : responsesReceived > 0 ? ( ) : responsesReceived > 0 ? (
<Users className="w-6 h-6 text-amber-600" /> <Users className="w-6 h-6 text-re-red" />
) : ( ) : (
<Clock className="w-6 h-6 text-slate-400" /> <Clock className="w-6 h-6 text-slate-400" />
)} )}
@ -814,7 +892,7 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
) )
? "bg-green-600" ? "bg-green-600"
: responsesReceived > 0 : responsesReceived > 0
? "bg-amber-600" ? "bg-re-red"
: "bg-slate-400" : "bg-slate-400"
} }
> >
@ -843,7 +921,7 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
fnfCase.status, fnfCase.status,
) )
? "bg-green-50 border-green-200" ? "bg-green-50 border-green-200"
: "bg-blue-50 border-amber-200" : "bg-red-50 border-red-200"
} }
> >
<CardContent className="p-4"> <CardContent className="p-4">
@ -910,14 +988,14 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
className={`size-12 shrink-0 aspect-square rounded-full flex items-center justify-center border-2 ${fnfCase.status === "Completed" className={`size-12 shrink-0 aspect-square rounded-full flex items-center justify-center border-2 ${fnfCase.status === "Completed"
? "bg-green-100 border-green-600" ? "bg-green-100 border-green-600"
: fnfCase.status === "Finance Approval" : fnfCase.status === "Finance Approval"
? "bg-amber-100 border-amber-600" ? "bg-red-50 border-re-red"
: "bg-slate-100 border-slate-300" : "bg-slate-100 border-slate-300"
}`} }`}
> >
{fnfCase.status === "Completed" ? ( {fnfCase.status === "Completed" ? (
<Check className="w-6 h-6 text-green-600" /> <Check className="w-6 h-6 text-green-600" />
) : fnfCase.status === "Finance Approval" ? ( ) : fnfCase.status === "Finance Approval" ? (
<FileCheck className="w-6 h-6 text-amber-600" /> <FileCheck className="w-6 h-6 text-re-red" />
) : ( ) : (
<Clock className="w-6 h-6 text-slate-400" /> <Clock className="w-6 h-6 text-slate-400" />
)} )}
@ -940,7 +1018,7 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
fnfCase.status === "Completed" fnfCase.status === "Completed"
? "bg-green-600" ? "bg-green-600"
: fnfCase.status === "Finance Approval" : fnfCase.status === "Finance Approval"
? "bg-amber-600" ? "bg-re-red"
: "bg-slate-400" : "bg-slate-400"
} }
> >
@ -964,7 +1042,7 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
className={ className={
fnfCase.status === "Completed" fnfCase.status === "Completed"
? "bg-green-50 border-green-200" ? "bg-green-50 border-green-200"
: "bg-blue-50 border-amber-200" : "bg-red-50 border-red-200"
} }
> >
<CardContent className="p-4"> <CardContent className="p-4">
@ -989,8 +1067,8 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
"0"} "0"}
</p> </p>
</div> </div>
<div className="text-center p-3 bg-amber-100 rounded-lg"> <div className="text-center p-3 bg-red-50 rounded-lg">
<p className="text-xs text-blue-700 mb-1"> <p className="text-xs text-re-red-hover mb-1">
Net Amount Net Amount
</p> </p>
<p <p
@ -1022,14 +1100,14 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
className={`size-12 shrink-0 aspect-square rounded-full flex items-center justify-center border-2 ${fnfCase.status === "Completed" className={`size-12 shrink-0 aspect-square rounded-full flex items-center justify-center border-2 ${fnfCase.status === "Completed"
? "bg-green-100 border-green-600" ? "bg-green-100 border-green-600"
: fnfCase.status === "Finance Approval" : fnfCase.status === "Finance Approval"
? "bg-amber-100 border-amber-600" ? "bg-red-50 border-re-red"
: "bg-slate-100 border-slate-300" : "bg-slate-100 border-slate-300"
}`} }`}
> >
{fnfCase.status === "Completed" ? ( {fnfCase.status === "Completed" ? (
<Check className="w-6 h-6 text-green-600" /> <Check className="w-6 h-6 text-green-600" />
) : fnfCase.status === "Finance Approval" ? ( ) : fnfCase.status === "Finance Approval" ? (
<MessageSquare className="w-6 h-6 text-amber-600" /> <MessageSquare className="w-6 h-6 text-re-red" />
) : ( ) : (
<Clock className="w-6 h-6 text-slate-400" /> <Clock className="w-6 h-6 text-slate-400" />
)} )}
@ -1052,7 +1130,7 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
fnfCase.status === "Completed" fnfCase.status === "Completed"
? "bg-green-600" ? "bg-green-600"
: fnfCase.status === "Finance Approval" : fnfCase.status === "Finance Approval"
? "bg-amber-600" ? "bg-re-red"
: "bg-slate-400" : "bg-slate-400"
} }
> >
@ -1184,7 +1262,7 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
Case closed. All obligations fulfilled. Case closed. All obligations fulfilled.
</p> </p>
{fnfCase.status === "Completed" && ( {fnfCase.status === "Completed" && (
<Card className="bg-gradient-to-r from-green-50 to-blue-50 border-green-300"> <Card className="bg-gradient-to-r from-green-50 to-red-50 border-green-300">
<CardContent className="p-4"> <CardContent className="p-4">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="size-12 shrink-0 aspect-square rounded-full bg-green-600 flex items-center justify-center"> <div className="size-12 shrink-0 aspect-square rounded-full bg-green-600 flex items-center justify-center">
@ -1255,9 +1333,9 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
</CardContent> </CardContent>
</Card> </Card>
<Card className="border-amber-200 bg-blue-50/30"> <Card className="border-red-200 bg-red-50/30">
<CardHeader> <CardHeader>
<CardTitle className="text-blue-900"> <CardTitle className="text-re-red-hover">
F&F Settlement Information F&F Settlement Information
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
@ -1426,7 +1504,7 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
<Button <Button
variant="ghost" variant="ghost"
size="sm" size="sm"
className="text-amber-600 hover:text-blue-700" className="text-re-red hover:text-re-red-hover"
onClick={() => { onClick={() => {
setSelectedDept(dept); setSelectedDept(dept);
setClearanceForm({ setClearanceForm({
@ -1451,155 +1529,171 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
</Table> </Table>
</CardContent> </CardContent>
</Card> </Card>
</TabsContent>
{/* Financial Summary Tab */} {/* Department Claim vs Finance Validation */}
<TabsContent value="financial"> <Card className="border-red-200 bg-red-50 mt-6">
<div className="space-y-6"> <CardHeader>
<Card className="border-blue-200 bg-blue-50"> <CardTitle>Department Claim vs Finance Validation</CardTitle>
<CardHeader> <CardDescription>
<CardTitle>Department Claim vs Finance Validation</CardTitle> Final settlement totals are based on finance validated values.
<CardDescription> </CardDescription>
Final settlement totals are based on finance validated values. </CardHeader>
</CardDescription> <CardContent>
</CardHeader> <Table>
<CardContent> <TableHeader>
<Table> <TableRow>
<TableHeader> <TableHead>Department</TableHead>
<TableRow> <TableHead>Department Claim</TableHead>
<TableHead>Department</TableHead> <TableHead>Finance Validated</TableHead>
<TableHead>Department Claim</TableHead> <TableHead>Variance</TableHead>
<TableHead>Finance Validated</TableHead> </TableRow>
<TableHead>Variance</TableHead> </TableHeader>
<TableBody>
{departmentReconciliation.map((row) => (
<TableRow key={row.department}>
<TableCell>{row.department}</TableCell>
<TableCell>{row.claimAmount > 0 ? `${row.claimType}${row.claimAmount.toLocaleString()}` : '-'}</TableCell>
<TableCell>{row.validatedAmount > 0 ? `${row.validatedType}${row.validatedAmount.toLocaleString()}` : '-'}</TableCell>
<TableCell className={row.variance === 0 ? 'text-slate-600' : row.variance > 0 ? 'text-red-600' : 'text-green-600'}>
{row.claimAmount === 0 && row.validatedAmount === 0 ? '-' : `${row.variance.toLocaleString()}`}
</TableCell>
</TableRow> </TableRow>
</TableHeader> ))}
<TableBody> </TableBody>
{departmentReconciliation.map((row) => ( </Table>
<TableRow key={row.department}> </CardContent>
<TableCell>{row.department}</TableCell> </Card>
<TableCell>{row.claimAmount > 0 ? `${row.claimType}${row.claimAmount.toLocaleString()}` : '-'}</TableCell>
<TableCell>{row.validatedAmount > 0 ? `${row.validatedType}${row.validatedAmount.toLocaleString()}` : '-'}</TableCell>
<TableCell className={row.variance === 0 ? 'text-slate-600' : row.variance > 0 ? 'text-red-600' : 'text-green-600'}>
{row.claimAmount === 0 && row.validatedAmount === 0 ? '-' : `${row.variance.toLocaleString()}`}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
</Card>
<Card> {/* Financial Summary */}
<CardHeader> <Card className="mt-6">
<CardTitle>Financial Summary</CardTitle> <CardHeader>
<CardDescription> <CardTitle>Financial Summary</CardTitle>
Consolidated view of all payable and receivable amounts <CardDescription>
</CardDescription> Consolidated view of all payable and receivable amounts
</CardHeader> </CardDescription>
<CardContent> </CardHeader>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6"> <CardContent>
<div className="p-6 bg-green-50 rounded-lg border border-green-200"> <div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<p className="text-sm text-green-700 mb-2"> <div className="p-6 bg-green-50 rounded-lg border border-green-200">
Total Payable Amount <p className="text-sm text-green-700 mb-2">
</p> Total Payable Amount
<p className="text-3xl text-green-600"> </p>
{fnfCase.totalPayableAmount?.toLocaleString() || "0"} <p className="text-3xl text-green-600">
</p> {fnfCase.totalPayableAmount?.toLocaleString() || "0"}
<p className="text-xs text-green-600 mt-1"> </p>
Amount to be paid to dealer <p className="text-xs text-green-600 mt-1">
</p> Amount to be paid to dealer
</div> </p>
<div className="p-6 bg-red-50 rounded-lg border border-red-200">
<p className="text-sm text-red-700 mb-2">
Total receivable amount
</p>
<p className="text-3xl text-red-600">
{fnfCase.totalRecoveryAmount?.toLocaleString() || "0"}
</p>
<p className="text-xs text-red-600 mt-1">
Amount receivable from dealer
</p>
</div>
<div className="p-6 bg-amber-50 rounded-lg border border-amber-200">
<p className="text-sm text-amber-700 mb-2">
Total Deductions
</p>
<p className="text-3xl text-amber-600 font-bold">
{fnfCase.totalDeductions?.toLocaleString() || "0"}
</p>
<p className="text-xs text-amber-600 mt-1">
Warranty holdbacks / Policy penalties
</p>
</div>
<div className="p-6 bg-blue-50 rounded-lg border border-blue-200">
<p className="text-sm text-blue-700 mb-2">Net Settlement Amount</p>
<p
className={`text-3xl font-extrabold ${(fnfCase.netAmount || 0) < 0
? "text-red-600"
: "text-green-600"
}`}
>
{Math.abs(fnfCase.netAmount || 0).toLocaleString()}
</p>
<p className="text-xs text-blue-600 mt-1">
{(fnfCase.netAmount || 0) < 0
? "Receivable from dealer"
: "Payment to dealer"}
</p>
</div>
</div> </div>
</CardContent> <div className="p-6 bg-red-50 rounded-lg border border-red-200">
</Card> <p className="text-sm text-red-700 mb-2">
Total receivable amount
<Card> </p>
<CardHeader> <p className="text-3xl text-red-600">
<CardTitle>Finance Report Status</CardTitle> {fnfCase.totalRecoveryAmount?.toLocaleString() || "0"}
</CardHeader> </p>
<CardContent> <p className="text-xs text-red-600 mt-1">
<div className="flex items-center gap-4"> Amount receivable from dealer
<Badge </p>
className={ </div>
fnfCase.financeReportStatus === "Completed" <div className="p-6 bg-red-50 rounded-lg border border-red-200">
? "bg-green-100 text-green-700 border-green-300" <p className="text-sm text-re-red-hover mb-2">
: fnfCase.financeReportStatus === "In Progress" Total Deductions
? "bg-yellow-100 text-yellow-700 border-yellow-300" </p>
: "bg-slate-100 text-slate-700 border-slate-300" <p className="text-3xl text-re-red font-bold">
} {fnfCase.totalDeductions?.toLocaleString() || "0"}
</p>
<p className="text-xs text-re-red mt-1">
Warranty holdbacks / Policy penalties
</p>
</div>
<div className="p-6 bg-red-50 rounded-lg border border-red-200">
<p className="text-sm text-re-red-hover mb-2">Net Settlement Amount</p>
<p
className={`text-3xl font-extrabold ${(fnfCase.netAmount || 0) < 0
? "text-red-600"
: "text-green-600"
}`}
> >
{fnfCase.financeReportStatus} {Math.abs(fnfCase.netAmount || 0).toLocaleString()}
</Badge> </p>
{fnfCase.financeReportStatus === "Pending" && ( <p className="text-xs text-re-red mt-1">
<p className="text-slate-600 text-sm"> {(fnfCase.netAmount || 0) < 0
Waiting for all department responses before finance can ? "Receivable from dealer"
prepare final report : "Payment to dealer"}
</p> </p>
)}
{fnfCase.financeReportStatus === "In Progress" && (
<p className="text-slate-600 text-sm">
Finance team is reviewing department responses and
preparing final settlement report
</p>
)}
</div> </div>
{fnfCase.financeRemarks && ( </div>
<div className="mt-4 p-4 bg-slate-50 rounded-lg"> </CardContent>
<Label className="text-slate-600">Finance Remarks</Label> </Card>
<p className="mt-1">{fnfCase.financeRemarks}</p>
</div> {/* Finance Report Status */}
<Card className="mt-6">
<CardHeader>
<CardTitle>Finance Report Status</CardTitle>
</CardHeader>
<CardContent>
<div className="flex items-center gap-4">
<Badge
className={
fnfCase.financeReportStatus === "Completed"
? "bg-green-100 text-green-700 border-green-300"
: fnfCase.financeReportStatus === "In Progress"
? "bg-yellow-100 text-yellow-700 border-yellow-300"
: "bg-slate-100 text-slate-700 border-slate-300"
}
>
{fnfCase.financeReportStatus}
</Badge>
{fnfCase.financeReportStatus === "Pending" && (
<p className="text-slate-600 text-sm">
Waiting for all department responses before finance can
prepare final report
</p>
)} )}
</CardContent> {fnfCase.financeReportStatus === "In Progress" && (
</Card> <p className="text-slate-600 text-sm">
</div> Finance team is reviewing department responses and
preparing final settlement report
</p>
)}
</div>
{fnfCase.financeRemarks && (
<div className="mt-4 p-4 bg-slate-50 rounded-lg">
<Label className="text-slate-600">Finance Remarks</Label>
<p className="mt-1">{fnfCase.financeRemarks}</p>
</div>
)}
</CardContent>
</Card>
</TabsContent> </TabsContent>
{/* Documents Tab */} {/* Documents Tab — admin / super admin only */}
{canViewDocuments && (
<TabsContent value="documents"> <TabsContent value="documents">
<Card> <Card>
<CardHeader> <CardHeader className="flex flex-row items-start justify-between gap-4">
<CardTitle>Documents</CardTitle> <div>
<CardDescription> <CardTitle>Documents</CardTitle>
All NOC documents and due statements from departments <CardDescription>
</CardDescription> All NOC documents and due statements from departments
</CardDescription>
</div>
<div className="flex items-center gap-2">
<Button
onClick={() => setUploadDialogOpen(true)}
disabled={isUploadingDocs}
className="bg-re-red hover:bg-re-red-hover text-white"
data-testid="fnf-upload-docs-btn"
>
{isUploadingDocs ? (
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
) : (
<Upload className="w-4 h-4 mr-2" />
)}
{isUploadingDocs ? "Uploading..." : "Upload Document"}
</Button>
</div>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
<Table> <Table>
@ -1613,7 +1707,17 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
</TableRow> </TableRow>
</TableHeader> </TableHeader>
<TableBody> <TableBody>
{fnfCase.documents.map((doc: any) => ( {(fnfCase.documents || []).length === 0 && (
<TableRow>
<TableCell
colSpan={5}
className="text-center text-slate-500 py-8"
>
No documents uploaded yet. Click <span className="font-medium">Upload Documents</span> to add files.
</TableCell>
</TableRow>
)}
{(fnfCase.documents || []).map((doc: any) => (
<TableRow key={doc.id}> <TableRow key={doc.id}>
<TableCell> <TableCell>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
@ -1663,6 +1767,7 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
</CardContent> </CardContent>
</Card> </Card>
</TabsContent> </TabsContent>
)}
{/* Bank Details Tab */} {/* Bank Details Tab */}
<TabsContent value="bank"> <TabsContent value="bank">
@ -1679,7 +1784,7 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
setEditingBank(null); setEditingBank(null);
setIsBankModalOpen(true); setIsBankModalOpen(true);
}} }}
className="bg-amber-600" className="bg-re-red"
> >
<Plus className="w-4 h-4 mr-2" /> <Plus className="w-4 h-4 mr-2" />
Add Account Add Account
@ -1689,16 +1794,16 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{bankDetails.length > 0 ? ( {bankDetails.length > 0 ? (
bankDetails.map((bank: any) => ( bankDetails.map((bank: any) => (
<Card key={bank.id} className={`relative overflow-hidden ${bank.isPrimary ? 'border-amber-500 bg-blue-50/30' : ''}`}> <Card key={bank.id} className={`relative overflow-hidden ${bank.isPrimary ? 'border-re-red bg-red-50/30' : ''}`}>
{bank.isPrimary && ( {bank.isPrimary && (
<div className="absolute top-0 right-0 p-1 bg-amber-600 text-white text-[10px] uppercase font-bold px-2 rounded-bl"> <div className="absolute top-0 right-0 p-1 bg-re-red text-white text-[10px] uppercase font-bold px-2 rounded-bl">
Primary Primary
</div> </div>
)} )}
<CardContent className="p-5"> <CardContent className="p-5">
<div className="flex items-center gap-3 mb-4"> <div className="flex items-center gap-3 mb-4">
<div className="p-2 bg-amber-100 rounded-lg"> <div className="p-2 bg-red-50 rounded-lg">
<Building2 className="w-5 h-5 text-amber-600" /> <Building2 className="w-5 h-5 text-re-red" />
</div> </div>
<div> <div>
<p className="font-bold text-slate-900">{bank.bankName}</p> <p className="font-bold text-slate-900">{bank.bankName}</p>
@ -1729,7 +1834,7 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
<Button <Button
variant="ghost" variant="ghost"
size="sm" size="sm"
className="h-8 text-amber-600" className="h-8 text-re-red"
onClick={() => { onClick={() => {
setEditingBank(bank); setEditingBank(bank);
setIsBankModalOpen(true); setIsBankModalOpen(true);
@ -1789,12 +1894,12 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
<div className="flex-1"> <div className="flex-1">
<div className="flex items-center justify-between mb-1"> <div className="flex items-center justify-between mb-1">
<p className="font-semibold text-slate-900 flex items-center gap-2"> <p className="font-semibold text-slate-900 flex items-center gap-2">
{log.action === 'FNF_CREATED' && <Badge className="bg-amber-600 h-2 w-2 p-0 rounded-full" />} {log.action === 'FNF_CREATED' && <Badge className="bg-re-red h-2 w-2 p-0 rounded-full" />}
{(log.description && !log.newData?.action) ? log.description : ( {(log.description && !log.newData?.action) ? log.description : (
<> <>
{getFriendlyActionName(log.newData?.action || log.action)} {getFriendlyActionName(log.newData?.action || log.action)}
{log.newData?.department && ( {log.newData?.department && (
<span className="text-amber-600 ml-1 font-bold"> <span className="text-re-red ml-1 font-bold">
- {log.newData.department} - {log.newData.department}
</span> </span>
)} )}
@ -1857,11 +1962,11 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
</DialogHeader> </DialogHeader>
<div className="space-y-4"> <div className="space-y-4">
<div className="p-4 bg-blue-50 rounded-lg border border-amber-200"> <div className="p-4 bg-red-50 rounded-lg border border-red-200">
<p className="text-sm text-blue-900 mb-2"> <p className="text-sm text-re-red-hover mb-2">
Notifications will be sent to: Notifications will be sent to:
</p> </p>
<ul className="text-sm text-blue-800 space-y-1 ml-4"> <ul className="text-sm text-re-red-hover space-y-1 ml-4">
<li> All 16 departments</li> <li> All 16 departments</li>
<li> Case Number: {fnfCase.caseNumber}</li> <li> Case Number: {fnfCase.caseNumber}</li>
<li> Dealer: {fnfCase.dealerName}</li> <li> Dealer: {fnfCase.dealerName}</li>
@ -1879,7 +1984,7 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
</Button> </Button>
<Button <Button
onClick={handleSendToStakeholders} onClick={handleSendToStakeholders}
className="bg-amber-600 hover:bg-blue-700" className="bg-re-red hover:bg-re-red-hover"
> >
<Send className="w-4 h-4 mr-2" /> <Send className="w-4 h-4 mr-2" />
Send Notifications Send Notifications
@ -1953,7 +2058,7 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
Cancel Cancel
</Button> </Button>
<Button <Button
className="bg-amber-600 hover:bg-blue-700" className="bg-re-red hover:bg-re-red-hover"
onClick={handleUpdateClearance} onClick={handleUpdateClearance}
disabled={isUpdatingClearance} disabled={isUpdatingClearance}
> >
@ -1963,6 +2068,96 @@ export function FnFDetails({ fnfId, onBack, currentUser }: FnFDetailsProps) {
</DialogContent> </DialogContent>
</Dialog> </Dialog>
{/* Upload Document Dialog */}
<Dialog
open={uploadDialogOpen}
onOpenChange={(open) => {
if (!open) resetUploadDialog();
else setUploadDialogOpen(true);
}}
>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle>Upload Document</DialogTitle>
<DialogDescription>
Provide a name for this document and attach a file. The name is required.
</DialogDescription>
</DialogHeader>
<div className="space-y-4 py-2">
<div className="space-y-1.5">
<Label htmlFor="fnf-upload-doc-name">
Document Name <span className="text-red-600">*</span>
</Label>
<input
id="fnf-upload-doc-name"
type="text"
placeholder="e.g. Final NOC — Sales"
className="flex h-10 w-full rounded-md border border-slate-200 bg-white px-3 py-2 text-sm"
value={uploadDocName}
onChange={(e) => setUploadDocName(e.target.value)}
data-testid="fnf-upload-doc-name"
/>
</div>
<div className="space-y-1.5">
<Label htmlFor="fnf-upload-doc-type">Document Type (optional)</Label>
<input
id="fnf-upload-doc-type"
type="text"
placeholder="e.g. NOC, Statement, Receipt"
className="flex h-10 w-full rounded-md border border-slate-200 bg-white px-3 py-2 text-sm"
value={uploadDocType}
onChange={(e) => setUploadDocType(e.target.value)}
data-testid="fnf-upload-doc-type"
/>
</div>
<div className="space-y-1.5">
<Label htmlFor="fnf-upload-doc-file">
File <span className="text-red-600">*</span>
</Label>
<input
id="fnf-upload-doc-file"
ref={documentInputRef}
type="file"
className="flex w-full text-sm"
onChange={handlePickDocumentFile}
data-testid="fnf-upload-docs-input"
/>
{uploadFile && (
<p className="text-xs text-slate-500 truncate">
Selected: <span className="font-medium">{uploadFile.name}</span>
</p>
)}
</div>
</div>
<DialogFooter>
<Button
variant="outline"
onClick={resetUploadDialog}
disabled={isUploadingDocs}
>
Cancel
</Button>
<Button
className="bg-re-red hover:bg-re-red-hover text-white"
onClick={handleSubmitDocumentUpload}
disabled={isUploadingDocs || !uploadDocName.trim() || !uploadFile}
data-testid="fnf-upload-doc-submit"
>
{isUploadingDocs ? (
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
) : (
<Upload className="w-4 h-4 mr-2" />
)}
{isUploadingDocs ? "Uploading..." : "Upload"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* Bank Details Modal */} {/* Bank Details Modal */}
<BankDetailsModal <BankDetailsModal
isOpen={isBankModalOpen} isOpen={isBankModalOpen}

View File

@ -17,7 +17,7 @@ interface FnFPageProps {
const getStatusColor = (status: string) => { const getStatusColor = (status: string) => {
switch (status) { switch (status) {
case 'Initiated': case 'Initiated':
return 'bg-blue-100 text-blue-700 border-blue-300'; return 'bg-red-100 text-re-red-hover border-red-300';
case 'DD Clearance': case 'DD Clearance':
case 'Legal Clearance': case 'Legal Clearance':
return 'bg-yellow-100 text-yellow-700 border-yellow-300'; return 'bg-yellow-100 text-yellow-700 border-yellow-300';
@ -34,7 +34,7 @@ const getStatusColor = (status: string) => {
const getTypeColor = (type: string) => { const getTypeColor = (type: string) => {
return type === 'Resignation' return type === 'Resignation'
? 'bg-amber-100 text-amber-700 border-amber-300' ? 'bg-red-50 text-re-red-hover border-red-300'
: 'bg-red-100 text-red-700 border-red-300'; : 'bg-red-100 text-red-700 border-red-300';
}; };
@ -65,7 +65,7 @@ export function FnFPage({ currentUser, onViewDetails }: FnFPageProps) {
if (loading) { if (loading) {
return ( return (
<div className="flex items-center justify-center p-12"> <div className="flex items-center justify-center p-12">
<Loader2 className="w-8 h-8 animate-spin text-blue-600" /> <Loader2 className="w-8 h-8 animate-spin text-re-red" />
</div> </div>
); );
} }
@ -102,7 +102,7 @@ export function FnFPage({ currentUser, onViewDetails }: FnFPageProps) {
<Card> <Card>
<CardHeader className="pb-3"> <CardHeader className="pb-3">
<CardDescription>Initiated</CardDescription> <CardDescription>Initiated</CardDescription>
<CardTitle className="text-3xl text-blue-600"> <CardTitle className="text-3xl text-re-red">
{initiatedCases.length} {initiatedCases.length}
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
@ -186,8 +186,8 @@ export function FnFPage({ currentUser, onViewDetails }: FnFPageProps) {
<CardContent className="pt-6"> <CardContent className="pt-6">
<div className="flex items-start justify-between"> <div className="flex items-start justify-between">
<div className="flex items-start gap-4 flex-1"> <div className="flex items-start gap-4 flex-1">
<div className="p-3 bg-blue-100 rounded-lg"> <div className="p-3 bg-red-100 rounded-lg">
<FileCheck className="w-6 h-6 text-blue-600" /> <FileCheck className="w-6 h-6 text-re-red" />
</div> </div>
<div className="flex-1"> <div className="flex-1">
<div className="flex items-center gap-3 mb-2"> <div className="flex items-center gap-3 mb-2">
@ -239,7 +239,7 @@ export function FnFPage({ currentUser, onViewDetails }: FnFPageProps) {
<Button <Button
size="sm" size="sm"
variant="outline" variant="outline"
className="text-blue-600 border-blue-300 hover:bg-blue-50" className="text-re-red border-red-300 hover:bg-red-50"
onClick={() => handleSendToStakeholders(fnfCase.id)} onClick={() => handleSendToStakeholders(fnfCase.id)}
> >
<Send className="w-4 h-4 mr-2" /> <Send className="w-4 h-4 mr-2" />
@ -276,12 +276,12 @@ export function FnFPage({ currentUser, onViewDetails }: FnFPageProps) {
<CardContent className="pt-6"> <CardContent className="pt-6">
<div className="flex items-start justify-between"> <div className="flex items-start justify-between">
<div className="flex items-start gap-4 flex-1"> <div className="flex items-start gap-4 flex-1">
<div className={`p-3 rounded-lg ${fnfCase.status === 'Initiated' ? 'bg-blue-100' : <div className={`p-3 rounded-lg ${fnfCase.status === 'Initiated' ? 'bg-red-100' :
(fnfCase.status === 'DD Clearance' || fnfCase.status === 'Legal Clearance') ? 'bg-yellow-100' : (fnfCase.status === 'DD Clearance' || fnfCase.status === 'Legal Clearance') ? 'bg-yellow-100' :
(fnfCase.status === 'Finance Approval' || fnfCase.status === 'Calculated') ? 'bg-orange-100' : (fnfCase.status === 'Finance Approval' || fnfCase.status === 'Calculated') ? 'bg-orange-100' :
'bg-green-100' 'bg-green-100'
}`}> }`}>
<IndianRupee className={`w-6 h-6 ${fnfCase.status === 'Initiated' ? 'text-blue-600' : <IndianRupee className={`w-6 h-6 ${fnfCase.status === 'Initiated' ? 'text-re-red' :
(fnfCase.status === 'DD Clearance' || fnfCase.status === 'Legal Clearance') ? 'text-yellow-600' : (fnfCase.status === 'DD Clearance' || fnfCase.status === 'Legal Clearance') ? 'text-yellow-600' :
(fnfCase.status === 'Finance Approval' || fnfCase.status === 'Calculated') ? 'text-orange-600' : (fnfCase.status === 'Finance Approval' || fnfCase.status === 'Calculated') ? 'text-orange-600' :
'text-green-600' 'text-green-600'
@ -322,7 +322,7 @@ export function FnFPage({ currentUser, onViewDetails }: FnFPageProps) {
<Button <Button
size="sm" size="sm"
variant="outline" variant="outline"
className="text-blue-600 border-blue-300 hover:bg-blue-50" className="text-re-red border-red-300 hover:bg-red-50"
onClick={() => handleSendToStakeholders(fnfCase.id)} onClick={() => handleSendToStakeholders(fnfCase.id)}
> >
<Send className="w-4 h-4 mr-2" /> <Send className="w-4 h-4 mr-2" />

View File

@ -181,7 +181,7 @@ export const ASMDialog: React.FC<ASMDialogProps> = ({
return ( return (
<div key={state} className="mb-4 last:mb-0"> <div key={state} className="mb-4 last:mb-0">
<h4 className="text-sm text-amber-700 mb-2 pb-1 border-b border-slate-200">{state}</h4> <h4 className="text-sm text-re-red-hover mb-2 pb-1 border-b border-slate-200">{state}</h4>
<div className="space-y-2 ml-2"> <div className="space-y-2 ml-2">
{districts.map((district: any) => ( {districts.map((district: any) => (
<div key={district.id}> <div key={district.id}>
@ -280,7 +280,7 @@ export const ASMDialog: React.FC<ASMDialogProps> = ({
<div className="flex gap-3 pt-4"> <div className="flex gap-3 pt-4">
<Button variant="outline" className="flex-1" onClick={() => onOpenChange(false)}>Cancel</Button> <Button variant="outline" className="flex-1" onClick={() => onOpenChange(false)}>Cancel</Button>
<Button className="flex-1 bg-amber-600 hover:bg-amber-700" onClick={onSave}>Save DD-AM</Button> <Button className="flex-1 bg-re-red hover:bg-re-red-hover" onClick={onSave}>Save DD-AM</Button>
</div> </div>
</div> </div>
</DialogContent> </DialogContent>

View File

@ -41,7 +41,7 @@ export const ASMManagement: React.FC<ASMManagementProps> = ({
<CardTitle>District Development Area Managers (DD-AM)</CardTitle> <CardTitle>District Development Area Managers (DD-AM)</CardTitle>
<CardDescription>Manage DD-AM users across districts (multi-district)</CardDescription> <CardDescription>Manage DD-AM users across districts (multi-district)</CardDescription>
</div> </div>
<Button onClick={onAddASM} className="bg-amber-600 hover:bg-amber-700"> <Button onClick={onAddASM} className="bg-re-red hover:bg-re-red-hover">
<Plus className="w-4 h-4 mr-2" /> <Plus className="w-4 h-4 mr-2" />
Add DD-AM Add DD-AM
</Button> </Button>
@ -86,7 +86,7 @@ export const ASMManagement: React.FC<ASMManagementProps> = ({
<Badge <Badge
key={idx} key={idx}
variant={isShared ? "outline" : "secondary"} variant={isShared ? "outline" : "secondary"}
className={`text-xs ${isShared ? "border-amber-300 bg-amber-50 text-amber-700 font-medium" : ""}`} className={`text-xs ${isShared ? "border-red-300 bg-red-50 text-re-red-hover font-medium" : ""}`}
title={isShared ? `Also managed by: ${otherManagers.join(', ')}` : undefined} title={isShared ? `Also managed by: ${otherManagers.join(', ')}` : undefined}
> >
{areaName} {areaName}
@ -112,7 +112,7 @@ export const ASMManagement: React.FC<ASMManagementProps> = ({
<Button variant="ghost" size="sm" onClick={() => onEditASM(asm)}> <Button variant="ghost" size="sm" onClick={() => onEditASM(asm)}>
<Edit2 className="w-4 h-4" /> <Edit2 className="w-4 h-4" />
</Button> </Button>
<Button variant="ghost" size="sm" onClick={() => onDeleteASM(asm.id, asm.name)} className="text-red-600 hover:text-red-700 hover:bg-red-50"> <Button variant="ghost" size="sm" onClick={() => onDeleteASM(asm.id, asm.name)} className="text-re-red hover:text-re-red-hover hover:bg-red-50">
<Trash2 className="w-4 h-4" /> <Trash2 className="w-4 h-4" />
</Button> </Button>
</div> </div>

View File

@ -83,7 +83,7 @@ export const AutoAssignmentSettings: React.FC = () => {
if (loading) { if (loading) {
return ( return (
<div className="flex flex-col items-center justify-center p-12 space-y-4"> <div className="flex flex-col items-center justify-center p-12 space-y-4">
<RefreshCcw className="w-8 h-8 animate-spin text-amber-600" /> <RefreshCcw className="w-8 h-8 animate-spin text-re-red" />
<p className="text-slate-500 font-medium">Loading governance controls...</p> <p className="text-slate-500 font-medium">Loading governance controls...</p>
</div> </div>
); );
@ -94,8 +94,8 @@ export const AutoAssignmentSettings: React.FC = () => {
<Card className="border-none shadow-md overflow-hidden bg-white/50 backdrop-blur-sm"> <Card className="border-none shadow-md overflow-hidden bg-white/50 backdrop-blur-sm">
<CardHeader className="bg-gradient-to-r from-slate-900 to-slate-800 text-white p-6"> <CardHeader className="bg-gradient-to-r from-slate-900 to-slate-800 text-white p-6">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="p-2 bg-amber-500/20 rounded-lg"> <div className="p-2 bg-red-500/20 rounded-lg">
<Settings2 className="w-6 h-6 text-amber-400" /> <Settings2 className="w-6 h-6 text-red-400" />
</div> </div>
<div> <div>
<CardTitle className="text-xl font-bold">Auto-Assignment Governance</CardTitle> <CardTitle className="text-xl font-bold">Auto-Assignment Governance</CardTitle>
@ -148,6 +148,7 @@ export const AutoAssignmentSettings: React.FC = () => {
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<Switch <Switch
className="data-[state=checked]:bg-re-red"
checked={isEnabled} checked={isEnabled}
onCheckedChange={(val) => { onCheckedChange={(val) => {
handleToggle(mod.key, val); handleToggle(mod.key, val);
@ -164,9 +165,9 @@ export const AutoAssignmentSettings: React.FC = () => {
})} })}
</div> </div>
<div className="mt-8 p-4 bg-amber-50 border border-amber-100 rounded-lg flex items-start gap-3"> <div className="mt-8 p-4 bg-red-50 border border-red-100 rounded-lg flex items-start gap-3">
<Info className="w-5 h-5 text-amber-600 shrink-0 mt-0.5" /> <Info className="w-5 h-5 text-re-red shrink-0 mt-0.5" />
<div className="text-sm text-amber-800"> <div className="text-sm text-red-800">
<p className="font-semibold mb-1">Impact of Manual Mode:</p> <p className="font-semibold mb-1">Impact of Manual Mode:</p>
<p>Turning OFF auto-assignment will ONLY affect new requests. Existing requests will retain their current participant mappings. You will need to use the "Add Participant" button in the worknotes or application details to grant access to stakeholders.</p> <p>Turning OFF auto-assignment will ONLY affect new requests. Existing requests will retain their current participant mappings. You will need to use the "Add Participant" button in the worknotes or application details to grant access to stakeholders.</p>
</div> </div>

View File

@ -127,7 +127,7 @@ export const DDLeadDialog: React.FC<DDLeadDialogProps> = ({
<div className="flex gap-3 pt-6"> <div className="flex gap-3 pt-6">
<Button variant="outline" className="flex-1 border-slate-200" onClick={() => onOpenChange(false)}>Cancel</Button> <Button variant="outline" className="flex-1 border-slate-200" onClick={() => onOpenChange(false)}>Cancel</Button>
<Button className="flex-1 bg-amber-600 hover:bg-amber-700 shadow-sm" onClick={onSave}>Save DD Lead</Button> <Button className="flex-1 bg-re-red hover:bg-re-red-hover shadow-sm" onClick={onSave}>Save DD Lead</Button>
</div> </div>
</div> </div>
</DialogContent> </DialogContent>

View File

@ -36,7 +36,7 @@ export const DDLeadManagement: React.FC<DDLeadManagementProps> = ({
<CardTitle>DD-Leads (Dealer Development Lead)</CardTitle> <CardTitle>DD-Leads (Dealer Development Lead)</CardTitle>
<CardDescription>Manage DD-Leads and their zonal assignments</CardDescription> <CardDescription>Manage DD-Leads and their zonal assignments</CardDescription>
</div> </div>
<Button onClick={onAddLead} className="bg-amber-600 hover:bg-amber-700"> <Button onClick={onAddLead} className="bg-re-red hover:bg-re-red-hover">
<Plus className="w-4 h-4 mr-2" /> <Plus className="w-4 h-4 mr-2" />
Add DD-Lead Add DD-Lead
</Button> </Button>
@ -60,7 +60,7 @@ export const DDLeadManagement: React.FC<DDLeadManagementProps> = ({
<TableRow key={lead.id}> <TableRow key={lead.id}>
<TableCell> <TableCell>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Users className="w-4 h-4 text-amber-600" /> <Users className="w-4 h-4 text-re-red" />
<span className="font-medium">{lead.leadCode || 'N/A'}</span> <span className="font-medium">{lead.leadCode || 'N/A'}</span>
</div> </div>
</TableCell> </TableCell>
@ -95,7 +95,7 @@ export const DDLeadManagement: React.FC<DDLeadManagementProps> = ({
<Button variant="ghost" size="sm" onClick={() => onEditLead(lead)}> <Button variant="ghost" size="sm" onClick={() => onEditLead(lead)}>
<Edit2 className="w-4 h-4" /> <Edit2 className="w-4 h-4" />
</Button> </Button>
<Button variant="ghost" size="sm" onClick={() => onDeleteLead(lead.id, lead.name)} className="text-red-600 hover:text-red-700 hover:bg-red-50"> <Button variant="ghost" size="sm" onClick={() => onDeleteLead(lead.id, lead.name)} className="text-re-red hover:text-re-red-hover hover:bg-red-50">
<Trash2 className="w-4 h-4" /> <Trash2 className="w-4 h-4" />
</Button> </Button>
</div> </div>

View File

@ -193,7 +193,10 @@ export const DealerAsmAssignment: React.FC = () => {
</TableCell> </TableCell>
<TableCell>{dealer.dealerCode || 'N/A'}</TableCell> <TableCell>{dealer.dealerCode || 'N/A'}</TableCell>
<TableCell> <TableCell>
<Badge variant={String(dealer.status || '').toLowerCase() === 'active' ? 'default' : 'secondary'}> <Badge
variant={String(dealer.status || '').toLowerCase() === 'active' ? 'default' : 'secondary'}
className={String(dealer.status || '').toLowerCase() === 'active' ? 'bg-emerald-100 text-emerald-700 hover:bg-emerald-200' : ''}
>
{dealer.status || 'Unknown'} {dealer.status || 'Unknown'}
</Badge> </Badge>
</TableCell> </TableCell>
@ -215,7 +218,7 @@ export const DealerAsmAssignment: React.FC = () => {
onChange={(val) => setDraft((prev) => ({ ...prev, [dealer.dealerId]: val }))} onChange={(val) => setDraft((prev) => ({ ...prev, [dealer.dealerId]: val }))}
className="flex-1 min-w-[180px]" className="flex-1 min-w-[180px]"
/> />
<Button size="sm" className="shrink-0" onClick={() => saveMapping(dealer.dealerId)}> <Button size="sm" className="shrink-0 bg-re-red hover:bg-re-red-hover text-white" onClick={() => saveMapping(dealer.dealerId)}>
Assign Assign
</Button> </Button>
</div> </div>

View File

@ -179,7 +179,7 @@ export const DocumentConfigManagement: React.FC = () => {
if (metadataLoading) { if (metadataLoading) {
return ( return (
<div className="h-96 flex flex-col items-center justify-center gap-4"> <div className="h-96 flex flex-col items-center justify-center gap-4">
<Database className="w-10 h-10 text-amber-600 animate-bounce" /> <Database className="w-10 h-10 text-re-red animate-bounce" />
<p className="text-slate-500 font-bold uppercase tracking-widest text-xs">Connecting to Governance Engine...</p> <p className="text-slate-500 font-bold uppercase tracking-widest text-xs">Connecting to Governance Engine...</p>
</div> </div>
); );
@ -189,8 +189,8 @@ export const DocumentConfigManagement: React.FC = () => {
<Card className="border-slate-200 shadow-sm overflow-hidden bg-white"> <Card className="border-slate-200 shadow-sm overflow-hidden bg-white">
<CardHeader className="bg-slate-50/80 border-b border-slate-200 py-4 relative"> <CardHeader className="bg-slate-50/80 border-b border-slate-200 py-4 relative">
{backgroundLoading && ( {backgroundLoading && (
<div className="absolute top-0 left-0 right-0 h-1 bg-amber-100 overflow-hidden"> <div className="absolute top-0 left-0 right-0 h-1 bg-red-100 overflow-hidden">
<div className="h-full bg-amber-600 animate-[loading_1.5s_infinite_linear]" style={{width: '30%', transformOrigin: 'left'}} /> <div className="h-full bg-re-red animate-[loading_1.5s_infinite_linear]" style={{width: '30%', transformOrigin: 'left'}} />
<style>{` <style>{`
@keyframes loading { @keyframes loading {
0% { left: -30%; } 0% { left: -30%; }
@ -202,7 +202,7 @@ export const DocumentConfigManagement: React.FC = () => {
<div className="flex flex-row items-center justify-between mb-4"> <div className="flex flex-row items-center justify-between mb-4">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="p-2 bg-white rounded-xl shadow-sm ring-1 ring-slate-200 text-amber-600"> <div className="p-2 bg-white rounded-xl shadow-sm ring-1 ring-slate-200 text-re-red">
<Layers className="w-5 h-5" /> <Layers className="w-5 h-5" />
</div> </div>
<div> <div>
@ -218,7 +218,7 @@ export const DocumentConfigManagement: React.FC = () => {
<div className="flex gap-4"> <div className="flex gap-4">
<div className="w-64"> <div className="w-64">
<Select value={selectedModule} onValueChange={(val) => { setSelectedModule(val); setPage(1); }}> <Select value={selectedModule} onValueChange={(val) => { setSelectedModule(val); setPage(1); }}>
<SelectTrigger className="h-10 rounded-xl bg-white border-slate-200 focus:ring-amber-500 font-bold text-slate-700 shadow-sm"> <SelectTrigger className="h-10 rounded-xl bg-white border-slate-200 focus:ring-red-500 font-bold text-slate-700 shadow-sm">
<SelectValue placeholder="Target Module" /> <SelectValue placeholder="Target Module" />
</SelectTrigger> </SelectTrigger>
<SelectContent className="rounded-xl shadow-2xl border-none"> <SelectContent className="rounded-xl shadow-2xl border-none">
@ -236,7 +236,7 @@ export const DocumentConfigManagement: React.FC = () => {
placeholder="Search policies, stages or documents..." placeholder="Search policies, stages or documents..."
value={search} value={search}
onChange={(e) => { setSearch(e.target.value); setPage(1); }} onChange={(e) => { setSearch(e.target.value); setPage(1); }}
className="pl-10 h-10 rounded-xl bg-white border-slate-200 focus:ring-amber-500 shadow-sm font-medium" className="pl-10 h-10 rounded-xl bg-white border-slate-200 focus:ring-red-500 shadow-sm font-medium"
/> />
</div> </div>
</div> </div>
@ -244,7 +244,7 @@ export const DocumentConfigManagement: React.FC = () => {
<CardContent className="p-0 min-h-[400px] relative"> <CardContent className="p-0 min-h-[400px] relative">
{loading ? ( {loading ? (
<div className="absolute inset-0 z-10 bg-white/60 backdrop-blur-[1px] flex flex-col items-center justify-center gap-3"> <div className="absolute inset-0 z-10 bg-white/60 backdrop-blur-[1px] flex flex-col items-center justify-center gap-3">
<Loader2 className="w-8 h-8 text-amber-600 animate-spin" /> <Loader2 className="w-8 h-8 text-re-red animate-spin" />
<span className="text-slate-500 text-sm font-bold animate-pulse">Syncing Policies...</span> <span className="text-slate-500 text-sm font-bold animate-pulse">Syncing Policies...</span>
</div> </div>
) : null} ) : null}
@ -274,7 +274,7 @@ export const DocumentConfigManagement: React.FC = () => {
) : configs.map((config) => ( ) : configs.map((config) => (
<TableRow key={config.id} className="hover:bg-slate-50/80 transition-colors group h-14"> <TableRow key={config.id} className="hover:bg-slate-50/80 transition-colors group h-14">
<TableCell> <TableCell>
<div className="font-bold text-slate-900 group-hover:text-amber-700 transition-colors uppercase text-[12px]">{config.documentType}</div> <div className="font-bold text-slate-900 group-hover:text-re-red-hover transition-colors uppercase text-[12px]">{config.documentType}</div>
</TableCell> </TableCell>
<TableCell> <TableCell>
<Badge variant="outline" className="bg-blue-50/50 border-blue-100 text-blue-700 font-bold px-2 py-0.5 whitespace-nowrap text-[10px] rounded-md uppercase"> <Badge variant="outline" className="bg-blue-50/50 border-blue-100 text-blue-700 font-bold px-2 py-0.5 whitespace-nowrap text-[10px] rounded-md uppercase">
@ -305,7 +305,7 @@ export const DocumentConfigManagement: React.FC = () => {
<TableCell> <TableCell>
<div className="flex gap-2"> <div className="flex gap-2">
{config.isMandatory && ( {config.isMandatory && (
<Badge className="bg-red-600 text-white border-transparent text-[10px] font-bold h-5 px-1.5 rounded-sm uppercase tracking-tighter">BLOCKING</Badge> <Badge className="bg-re-red text-white border-transparent text-[10px] font-bold h-5 px-1.5 rounded-sm uppercase tracking-tighter">BLOCKING</Badge>
)} )}
{!config.isActive && ( {!config.isActive && (
<Badge className="bg-slate-200 text-slate-500 border-transparent text-[10px] h-5 px-1.5 rounded-sm uppercase tracking-tighter">DORMANT</Badge> <Badge className="bg-slate-200 text-slate-500 border-transparent text-[10px] h-5 px-1.5 rounded-sm uppercase tracking-tighter">DORMANT</Badge>
@ -320,7 +320,7 @@ export const DocumentConfigManagement: React.FC = () => {
<Button variant="ghost" size="icon" onClick={() => openEdit(config)} className="h-8 w-8 text-slate-400 hover:text-blue-600 hover:bg-blue-50 rounded-lg transition-transform active:scale-90"> <Button variant="ghost" size="icon" onClick={() => openEdit(config)} className="h-8 w-8 text-slate-400 hover:text-blue-600 hover:bg-blue-50 rounded-lg transition-transform active:scale-90">
<Edit2 className="w-4 h-4" /> <Edit2 className="w-4 h-4" />
</Button> </Button>
<Button variant="ghost" size="icon" onClick={() => handleDelete(config.id)} className="h-8 w-8 text-slate-400 hover:text-red-600 hover:bg-red-50 rounded-lg transition-transform active:scale-90"> <Button variant="ghost" size="icon" onClick={() => handleDelete(config.id)} className="h-8 w-8 text-slate-400 hover:text-re-red hover:bg-red-50 rounded-lg transition-transform active:scale-90">
<Trash2 className="w-4 h-4" /> <Trash2 className="w-4 h-4" />
</Button> </Button>
</div> </div>
@ -333,7 +333,7 @@ export const DocumentConfigManagement: React.FC = () => {
{/* Pagination Controls */} {/* Pagination Controls */}
<div className="flex items-center justify-between px-6 py-4 bg-slate-50/50 border-t border-slate-200 mt-auto"> <div className="flex items-center justify-between px-6 py-4 bg-slate-50/50 border-t border-slate-200 mt-auto">
<div className="text-[11px] text-slate-500 font-bold uppercase tracking-tight"> <div className="text-[11px] text-slate-500 font-bold uppercase tracking-tight">
Dataset Index <span className="text-slate-900 border-b border-slate-300 mx-1">{configs.length > 0 ? (page - 1) * limit + 1 : 0} - {Math.min(page * limit, pagination.total)}</span> Total Found <span className="text-amber-700 font-extrabold ml-1">{pagination.total}</span> Dataset Index <span className="text-slate-900 border-b border-slate-300 mx-1">{configs.length > 0 ? (page - 1) * limit + 1 : 0} - {Math.min(page * limit, pagination.total)}</span> Total Found <span className="text-re-red-hover font-extrabold ml-1">{pagination.total}</span>
</div> </div>
<div className="flex gap-3 items-center"> <div className="flex gap-3 items-center">
<Button <Button
@ -346,7 +346,7 @@ export const DocumentConfigManagement: React.FC = () => {
<ChevronLeft className="w-4 h-4 mr-1 text-slate-600" /> Prev <ChevronLeft className="w-4 h-4 mr-1 text-slate-600" /> Prev
</Button> </Button>
<div className="flex items-center px-4 h-9 bg-white border border-slate-200 rounded-xl text-xs font-extrabold text-slate-800 shadow-inner"> <div className="flex items-center px-4 h-9 bg-white border border-slate-200 rounded-xl text-xs font-extrabold text-slate-800 shadow-inner">
<span className="text-amber-600">{page}</span> <span className="mx-2 text-slate-300">/</span> {pagination.pages} <span className="text-re-red">{page}</span> <span className="mx-2 text-slate-300">/</span> {pagination.pages}
</div> </div>
<Button <Button
variant="outline" variant="outline"
@ -366,7 +366,7 @@ export const DocumentConfigManagement: React.FC = () => {
<DialogHeader className="bg-slate-900 text-white p-7"> <DialogHeader className="bg-slate-900 text-white p-7">
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<div className="p-3 bg-white/10 rounded-2xl backdrop-blur-md ring-1 ring-white/20"> <div className="p-3 bg-white/10 rounded-2xl backdrop-blur-md ring-1 ring-white/20">
<Settings2 className="w-7 h-7 text-amber-400" /> <Settings2 className="w-7 h-7 text-red-400" />
</div> </div>
<div> <div>
<DialogTitle className="text-2xl font-black tracking-tight uppercase"> <DialogTitle className="text-2xl font-black tracking-tight uppercase">
@ -385,12 +385,12 @@ export const DocumentConfigManagement: React.FC = () => {
value={formData.module} value={formData.module}
onValueChange={(val) => setFormData(prev => ({ ...prev, module: val, stageCode: 'General' }))} onValueChange={(val) => setFormData(prev => ({ ...prev, module: val, stageCode: 'General' }))}
> >
<SelectTrigger className="h-12 rounded-xl border-slate-200 focus:ring-amber-500 shadow-sm bg-slate-50 font-black text-xs uppercase"> <SelectTrigger className="h-12 rounded-xl border-slate-200 focus:ring-red-500 shadow-sm bg-slate-50 font-black text-xs uppercase">
<SelectValue placeholder="Module" /> <SelectValue placeholder="Module" />
</SelectTrigger> </SelectTrigger>
<SelectContent className="rounded-xl border-none shadow-2xl"> <SelectContent className="rounded-xl border-none shadow-2xl">
{modules.map(m => ( {modules.map(m => (
<SelectItem key={m} value={m} className="py-3 px-4 rounded-lg focus:bg-amber-50 font-black text-[10px] uppercase"> <SelectItem key={m} value={m} className="py-3 px-4 rounded-lg focus:bg-red-50 font-black text-[10px] uppercase">
{m.replace(/_/g, ' ')} {m.replace(/_/g, ' ')}
</SelectItem> </SelectItem>
))} ))}
@ -403,12 +403,12 @@ export const DocumentConfigManagement: React.FC = () => {
value={formData.stageCode} value={formData.stageCode}
onValueChange={(val) => setFormData(prev => ({ ...prev, stageCode: val }))} onValueChange={(val) => setFormData(prev => ({ ...prev, stageCode: val }))}
> >
<SelectTrigger className="h-12 rounded-xl border-slate-200 focus:ring-amber-500 shadow-sm bg-white font-black text-xs uppercase"> <SelectTrigger className="h-12 rounded-xl border-slate-200 focus:ring-red-500 shadow-sm bg-white font-black text-xs uppercase">
<SelectValue placeholder="Select Stage" /> <SelectValue placeholder="Select Stage" />
</SelectTrigger> </SelectTrigger>
<SelectContent className="rounded-xl border-none shadow-2xl"> <SelectContent className="rounded-xl border-none shadow-2xl">
{(stagesMap[formData.module] || ['General']).map(stage => ( {(stagesMap[formData.module] || ['General']).map(stage => (
<SelectItem key={stage} value={stage} className="py-3 px-4 rounded-lg focus:bg-amber-50 font-black text-[10px] uppercase">{stage}</SelectItem> <SelectItem key={stage} value={stage} className="py-3 px-4 rounded-lg focus:bg-red-50 font-black text-[10px] uppercase">{stage}</SelectItem>
))} ))}
</SelectContent> </SelectContent>
</Select> </Select>
@ -421,28 +421,28 @@ export const DocumentConfigManagement: React.FC = () => {
value={formData.documentType} value={formData.documentType}
onChange={(e) => setFormData(prev => ({ ...prev, documentType: e.target.value }))} onChange={(e) => setFormData(prev => ({ ...prev, documentType: e.target.value }))}
placeholder="e.g., PAN Card, Blueprint" placeholder="e.g., PAN Card, Blueprint"
className="h-12 rounded-xl border-slate-200 focus:ring-amber-500 shadow-sm font-black text-sm uppercase placeholder:font-bold placeholder:text-slate-300" className="h-12 rounded-xl border-slate-200 focus:ring-red-500 shadow-sm font-black text-sm uppercase placeholder:font-bold placeholder:text-slate-300"
/> />
</div> </div>
<div className="space-y-4 p-5 bg-slate-50 rounded-2xl border border-slate-100 shadow-inner"> <div className="space-y-4 p-5 bg-slate-50 rounded-2xl border border-slate-100 shadow-inner">
<Label className="text-slate-900 font-black flex items-center gap-2 mb-2 text-[11px] uppercase tracking-wider"> <Label className="text-slate-900 font-black flex items-center gap-2 mb-2 text-[11px] uppercase tracking-wider">
<ShieldCheck className="w-4 h-4 text-amber-600" /> Visibility Matrix <ShieldCheck className="w-4 h-4 text-re-red" /> Visibility Matrix
</Label> </Label>
<div className="grid grid-cols-3 gap-3"> <div className="grid grid-cols-3 gap-3">
{ROLE_LIST.map((role: string) => ( {ROLE_LIST.map((role: string) => (
<div <div
key={role} key={role}
className={`flex items-center space-x-2 p-3 rounded-xl border transition-all cursor-pointer group active:scale-95 ${formData.allowedRoles.includes(role) ? 'bg-amber-50 border-amber-300 shadow-sm' : 'bg-white border-slate-200 hover:border-amber-200 hover:shadow-sm'}`} className={`flex items-center space-x-2 p-3 rounded-xl border transition-all cursor-pointer group active:scale-95 ${formData.allowedRoles.includes(role) ? 'bg-red-50 border-red-300 shadow-sm' : 'bg-white border-slate-200 hover:border-red-200 hover:shadow-sm'}`}
onClick={() => toggleRole(role)} onClick={() => toggleRole(role)}
> >
<Checkbox <Checkbox
id={`role-${role}`} id={`role-${role}`}
checked={formData.allowedRoles.includes(role)} checked={formData.allowedRoles.includes(role)}
onCheckedChange={() => toggleRole(role)} onCheckedChange={() => toggleRole(role)}
className="w-4 h-4 data-[state=checked]:bg-amber-600 data-[state=checked]:border-amber-600 rounded" className="w-4 h-4 data-[state=checked]:bg-re-red data-[state=checked]:border-re-red rounded"
/> />
<Label htmlFor={`role-${role}`} className={`text-[10px] font-black cursor-pointer uppercase truncate ${formData.allowedRoles.includes(role) ? 'text-amber-800' : 'text-slate-500 group-hover:text-amber-700'}`}>{role}</Label> <Label htmlFor={`role-${role}`} className={`text-[10px] font-black cursor-pointer uppercase truncate ${formData.allowedRoles.includes(role) ? 'text-red-800' : 'text-slate-500 group-hover:text-re-red-hover'}`}>{role}</Label>
</div> </div>
))} ))}
</div> </div>
@ -457,7 +457,7 @@ export const DocumentConfigManagement: React.FC = () => {
id="mandatory" id="mandatory"
checked={formData.isMandatory} checked={formData.isMandatory}
onCheckedChange={(checked) => setFormData(prev => ({ ...prev, isMandatory: !!checked }))} onCheckedChange={(checked) => setFormData(prev => ({ ...prev, isMandatory: !!checked }))}
className="w-5 h-5 border-slate-300 data-[state=checked]:bg-red-600 data-[state=checked]:border-red-600 rounded-md" className="w-5 h-5 border-slate-300 data-[state=checked]:bg-re-red data-[state=checked]:border-re-red rounded-md"
/> />
<div className="space-y-0.5"> <div className="space-y-0.5">
<Label htmlFor="mandatory" className="text-xs font-black text-slate-800 cursor-pointer group-hover:text-red-900 transition-colors uppercase tracking-tight">Mandatory Policy</Label> <Label htmlFor="mandatory" className="text-xs font-black text-slate-800 cursor-pointer group-hover:text-red-900 transition-colors uppercase tracking-tight">Mandatory Policy</Label>

View File

@ -89,9 +89,9 @@ export const EmailTemplateBodyEditor = React.forwardRef<EmailTemplateBodyEditorH
</TabsList> </TabsList>
{advanced && ( {advanced && (
<Alert className="border-amber-200 bg-amber-50 py-2"> <Alert className="border-red-200 bg-red-50 py-2">
<Info className="h-4 w-4 text-amber-700" /> <Info className="h-4 w-4 text-re-red-hover" />
<AlertDescription className="text-[11px] text-amber-900"> <AlertDescription className="text-[11px] text-red-900">
This template uses layout partials (<code className="font-mono">{'{{> ...}}'}</code>), block helpers, or a full HTML This template uses layout partials (<code className="font-mono">{'{{> ...}}'}</code>), block helpers, or a full HTML
document. Edit it in <strong>HTML source</strong> so nothing is stripped. Use placeholders on the left to insert document. Edit it in <strong>HTML source</strong> so nothing is stripped. Use placeholders on the left to insert
fields safely. fields safely.

View File

@ -45,8 +45,8 @@ export const EmailTemplates: React.FC<EmailTemplatesProps> = ({
<TableRow key={template.id}> <TableRow key={template.id}>
<TableCell> <TableCell>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="w-8 h-8 bg-amber-50 rounded-lg flex items-center justify-center"> <div className="w-8 h-8 bg-red-50 rounded-lg flex items-center justify-center">
<Mail className="w-4 h-4 text-amber-600" /> <Mail className="w-4 h-4 text-re-red" />
</div> </div>
<span className="font-medium text-slate-900">{template.name || template.templateCode}</span> <span className="font-medium text-slate-900">{template.name || template.templateCode}</span>
</div> </div>
@ -68,7 +68,7 @@ export const EmailTemplates: React.FC<EmailTemplatesProps> = ({
<Button variant="ghost" size="sm" onClick={() => onEditTemplate(template)}> <Button variant="ghost" size="sm" onClick={() => onEditTemplate(template)}>
<Edit2 className="w-4 h-4" /> <Edit2 className="w-4 h-4" />
</Button> </Button>
<Button variant="ghost" size="sm" onClick={() => onDeleteTemplate(template.id)} className="text-red-500 hover:text-red-600 hover:bg-red-50"> <Button variant="ghost" size="sm" onClick={() => onDeleteTemplate(template.id)} className="text-red-500 hover:text-re-red hover:bg-red-50">
<Trash2 className="w-4 h-4" /> <Trash2 className="w-4 h-4" />
</Button> </Button>
</div> </div>

View File

@ -316,7 +316,7 @@ const InterviewConfigManagement: React.FC = () => {
</div> </div>
</div> </div>
) : ( ) : (
<div className="mb-6 p-4 bg-amber-50 border border-amber-200 rounded-lg text-amber-800"> <div className="mb-6 p-4 bg-red-50 border border-red-200 rounded-lg text-red-800">
No active configuration found. Click "Publish New Version" to create one, or "Reset to Defaults" to initialize system defaults. No active configuration found. Click "Publish New Version" to create one, or "Reset to Defaults" to initialize system defaults.
</div> </div>
)} )}
@ -338,7 +338,7 @@ const InterviewConfigManagement: React.FC = () => {
<Button variant="ghost" size="sm" onClick={() => cfg.id && handleEdit(cfg.id)}> <Button variant="ghost" size="sm" onClick={() => cfg.id && handleEdit(cfg.id)}>
<Edit3 size={14} /> <Edit3 size={14} />
</Button> </Button>
<Button variant="ghost" size="sm" className="text-red-600 hover:text-red-700" onClick={() => cfg.id && handleDelete(cfg.id)}> <Button variant="ghost" size="sm" className="text-re-red hover:text-re-red-hover" onClick={() => cfg.id && handleDelete(cfg.id)}>
<Trash2 size={14} /> <Trash2 size={14} />
</Button> </Button>
</div> </div>
@ -366,7 +366,7 @@ const InterviewConfigManagement: React.FC = () => {
</DialogDescription> </DialogDescription>
</div> </div>
{editingConfig?.configType === 'KT_MATRIX' && ( {editingConfig?.configType === 'KT_MATRIX' && (
<div className={`px-3 py-1.5 rounded-md text-[10px] font-black uppercase tracking-tight ${totalWeight === 100 ? 'bg-emerald-50 text-emerald-700 border border-emerald-100' : 'bg-amber-50 text-amber-700 border border-amber-100'}`}> <div className={`px-3 py-1.5 rounded-md text-[10px] font-black uppercase tracking-tight ${totalWeight === 100 ? 'bg-emerald-50 text-emerald-700 border border-emerald-100' : 'bg-red-50 text-re-red-hover border border-red-100'}`}>
Weight: {totalWeight}% / 100% Weight: {totalWeight}% / 100%
</div> </div>
)} )}
@ -404,7 +404,7 @@ const InterviewConfigManagement: React.FC = () => {
</div> </div>
{/* Items Area with Horizontal Guard */} {/* Items Area with Horizontal Guard */}
<div className="overflow-x-auto pb-4 -mx-1 px-1"> <div className="overflow-x-auto custom-scrollbar-x-slim pb-4 -mx-1 px-1">
<div className="min-w-[900px] border border-slate-100 rounded-xl"> <div className="min-w-[900px] border border-slate-100 rounded-xl">
<table className="w-full text-left border-collapse table-fixed"> <table className="w-full text-left border-collapse table-fixed">
<thead> <thead>
@ -449,9 +449,9 @@ const InterviewConfigManagement: React.FC = () => {
<SelectValue /> <SelectValue />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value="select">Selection</SelectItem> <SelectItem value="select">Options</SelectItem>
<SelectItem value="text">Text</SelectItem> <SelectItem value="text">One Liner</SelectItem>
<SelectItem value="textarea">Comment</SelectItem> <SelectItem value="textarea">Paragraph</SelectItem>
<SelectItem value="number">Numeric</SelectItem> <SelectItem value="number">Numeric</SelectItem>
</SelectContent> </SelectContent>
</Select> </Select>
@ -535,7 +535,7 @@ const InterviewConfigManagement: React.FC = () => {
<div className="flex flex-col gap-3"> <div className="flex flex-col gap-3">
<div className="flex items-center justify-between border-b border-slate-200/50 pb-2 mb-2"> <div className="flex items-center justify-between border-b border-slate-200/50 pb-2 mb-2">
<p className="text-[10px] font-black text-slate-400 uppercase tracking-widest flex items-center gap-2"> <p className="text-[10px] font-black text-slate-400 uppercase tracking-widest flex items-center gap-2">
<div className="w-1 h-3 bg-amber-400 rounded-full" /> Selection Choices Profile <div className="w-1 h-3 bg-red-400 rounded-full" /> Options
</p> </p>
<Button variant="ghost" size="sm" className="h-7 px-3 text-[10px] font-bold uppercase text-slate-600 hover:bg-white border border-transparent hover:border-slate-200" onClick={() => addOption(index)}> <Button variant="ghost" size="sm" className="h-7 px-3 text-[10px] font-bold uppercase text-slate-600 hover:bg-white border border-transparent hover:border-slate-200" onClick={() => addOption(index)}>
<Plus className="w-3 h-3 mr-1.5" /> Append Option <Plus className="w-3 h-3 mr-1.5" /> Append Option

View File

@ -55,7 +55,7 @@ export const LocationDialog: React.FC<LocationDialogProps> = ({
setLocationDistrict(''); setLocationDistrict('');
}} }}
> >
<SelectTrigger className="mt-2 text-slate-900 border-slate-200 focus:ring-amber-500/30 focus:border-amber-500"> <SelectTrigger className="mt-2 text-slate-900 border-slate-200 focus:ring-red-500/30 focus:border-red-500">
<SelectValue placeholder="Select state" /> <SelectValue placeholder="Select state" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
@ -71,7 +71,7 @@ export const LocationDialog: React.FC<LocationDialogProps> = ({
<Label className="flex items-center gap-2 text-sm leading-none font-medium text-slate-700">City</Label> <Label className="flex items-center gap-2 text-sm leading-none font-medium text-slate-700">City</Label>
<Input <Input
placeholder="Enter city name" placeholder="Enter city name"
className="mt-2 text-slate-900 border-slate-200 focus-visible:ring-amber-500/30 focus-visible:border-amber-500" className="mt-2 text-slate-900 border-slate-200 focus-visible:ring-red-500/30 focus-visible:border-red-500"
value={locationCity} value={locationCity}
onChange={(e) => setLocationCity(e.target.value)} onChange={(e) => setLocationCity(e.target.value)}
/> />
@ -79,7 +79,7 @@ export const LocationDialog: React.FC<LocationDialogProps> = ({
<div> <div>
<Label className="flex items-center gap-2 text-sm leading-none font-medium text-slate-700">District</Label> <Label className="flex items-center gap-2 text-sm leading-none font-medium text-slate-700">District</Label>
<Select value={locationDistrict} onValueChange={setLocationDistrict} disabled={!locationState}> <Select value={locationDistrict} onValueChange={setLocationDistrict} disabled={!locationState}>
<SelectTrigger className="mt-2 text-slate-900 border-slate-200 focus:ring-amber-500/30 focus:border-amber-500"> <SelectTrigger className="mt-2 text-slate-900 border-slate-200 focus:ring-red-500/30 focus:border-red-500">
<SelectValue placeholder={locationState ? 'Select district' : 'Select state first'} /> <SelectValue placeholder={locationState ? 'Select district' : 'Select state first'} />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
@ -120,7 +120,7 @@ export const LocationDialog: React.FC<LocationDialogProps> = ({
<div> <div>
<Label className="flex items-center gap-2 text-sm leading-none font-medium text-slate-700">Opportunity</Label> <Label className="flex items-center gap-2 text-sm leading-none font-medium text-slate-700">Opportunity</Label>
<Select value={locationStatus} onValueChange={setLocationStatus}> <Select value={locationStatus} onValueChange={setLocationStatus}>
<SelectTrigger className="mt-2 text-slate-900 border-slate-200 focus:ring-amber-500/30 focus:border-amber-500"> <SelectTrigger className="mt-2 text-slate-900 border-slate-200 focus:ring-red-500/30 focus:border-red-500">
<SelectValue placeholder="Is this an active Opportunity?" /> <SelectValue placeholder="Is this an active Opportunity?" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
@ -132,7 +132,7 @@ export const LocationDialog: React.FC<LocationDialogProps> = ({
<div className="flex gap-3 pt-4"> <div className="flex gap-3 pt-4">
<Button variant="outline" className="flex-1" onClick={() => onOpenChange(false)}>Cancel</Button> <Button variant="outline" className="flex-1" onClick={() => onOpenChange(false)}>Cancel</Button>
<Button className="flex-1 bg-amber-600 hover:bg-amber-700 text-white shadow-md hover:shadow-lg transition-all" onClick={onSave}>Save Location</Button> <Button className="flex-1 bg-re-red hover:bg-re-red-hover text-white shadow-md hover:shadow-lg transition-all" onClick={onSave}>Save Location</Button>
</div> </div>
</div> </div>
</DialogContent> </DialogContent>

View File

@ -53,7 +53,7 @@ export const LocationManagement: React.FC<LocationManagementProps> = ({
placeholder="Search locations..." placeholder="Search locations..."
value={searchTerm} value={searchTerm}
onChange={(e) => onSearch(e.target.value)} onChange={(e) => onSearch(e.target.value)}
className="pl-9 pr-4 py-2 bg-slate-50 border border-slate-200 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-amber-500 w-64 transition-all" className="pl-9 pr-4 py-2 bg-slate-50 border border-slate-200 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-red-500 w-64 transition-all"
/> />
</div> </div>
@ -80,7 +80,7 @@ export const LocationManagement: React.FC<LocationManagementProps> = ({
</SelectContent> </SelectContent>
</Select> </Select>
<Button onClick={onAddLocation} className="bg-amber-600 hover:bg-amber-700 whitespace-nowrap"> <Button onClick={onAddLocation} className="bg-re-red hover:bg-re-red-hover whitespace-nowrap">
<Plus className="w-4 h-4 mr-2" /> <Plus className="w-4 h-4 mr-2" />
Add Location Add Location
</Button> </Button>
@ -112,7 +112,7 @@ export const LocationManagement: React.FC<LocationManagementProps> = ({
<TableRow key={district.id}> <TableRow key={district.id}>
<TableCell> <TableCell>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<MapPin className="w-4 h-4 text-amber-600" /> <MapPin className="w-4 h-4 text-re-red" />
<span className="font-medium">{district.stateName || 'N/A'}</span> <span className="font-medium">{district.stateName || 'N/A'}</span>
</div> </div>
</TableCell> </TableCell>
@ -151,7 +151,7 @@ export const LocationManagement: React.FC<LocationManagementProps> = ({
<Button variant="outline" size="sm" onClick={() => onEditLocation(district)} className="h-8 w-8 p-0"> <Button variant="outline" size="sm" onClick={() => onEditLocation(district)} className="h-8 w-8 p-0">
<Edit2 className="w-4 h-4" /> <Edit2 className="w-4 h-4" />
</Button> </Button>
<Button variant="outline" size="sm" onClick={() => onDeleteLocation(district.id)} className="h-8 w-8 p-0 text-red-600 hover:bg-red-50 hover:text-red-700"> <Button variant="outline" size="sm" onClick={() => onDeleteLocation(district.id)} className="h-8 w-8 p-0 text-re-red hover:bg-red-50 hover:text-re-red-hover">
<Trash2 className="w-4 h-4" /> <Trash2 className="w-4 h-4" />
</Button> </Button>
</div> </div>
@ -163,7 +163,7 @@ export const LocationManagement: React.FC<LocationManagementProps> = ({
</Table> </Table>
{isAreasLoading && ( {isAreasLoading && (
<div className="absolute inset-0 flex items-center justify-center bg-white/10 backdrop-blur-[1px]"> <div className="absolute inset-0 flex items-center justify-center bg-white/10 backdrop-blur-[1px]">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-amber-600"></div> <div className="animate-spin rounded-full h-8 w-8 border-b-2 border-re-red"></div>
</div> </div>
)} )}
</div> </div>

View File

@ -189,7 +189,7 @@ export const RegionDialog: React.FC<RegionDialogProps> = ({
<div> <div>
<Label>States Covered</Label> <Label>States Covered</Label>
{!selectedRegionZone && ( {!selectedRegionZone && (
<p className="text-xs text-amber-600 mt-1">Select a zone first to see available states</p> <p className="text-xs text-re-red mt-1">Select a zone first to see available states</p>
)} )}
<div className="mt-2 border rounded-lg p-3 max-h-40 overflow-y-auto bg-slate-50"> <div className="mt-2 border rounded-lg p-3 max-h-40 overflow-y-auto bg-slate-50">
{statesForZone.length === 0 ? ( {statesForZone.length === 0 ? (
@ -234,7 +234,7 @@ export const RegionDialog: React.FC<RegionDialogProps> = ({
<TooltipProvider> <TooltipProvider>
{districtsByState.map(({ stateName, districts }) => ( {districtsByState.map(({ stateName, districts }) => (
<div key={stateName} className="mb-4 last:mb-0"> <div key={stateName} className="mb-4 last:mb-0">
<h4 className="text-xs font-semibold text-amber-700 uppercase tracking-wide mb-2 pb-1 border-b border-slate-200"> <h4 className="text-xs font-semibold text-re-red-hover uppercase tracking-wide mb-2 pb-1 border-b border-slate-200">
{stateName} {stateName}
</h4> </h4>
<div className="space-y-2 ml-1"> <div className="space-y-2 ml-1">
@ -302,7 +302,7 @@ export const RegionDialog: React.FC<RegionDialogProps> = ({
{/* Actions */} {/* Actions */}
<div className="flex gap-3 pt-4"> <div className="flex gap-3 pt-4">
<Button variant="outline" className="flex-1" onClick={() => onOpenChange(false)}>Cancel</Button> <Button variant="outline" className="flex-1" onClick={() => onOpenChange(false)}>Cancel</Button>
<Button className="flex-1 bg-amber-600 hover:bg-amber-700" onClick={onSave}>Save Regional Office</Button> <Button className="flex-1 bg-re-red hover:bg-re-red-hover" onClick={onSave}>Save Regional Office</Button>
</div> </div>
</div> </div>
</DialogContent> </DialogContent>

View File

@ -29,7 +29,7 @@ export const RegionalManagement: React.FC<RegionalManagementProps> = ({
<CardTitle>Regional Offices</CardTitle> <CardTitle>Regional Offices</CardTitle>
<CardDescription>Manage regional offices within zones</CardDescription> <CardDescription>Manage regional offices within zones</CardDescription>
</div> </div>
<Button onClick={onAddRegion} className="bg-amber-600 hover:bg-amber-700"> <Button onClick={onAddRegion} className="bg-re-red hover:bg-re-red-hover">
<Plus className="w-4 h-4 mr-2" /> <Plus className="w-4 h-4 mr-2" />
Add Regional Office Add Regional Office
</Button> </Button>
@ -114,7 +114,7 @@ export const RegionalManagement: React.FC<RegionalManagementProps> = ({
<Button variant="ghost" size="sm" onClick={() => onEditRegion(region)}> <Button variant="ghost" size="sm" onClick={() => onEditRegion(region)}>
<Edit2 className="w-4 h-4" /> <Edit2 className="w-4 h-4" />
</Button> </Button>
<Button variant="ghost" size="sm" onClick={() => onDeleteRegion(region.id, region.name)} className="text-red-600 hover:text-red-700 hover:bg-red-50"> <Button variant="ghost" size="sm" onClick={() => onDeleteRegion(region.id, region.name)} className="text-re-red hover:text-re-red-hover hover:bg-red-50">
<Trash2 className="w-4 h-4" /> <Trash2 className="w-4 h-4" />
</Button> </Button>
</div> </div>

View File

@ -79,8 +79,8 @@ export const RoleDialog: React.FC<RoleDialogProps> = ({
}, },
{ {
title: "Application Stage Access", title: "Application Stage Access",
color: "from-amber-50 to-orange-50 border-amber-200", color: "from-red-50 to-orange-50 border-red-200",
textColor: "text-amber-900", textColor: "text-red-900",
permissions: [ permissions: [
{ id: "stage:initial_review", label: "Initial Review" }, { id: "stage:initial_review", label: "Initial Review" },
{ id: "stage:field_verification", label: "Field Verification" }, { id: "stage:field_verification", label: "Field Verification" },
@ -126,7 +126,7 @@ export const RoleDialog: React.FC<RoleDialogProps> = ({
<div className="space-y-5"> <div className="space-y-5">
<h4 className="text-sm font-bold text-slate-800 flex items-center gap-2"> <h4 className="text-sm font-bold text-slate-800 flex items-center gap-2">
<span className="w-1.5 h-1.5 bg-amber-500 rounded-full"></span> <span className="w-1.5 h-1.5 bg-red-500 rounded-full"></span>
Configure Default Permissions Configure Default Permissions
</h4> </h4>

View File

@ -22,7 +22,7 @@ export const RolePermissions: React.FC<RolePermissionsProps> = ({ onAddRole, onE
<CardTitle className="leading-none text-xl font-bold">Role Definitions</CardTitle> <CardTitle className="leading-none text-xl font-bold">Role Definitions</CardTitle>
<CardDescription className="text-muted-foreground mt-1.5">Overview of available roles and their access levels</CardDescription> <CardDescription className="text-muted-foreground mt-1.5">Overview of available roles and their access levels</CardDescription>
</div> </div>
<Button onClick={onAddRole} className="bg-amber-600 hover:bg-amber-700 h-9"> <Button onClick={onAddRole} className="bg-re-red hover:bg-re-red-hover h-9">
<Plus className="w-4 h-4 mr-2" /> <Plus className="w-4 h-4 mr-2" />
Add Role Add Role
</Button> </Button>
@ -34,7 +34,7 @@ export const RolePermissions: React.FC<RolePermissionsProps> = ({ onAddRole, onE
<div key={role.id} className="border rounded-lg p-4 space-y-3 bg-gradient-to-br from-white to-slate-50 hover:shadow-md transition-shadow"> <div key={role.id} className="border rounded-lg p-4 space-y-3 bg-gradient-to-br from-white to-slate-50 hover:shadow-md transition-shadow">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Shield className="w-5 h-5 text-amber-600" /> <Shield className="w-5 h-5 text-re-red" />
<h3 className="text-slate-900 font-bold">{role.name}</h3> <h3 className="text-slate-900 font-bold">{role.name}</h3>
</div> </div>
<Badge variant="secondary" className="border-transparent bg-secondary text-secondary-foreground text-xs font-medium"> <Badge variant="secondary" className="border-transparent bg-secondary text-secondary-foreground text-xs font-medium">

View File

@ -29,7 +29,7 @@ export const SLAConfiguration: React.FC<SLAConfigurationProps> = ({ onConfigureS
<div key={sla.id} className="border rounded-lg p-4 bg-gradient-to-br from-white to-slate-50 hover:shadow-md transition-shadow"> <div key={sla.id} className="border rounded-lg p-4 bg-gradient-to-br from-white to-slate-50 hover:shadow-md transition-shadow">
<div className="flex items-start justify-between mb-3"> <div className="flex items-start justify-between mb-3">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<Clock className="w-5 h-5 text-amber-600" /> <Clock className="w-5 h-5 text-re-red" />
<div> <div>
<h4 className="text-slate-900 font-medium">{sla.activityName}</h4> <h4 className="text-slate-900 font-medium">{sla.activityName}</h4>
<p className="text-xs text-slate-600">TAT: {sla.tatHours} {sla.tatUnit}</p> <p className="text-xs text-slate-600">TAT: {sla.tatHours} {sla.tatUnit}</p>
@ -73,7 +73,7 @@ export const SLAConfiguration: React.FC<SLAConfigurationProps> = ({ onConfigureS
<div className="border-l-2 border-red-400 pl-3"> <div className="border-l-2 border-red-400 pl-3">
<div className="flex items-center gap-2 mb-2"> <div className="flex items-center gap-2 mb-2">
<AlertTriangle className="w-4 h-4 text-red-600" /> <AlertTriangle className="w-4 h-4 text-re-red" />
<span className="text-xs text-slate-700">Escalations ({sla.escalationConfigs?.length || 0})</span> <span className="text-xs text-slate-700">Escalations ({sla.escalationConfigs?.length || 0})</span>
</div> </div>
<div className="space-y-1"> <div className="space-y-1">

View File

@ -184,6 +184,7 @@ export const SLADialog: React.FC<SLADialogProps> = ({ isOpen, onClose, sla, onSa
<div className="flex items-center space-x-2 pt-8"> <div className="flex items-center space-x-2 pt-8">
<Switch <Switch
id="isActive" id="isActive"
className="data-[state=checked]:bg-re-red"
checked={formData.isActive} checked={formData.isActive}
onCheckedChange={(checked) => setFormData({ ...formData, isActive: checked })} onCheckedChange={(checked) => setFormData({ ...formData, isActive: checked })}
/> />
@ -252,7 +253,7 @@ export const SLADialog: React.FC<SLADialogProps> = ({ isOpen, onClose, sla, onSa
<div className="space-y-4"> <div className="space-y-4">
<div className="flex items-center justify-between border-b pb-2"> <div className="flex items-center justify-between border-b pb-2">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<AlertTriangle className="w-4 h-4 text-red-600" /> <AlertTriangle className="w-4 h-4 text-re-red" />
<h4 className="font-medium text-sm">Escalation Levels</h4> <h4 className="font-medium text-sm">Escalation Levels</h4>
</div> </div>
<Button type="button" variant="outline" size="sm" onClick={handleAddEscalation} className="h-7 text-xs"> <Button type="button" variant="outline" size="sm" onClick={handleAddEscalation} className="h-7 text-xs">
@ -264,7 +265,7 @@ export const SLADialog: React.FC<SLADialogProps> = ({ isOpen, onClose, sla, onSa
{formData.escalationConfigs.map((esc: any, idx: number) => ( {formData.escalationConfigs.map((esc: any, idx: number) => (
<div key={idx} className="bg-slate-50 p-3 rounded-lg border border-slate-100 space-y-3"> <div key={idx} className="bg-slate-50 p-3 rounded-lg border border-slate-100 space-y-3">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<Badge variant="outline" className="bg-red-50 text-red-700 border-red-100"> <Badge variant="outline" className="bg-red-50 text-re-red-hover border-red-100">
Level {esc.level} Level {esc.level}
</Badge> </Badge>
<Button <Button
@ -345,7 +346,7 @@ export const SLADialog: React.FC<SLADialogProps> = ({ isOpen, onClose, sla, onSa
<Button type="button" variant="outline" onClick={onClose} disabled={loading}> <Button type="button" variant="outline" onClick={onClose} disabled={loading}>
Cancel Cancel
</Button> </Button>
<Button type="submit" disabled={loading}> <Button type="submit" disabled={loading} className="bg-re-red hover:bg-re-red-hover text-white">
{loading ? 'Saving...' : 'Save Configuration'} {loading ? 'Saving...' : 'Save Configuration'}
</Button> </Button>
</DialogFooter> </DialogFooter>

View File

@ -0,0 +1,627 @@
import React, { useCallback, useEffect, useState } from 'react';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow
} from '@/components/ui/table';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue
} from '@/components/ui/select';
import { Checkbox } from '@/components/ui/checkbox';
import { Label } from '@/components/ui/label';
import {
Activity,
AlertTriangle,
BarChart3,
CheckCircle2,
Clock,
Download,
ExternalLink,
Mail,
RefreshCw,
Server,
Timer
} from 'lucide-react';
import { Input } from '@/components/ui/input';
import { toast } from 'sonner';
import {
slaService,
SlaBucket,
SlaOperationsDashboard,
SlaQueueItem,
QuestionnaireReminderSettings
} from '@/services/sla.service';
const BUCKET_LABEL: Record<SlaBucket, string> = {
healthy: '025% elapsed',
warning: '2675% elapsed',
critical: '7699% elapsed',
breached: 'Breached'
};
const BUCKET_CLASS: Record<SlaBucket, string> = {
healthy: 'bg-emerald-100 text-emerald-800 border-emerald-200',
warning: 'bg-red-50 text-red-800 border-red-200',
critical: 'bg-orange-100 text-orange-800 border-orange-200',
breached: 'bg-red-100 text-red-800 border-red-200'
};
const MODULES = ['ALL', 'ONBOARDING', 'TERMINATION', 'RESIGNATION', 'RELOCATION', 'CONSTITUTIONAL', 'FNF'];
function BucketBadge({ bucket }: { bucket: SlaBucket }) {
return (
<Badge variant="outline" className={`text-[10px] font-semibold ${BUCKET_CLASS[bucket]}`}>
{BUCKET_LABEL[bucket]}
</Badge>
);
}
function ProgressCell({ percent, bucket }: { percent: number; bucket: SlaBucket }) {
const barColor =
bucket === 'breached'
? 'bg-red-500'
: bucket === 'critical'
? 'bg-orange-500'
: bucket === 'warning'
? 'bg-red-500'
: 'bg-emerald-500';
const capped = Math.min(percent, 100);
return (
<div className="min-w-[100px]">
<div className="h-1.5 w-full bg-slate-100 rounded-full overflow-hidden mb-1">
<div className={`h-full rounded-full ${barColor}`} style={{ width: `${capped}%` }} />
</div>
<span className="text-[10px] text-slate-500">{percent}% of TAT</span>
<div className="mt-1">
<BucketBadge bucket={bucket} />
</div>
</div>
);
}
function QueueTable({ items, emptyMessage }: { items: SlaQueueItem[]; emptyMessage: string }) {
if (items.length === 0) {
return <p className="text-sm text-slate-500 py-8 text-center">{emptyMessage}</p>;
}
return (
<Table>
<TableHeader>
<TableRow>
<TableHead>Case</TableHead>
<TableHead>Module</TableHead>
<TableHead>Stage</TableHead>
<TableHead>Owner</TableHead>
<TableHead>Progress</TableHead>
<TableHead>Time</TableHead>
<TableHead className="w-10" />
</TableRow>
</TableHeader>
<TableBody>
{items.map((row) => (
<TableRow key={row.trackingId} className={row.bucket === 'breached' ? 'bg-red-50/40' : undefined}>
<TableCell className="font-medium text-slate-900">{row.caseRef}</TableCell>
<TableCell>
<Badge variant="secondary" className="text-[10px]">
{row.module}
</Badge>
</TableCell>
<TableCell className="text-slate-700 max-w-[200px] truncate" title={row.stageName}>
{row.stageName}
</TableCell>
<TableCell className="text-slate-600 text-xs">{row.ownerRole}</TableCell>
<TableCell>
<ProgressCell percent={row.percentUsed} bucket={row.bucket} />
</TableCell>
<TableCell className="text-xs text-slate-600">
<div>{row.remainingLabel}</div>
<span className="text-[10px] text-slate-400 block">
Due {new Date(row.deadline).toLocaleString()}
</span>
</TableCell>
<TableCell>
<a
href={row.link}
target="_blank"
rel="noopener noreferrer"
className="text-re-red hover:text-red-800"
title="Open case"
>
<ExternalLink className="w-4 h-4" />
</a>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
);
}
export const SLAMonitorPanel: React.FC = () => {
const [data, setData] = useState<SlaOperationsDashboard | null>(null);
const [loading, setLoading] = useState(true);
const [moduleFilter, setModuleFilter] = useState('ALL');
const [tab, setTab] = useState('queue');
const [mineOnly, setMineOnly] = useState(false);
const [qSettings, setQSettings] = useState<QuestionnaireReminderSettings | null>(null);
const [qDraft, setQDraft] = useState<Partial<QuestionnaireReminderSettings>>({});
const [savingQ, setSavingQ] = useState(false);
const load = useCallback(async () => {
setLoading(true);
try {
const res = await slaService.getOperationsDashboard({
module: moduleFilter === 'ALL' ? undefined : moduleFilter,
mineOnly
});
if (res?.success) {
setData(res.data);
} else {
toast.error('Failed to load SLA monitor');
}
} catch {
toast.error('Failed to load SLA monitor');
} finally {
setLoading(false);
}
}, [moduleFilter, tab, mineOnly]);
useEffect(() => {
load();
}, [load]);
useEffect(() => {
slaService
.getQuestionnaireReminderSettings()
.then((res) => {
if (res?.success && res.data) {
setQSettings(res.data);
setQDraft(res.data);
}
})
.catch(() => undefined);
}, []);
const saveQuestionnaireSettings = async () => {
setSavingQ(true);
try {
const res = await slaService.updateQuestionnaireReminderSettings(qDraft);
if (res?.success) {
setQSettings(res.data);
setQDraft(res.data);
toast.success('Questionnaire reminder settings saved');
}
} catch {
toast.error('Failed to save settings');
} finally {
setSavingQ(false);
}
};
const summary = data?.summary;
const analytics = data?.analytics;
const queue = data?.activeQueue ?? [];
const breachedOnly = queue.filter((q) => q.bucket === 'breached');
const dueSoon = queue.filter((q) => q.bucket === 'warning' || q.bucket === 'critical');
return (
<div className="space-y-6">
<div className="flex flex-wrap items-center justify-between gap-3">
<div>
<h2 className="text-lg font-semibold text-slate-900 flex items-center gap-2">
<Activity className="w-5 h-5 text-re-red" />
SLA Operations Monitor
</h2>
<p className="text-sm text-slate-500">
Live queue, aging buckets, breaches, and scheduler health
{data?.generatedAt && (
<span className="ml-2 text-slate-400">
· Updated {new Date(data.generatedAt).toLocaleTimeString()}
</span>
)}
</p>
</div>
<div className="flex flex-wrap items-center gap-2">
<div className="flex items-center gap-2 mr-2">
<Checkbox id="sla-mine-only" checked={mineOnly} onCheckedChange={(v) => setMineOnly(Boolean(v))} />
<Label htmlFor="sla-mine-only" className="text-sm cursor-pointer">
My queue only
</Label>
</div>
<Select value={moduleFilter} onValueChange={setModuleFilter}>
<SelectTrigger className="w-[180px] h-9">
<SelectValue placeholder="Module" />
</SelectTrigger>
<SelectContent>
{MODULES.map((m) => (
<SelectItem key={m} value={m}>
{m === 'ALL' ? 'All modules' : m}
</SelectItem>
))}
</SelectContent>
</Select>
<Button
variant="outline"
size="sm"
onClick={() =>
slaService
.exportOperationsCsv({
module: moduleFilter === 'ALL' ? undefined : moduleFilter,
mineOnly
})
.catch(() => toast.error('Export failed'))
}
>
<Download className="w-4 h-4 mr-1" />
Export CSV
</Button>
<Button variant="outline" size="sm" onClick={load} disabled={loading}>
<RefreshCw className={`w-4 h-4 mr-1 ${loading ? 'animate-spin' : ''}`} />
Refresh
</Button>
</div>
</div>
<div className="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-5 gap-3">
<SummaryCard label="In queue" value={summary?.activeCount ?? '—'} icon={<Clock className="w-4 h-4 text-blue-600" />} />
<SummaryCard
label="Breached"
value={summary?.breachedCount ?? '—'}
icon={<AlertTriangle className="w-4 h-4 text-red-600" />}
highlight="red"
/>
<SummaryCard label="Due soon" value={summary?.dueSoonCount ?? '—'} icon={<Timer className="w-4 h-4 text-re-red" />} />
<SummaryCard
label="On track"
value={summary?.onTrackCount ?? '—'}
icon={<CheckCircle2 className="w-4 h-4 text-emerald-600" />}
/>
<SummaryCard
label="Open breaches"
value={summary?.openBreachesCount ?? '—'}
icon={<AlertTriangle className="w-4 h-4 text-re-red" />}
/>
</div>
{analytics && (
<Card className="border-slate-200">
<CardHeader className="pb-2">
<CardTitle className="text-sm flex items-center gap-2">
<BarChart3 className="w-4 h-4 text-re-red" />
Analytics (last {analytics.periodDays} days)
</CardTitle>
<CardDescription>Breach rate, resolution time, and top delayed stages</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
<AnalyticsStat label="Tracks started" value={analytics.tracksStarted} />
<AnalyticsStat label="Breach rate" value={`${analytics.breachRatePercent}%`} highlight />
<AnalyticsStat
label="Avg resolution"
value={analytics.avgResolutionHours != null ? `${analytics.avgResolutionHours}h` : '—'}
/>
<AnalyticsStat label="Completed tracks" value={analytics.completedTracks} />
</div>
{analytics.topDelayedStages.length > 0 && (
<div>
<p className="text-xs font-semibold text-slate-500 uppercase mb-2">Top delayed stages</p>
<Table>
<TableHeader>
<TableRow>
<TableHead>Stage</TableHead>
<TableHead>Breaches (30d)</TableHead>
<TableHead>Active breached</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{analytics.topDelayedStages.map((row) => (
<TableRow key={row.stageName}>
<TableCell className="font-medium">{row.stageName}</TableCell>
<TableCell>{row.breachCount}</TableCell>
<TableCell>{row.currentlyBreached}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
)}
{Object.keys(analytics.breachesByModule).length > 0 && (
<div className="flex flex-wrap gap-2">
{Object.entries(analytics.breachesByModule).map(([mod, count]) => (
<Badge key={mod} variant="outline">
{mod}: {count} breaches
</Badge>
))}
</div>
)}
</CardContent>
</Card>
)}
{summary && (
<Card className="border-slate-200">
<CardHeader className="pb-2">
<CardTitle className="text-sm">Aging buckets</CardTitle>
<CardDescription>Percent of configured TAT elapsed</CardDescription>
</CardHeader>
<CardContent className="flex flex-wrap gap-2">
{(Object.keys(BUCKET_LABEL) as SlaBucket[]).map((b) => (
<Badge key={b} variant="outline" className={`px-3 py-1 ${BUCKET_CLASS[b]}`}>
{BUCKET_LABEL[b]}: {summary.buckets[b] ?? 0}
</Badge>
))}
{summary.tracksWithoutConfig > 0 && (
<Badge variant="outline" className="bg-slate-100 text-slate-600">
No config match: {summary.tracksWithoutConfig}
</Badge>
)}
</CardContent>
</Card>
)}
<Tabs value={tab} onValueChange={setTab}>
<TabsList className="bg-slate-100">
<TabsTrigger value="queue">Active queue ({queue.length})</TabsTrigger>
<TabsTrigger value="breached">Breached ({breachedOnly.length})</TabsTrigger>
<TabsTrigger value="due">Due soon ({dueSoon.length})</TabsTrigger>
<TabsTrigger value="breach-log">Open breaches ({data?.breaches?.length ?? 0})</TabsTrigger>
<TabsTrigger value="schedulers">Schedulers</TabsTrigger>
</TabsList>
<TabsContent value="queue" className="mt-4">
<Card>
<CardContent className="pt-4">
<QueueTable items={queue} emptyMessage="No active SLA timers in queue." />
</CardContent>
</Card>
</TabsContent>
<TabsContent value="breached" className="mt-4">
<Card>
<CardContent className="pt-4">
<QueueTable items={breachedOnly} emptyMessage="No breached items — all active SLAs are within TAT." />
</CardContent>
</Card>
</TabsContent>
<TabsContent value="due" className="mt-4">
<Card>
<CardContent className="pt-4">
<QueueTable items={dueSoon} emptyMessage="Nothing in the 2699% window right now." />
</CardContent>
</Card>
</TabsContent>
<TabsContent value="breach-log" className="mt-4">
<Card>
<CardContent className="pt-4">
{!data?.breaches?.length ? (
<p className="text-sm text-slate-500 py-8 text-center">No open breach records.</p>
) : (
<Table>
<TableHeader>
<TableRow>
<TableHead>Case</TableHead>
<TableHead>Module</TableHead>
<TableHead>Stage</TableHead>
<TableHead>Breached at</TableHead>
<TableHead>Status</TableHead>
<TableHead />
</TableRow>
</TableHeader>
<TableBody>
{data.breaches.map((b) => (
<TableRow key={b.id}>
<TableCell className="font-medium">{b.caseRef}</TableCell>
<TableCell>
<Badge variant="secondary" className="text-[10px]">
{b.module}
</Badge>
</TableCell>
<TableCell className="truncate max-w-[200px]">{b.stageName}</TableCell>
<TableCell className="text-xs">{new Date(b.breachedAt).toLocaleString()}</TableCell>
<TableCell>
<Badge variant="outline">{b.status}</Badge>
</TableCell>
<TableCell>
<a href={b.link} target="_blank" rel="noopener noreferrer" className="text-re-red">
<ExternalLink className="w-4 h-4" />
</a>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
)}
</CardContent>
</Card>
</TabsContent>
<TabsContent value="schedulers" className="mt-4">
<div className="grid gap-4 md:grid-cols-2">
<Card>
<CardHeader>
<CardTitle className="text-sm flex items-center gap-2">
<Mail className="w-4 h-4" />
Prospect questionnaire reminders
</CardTitle>
<CardDescription>
Email/WhatsApp to applicants in Questionnaire Pending (not internal SLA)
{qSettings?.source && (
<span className="block text-slate-400">Source: {qSettings.source}</span>
)}
</CardDescription>
</CardHeader>
<CardContent className="space-y-3 text-sm">
<div className="flex items-center gap-2">
<Checkbox
id="q-enabled"
checked={Boolean(qDraft.enabled)}
onCheckedChange={(v) => setQDraft((d) => ({ ...d, enabled: Boolean(v) }))}
/>
<Label htmlFor="q-enabled">Scheduler enabled</Label>
</div>
<div className="grid grid-cols-3 gap-2">
<div>
<Label className="text-xs">First after (days)</Label>
<Input
type="number"
min={1}
value={qDraft.firstAfterDays ?? ''}
onChange={(e) =>
setQDraft((d) => ({ ...d, firstAfterDays: Number(e.target.value) }))
}
/>
</div>
<div>
<Label className="text-xs">Interval (days)</Label>
<Input
type="number"
min={1}
value={qDraft.intervalDays ?? ''}
onChange={(e) =>
setQDraft((d) => ({ ...d, intervalDays: Number(e.target.value) }))
}
/>
</div>
<div>
<Label className="text-xs">Max reminders</Label>
<Input
type="number"
min={1}
value={qDraft.maxCount ?? ''}
onChange={(e) =>
setQDraft((d) => ({ ...d, maxCount: Number(e.target.value) }))
}
/>
</div>
</div>
<Button size="sm" onClick={saveQuestionnaireSettings} disabled={savingQ}>
{savingQ ? 'Saving…' : 'Save reminder settings'}
</Button>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="text-sm flex items-center gap-2">
<Server className="w-4 h-4" />
Infrastructure
</CardTitle>
</CardHeader>
<CardContent className="space-y-2 text-sm">
<StatusRow label="Redis" value={data?.scheduler?.redisEnabled ? 'Enabled' : 'Disabled'} ok={data?.scheduler?.redisEnabled} />
<StatusRow label="SLA fast mode" value={data?.scheduler?.slaFastMode ? 'On' : 'Off'} ok={!data?.scheduler?.slaFastMode} />
<StatusRow
label="Questionnaire fast mode"
value={data?.scheduler?.questionnaireFastMode ? 'On' : 'Off'}
ok={!data?.scheduler?.questionnaireFastMode}
/>
</CardContent>
</Card>
{(data?.scheduler?.queues ?? []).map((q) => (
<Card key={q.name}>
<CardHeader className="pb-2">
<CardTitle className="text-sm">{q.name}</CardTitle>
{q.key && <CardDescription>{q.key}</CardDescription>}
</CardHeader>
<CardContent className="text-sm">
{q.error ? (
<p className="text-red-600">{q.error}</p>
) : (
<>
{q.counts && (
<div className="flex flex-wrap gap-2 mb-3">
{Object.entries(q.counts).map(([k, v]) => (
<Badge key={k} variant="outline">
{k}: {v}
</Badge>
))}
</div>
)}
{q.repeatable?.length ? (
<ul className="text-xs text-slate-600 space-y-1">
{q.repeatable.map((r, i) => (
<li key={i}>
{r.name || r.pattern}
{r.next ? ` · next ${new Date(r.next).toLocaleString()}` : ''}
</li>
))}
</ul>
) : (
<p className="text-slate-400 text-xs">No repeatable jobs registered</p>
)}
</>
)}
</CardContent>
</Card>
))}
</div>
</TabsContent>
</Tabs>
</div>
);
};
function AnalyticsStat({
label,
value,
highlight
}: {
label: string;
value: string | number;
highlight?: boolean;
}) {
return (
<div
className={`rounded-lg border p-3 ${highlight ? 'border-red-200 bg-red-50/40' : 'border-slate-200 bg-slate-50/50'}`}
>
<p className="text-[10px] uppercase tracking-wide text-slate-500">{label}</p>
<p className="text-xl font-bold text-slate-900">{value}</p>
</div>
);
}
function SummaryCard({
label,
value,
icon,
highlight
}: {
label: string;
value: number | string;
icon: React.ReactNode;
highlight?: 'red';
}) {
return (
<Card className={highlight === 'red' ? 'border-red-200 bg-red-50/30' : 'border-slate-200'}>
<CardContent className="pt-4 pb-3">
<div className="flex items-center justify-between">
<div>
<p className="text-xs text-slate-500 uppercase tracking-wide">{label}</p>
<p className="text-2xl font-bold text-slate-900">{value}</p>
</div>
{icon}
</div>
</CardContent>
</Card>
);
}
function StatusRow({ label, value, ok }: { label: string; value: string; ok?: boolean }) {
return (
<div className="flex justify-between">
<span className="text-slate-600">{label}</span>
<span className={ok === false ? 'text-re-red-hover font-medium' : 'text-slate-900'}>{value}</span>
</div>
);
}

View File

@ -54,7 +54,7 @@ export const SecurityDepositMaster: React.FC = () => {
if (loading) { if (loading) {
return ( return (
<div className="flex flex-col items-center justify-center p-20 space-y-4"> <div className="flex flex-col items-center justify-center p-20 space-y-4">
<div className="w-10 h-10 border-4 border-amber-600 border-t-transparent rounded-full animate-spin"></div> <div className="w-10 h-10 border-4 border-re-red border-t-transparent rounded-full animate-spin"></div>
<p className="text-slate-600 animate-pulse">Loading settings...</p> <p className="text-slate-600 animate-pulse">Loading settings...</p>
</div> </div>
); );
@ -65,15 +65,15 @@ export const SecurityDepositMaster: React.FC = () => {
<Card className="border-none shadow-lg bg-white/80 backdrop-blur-md"> <Card className="border-none shadow-lg bg-white/80 backdrop-blur-md">
<CardHeader className="py-4 border-b bg-slate-50/50"> <CardHeader className="py-4 border-b bg-slate-50/50">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="w-8 h-8 rounded-lg bg-amber-100 flex items-center justify-center"> <div className="w-8 h-8 rounded-lg bg-red-100 flex items-center justify-center">
<Settings className="w-5 h-5 text-amber-600" /> <Settings className="w-5 h-5 text-re-red" />
</div> </div>
<div> <div>
<CardTitle className="text-lg font-bold text-slate-900"> <CardTitle className="text-lg font-bold text-slate-900">
Global Payment Settings Global Payment Settings
</CardTitle> </CardTitle>
<CardDescription className="text-xs"> <CardDescription className="text-xs">
Configure base security deposit amounts for onboarding workflows. Configure base Security Deposit amounts for onboarding workflows.
</CardDescription> </CardDescription>
</div> </div>
</div> </div>
@ -98,7 +98,7 @@ export const SecurityDepositMaster: React.FC = () => {
<Input <Input
id={`amount-${config.id}`} id={`amount-${config.id}`}
type="number" type="number"
className="pl-8 h-9 text-base font-bold bg-slate-50/50 border-slate-200 focus:ring-amber-500 focus:border-amber-500 rounded-lg" className="pl-8 h-9 text-base font-bold bg-slate-50/50 border-slate-200 focus:ring-red-500 focus:border-red-500 rounded-lg"
value={config.value?.amount || ''} value={config.value?.amount || ''}
onChange={(e) => handleUpdateAmount(config.id, e.target.value)} onChange={(e) => handleUpdateAmount(config.id, e.target.value)}
/> />
@ -109,7 +109,7 @@ export const SecurityDepositMaster: React.FC = () => {
<Button <Button
onClick={() => handleSave(config)} onClick={() => handleSave(config)}
disabled={isSaving === config.id} disabled={isSaving === config.id}
className="h-9 px-4 bg-amber-600 hover:bg-amber-700 text-white rounded-lg shadow-md shadow-amber-600/10 active:scale-95 transition-all flex items-center gap-1.5 group" className="h-9 px-4 bg-re-red hover:bg-re-red-hover text-white rounded-lg shadow-md shadow-re-red/10 active:scale-95 transition-all flex items-center gap-1.5 group"
> >
{isSaving === config.id ? ( {isSaving === config.id ? (
<RefreshCw className="w-4 h-4 animate-spin" /> <RefreshCw className="w-4 h-4 animate-spin" />
@ -140,13 +140,13 @@ export const SecurityDepositMaster: React.FC = () => {
</CardContent> </CardContent>
</Card> </Card>
<div className="bg-amber-50/50 rounded-xl p-4 border border-amber-100/50 flex items-start gap-3"> <div className="bg-red-50/50 rounded-xl p-4 border border-red-100/50 flex items-start gap-3">
<div className="w-8 h-8 rounded-full bg-amber-100 flex items-center justify-center flex-shrink-0"> <div className="w-8 h-8 rounded-full bg-red-100 flex items-center justify-center flex-shrink-0">
<Settings className="w-4 h-4 text-amber-700" /> <Settings className="w-4 h-4 text-re-red-hover" />
</div> </div>
<div> <div>
<h5 className="font-bold text-amber-900 text-sm">Super Admin Notice</h5> <h5 className="font-bold text-red-900 text-sm">Super Admin Notice</h5>
<p className="text-[11px] text-amber-800/80 leading-snug"> <p className="text-[11px] text-red-800/80 leading-snug">
Updates made here take immediate effect. These values define the default expected amounts for all current and future onboarding payments. Updates made here take immediate effect. These values define the default expected amounts for all current and future onboarding payments.
</p> </p>
</div> </div>

View File

@ -96,7 +96,7 @@ export const TemplateDialog: React.FC<TemplateDialogProps> = ({
<div className="space-y-4"> <div className="space-y-4">
<div className="bg-slate-50 p-4 rounded-lg border border-slate-200 space-y-4"> <div className="bg-slate-50 p-4 rounded-lg border border-slate-200 space-y-4">
<h3 className="text-sm font-semibold text-slate-800 flex items-center gap-2"> <h3 className="text-sm font-semibold text-slate-800 flex items-center gap-2">
<Settings className="w-4 h-4 text-amber-600" /> <Settings className="w-4 h-4 text-re-red" />
General Settings General Settings
</h3> </h3>
<div> <div>
@ -149,12 +149,12 @@ export const TemplateDialog: React.FC<TemplateDialogProps> = ({
</div> </div>
</div> </div>
<div className="bg-amber-50 p-4 rounded-lg border border-amber-100"> <div className="bg-red-50 p-4 rounded-lg border border-red-100">
<h3 className="text-sm font-semibold text-amber-900 flex items-center gap-2 mb-3"> <h3 className="text-sm font-semibold text-red-900 flex items-center gap-2 mb-3">
<Info className="w-4 h-4" /> <Info className="w-4 h-4" />
Available Placeholders Available Placeholders
</h3> </h3>
<p className="text-[10px] text-amber-700 mb-4 leading-relaxed"> <p className="text-[10px] text-re-red-hover mb-4 leading-relaxed">
Click a placeholder to insert it at the cursor. Click a placeholder to insert it at the cursor.
</p> </p>
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
@ -164,14 +164,14 @@ export const TemplateDialog: React.FC<TemplateDialogProps> = ({
key={p} key={p}
type="button" type="button"
onClick={() => insertPlaceholder(p)} onClick={() => insertPlaceholder(p)}
className="px-2 py-1 bg-white border border-amber-200 rounded text-[11px] font-mono text-amber-800 hover:bg-amber-600 hover:text-white hover:border-amber-600 transition-all flex items-center gap-1 shadow-sm" className="px-2 py-1 bg-white border border-red-200 rounded text-[11px] font-mono text-red-800 hover:bg-re-red hover:text-white hover:border-re-red transition-all flex items-center gap-1 shadow-sm"
> >
{`{{${p}}}`} {`{{${p}}}`}
</button> </button>
)) ))
) : ( ) : (
<div className="w-full py-4 text-center border-2 border-dashed border-amber-200 rounded-lg"> <div className="w-full py-4 text-center border-2 border-dashed border-red-200 rounded-lg">
<p className="text-[10px] text-amber-600">No placeholders defined for this trigger</p> <p className="text-[10px] text-re-red">No placeholders defined for this trigger</p>
</div> </div>
)} )}
</div> </div>
@ -182,7 +182,7 @@ export const TemplateDialog: React.FC<TemplateDialogProps> = ({
<div className="lg:col-span-4 space-y-4 flex flex-col h-full min-h-[600px]"> <div className="lg:col-span-4 space-y-4 flex flex-col h-full min-h-[600px]">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<h3 className="font-semibold text-slate-900 flex items-center gap-2"> <h3 className="font-semibold text-slate-900 flex items-center gap-2">
<Edit2 className="w-4 h-4 text-amber-600" /> <Edit2 className="w-4 h-4 text-re-red" />
Template Designer Template Designer
</h3> </h3>
</div> </div>
@ -230,7 +230,7 @@ export const TemplateDialog: React.FC<TemplateDialogProps> = ({
'{"applicantName": "Rajesh Kumar", "location": "Mumbai South", "applicationId": "APP-2026-X12", "link": "https://example.com/app", "portalLink": "https://example.com/app"}' '{"applicantName": "Rajesh Kumar", "location": "Mumbai South", "applicationId": "APP-2026-X12", "link": "https://example.com/app", "portalLink": "https://example.com/app"}'
) )
} }
className="text-[10px] text-amber-600 hover:underline" className="text-[10px] text-re-red hover:underline"
> >
Reset to Sample Reset to Sample
</button> </button>
@ -302,7 +302,7 @@ export const TemplateDialog: React.FC<TemplateDialogProps> = ({
Cancel Cancel
</Button> </Button>
<Button <Button
className="flex-1 bg-amber-600 hover:bg-amber-700" className="flex-1 bg-re-red hover:bg-re-red-hover"
type="button" type="button"
onClick={() => handleSaveTemplate(composeFullBody())} onClick={() => handleSaveTemplate(composeFullBody())}
disabled={!editingTemplate?.id || !editingTemplate?.templateCode?.trim()} disabled={!editingTemplate?.id || !editingTemplate?.templateCode?.trim()}

View File

@ -44,7 +44,7 @@ export const UserManagementTable: React.FC<UserManagementTableProps> = ({ userAs
</TableCell> </TableCell>
<TableCell> <TableCell>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Shield className="w-3 h-3 text-amber-600" /> <Shield className="w-3 h-3 text-re-red" />
<span className="text-sm font-medium">{user.role}</span> <span className="text-sm font-medium">{user.role}</span>
</div> </div>
</TableCell> </TableCell>

View File

@ -146,7 +146,7 @@ export const ZMDialog: React.FC<ZMDialogProps> = ({
<div className="flex gap-3 pt-6"> <div className="flex gap-3 pt-6">
<Button variant="outline" className="flex-1" onClick={() => onOpenChange(false)}>Cancel</Button> <Button variant="outline" className="flex-1" onClick={() => onOpenChange(false)}>Cancel</Button>
<Button className="flex-1 bg-amber-600 hover:bg-amber-700" onClick={onSave}>Save Zonal Manager</Button> <Button className="flex-1 bg-re-red hover:bg-re-red-hover" onClick={onSave}>Save Zonal Manager</Button>
</div> </div>
</div> </div>
</DialogContent> </DialogContent>

View File

@ -33,7 +33,7 @@ export const ZMManagement: React.FC<ZMManagementProps> = ({
<CardTitle>Zonal Managers (DD-ZM)</CardTitle> <CardTitle>Zonal Managers (DD-ZM)</CardTitle>
<CardDescription>Manage Zonal Managers and their region assignments</CardDescription> <CardDescription>Manage Zonal Managers and their region assignments</CardDescription>
</div> </div>
<Button onClick={onAddZM} className="bg-amber-600 hover:bg-amber-700"> <Button onClick={onAddZM} className="bg-re-red hover:bg-re-red-hover">
<Plus className="w-4 h-4 mr-2" /> <Plus className="w-4 h-4 mr-2" />
Add ZM Add ZM
</Button> </Button>
@ -99,7 +99,7 @@ export const ZMManagement: React.FC<ZMManagementProps> = ({
<Button variant="ghost" size="sm" onClick={() => onEditZM(zm)}> <Button variant="ghost" size="sm" onClick={() => onEditZM(zm)}>
<Edit2 className="w-4 h-4" /> <Edit2 className="w-4 h-4" />
</Button> </Button>
<Button variant="ghost" size="sm" onClick={() => onDeleteZM(zm.id, zm.name)} className="text-red-600 hover:text-red-700 hover:bg-red-50"> <Button variant="ghost" size="sm" onClick={() => onDeleteZM(zm.id, zm.name)} className="text-re-red hover:text-re-red-hover hover:bg-red-50">
<Trash2 className="w-4 h-4" /> <Trash2 className="w-4 h-4" />
</Button> </Button>
</div> </div>

View File

@ -27,7 +27,7 @@ export const ZoneDetails: React.FC<ZoneDetailsProps> = ({ selectedZone, onAddZon
<CardTitle>Zone Details</CardTitle> <CardTitle>Zone Details</CardTitle>
<CardDescription>Geographical coverage and state mappings for each zone</CardDescription> <CardDescription>Geographical coverage and state mappings for each zone</CardDescription>
</div> </div>
<Button onClick={onAddZone} className="bg-amber-600 hover:bg-amber-700"> <Button onClick={onAddZone} className="bg-re-red hover:bg-re-red-hover">
<Plus className="w-4 h-4 mr-2" /> <Plus className="w-4 h-4 mr-2" />
Add Zone Add Zone
</Button> </Button>
@ -40,7 +40,7 @@ export const ZoneDetails: React.FC<ZoneDetailsProps> = ({ selectedZone, onAddZon
<div key={zone.id} className="border rounded-lg p-5 space-y-4 bg-gradient-to-br from-white to-slate-50"> <div key={zone.id} className="border rounded-lg p-5 space-y-4 bg-gradient-to-br from-white to-slate-50">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="w-12 h-12 bg-gradient-to-br from-amber-500 to-amber-600 rounded-lg flex items-center justify-center shadow-md"> <div className="w-12 h-12 bg-gradient-to-br from-red-500 to-re-red rounded-lg flex items-center justify-center shadow-md">
<Globe className="w-6 h-6 text-white" /> <Globe className="w-6 h-6 text-white" />
</div> </div>
<div> <div>
@ -86,11 +86,11 @@ export const ZoneDetails: React.FC<ZoneDetailsProps> = ({ selectedZone, onAddZon
<Label className="text-xs text-slate-600 mb-2 block"> <Label className="text-xs text-slate-600 mb-2 block">
Zonal Business Head (ZBH) Zonal Business Head (ZBH)
</Label> </Label>
<div className="bg-amber-50 border border-amber-100 rounded-lg p-3 space-y-2"> <div className="bg-red-50 border border-red-100 rounded-lg p-3 space-y-2">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Shield className="w-4 h-4 text-amber-600" /> <Shield className="w-4 h-4 text-re-red" />
<span className="text-sm font-semibold text-slate-900">{zone.zonalBusinessHead.name}</span> <span className="text-sm font-semibold text-slate-900">{zone.zonalBusinessHead.name}</span>
<Badge className="bg-amber-600 text-white text-[10px] ml-auto">ZBH</Badge> <Badge className="bg-re-red text-white text-[10px] ml-auto">ZBH</Badge>
</div> </div>
<div className="flex items-center gap-2 ml-6 text-slate-600"> <div className="flex items-center gap-2 ml-6 text-slate-600">

View File

@ -106,7 +106,7 @@ export const ZoneDialog: React.FC<ZoneDialogProps> = ({
</div> </div>
<div className="flex gap-3 pt-4"> <div className="flex gap-3 pt-4">
<Button variant="outline" className="flex-1" onClick={() => onOpenChange(false)}>Cancel</Button> <Button variant="outline" className="flex-1" onClick={() => onOpenChange(false)}>Cancel</Button>
<Button className="flex-1 bg-amber-600 hover:bg-amber-700" onClick={onSave}>Save Zone</Button> <Button className="flex-1 bg-re-red hover:bg-re-red-hover" onClick={onSave}>Save Zone</Button>
</div> </div>
</div> </div>
</DialogContent> </DialogContent>

View File

@ -19,14 +19,14 @@ export const ZonesOverview: React.FC<ZonesOverviewProps> = ({ selectedZone, onZo
return ( return (
<Card <Card
key={zone.id} key={zone.id}
className={`border-2 transition-all cursor-pointer ${selectedZone === zone.id ? 'border-amber-600 shadow-lg' : 'hover:border-amber-400' className={`border-2 transition-all cursor-pointer ${selectedZone === zone.id ? 'border-re-red shadow-lg' : 'hover:border-red-400'
}`} }`}
onClick={() => onZoneClick(zone.id)} onClick={() => onZoneClick(zone.id)}
> >
<CardHeader className="pb-3"> <CardHeader className="pb-3">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Globe className="w-5 h-5 text-amber-600" /> <Globe className="w-5 h-5 text-re-red" />
<CardTitle className="text-lg">{zone.name.toUpperCase().endsWith('ZONE') ? zone.name : `${zone.name} Zone`}</CardTitle> <CardTitle className="text-lg">{zone.name.toUpperCase().endsWith('ZONE') ? zone.name : `${zone.name} Zone`}</CardTitle>
</div> </div>
<Badge variant="outline" className="text-xs">{zone.code}</Badge> <Badge variant="outline" className="text-xs">{zone.code}</Badge>

View File

@ -1,33 +1,67 @@
/** /**
* Allowed email template triggers keep aligned with backend * Allowed email template triggers keep aligned with backend
* Dealer_Onboarding_Backend/src/constants/allowed-email-template-codes.ts and seed-master-emails. * `backend/src/constants/allowed-email-template-codes.ts` and `seed-master-emails.ts`.
*/ */
export const ALLOWED_EMAIL_TEMPLATE_CODES = [ export const ALLOWED_EMAIL_TEMPLATE_CODES = [
'APPLICANT_SHORTLISTED', 'APPLICANT_SHORTLISTED',
'APPLICANT_REJECTED',
'ARCHITECTURAL_PLAN_REQUEST',
'CONSTITUTIONAL_CHANGE_SUBMITTED', 'CONSTITUTIONAL_CHANGE_SUBMITTED',
'DEALERSHIP_AGREEMENT_SIGNATURE_REQUEST',
'CONSTITUTIONAL_CHANGE_APPROVED',
'CONSTITUTIONAL_CHANGE_UPDATE', 'CONSTITUTIONAL_CHANGE_UPDATE',
'DEALER_CODE_READY', 'DEALER_CODE_READY',
'DOCUMENT_RECEIVED_ACKNOWLEDGEMENT',
'DOCUMENT_REJECTED_RESUBMIT',
'DOCUMENT_SUBMISSION_REMINDER',
'EOR_COMPLETED',
'FDD_DOCUMENT_REQUEST',
'FNF_INITIATED',
'FNF_LWD_READY',
'FNF_SUMMARY_PREPARED',
'FNF_SETTLEMENT_APPROVED',
'GENERIC_NOTIFICATION', 'GENERIC_NOTIFICATION',
'INAUGURATION_COMPLETED',
'INTERVIEW_SCHEDULED', 'INTERVIEW_SCHEDULED',
'INTERVIEW_SCHEDULED_APPLICANT',
'INTERVIEW_SCHEDULED_PANELIST',
'INTERVIEW_RESCHEDULED_APPLICANT',
'INTERVIEW_RESCHEDULED_PANELIST',
'INTERVIEW_CANCELLED_APPLICANT',
'INTERVIEW_CANCELLED_PANELIST',
'LOA_ISSUED', 'LOA_ISSUED',
'LOI_ACKNOWLEDGEMENT_REQUEST',
'LOI_ISSUED', 'LOI_ISSUED',
'NON_OPPORTUNITY', 'NON_OPPORTUNITY',
'ONBOARDING_PAYMENT_VERIFIED',
'ONBOARDING_STATUS_UPDATE', 'ONBOARDING_STATUS_UPDATE',
'OPPORTUNITY', 'OPPORTUNITY',
'PROSPECT_DOCUMENT_REQUEST',
'QUESTIONNAIRE_REMINDER', 'QUESTIONNAIRE_REMINDER',
'QUESTIONNAIRE_SUBMITTED', 'QUESTIONNAIRE_SUBMITTED',
'SECURITY_DEPOSIT_REQUEST',
'RELOCATION_RECEIVED', 'RELOCATION_RECEIVED',
'RELOCATION_SUBMITTED', 'RELOCATION_SUBMITTED',
'RELOCATION_APPROVED',
'RELOCATION_UPDATE', 'RELOCATION_UPDATE',
'RESIGNATION_APPROVED', 'RESIGNATION_APPROVED',
'RESIGNATION_RECEIVED', 'RESIGNATION_RECEIVED',
'RESIGNATION_SUBMITTED', 'RESIGNATION_SUBMITTED',
'RESIGNATION_UPDATE', 'RESIGNATION_UPDATE',
'SLA_BREACH_WARNING', 'SLA_BREACH_WARNING',
'STATUTORY_DOCUMENT_REQUEST',
'SLA_REMINDER',
'SLA_BREACH',
'SLA_ESCALATION',
'TERMINATION_INITIATED',
'TERMINATION_SCN_ISSUED', 'TERMINATION_SCN_ISSUED',
'TERMINATION_LETTER_ISSUED',
'TERMINATION_FINAL_CLOSURE_DEALER',
'TERMINATION_UPDATE', 'TERMINATION_UPDATE',
'USER_ASSIGNED', 'USER_ASSIGNED',
'WORKNOTE_NOTIFICATION' 'WORKNOTE_NOTIFICATION',
'WORKFLOW_ACTION_REQUIRED',
'WORKFLOW_STATUS_UPDATE_DEALER'
] as const; ] as const;
const ALLOWED_SET = new Set<string>(ALLOWED_EMAIL_TEMPLATE_CODES); const ALLOWED_SET = new Set<string>(ALLOWED_EMAIL_TEMPLATE_CODES);

View File

@ -1,10 +1,9 @@
import React, { useState, useEffect, useCallback, useMemo } from 'react'; import React, { useState, useEffect, useCallback, useMemo } from 'react';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { import {
Tabs, TabsContent, TabsList, TabsTrigger Tabs, TabsContent, TabsList, TabsTrigger
} from '@/components/ui/tabs'; } from '@/components/ui/tabs';
import { Globe, Shield, Mail, MapPin, SlidersHorizontal, Settings, FileText, Settings2 } from 'lucide-react'; import { Globe, Shield, Mail, MapPin, SlidersHorizontal, Settings, FileText, Settings2 } from 'lucide-react';
import { Badge } from '@/components/ui/badge';
import { toast } from 'sonner'; import { toast } from 'sonner';
// Services & Hooks // Services & Hooks
@ -38,13 +37,13 @@ import { RootState } from '@/store';
export const MasterPage: React.FC = () => { export const MasterPage: React.FC = () => {
const { fetchInitialData, fetchAreas } = useMasterData(); const { fetchInitialData, fetchAreas } = useMasterData();
const { const {
asms, zonalManagerMappings, asms, zonalManagerMappings,
allStates, allStates,
allDistricts, allDistricts,
users, users,
roles, roles,
loading loading
} = useSelector((state: RootState) => state.master); } = useSelector((state: RootState) => state.master);
// Tab & Selection State // Tab & Selection State
@ -67,7 +66,7 @@ export const MasterPage: React.FC = () => {
const [selectedASMStates, setSelectedASMStates] = useState<string[]>([]); const [selectedASMStates, setSelectedASMStates] = useState<string[]>([]);
const [selectedASMDistricts, setSelectedASMDistricts] = useState<string[]>([]); const [selectedASMDistricts, setSelectedASMDistricts] = useState<string[]>([]);
const [asmRoleCode, setAsmRoleCode] = useState<'ASM' | 'DD-AM'>('DD-AM'); const [asmRoleCode, setAsmRoleCode] = useState<'ASM' | 'DD-AM'>('DD-AM');
// ZM Management State // ZM Management State
const [showZMDialog, setShowZMDialog] = useState(false); const [showZMDialog, setShowZMDialog] = useState(false);
const [editingZMId, setEditingZMId] = useState<string | null>(null); const [editingZMId, setEditingZMId] = useState<string | null>(null);
@ -161,11 +160,11 @@ export const MasterPage: React.FC = () => {
return; return;
} }
try { try {
const payload = { const payload = {
userId: asmManagerId, userId: asmManagerId,
roleCode: asmRoleCode, roleCode: asmRoleCode,
districts: selectedASMDistricts, districts: selectedASMDistricts,
status: asmStatus status: asmStatus
}; };
const res = await masterService.saveASM(payload) as any; const res = await masterService.saveASM(payload) as any;
if (res.success) { if (res.success) {
@ -175,9 +174,9 @@ export const MasterPage: React.FC = () => {
} else { } else {
toast.error(res.message || 'Failed to save ASM'); toast.error(res.message || 'Failed to save ASM');
} }
} catch (error: any) { } catch (error: any) {
const msg = error?.response?.data?.message || error?.message || 'Failed to save ASM'; const msg = error?.response?.data?.message || error?.message || 'Failed to save ASM';
toast.error(msg); toast.error(msg);
} }
}; };
@ -209,13 +208,13 @@ export const MasterPage: React.FC = () => {
return; return;
} }
try { try {
const payload = { const payload = {
userId: zmManagerId, userId: zmManagerId,
zoneId: selectedZMZone, zoneId: selectedZMZone,
regionIds: selectedZMRegions, regionIds: selectedZMRegions,
status: zmStatus status: zmStatus
}; };
const res = await (masterService as any).saveZonalManager(payload) as any; const res = await (masterService as any).saveZonalManager(payload) as any;
if (res.success) { if (res.success) {
toast.success(`Zonal Manager ${editingZMId ? 'updated' : 'assigned'} successfully`); toast.success(`Zonal Manager ${editingZMId ? 'updated' : 'assigned'} successfully`);
@ -224,9 +223,9 @@ export const MasterPage: React.FC = () => {
} else { } else {
toast.error(res.message || 'Failed to save Zonal Manager'); toast.error(res.message || 'Failed to save Zonal Manager');
} }
} catch (error: any) { } catch (error: any) {
const msg = error?.response?.data?.message || error?.message || 'Failed to save Zonal Manager'; const msg = error?.response?.data?.message || error?.message || 'Failed to save Zonal Manager';
toast.error(msg); toast.error(msg);
} }
}; };
@ -234,99 +233,99 @@ export const MasterPage: React.FC = () => {
const handleSaveZone = async () => { const handleSaveZone = async () => {
try { try {
const payload = { const payload = {
id: editingZoneId, id: editingZoneId,
name: zoneName, name: zoneName,
code: zoneCode, code: zoneCode,
description: zoneDescription, description: zoneDescription,
managerId: zonalBusinessHeadId === 'none' ? null : zonalBusinessHeadId managerId: zonalBusinessHeadId === 'none' ? null : zonalBusinessHeadId
}; };
const res = await masterService.saveZone(payload) as any; const res = await masterService.saveZone(payload) as any;
if (res.success) { if (res.success) {
toast.success('Zone saved successfully'); toast.success('Zone saved successfully');
setShowZoneDialog(false); setShowZoneDialog(false);
fetchInitialData(); fetchInitialData();
} else { } else {
toast.error(res.message || 'Error saving zone'); toast.error(res.message || 'Error saving zone');
} }
} catch (error: any) { } catch (error: any) {
const msg = error?.response?.data?.message || error?.message || 'Error saving zone'; const msg = error?.response?.data?.message || error?.message || 'Error saving zone';
toast.error(msg); toast.error(msg);
} }
}; };
const handleSaveRegion = async () => { const handleSaveRegion = async () => {
try { try {
const payload = { const payload = {
...(editingRegionId ? { id: editingRegionId } : {}), ...(editingRegionId ? { id: editingRegionId } : {}),
name: regionName, name: regionName,
description: regionDescription, description: regionDescription,
parentId: selectedRegionZone, parentId: selectedRegionZone,
managerId: regionalManagerId, managerId: regionalManagerId,
districts: selectedRegionDistricts, districts: selectedRegionDistricts,
status: 'Active' status: 'Active'
}; };
const res = await masterService.saveRegion(payload) as any; const res = await masterService.saveRegion(payload) as any;
if (res.success) { if (res.success) {
toast.success('Region saved successfully'); toast.success('Region saved successfully');
setShowRegionDialog(false); setShowRegionDialog(false);
fetchInitialData(); fetchInitialData();
} else { } else {
toast.error(res.message || 'Error saving region'); toast.error(res.message || 'Error saving region');
} }
} catch (error: any) { } catch (error: any) {
const msg = error?.response?.data?.message || error?.message || 'Error saving region'; const msg = error?.response?.data?.message || error?.message || 'Error saving region';
toast.error(msg); toast.error(msg);
} }
}; };
const handleSaveTemplate = async (body: string) => { const handleSaveTemplate = async (body: string) => {
try { try {
if (!editingTemplate?.id) { if (!editingTemplate?.id) {
toast.error('Open a template from the list to edit.'); toast.error('Open a template from the list to edit.');
return; return;
} }
const res = await masterService.updateEmailTemplate(editingTemplate.id, { const res = await masterService.updateEmailTemplate(editingTemplate.id, {
...editingTemplate, ...editingTemplate,
body body
}) as any; }) as any;
if (res.success) { if (res.success) {
toast.success('Template saved'); toast.success('Template saved');
setShowTemplateDialog(false); setShowTemplateDialog(false);
fetchInitialData(); fetchInitialData();
} else { } else {
toast.error(res.message || 'Error saving template'); toast.error(res.message || 'Error saving template');
} }
} catch (error: any) { } catch (error: any) {
const msg = error?.response?.data?.message || error?.message || 'Error saving template'; const msg = error?.response?.data?.message || error?.message || 'Error saving template';
toast.error(msg); toast.error(msg);
} }
}; };
const handlePreviewTemplate = async (body: string) => { const handlePreviewTemplate = async (body: string) => {
setPreviewLoading(true); setPreviewLoading(true);
try { try {
let data: Record<string, unknown>; let data: Record<string, unknown>;
try { try {
data = JSON.parse(testDataInput) as Record<string, unknown>; data = JSON.parse(testDataInput) as Record<string, unknown>;
} catch { } catch {
toast.error('Mock test data must be valid JSON'); toast.error('Mock test data must be valid JSON');
return; return;
} }
const res = await masterService.previewEmailTemplate({ const res = await masterService.previewEmailTemplate({
subject: editingTemplate?.subject, subject: editingTemplate?.subject,
body, body,
data data
}) as any; }) as any;
if (res.success) { if (res.success) {
setPreviewContent(res.data); setPreviewContent(res.data);
} else { } else {
toast.error(res.message || 'Preview failed'); toast.error(res.message || 'Preview failed');
} }
} catch (error: any) { } catch (error: any) {
const d = error?.response?.data; const d = error?.response?.data;
const detail = d?.error || d?.message; const detail = d?.error || d?.message;
toast.error(detail || error?.message || 'Preview failed'); toast.error(detail || error?.message || 'Preview failed');
} finally { setPreviewLoading(false); } } finally { setPreviewLoading(false); }
}; };
@ -340,9 +339,9 @@ export const MasterPage: React.FC = () => {
} else { } else {
toast.error(res.message || 'Error saving role permissions'); toast.error(res.message || 'Error saving role permissions');
} }
} catch (error: any) { } catch (error: any) {
const msg = error?.response?.data?.message || error?.message || 'Error saving role permissions'; const msg = error?.response?.data?.message || error?.message || 'Error saving role permissions';
toast.error(msg); toast.error(msg);
} }
}; };
@ -385,48 +384,48 @@ export const MasterPage: React.FC = () => {
const handleSaveLocation = async () => { const handleSaveLocation = async () => {
try { try {
if (!locationState) { if (!locationState) {
toast.error('Please select a state'); toast.error('Please select a state');
return; return;
} }
if (!locationDistrict) { if (!locationDistrict) {
toast.error('Please select a district'); toast.error('Please select a district');
return; return;
} }
const selectedState = allStates.find((s: any) => s.id === locationState); const selectedState = allStates.find((s: any) => s.id === locationState);
const selectedDistrict = allDistricts.find((d: any) => d.id === locationDistrict); const selectedDistrict = allDistricts.find((d: any) => d.id === locationDistrict);
const payload = { const payload = {
id: editingLocationId, id: editingLocationId,
stateId: locationState, stateId: locationState,
stateName: (selectedState as any)?.name || (selectedState as any)?.stateName || '', stateName: (selectedState as any)?.name || (selectedState as any)?.stateName || '',
districtId: locationDistrict, districtId: locationDistrict,
name: locationCity || selectedDistrict?.name || 'New Location', name: locationCity || selectedDistrict?.name || 'New Location',
city: locationCity, city: locationCity,
status: locationStatus, status: locationStatus,
openFrom: locationActiveFrom, openFrom: locationActiveFrom,
openTo: locationActiveTo, openTo: locationActiveTo,
isOpportunity: locationStatus === 'active' isOpportunity: locationStatus === 'active'
}; };
const res = await (editingLocationId const res = await (editingLocationId
? masterService.updateArea(editingLocationId, payload) ? masterService.updateArea(editingLocationId, payload)
: masterService.createArea(payload)) as any; : masterService.createArea(payload)) as any;
if (res.success) { if (res.success) {
toast.success('Location saved'); toast.success('Location saved');
setShowLocationDialog(false); setShowLocationDialog(false);
fetchAreas({ search: districtsSearch, page: districtsPage }); fetchAreas({ search: districtsSearch, page: districtsPage });
} }
} catch (error) { toast.error('Error saving location'); } } catch (error) { toast.error('Error saving location'); }
}; };
useEffect(() => { useEffect(() => {
const handler = setTimeout(() => { const handler = setTimeout(() => {
fetchAreas({ fetchAreas({
search: districtsSearch, search: districtsSearch,
page: districtsPage, page: districtsPage,
stateId: locationStateFilter === 'all' ? undefined : locationStateFilter, stateId: locationStateFilter === 'all' ? undefined : locationStateFilter,
isOpportunity: locationStatusFilter === 'all' ? undefined : (locationStatusFilter === 'active' ? 'true' : 'false') isOpportunity: locationStatusFilter === 'all' ? undefined : (locationStatusFilter === 'active' ? 'true' : 'false')
}); });
}, 500); }, 500);
return () => clearTimeout(handler); return () => clearTimeout(handler);
}, [districtsSearch, districtsPage, locationStateFilter, locationStatusFilter, fetchAreas]); }, [districtsSearch, districtsPage, locationStateFilter, locationStatusFilter, fetchAreas]);
@ -438,67 +437,66 @@ export const MasterPage: React.FC = () => {
<h1 className="text-slate-900 mb-2 font-bold text-2xl">Master Configuration</h1> <h1 className="text-slate-900 mb-2 font-bold text-2xl">Master Configuration</h1>
<p className="text-slate-600">Centralized governance for locations, roles, and operational policies</p> <p className="text-slate-600">Centralized governance for locations, roles, and operational policies</p>
</div> </div>
<Badge className="bg-gradient-to-r from-purple-600 to-indigo-600 px-4 py-1">Admin Control Panel</Badge>
</div> </div>
{loading ? ( {loading ? (
<div className="flex flex-col items-center justify-center p-20 space-y-4"> <div className="flex flex-col items-center justify-center p-20 space-y-4">
<div className="w-12 h-12 border-4 border-amber-600 border-t-transparent rounded-full animate-spin"></div> <div className="w-12 h-12 border-4 border-re-red border-t-transparent rounded-full animate-spin"></div>
<p className="text-slate-600 font-medium animate-pulse">Synchronizing Global Settings...</p> <p className="text-slate-600 font-medium animate-pulse">Synchronizing Global Settings...</p>
</div> </div>
) : ( ) : (
<Tabs value={activeTab} onValueChange={setActiveTab} className="space-y-6"> <Tabs value={activeTab} onValueChange={setActiveTab} className="space-y-6">
<TabsList className="grid w-full grid-cols-8 h-auto sticky top-0 z-10 bg-white/80 backdrop-blur-sm border shadow-sm rounded-xl p-1"> <TabsList className="grid w-full grid-cols-8 h-auto sticky top-0 z-10 bg-white/80 backdrop-blur-sm border shadow-sm rounded-xl p-1">
<TabsTrigger value="hierarchy" className="flex items-center gap-2 py-3 rounded-lg data-[state=active]:bg-amber-600 data-[state=active]:text-white"> <TabsTrigger value="hierarchy" className="flex items-center gap-2 py-3 rounded-lg data-[state=active]:bg-re-red data-[state=active]:text-white">
<Globe className="w-4 h-4" /> Organisation <Globe className="w-4 h-4" /> Organisation
</TabsTrigger> </TabsTrigger>
<TabsTrigger value="roles" className="flex items-center gap-2 py-3 rounded-lg data-[state=active]:bg-amber-600 data-[state=active]:text-white"> <TabsTrigger value="roles" className="flex items-center gap-2 py-3 rounded-lg data-[state=active]:bg-re-red data-[state=active]:text-white">
<Shield className="w-4 h-4" /> Roles <Shield className="w-4 h-4" /> Roles
</TabsTrigger> </TabsTrigger>
<TabsTrigger value="templates" className="flex items-center gap-2 py-3 rounded-lg data-[state=active]:bg-amber-600 data-[state=active]:text-white"> <TabsTrigger value="templates" className="flex items-center gap-2 py-3 rounded-lg data-[state=active]:bg-re-red data-[state=active]:text-white">
<Mail className="w-4 h-4" /> Emails <Mail className="w-4 h-4" /> Emails
</TabsTrigger> </TabsTrigger>
<TabsTrigger value="locations" className="flex items-center gap-2 py-3 rounded-lg data-[state=active]:bg-amber-600 data-[state=active]:text-white"> <TabsTrigger value="locations" className="flex items-center gap-2 py-3 rounded-lg data-[state=active]:bg-re-red data-[state=active]:text-white">
<MapPin className="w-4 h-4" /> Locations <MapPin className="w-4 h-4" /> Locations
</TabsTrigger> </TabsTrigger>
<TabsTrigger value="approvals" className="flex items-center gap-2 py-3 rounded-lg data-[state=active]:bg-amber-600 data-[state=active]:text-white transition-all transform hover:scale-[1.02]"> <TabsTrigger value="approvals" className="flex items-center gap-2 py-3 rounded-lg data-[state=active]:bg-re-red data-[state=active]:text-white transition-all transform hover:scale-[1.02]">
<SlidersHorizontal className="w-4 h-4" /> Approvals <SlidersHorizontal className="w-4 h-4" /> Approvals
</TabsTrigger> </TabsTrigger>
<TabsTrigger value="documents" className="flex items-center gap-2 py-3 rounded-lg data-[state=active]:bg-amber-600 data-[state=active]:text-white transition-all transform hover:scale-[1.02]"> <TabsTrigger value="documents" className="flex items-center gap-2 py-3 rounded-lg data-[state=active]:bg-re-red data-[state=active]:text-white transition-all transform hover:scale-[1.02]">
<FileText className="w-4 h-4" /> Docs Config <FileText className="w-4 h-4" /> Docs Config
</TabsTrigger> </TabsTrigger>
<TabsTrigger value="governance" className="flex items-center gap-2 py-3 rounded-lg data-[state=active]:bg-amber-600 data-[state=active]:text-white transition-all transform hover:scale-[1.02]"> <TabsTrigger value="governance" className="flex items-center gap-2 py-3 rounded-lg data-[state=active]:bg-re-red data-[state=active]:text-white transition-all transform hover:scale-[1.02]">
<Settings2 className="w-4 h-4" /> Governance <Settings2 className="w-4 h-4" /> Governance
</TabsTrigger> </TabsTrigger>
<TabsTrigger value="settings" className="flex items-center gap-2 py-3 rounded-lg data-[state=active]:bg-amber-600 data-[state=active]:text-white transition-all transform hover:scale-[1.02]"> <TabsTrigger value="settings" className="flex items-center gap-2 py-3 rounded-lg data-[state=active]:bg-re-red data-[state=active]:text-white transition-all transform hover:scale-[1.02]">
<Settings className="w-4 h-4" /> App Settings <Settings className="w-4 h-4" /> App Settings
</TabsTrigger> </TabsTrigger>
</TabsList> </TabsList>
<TabsContent value="hierarchy" className="space-y-8 animate-in fade-in slide-in-from-bottom-2 duration-300"> <TabsContent value="hierarchy" className="space-y-8 animate-in fade-in slide-in-from-bottom-2 duration-300">
<ZonesOverview selectedZone={selectedZone} onZoneClick={(id) => setSelectedZone(selectedZone === id ? 'all' : id)} /> <ZonesOverview selectedZone={selectedZone} onZoneClick={(id) => setSelectedZone(selectedZone === id ? 'all' : id)} />
<ZoneDetails selectedZone={selectedZone} onAddZone={() => { setEditingZoneId(null); setZoneName(''); setZoneCode(''); setZoneDescription(''); setZonalBusinessHeadId('none'); setShowZoneDialog(true); }} <ZoneDetails selectedZone={selectedZone} onAddZone={() => { setEditingZoneId(null); setZoneName(''); setZoneCode(''); setZoneDescription(''); setZonalBusinessHeadId('none'); setShowZoneDialog(true); }}
onEditZone={(z) => { setEditingZoneId(z.id); setZoneName(z.name); setZoneCode(z.code); setZoneDescription(z.description); setZonalBusinessHeadId(z.zonalBusinessHead?.id || 'none'); setShowZoneDialog(true); }} /> onEditZone={(z) => { setEditingZoneId(z.id); setZoneName(z.name); setZoneCode(z.code); setZoneDescription(z.description); setZonalBusinessHeadId(z.zonalBusinessHead?.id || 'none'); setShowZoneDialog(true); }} />
<RegionalManagement selectedZone={selectedZone} onAddRegion={() => { setEditingRegionId(null); setRegionName(''); setSelectedRegionZone(selectedZone === 'all' ? '' : selectedZone); setRegionalManagerId(''); setSelectedRegionDistricts([]); setShowRegionDialog(true); }} <RegionalManagement selectedZone={selectedZone} onAddRegion={() => { setEditingRegionId(null); setRegionName(''); setSelectedRegionZone(selectedZone === 'all' ? '' : selectedZone); setRegionalManagerId(''); setSelectedRegionDistricts([]); setShowRegionDialog(true); }}
onEditRegion={(r) => { onEditRegion={(r) => {
setEditingRegionId(r.id); setEditingRegionId(r.id);
setRegionName(r.name); setRegionName(r.name);
setSelectedRegionZone(r.zoneId); setSelectedRegionZone(r.zoneId);
setRegionalManagerId(r.regionalManager?.id || ''); setRegionalManagerId(r.regionalManager?.id || '');
setSelectedRegionDistricts(r.districts?.map((d: any) => d.id) || []); setSelectedRegionDistricts(r.districts?.map((d: any) => d.id) || []);
setShowRegionDialog(true); setShowRegionDialog(true);
}} }}
onDeleteRegion={() => toast.error('Regional office deletion is restricted via portal')} /> onDeleteRegion={() => toast.error('Regional office deletion is restricted via portal')} />
<ZMManagement selectedZone={selectedZone} <ZMManagement selectedZone={selectedZone}
onAddZM={() => { onAddZM={() => {
setEditingZMId(null); setZmManagerId(''); setEditingZMId(null); setZmManagerId('');
setZmStatus('active'); setSelectedZMZone(selectedZone === 'all' ? '' : selectedZone); setZmStatus('active'); setSelectedZMZone(selectedZone === 'all' ? '' : selectedZone);
setSelectedZMRegions([]); setSelectedZMRegions([]);
setShowZMDialog(true); setShowZMDialog(true);
}} }}
onEditZM={handleEditZM} onEditZM={handleEditZM}
onDeleteZM={() => toast.error('ZM deletion restricted')} /> onDeleteZM={() => toast.error('ZM deletion restricted')} />
<ASMManagement selectedZone={selectedZone} onAddASM={() => { setEditingASMId(null); setAsmManagerId(''); setSelectedASMZone(selectedZone === 'all' ? '' : selectedZone); setSelectedASMRegion(''); setSelectedASMStates([]); setSelectedASMDistricts([]); setAsmRoleCode('DD-AM'); setShowASMDialog(true); }} <ASMManagement selectedZone={selectedZone} onAddASM={() => { setEditingASMId(null); setAsmManagerId(''); setSelectedASMZone(selectedZone === 'all' ? '' : selectedZone); setSelectedASMRegion(''); setSelectedASMStates([]); setSelectedASMDistricts([]); setAsmRoleCode('DD-AM'); setShowASMDialog(true); }}
@ -529,13 +527,13 @@ export const MasterPage: React.FC = () => {
setTestDataInput('{}'); setTestDataInput('{}');
} }
setShowTemplateDialog(true); setShowTemplateDialog(true);
}} }}
onDeleteTemplate={() => toast.error('Delete Template restricted')} onDeleteTemplate={() => toast.error('Delete Template restricted')}
/> />
</TabsContent> </TabsContent>
<TabsContent value="locations" className="animate-in fade-in duration-300"> <TabsContent value="locations" className="animate-in fade-in duration-300">
<LocationManagement <LocationManagement
states={allStates} states={allStates}
stateFilter={locationStateFilter} stateFilter={locationStateFilter}
onStateFilterChange={(val: string) => { onStateFilterChange={(val: string) => {
@ -557,21 +555,21 @@ export const MasterPage: React.FC = () => {
setLocationStatus('active'); setLocationStatus('active');
setShowLocationDialog(true); setShowLocationDialog(true);
}} }}
onEditLocation={handleEditLocation} onEditLocation={handleEditLocation}
onDeleteLocation={(id) => { onDeleteLocation={(id) => {
if (window.confirm('Are you sure you want to delete this location?')) { if (window.confirm('Are you sure you want to delete this location?')) {
(masterService as any).deleteArea(id).then((res: any) => { (masterService as any).deleteArea(id).then((res: any) => {
if (res.success) { if (res.success) {
toast.success('Location deleted'); toast.success('Location deleted');
fetchAreas({ fetchAreas({
search: districtsSearch, search: districtsSearch,
page: districtsPage, page: districtsPage,
stateId: locationStateFilter === 'all' ? undefined : locationStateFilter stateId: locationStateFilter === 'all' ? undefined : locationStateFilter
}); });
} }
}); });
} }
}} }}
onSearch={(term) => { onSearch={(term) => {
setDistrictsSearch(term); setDistrictsSearch(term);
setDistrictsPage(1); // Reset to first page on search setDistrictsPage(1); // Reset to first page on search
@ -582,41 +580,41 @@ export const MasterPage: React.FC = () => {
</TabsContent> </TabsContent>
<TabsContent value="approvals" className="animate-in fade-in duration-300"> <TabsContent value="approvals" className="animate-in fade-in duration-300">
<ApprovalPoliciesPage /> <ApprovalPoliciesPage />
</TabsContent> </TabsContent>
<TabsContent value="documents" className="animate-in fade-in duration-300"> <TabsContent value="documents" className="animate-in fade-in duration-300">
<DocumentConfigManagement /> <DocumentConfigManagement />
</TabsContent> </TabsContent>
<TabsContent value="governance" className="animate-in fade-in duration-300"> <TabsContent value="governance" className="animate-in fade-in duration-300">
<AutoAssignmentSettings /> <AutoAssignmentSettings />
</TabsContent> </TabsContent>
<TabsContent value="settings" className="animate-in fade-in duration-300"> <TabsContent value="settings" className="animate-in fade-in duration-300">
<SecurityDepositMaster /> <SecurityDepositMaster />
</TabsContent> </TabsContent>
</Tabs> </Tabs>
)} )}
{/* Main Dialogs */} {/* Main Dialogs */}
<ZoneDialog isOpen={showZoneDialog} onOpenChange={setShowZoneDialog} editingZoneId={editingZoneId} zoneName={zoneName} setZoneName={setZoneName} zoneCode={zoneCode} setZoneCode={setZoneCode} zoneDescription={zoneDescription} setZoneDescription={setZoneDescription} zonalBusinessHeadId={zonalBusinessHeadId} setZonalBusinessHeadId={setZonalBusinessHeadId} onSave={handleSaveZone} userAssignedData={users.length > 0 ? users.map(u => ({...u, name: u.fullName || u.name, role: u.role?.roleName, roleCode: u.role?.roleCode, allRoles: u.allRoles})) : asms} /> <ZoneDialog isOpen={showZoneDialog} onOpenChange={setShowZoneDialog} editingZoneId={editingZoneId} zoneName={zoneName} setZoneName={setZoneName} zoneCode={zoneCode} setZoneCode={setZoneCode} zoneDescription={zoneDescription} setZoneDescription={setZoneDescription} zonalBusinessHeadId={zonalBusinessHeadId} setZonalBusinessHeadId={setZonalBusinessHeadId} onSave={handleSaveZone} userAssignedData={users.length > 0 ? users.map(u => ({ ...u, name: u.fullName || u.name, role: u.role?.roleName, roleCode: u.role?.roleCode, allRoles: u.allRoles })) : asms} />
<RegionDialog isOpen={showRegionDialog} onOpenChange={setShowRegionDialog} editingRegionId={editingRegionId} regionName={regionName} setRegionName={setRegionName} regionDescription={regionDescription} setRegionDescription={setRegionDescription} selectedRegionZone={selectedRegionZone} setSelectedRegionZone={setSelectedRegionZone} regionalManagerId={regionalManagerId} setRegionalManagerId={setRegionalManagerId} selectedRegionStates={selectedRegionDistricts} setSelectedRegionStates={setSelectedRegionDistricts} onSave={handleSaveRegion} userAssignedData={users.length > 0 ? users.map(u => ({...u, name: u.fullName || u.name, role: u.role?.roleName, roleCode: u.role?.roleCode, allRoles: u.allRoles})) : asms} /> <RegionDialog isOpen={showRegionDialog} onOpenChange={setShowRegionDialog} editingRegionId={editingRegionId} regionName={regionName} setRegionName={setRegionName} regionDescription={regionDescription} setRegionDescription={setRegionDescription} selectedRegionZone={selectedRegionZone} setSelectedRegionZone={setSelectedRegionZone} regionalManagerId={regionalManagerId} setRegionalManagerId={setRegionalManagerId} selectedRegionStates={selectedRegionDistricts} setSelectedRegionStates={setSelectedRegionDistricts} onSave={handleSaveRegion} userAssignedData={users.length > 0 ? users.map(u => ({ ...u, name: u.fullName || u.name, role: u.role?.roleName, roleCode: u.role?.roleCode, allRoles: u.allRoles })) : asms} />
<ASMDialog isOpen={showASMDialog} onOpenChange={setShowASMDialog} editingASMId={editingASMId} asmManagerId={asmManagerId} setAsmManagerId={setAsmManagerId} asmStatus={asmStatus} setAsmStatus={setAsmStatus} selectedASMZone={selectedASMZone} setSelectedASMZone={setSelectedASMZone} selectedASMRegion={selectedASMRegion} setSelectedASMRegion={setSelectedASMRegion} selectedASMStates={selectedASMStates} setSelectedASMStates={setSelectedASMStates} selectedASMDistricts={selectedASMDistricts} setSelectedASMDistricts={setSelectedASMDistricts} onSave={handleSaveASM} asmRoleCode={asmRoleCode} setAsmRoleCode={setAsmRoleCode} userAssignedData={users.length > 0 ? users.map(u => ({...u, name: u.fullName || u.name, role: u.role?.roleName, roleCode: u.role?.roleCode, allRoles: u.allRoles})) : asms} districtsAssignedToOthers={districtsAssignedToOthers} getDistrictsForSelectedState={(state) => getDistrictsForSelectedState(state, selectedASMRegion || undefined)} /> <ASMDialog isOpen={showASMDialog} onOpenChange={setShowASMDialog} editingASMId={editingASMId} asmManagerId={asmManagerId} setAsmManagerId={setAsmManagerId} asmStatus={asmStatus} setAsmStatus={setAsmStatus} selectedASMZone={selectedASMZone} setSelectedASMZone={setSelectedASMZone} selectedASMRegion={selectedASMRegion} setSelectedASMRegion={setSelectedASMRegion} selectedASMStates={selectedASMStates} setSelectedASMStates={setSelectedASMStates} selectedASMDistricts={selectedASMDistricts} setSelectedASMDistricts={setSelectedASMDistricts} onSave={handleSaveASM} asmRoleCode={asmRoleCode} setAsmRoleCode={setAsmRoleCode} userAssignedData={users.length > 0 ? users.map(u => ({ ...u, name: u.fullName || u.name, role: u.role?.roleName, roleCode: u.role?.roleCode, allRoles: u.allRoles })) : asms} districtsAssignedToOthers={districtsAssignedToOthers} getDistrictsForSelectedState={(state) => getDistrictsForSelectedState(state, selectedASMRegion || undefined)} />
<ZMDialog <ZMDialog
isOpen={showZMDialog} isOpen={showZMDialog}
onOpenChange={setShowZMDialog} onOpenChange={setShowZMDialog}
editingZMId={editingZMId} editingZMId={editingZMId}
zmManagerId={zmManagerId} zmManagerId={zmManagerId}
setZmManagerId={setZmManagerId} setZmManagerId={setZmManagerId}
zmStatus={zmStatus} zmStatus={zmStatus}
setZmStatus={setZmStatus} setZmStatus={setZmStatus}
selectedZone={selectedZMZone} selectedZone={selectedZMZone}
setSelectedZone={setSelectedZMZone} setSelectedZone={setSelectedZMZone}
selectedRegions={selectedZMRegions} selectedRegions={selectedZMRegions}
setSelectedRegions={setSelectedZMRegions} setSelectedRegions={setSelectedZMRegions}
onSave={handleSaveZM} onSave={handleSaveZM}
userAssignedData={users.length > 0 ? users.map(u => ({...u, name: u.fullName || u.name, role: u.role?.roleName, roleCode: u.role?.roleCode, allRoles: u.allRoles})) : asms} userAssignedData={users.length > 0 ? users.map(u => ({ ...u, name: u.fullName || u.name, role: u.role?.roleName, roleCode: u.role?.roleCode, allRoles: u.allRoles })) : asms}
/> />
<TemplateDialog isOpen={showTemplateDialog} onOpenChange={setShowTemplateDialog} editingTemplate={editingTemplate} setEditingTemplate={setEditingTemplate} testDataInput={testDataInput} setTestDataInput={setTestDataInput} previewLoading={previewLoading} handlePreviewTemplate={handlePreviewTemplate} previewContent={previewContent} handleSaveTemplate={handleSaveTemplate} /> <TemplateDialog isOpen={showTemplateDialog} onOpenChange={setShowTemplateDialog} editingTemplate={editingTemplate} setEditingTemplate={setEditingTemplate} testDataInput={testDataInput} setTestDataInput={setTestDataInput} previewLoading={previewLoading} handlePreviewTemplate={handlePreviewTemplate} previewContent={previewContent} handleSaveTemplate={handleSaveTemplate} />
<LocationDialog isOpen={showLocationDialog} onOpenChange={setShowLocationDialog} editingLocationId={editingLocationId} locationState={locationState} setLocationState={setLocationState} locationDistrict={locationDistrict} setLocationDistrict={setLocationDistrict} locationCity={locationCity} setLocationCity={setLocationCity} locationActiveFrom={locationActiveFrom} setLocationActiveFrom={setLocationActiveFrom} locationActiveTo={locationActiveTo} setLocationActiveTo={setLocationActiveTo} locationStatus={locationStatus} setLocationStatus={setLocationStatus} allStates={allStates} allDistricts={allDistricts} onSave={handleSaveLocation} /> <LocationDialog isOpen={showLocationDialog} onOpenChange={setShowLocationDialog} editingLocationId={editingLocationId} locationState={locationState} setLocationState={setLocationState} locationDistrict={locationDistrict} setLocationDistrict={setLocationDistrict} locationCity={locationCity} setLocationCity={setLocationCity} locationActiveFrom={locationActiveFrom} setLocationActiveFrom={setLocationActiveFrom} locationActiveTo={locationActiveTo} setLocationActiveTo={setLocationActiveTo} locationStatus={locationStatus} setLocationStatus={setLocationStatus} allStates={allStates} allDistricts={allDistricts} onSave={handleSaveLocation} />

View File

@ -4,7 +4,9 @@ import { RootState } from '@/store';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
import { Clock, Plus, Pen, Bell, AlertTriangle, CheckCircle, RefreshCw } from 'lucide-react'; import { Clock, Plus, Pen, Bell, AlertTriangle, CheckCircle, RefreshCw, Activity } from 'lucide-react';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { SLAMonitorPanel } from '@/features/master/components/SLAMonitorPanel';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { masterService } from '@/services/master.service'; import { masterService } from '@/services/master.service';
import { setMasterData } from '@/store/slices/masterSlice'; import { setMasterData } from '@/store/slices/masterSlice';
@ -15,6 +17,8 @@ export const SLAConfigPage: React.FC = () => {
const { slaConfigs, loading } = useSelector((state: RootState) => state.master); const { slaConfigs, loading } = useSelector((state: RootState) => state.master);
const [showSLADialog, setShowSLADialog] = useState(false); const [showSLADialog, setShowSLADialog] = useState(false);
const [selectedSLA, setSelectedSLA] = useState<any>(null); const [selectedSLA, setSelectedSLA] = useState<any>(null);
const [loadingMore, setLoadingMore] = useState(false);
const [mainTab, setMainTab] = useState('monitor');
const fetchConfigs = async () => { const fetchConfigs = async () => {
try { try {
@ -22,7 +26,7 @@ export const SLAConfigPage: React.FC = () => {
if (res && res.success) { if (res && res.success) {
dispatch(setMasterData({ slaConfigs: res.data })); dispatch(setMasterData({ slaConfigs: res.data }));
} }
} catch (error) { } catch {
toast.error('Failed to fetch SLA configurations'); toast.error('Failed to fetch SLA configurations');
} }
}; };
@ -39,7 +43,7 @@ export const SLAConfigPage: React.FC = () => {
toast.success('Default SLAs initialized successfully'); toast.success('Default SLAs initialized successfully');
fetchConfigs(); fetchConfigs();
} }
} catch (error) { } catch {
toast.error('Failed to initialize default SLAs'); toast.error('Failed to initialize default SLAs');
} finally { } finally {
setLoadingMore(false); setLoadingMore(false);
@ -56,125 +60,151 @@ export const SLAConfigPage: React.FC = () => {
setShowSLADialog(true); setShowSLADialog(true);
}; };
const [loadingMore, setLoadingMore] = useState(false);
return ( return (
<div className="space-y-6 max-w-7xl mx-auto"> <div className="space-y-6 max-w-7xl mx-auto">
<div className="flex items-center justify-between"> <div>
<div> <h1 className="text-2xl font-bold text-slate-900 flex items-center gap-2">
<h1 className="text-2xl font-bold text-slate-900 flex items-center gap-2"> <Clock className="w-6 h-6 text-re-red" />
<Clock className="w-6 h-6 text-amber-600" /> SLA & Escalation
SLA & Escalation Matrix </h1>
</h1> <p className="text-slate-500">Configure TAT rules and monitor live queue, breaches, and schedulers</p>
<p className="text-slate-500">Configure Turn Around Time (TAT) and escalation rules for each process stage</p>
</div>
<div className="flex items-center gap-3">
<Button variant="outline" onClick={handleInitialize} disabled={loading || loadingMore}>
<RefreshCw className={`w-4 h-4 mr-2 ${loadingMore ? 'animate-spin' : ''}`} />
Initialize Defaults
</Button>
<Button onClick={handleAddSLA} disabled={loading}>
<Plus className="w-4 h-4 mr-2" />
Add Manual SLA
</Button>
<Button variant="ghost" size="icon" onClick={fetchConfigs} disabled={loading}>
<RefreshCw className={`w-4 h-4 ${loading ? 'animate-spin' : ''}`} />
</Button>
</div>
</div> </div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6"> <Tabs value={mainTab} onValueChange={setMainTab}>
{slaConfigs.map((sla) => ( <TabsList className="bg-slate-100">
<Card key={sla.id} className="border-slate-200 hover:shadow-md transition-shadow group overflow-hidden"> <TabsTrigger value="monitor" className="flex items-center gap-1.5">
<CardHeader className="pb-3 bg-gradient-to-br from-white to-slate-50/50"> <Activity className="w-4 h-4" />
<div className="flex items-start justify-between"> Operations monitor
<div className="flex items-center gap-3"> </TabsTrigger>
<div className="w-10 h-10 rounded-xl bg-amber-50 border border-amber-100 flex items-center justify-center"> <TabsTrigger value="config">Configuration matrix</TabsTrigger>
<Clock className="w-5 h-5 text-amber-600" /> </TabsList>
</div>
<div>
<CardTitle className="text-lg">{sla.activityName}</CardTitle>
<CardDescription className="flex items-center gap-1.5 mt-0.5">
<span className="font-semibold text-amber-700">Target TAT: {sla.tatHours} {sla.tatUnit}</span>
</CardDescription>
</div>
</div>
<div className="flex items-center gap-2">
<Badge variant={sla.isActive ? "default" : "secondary"} className={sla.isActive ? "bg-emerald-600" : "bg-slate-400"}>
{sla.isActive ? (
<><CheckCircle className="w-3 h-3 mr-1" /> Active</>
) : (
'Disabled'
)}
</Badge>
<Button variant="ghost" size="icon" onClick={() => handleEdit(sla)} className="h-8 w-8 text-slate-400 hover:text-amber-600">
<Pen className="w-4 h-4" />
</Button>
</div>
</div>
</CardHeader>
<CardContent className="pt-4 grid grid-cols-2 gap-4">
<div className="border-l-2 border-blue-400 pl-3">
<div className="flex items-center gap-2 mb-2">
<Bell className="w-4 h-4 text-blue-600" />
<span className="text-xs font-bold text-slate-700 uppercase tracking-wider">Reminders ({sla.reminders?.length || 0})</span>
</div>
<div className="space-y-1.5">
{(sla.reminders || []).map((reminder: any, idx: number) => (
<div key={idx} className="text-xs text-slate-600 flex items-center gap-1">
<Badge variant="outline" className="text-[10px] h-4.5 bg-blue-50 border-blue-200">
{reminder.timeValue} {reminder.timeUnit}
</Badge>
<span>before SLA</span>
</div>
))}
{(!sla.reminders || sla.reminders.length === 0) && (
<p className="text-[10px] text-slate-400 italic">None configured</p>
)}
</div>
</div>
<div className="border-l-2 border-red-400 pl-3"> <TabsContent value="monitor" className="mt-6">
<div className="flex items-center gap-2 mb-2"> <SLAMonitorPanel />
<AlertTriangle className="w-4 h-4 text-red-600" /> </TabsContent>
<span className="text-xs font-bold text-slate-700 uppercase tracking-wider">Escalations ({sla.escalationConfigs?.length || 0})</span>
</div>
<div className="space-y-2">
{(sla.escalationConfigs || []).map((esc: any, idx: number) => (
<div key={idx} className="text-[11px]">
<div className="flex items-center gap-1.5 text-slate-900 font-medium">
<Badge variant="outline" className="text-[9px] h-4 px-1 bg-red-50 border-red-200 text-red-700">
L{esc.level}
</Badge>
<span>after {esc.timeValue} {esc.timeUnit}</span>
</div>
<p className="text-slate-500 ml-8 font-mono truncate">{esc.notifyEmail}</p>
</div>
))}
{(!sla.escalationConfigs || sla.escalationConfigs.length === 0) && (
<p className="text-[10px] text-slate-400 italic">None configured</p>
)}
</div>
</div>
</CardContent>
</Card>
))}
{slaConfigs.length === 0 && !loading && ( <TabsContent value="config" className="mt-6 space-y-6">
<div className="lg:col-span-2 py-20 text-center border-2 border-dashed rounded-2xl bg-slate-50/50"> <div className="flex items-center justify-end gap-3">
<Clock className="w-12 h-12 text-slate-200 mx-auto mb-4" /> <Button variant="outline" onClick={handleInitialize} disabled={loading || loadingMore}>
<h3 className="text-slate-600 font-medium">No SLA Workflows found</h3> <RefreshCw className={`w-4 h-4 mr-2 ${loadingMore ? 'animate-spin' : ''}`} />
<p className="text-slate-400 text-sm">Please initialize default configurations from the admin tools</p> Initialize Defaults
</Button>
<Button onClick={handleAddSLA} disabled={loading} className="bg-re-red hover:bg-re-red-hover text-white">
<Plus className="w-4 h-4 mr-2" />
Add Manual SLA
</Button>
<Button variant="ghost" size="icon" onClick={fetchConfigs} disabled={loading}>
<RefreshCw className={`w-4 h-4 ${loading ? 'animate-spin' : ''}`} />
</Button>
</div> </div>
)}
</div>
<SLADialog <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
isOpen={showSLADialog} {slaConfigs.map((sla) => (
onClose={() => setShowSLADialog(false)} <Card key={sla.id} className="border-slate-200 hover:shadow-md transition-shadow group overflow-hidden">
sla={selectedSLA} <CardHeader className="pb-3 bg-gradient-to-br from-white to-slate-50/50">
onSave={fetchConfigs} <div className="flex items-start justify-between">
/> <div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-xl bg-red-50 border border-red-100 flex items-center justify-center">
<Clock className="w-5 h-5 text-re-red" />
</div>
<div>
<CardTitle className="text-lg">{sla.activityName}</CardTitle>
<CardDescription className="flex items-center gap-1.5 mt-0.5">
<span className="font-semibold text-re-red-hover">
Target TAT: {sla.tatHours} {sla.tatUnit}
</span>
</CardDescription>
</div>
</div>
<div className="flex items-center gap-2">
<Badge
variant={sla.isActive ? 'default' : 'secondary'}
className={sla.isActive ? 'bg-emerald-600' : 'bg-slate-400'}
>
{sla.isActive ? (
<>
<CheckCircle className="w-3 h-3 mr-1" /> Active
</>
) : (
'Disabled'
)}
</Badge>
<Button
variant="ghost"
size="icon"
onClick={() => handleEdit(sla)}
className="h-8 w-8 text-slate-400 hover:text-re-red"
>
<Pen className="w-4 h-4" />
</Button>
</div>
</div>
</CardHeader>
<CardContent className="pt-4 grid grid-cols-2 gap-4">
<div className="border-l-2 border-blue-400 pl-3">
<div className="flex items-center gap-2 mb-2">
<Bell className="w-4 h-4 text-blue-600" />
<span className="text-xs font-bold text-slate-700 uppercase tracking-wider">
Reminders ({sla.reminders?.length || 0})
</span>
</div>
<div className="space-y-1.5">
{(sla.reminders || []).map((reminder: any, idx: number) => (
<div key={idx} className="text-xs text-slate-600 flex items-center gap-1">
<Badge variant="outline" className="text-[10px] h-4.5 bg-blue-50 border-blue-200">
{reminder.timeValue} {reminder.timeUnit}
</Badge>
<span>before SLA</span>
</div>
))}
{(!sla.reminders || sla.reminders.length === 0) && (
<p className="text-[10px] text-slate-400 italic">None configured</p>
)}
</div>
</div>
<div className="border-l-2 border-red-400 pl-3">
<div className="flex items-center gap-2 mb-2">
<AlertTriangle className="w-4 h-4 text-re-red" />
<span className="text-xs font-bold text-slate-700 uppercase tracking-wider">
Escalations ({sla.escalationConfigs?.length || 0})
</span>
</div>
<div className="space-y-2">
{(sla.escalationConfigs || []).map((esc: any, idx: number) => (
<div key={idx} className="text-[11px]">
<div className="flex items-center gap-1.5 text-slate-900 font-medium">
<Badge variant="outline" className="text-[9px] h-4 px-1 bg-red-50 border-red-200 text-re-red-hover">
L{esc.level}
</Badge>
<span>
after {esc.timeValue} {esc.timeUnit}
</span>
</div>
<p className="text-slate-500 ml-8 font-mono truncate">{esc.notifyEmail}</p>
</div>
))}
{(!sla.escalationConfigs || sla.escalationConfigs.length === 0) && (
<p className="text-[10px] text-slate-400 italic">None configured</p>
)}
</div>
</div>
</CardContent>
</Card>
))}
{slaConfigs.length === 0 && !loading && (
<div className="lg:col-span-2 py-20 text-center border-2 border-dashed rounded-2xl bg-slate-50/50">
<Clock className="w-12 h-12 text-slate-200 mx-auto mb-4" />
<h3 className="text-slate-600 font-medium">No SLA Workflows found</h3>
<p className="text-slate-400 text-sm">Please initialize default configurations from the admin tools</p>
</div>
)}
</div>
</TabsContent>
</Tabs>
<SLADialog isOpen={showSLADialog} onClose={() => setShowSLADialog(false)} sla={selectedSLA} onSave={fetchConfigs} />
</div> </div>
); );
}; };

View File

@ -1,6 +1,6 @@
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Progress } from '@/components/ui/progress'; import { ApplicationProgressBar } from '@/features/onboarding/components/ApplicationProgressBar';
import { Application } from '@/lib/mock-data'; import { Application } from '@/lib/mock-data';
import { MapPin, Phone, Mail, Award, Calendar, Building } from 'lucide-react'; import { MapPin, Phone, Mail, Award, Calendar, Building } from 'lucide-react';
import { formatDateTime } from '@/components/ui/utils'; import { formatDateTime } from '@/components/ui/utils';
@ -11,42 +11,20 @@ interface ApplicationCardProps {
} }
export function ApplicationCard({ application, onViewDetails }: ApplicationCardProps) { export function ApplicationCard({ application, onViewDetails }: ApplicationCardProps) {
/**
* Status badge classes see ApplicationsPage for the rationale.
* Three buckets only: positive (black), negative (muted brand red),
* default in-progress (light slate). Keeps the listing brand-aligned.
*/
const getStatusColor = (status: string) => { const getStatusColor = (status: string) => {
const statusColors: Record<string, string> = { const s = String(status || '');
'Submitted': 'bg-slate-500', if (s === 'Rejected' || s === 'Disqualified' || s.includes('Rejected')) {
'Questionnaire Pending': 'bg-orange-500', return 'bg-red-50 text-re-red-hover border border-red-200';
'Questionnaire Completed': 'bg-blue-500', }
'Shortlisted': 'bg-cyan-500', if (s === 'Approved' || s === 'Onboarded' || s === 'Completed') {
'Level 1 Pending': 'bg-amber-500', return 'bg-slate-900 text-white border border-transparent';
'Level 1 Approved': 'bg-green-500', }
'Level 2 Pending': 'bg-purple-500', return 'bg-slate-200 text-slate-800 border border-slate-300';
'Level 2 Approved': 'bg-green-600',
'Level 2 Recommended': 'bg-teal-500',
'Level 3 Pending': 'bg-indigo-500',
'FDD Verification': 'bg-violet-500',
'Payment Pending': 'bg-yellow-500',
'LOI Issued': 'bg-lime-500',
'Dealer Code Generation': 'bg-fuchsia-500',
'Architecture Team Assigned': 'bg-blue-500',
'Architecture Document Upload': 'bg-blue-500',
'Architecture Team Completion': 'bg-blue-500',
'Statutory GST': 'bg-emerald-500',
'Statutory PAN': 'bg-emerald-500',
'Statutory Nodal': 'bg-emerald-500',
'Statutory Check': 'bg-emerald-500',
'Statutory Partnership': 'bg-emerald-500',
'Statutory Firm Reg': 'bg-emerald-500',
'Statutory Virtual Code': 'bg-emerald-500',
'Statutory Domain': 'bg-emerald-500',
'Statutory MSD': 'bg-emerald-500',
'Statutory LOI Ack': 'bg-emerald-500',
'EOR In Progress': 'bg-sky-500',
'LOA Pending': 'bg-emerald-500',
'Approved': 'bg-green-700',
'Rejected': 'bg-red-500',
'Disqualified': 'bg-red-700'
};
return statusColors[status] || 'bg-slate-500';
}; };
return ( return (
@ -124,7 +102,12 @@ export function ApplicationCard({ application, onViewDetails }: ApplicationCardP
<span className="text-slate-600">Progress</span> <span className="text-slate-600">Progress</span>
<span className="text-slate-900" data-testid="onboarding-application-card-progress-text">{application.progress}%</span> <span className="text-slate-900" data-testid="onboarding-application-card-progress-text">{application.progress}%</span>
</div> </div>
<Progress value={application.progress} className="h-2" data-testid="onboarding-application-card-progress-bar" /> <ApplicationProgressBar
value={application.progress}
status={application.status}
barClassName="h-2 w-full bg-status-progress-soft"
data-testid="onboarding-application-card-progress-bar"
/>
</div> </div>
{/* Deadline Warning */} {/* Deadline Warning */}
@ -138,7 +121,7 @@ export function ApplicationCard({ application, onViewDetails }: ApplicationCardP
<Button <Button
onClick={() => onViewDetails(application.id)} onClick={() => onViewDetails(application.id)}
className="w-full bg-amber-600 hover:bg-amber-700" className="w-full bg-re-red hover:bg-re-red-hover"
data-testid="onboarding-application-card-view-button" data-testid="onboarding-application-card-view-button"
> >
View Details View Details

View File

@ -0,0 +1,44 @@
import { Progress } from '@/components/ui/progress';
import { cn } from '@/components/ui/utils';
import { getStatusProgressBarClass } from '@/lib/statusProgressTheme';
export interface ApplicationProgressBarProps {
value: number;
status?: string | null;
currentStage?: string | null;
/** Track styling — matches Application Details progress tab by default */
barClassName?: string;
showPercent?: boolean;
percentClassName?: string;
'data-testid'?: string;
}
/** Status-colored progress bar (amber / green / re-red) — same strategy as Application Details. */
export function ApplicationProgressBar({
value,
status,
currentStage,
barClassName = 'h-2 w-20 bg-status-progress-soft',
showPercent = false,
percentClassName = 'text-slate-600 shrink-0',
'data-testid': testId,
}: ApplicationProgressBarProps) {
return (
<div
className={cn('flex items-center gap-2 min-w-0', showPercent && 'w-full max-w-[8rem]')}
data-testid={testId ? `${testId}-container` : undefined}
>
<Progress
value={value}
className={barClassName}
indicatorClassName={getStatusProgressBarClass(status, currentStage)}
data-testid={testId}
/>
{showPercent && (
<span className={percentClassName} data-testid={testId ? `${testId}-text` : undefined}>
{value}%
</span>
)}
</div>
);
}

View File

@ -115,7 +115,7 @@ export const BankDetailsModal: React.FC<BankDetailsModalProps> = ({
id="isPrimaryModal" id="isPrimaryModal"
name="isPrimary" name="isPrimary"
defaultChecked={editingBank?.isPrimary} defaultChecked={editingBank?.isPrimary}
className="w-4 h-4 rounded border-slate-300 text-amber-600 focus:ring-amber-500" className="w-4 h-4 rounded border-slate-300 text-re-red focus:ring-re-red"
data-testid="onboarding-is-primary-checkbox" data-testid="onboarding-is-primary-checkbox"
/> />
<Label htmlFor="isPrimaryModal" className="text-xs font-medium cursor-pointer">Set as primary account</Label> <Label htmlFor="isPrimaryModal" className="text-xs font-medium cursor-pointer">Set as primary account</Label>
@ -124,7 +124,7 @@ export const BankDetailsModal: React.FC<BankDetailsModalProps> = ({
</div> </div>
<DialogFooter> <DialogFooter>
<Button type="button" variant="outline" size="sm" onClick={onClose} data-testid="onboarding-bank-details-cancel">Cancel</Button> <Button type="button" variant="outline" size="sm" onClick={onClose} data-testid="onboarding-bank-details-cancel">Cancel</Button>
<Button type="submit" disabled={isSubmitting} size="sm" className="bg-amber-600" data-testid="onboarding-bank-details-submit"> <Button type="submit" disabled={isSubmitting} size="sm" className="bg-re-red" data-testid="onboarding-bank-details-submit">
{isSubmitting ? <Loader2 className="w-4 h-4 animate-spin mr-2" /> : null} {isSubmitting ? <Loader2 className="w-4 h-4 animate-spin mr-2" /> : null}
{editingBank ? 'Update Account' : 'Save Bank Details'} {editingBank ? 'Update Account' : 'Save Bank Details'}
</Button> </Button>

View File

@ -33,11 +33,11 @@ const QuestionnaireResponseView: React.FC<QuestionnaireResponseViewProps> = ({ a
<div className="space-y-6" data-testid="onboarding-questionnaire-view"> <div className="space-y-6" data-testid="onboarding-questionnaire-view">
<div className="flex items-center justify-between mb-4"> <div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<ClipboardList className="w-5 h-5 text-amber-600" /> <ClipboardList className="w-5 h-5 text-re-red" />
<h3 className="text-slate-900">Questionnaire Responses</h3> <h3 className="text-slate-900">Questionnaire Responses</h3>
</div> </div>
{totalScore !== undefined && ( {totalScore !== undefined && (
<Badge className="bg-amber-600" data-testid="onboarding-questionnaire-total-score">Score: {totalScore}/100</Badge> <Badge className="bg-re-red" data-testid="onboarding-questionnaire-total-score">Score: {totalScore}/100</Badge>
)} )}
</div> </div>
@ -62,12 +62,12 @@ const QuestionnaireResponseView: React.FC<QuestionnaireResponseViewProps> = ({ a
return ( return (
<div <div
key={resp.id} key={resp.id}
className="border border-slate-200 rounded-lg p-5 hover:border-amber-300 transition-colors" className="border border-slate-200 rounded-lg p-5 hover:border-red-300 transition-colors"
data-testid={`onboarding-questionnaire-item-${index}`} data-testid={`onboarding-questionnaire-item-${index}`}
> >
<div className="flex items-start gap-3 mb-3"> <div className="flex items-start gap-3 mb-3">
<div className="w-8 h-8 rounded-full bg-amber-100 flex items-center justify-center flex-shrink-0"> <div className="w-8 h-8 rounded-full bg-red-50 flex items-center justify-center flex-shrink-0">
<span className="text-amber-600">{index + 1}</span> <span className="text-re-red">{index + 1}</span>
</div> </div>
<div className="flex-1"> <div className="flex-1">
<div className="flex items-center gap-2 mb-2"> <div className="flex items-center gap-2 mb-2">

View File

@ -126,9 +126,9 @@ export function ApplicantInformationCard({
data-testid="onboarding-applicant-info-edit-firm-type" data-testid="onboarding-applicant-info-edit-firm-type"
> >
Proposed Firm Type Proposed Firm Type
<Pencil className="w-3 h-3 text-slate-300 group-hover:text-amber-600 transition-colors" /> <Pencil className="w-3 h-3 text-slate-300 group-hover:text-re-red transition-colors" />
</p> </p>
<p className="text-slate-900 font-black text-amber-700 tracking-tight leading-none mt-1" data-testid="onboarding-applicant-info-firm-type"> <p className="text-slate-900 font-black text-re-red-hover tracking-tight leading-none mt-1" data-testid="onboarding-applicant-info-firm-type">
{application.constitutionType || 'Not Provided'} {application.constitutionType || 'Not Provided'}
</p> </p>
</div> </div>
@ -214,14 +214,14 @@ export function ApplicantInformationCard({
<div className="pt-6 border-t mt-6"> <div className="pt-6 border-t mt-6">
<div className="flex items-center justify-between mb-4"> <div className="flex items-center justify-between mb-4">
<h3 className="text-sm font-black text-slate-900 uppercase tracking-widest flex items-center gap-2"> <h3 className="text-sm font-black text-slate-900 uppercase tracking-widest flex items-center gap-2">
<CreditCard className="w-4 h-4 text-amber-600" /> Statutory & Bank Information <CreditCard className="w-4 h-4 text-re-red" /> Statutory & Bank Information
</h3> </h3>
{canEditStatutory && !isEditingStatutory && ( {canEditStatutory && !isEditingStatutory && (
<Button <Button
variant="ghost" variant="ghost"
size="sm" size="sm"
onClick={onEditStatutory} onClick={onEditStatutory}
className="h-8 text-amber-600 hover:text-amber-700 hover:bg-amber-50 gap-1.5" className="h-8 text-re-red hover:text-re-red-hover hover:bg-red-50 gap-1.5"
data-testid="onboarding-applicant-info-edit-statutory" data-testid="onboarding-applicant-info-edit-statutory"
> >
<Pencil className="w-3.5 h-3.5" /> <Pencil className="w-3.5 h-3.5" />
@ -232,7 +232,7 @@ export function ApplicantInformationCard({
{isEditingStatutory ? ( {isEditingStatutory ? (
<div <div
className="bg-slate-50/50 p-6 rounded-xl border-2 border-amber-100 space-y-4" className="bg-slate-50/50 p-6 rounded-xl border-2 border-red-100 space-y-4"
data-testid="onboarding-applicant-info-statutory-edit-form" data-testid="onboarding-applicant-info-statutory-edit-form"
> >
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
@ -324,7 +324,7 @@ export function ApplicantInformationCard({
size="sm" size="sm"
onClick={onSaveStatutory} onClick={onSaveStatutory}
disabled={isSavingStatutory} disabled={isSavingStatutory}
className="bg-amber-600 hover:bg-amber-700" className="bg-re-red hover:bg-re-red-hover"
data-testid="onboarding-applicant-info-statutory-save" data-testid="onboarding-applicant-info-statutory-save"
> >
{isSavingStatutory ? <Loader2 className="w-4 h-4 animate-spin" /> : 'Save Details'} {isSavingStatutory ? <Loader2 className="w-4 h-4 animate-spin" /> : 'Save Details'}

View File

@ -35,6 +35,8 @@ interface ApplicationDetailsActionModalsProps {
setInterviewIdToCancel: (value: string) => void; setInterviewIdToCancel: (value: string) => void;
isCancellingInterview: boolean; isCancellingInterview: boolean;
handleConfirmCancelInterview: () => void; handleConfirmCancelInterview: () => void;
interviewToReschedule: any;
setInterviewToReschedule: (value: any) => void;
interviewType: string; interviewType: string;
setInterviewType: (value: string) => void; setInterviewType: (value: string) => void;
interviewMode: string; interviewMode: string;
@ -99,6 +101,8 @@ export function ApplicationDetailsActionModals(props: ApplicationDetailsActionMo
setInterviewIdToCancel, setInterviewIdToCancel,
isCancellingInterview, isCancellingInterview,
handleConfirmCancelInterview, handleConfirmCancelInterview,
interviewToReschedule,
setInterviewToReschedule,
interviewType, interviewType,
setInterviewType, setInterviewType,
interviewMode, interviewMode,
@ -252,10 +256,13 @@ export function ApplicationDetailsActionModals(props: ApplicationDetailsActionMo
</DialogContent> </DialogContent>
</Dialog> </Dialog>
<Dialog open={showScheduleModal} onOpenChange={setShowScheduleModal}> <Dialog open={showScheduleModal} onOpenChange={(open) => {
setShowScheduleModal(open);
if (!open) setInterviewToReschedule(null);
}}>
<DialogContent data-testid="onboarding-schedule-modal"> <DialogContent data-testid="onboarding-schedule-modal">
<DialogHeader> <DialogHeader>
<DialogTitle>Schedule Interview</DialogTitle> <DialogTitle>{interviewToReschedule ? 'Reschedule Interview' : 'Schedule Interview'}</DialogTitle>
<DialogDescription>Set up an interview session with the applicant and relevant team members.</DialogDescription> <DialogDescription>Set up an interview session with the applicant and relevant team members.</DialogDescription>
</DialogHeader> </DialogHeader>
<div className="space-y-4"> <div className="space-y-4">
@ -264,9 +271,9 @@ export function ApplicationDetailsActionModals(props: ApplicationDetailsActionMo
<Select value={interviewType} onValueChange={setInterviewType}> <Select value={interviewType} onValueChange={setInterviewType}>
<SelectTrigger className="mt-2" data-testid="onboarding-schedule-type-select"><SelectValue placeholder="Select interview type" /></SelectTrigger> <SelectTrigger className="mt-2" data-testid="onboarding-schedule-type-select"><SelectValue placeholder="Select interview type" /></SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value="level1" disabled={isInterviewCompleted(1) || isInterviewActive(1)}><div className="flex items-center justify-between w-full"><span>Level 1</span>{isInterviewCompleted(1) && <CheckCircle className="w-4 h-4 text-green-500 ml-2 inline" />}{isInterviewActive(1) && <Clock className="w-4 h-4 text-amber-500 ml-2 inline" />}</div></SelectItem> <SelectItem value="level1" disabled={isInterviewCompleted(1) || isInterviewActive(1)}><div className="flex items-center justify-between w-full"><span>Level 1</span>{isInterviewCompleted(1) && <CheckCircle className="w-4 h-4 text-green-500 ml-2 inline" />}{isInterviewActive(1) && <Clock className="w-4 h-4 text-re-red ml-2 inline" />}</div></SelectItem>
<SelectItem value="level2" disabled={!isInterviewCompleted(1) || isInterviewCompleted(2) || isInterviewActive(2)}><div className="flex items-center justify-between w-full"><span>Level 2</span>{!isInterviewCompleted(1) && <span className="text-[10px] text-slate-400 ml-2">(Prerequisite: L1)</span>}{isInterviewCompleted(2) && <CheckCircle className="w-4 h-4 text-green-500 ml-2 inline" />}{isInterviewActive(2) && <Clock className="w-4 h-4 text-amber-500 ml-2 inline" />}</div></SelectItem> <SelectItem value="level2" disabled={!isInterviewCompleted(1) || isInterviewCompleted(2) || isInterviewActive(2)}><div className="flex items-center justify-between w-full"><span>Level 2</span>{!isInterviewCompleted(1) && <span className="text-[10px] text-slate-400 ml-2">(Prerequisite: L1)</span>}{isInterviewCompleted(2) && <CheckCircle className="w-4 h-4 text-green-500 ml-2 inline" />}{isInterviewActive(2) && <Clock className="w-4 h-4 text-re-red ml-2 inline" />}</div></SelectItem>
<SelectItem value="level3" disabled={!isInterviewCompleted(2) || isInterviewCompleted(3) || isInterviewActive(3)}><div className="flex items-center justify-between w-full"><span>Level 3</span>{!isInterviewCompleted(2) && <span className="text-[10px] text-slate-400 ml-2">(Prerequisite: L2)</span>}{isInterviewCompleted(3) && <CheckCircle className="w-4 h-4 text-green-500 ml-2 inline" />}{isInterviewActive(3) && <Clock className="w-4 h-4 text-amber-500 ml-2 inline" />}</div></SelectItem> <SelectItem value="level3" disabled={!isInterviewCompleted(2) || isInterviewCompleted(3) || isInterviewActive(3)}><div className="flex items-center justify-between w-full"><span>Level 3</span>{!isInterviewCompleted(2) && <span className="text-[10px] text-slate-400 ml-2">(Prerequisite: L2)</span>}{isInterviewCompleted(3) && <CheckCircle className="w-4 h-4 text-green-500 ml-2 inline" />}{isInterviewActive(3) && <Clock className="w-4 h-4 text-re-red ml-2 inline" />}</div></SelectItem>
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>
@ -304,10 +311,15 @@ export function ApplicationDetailsActionModals(props: ApplicationDetailsActionMo
</div> </div>
)} )}
</div> </div>
<div className="flex gap-3"> <div className="flex gap-3">
<Button variant="outline" className="flex-1" onClick={() => setShowScheduleModal(false)} disabled={isScheduling} data-testid="onboarding-schedule-cancel-button">Cancel</Button> <Button variant="outline" className="flex-1" onClick={() => {
<Button className="flex-1 bg-amber-600 hover:bg-amber-700" onClick={handleScheduleInterview} disabled={isScheduling} data-testid="onboarding-schedule-submit-button">{isScheduling ? 'Scheduling...' : 'Schedule'}</Button> setShowScheduleModal(false);
</div> setInterviewToReschedule(null);
}} disabled={isScheduling} data-testid="onboarding-schedule-cancel-button">Cancel</Button>
<Button className="flex-1 bg-primary-600 hover:bg-primary-700" onClick={handleScheduleInterview} disabled={isScheduling} data-testid="onboarding-schedule-submit-button">
{isScheduling ? (interviewToReschedule ? 'Rescheduling...' : 'Scheduling...') : (interviewToReschedule ? 'Reschedule' : 'Schedule')}
</Button>
</div>
</div> </div>
</DialogContent> </DialogContent>
</Dialog> </Dialog>

View File

@ -69,15 +69,12 @@ export function ApplicationDetailsExtendedModals(props: ApplicationDetailsExtend
getDocumentsForStage, getDocumentsForStage,
setPreviewDoc, setPreviewDoc,
setShowPreviewModal, setShowPreviewModal,
flattenedStages,
setSelectedStage,
uploadDocType, uploadDocType,
setUploadDocType, setUploadDocType,
setUploadFile, setUploadFile,
isUploading, isUploading,
handleUpload, handleUpload,
uploadFile, uploadFile,
documentConfigs,
showPreviewModal, showPreviewModal,
previewDoc, previewDoc,
showFddFinalizeModal, showFddFinalizeModal,
@ -126,7 +123,7 @@ export function ApplicationDetailsExtendedModals(props: ApplicationDetailsExtend
<div className="custom-scrollbar-slim min-h-0 flex-1 overflow-y-auto px-5 py-5"> <div className="custom-scrollbar-slim min-h-0 flex-1 overflow-y-auto px-5 py-5">
<div className="space-y-6"> <div className="space-y-6">
{ktCriteria.length === 0 && ( {ktCriteria.length === 0 && (
<div className="rounded-md border border-amber-200 bg-amber-50 px-3 py-2 text-sm text-amber-800"> <div className="rounded-md border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-800">
KT Matrix configuration is not available. Configure it in Master &gt; Interview Configurations. KT Matrix configuration is not available. Configure it in Master &gt; Interview Configurations.
</div> </div>
)} )}
@ -157,10 +154,10 @@ export function ApplicationDetailsExtendedModals(props: ApplicationDetailsExtend
</div> </div>
))} ))}
<div className="space-y-2 border-t border-border pt-6"> <div className="space-y-2 border-t border-border pt-6">
<Label htmlFor="kt-matrix-remarks" className="text-sm font-medium">Notes <span className="font-normal text-muted-foreground">(optional)</span></Label> <Label htmlFor="kt-matrix-remarks" className="text-sm font-medium">Remarks <span className="text-red-500">*</span></Label>
<Textarea <Textarea
id="kt-matrix-remarks" id="kt-matrix-remarks"
placeholder="Optional remarks…" placeholder="Enter remarks..."
className="min-h-[96px] resize-y text-sm leading-relaxed" className="min-h-[96px] resize-y text-sm leading-relaxed"
value={ktMatrixRemarks} value={ktMatrixRemarks}
onChange={(e) => setKtMatrixRemarks(e.target.value)} onChange={(e) => setKtMatrixRemarks(e.target.value)}
@ -186,7 +183,7 @@ export function ApplicationDetailsExtendedModals(props: ApplicationDetailsExtend
<p className="text-sm text-muted-foreground">Weighted total <span className="font-semibold tabular-nums text-foreground" data-testid="onboarding-kt-matrix-total-score">{calculateKTScore()}</span><span className="text-muted-foreground"> / 100</span></p> <p className="text-sm text-muted-foreground">Weighted total <span className="font-semibold tabular-nums text-foreground" data-testid="onboarding-kt-matrix-total-score">{calculateKTScore()}</span><span className="text-muted-foreground"> / 100</span></p>
<div className="flex gap-2 sm:shrink-0"> <div className="flex gap-2 sm:shrink-0">
<Button variant="outline" onClick={() => setShowKTMatrixModal(false)} data-testid="onboarding-kt-matrix-cancel">Cancel</Button> <Button variant="outline" onClick={() => setShowKTMatrixModal(false)} data-testid="onboarding-kt-matrix-cancel">Cancel</Button>
<Button onClick={handleSubmitKTMatrix} disabled={isSubmittingKT || ktCriteria.length === 0 || 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 || !ktMatrixRemarks?.trim()} data-testid="onboarding-kt-matrix-submit">{isSubmittingKT ? 'Saving…' : 'Submit'}</Button>
</div> </div>
</div> </div>
</DialogContent> </DialogContent>
@ -208,22 +205,10 @@ export function ApplicationDetailsExtendedModals(props: ApplicationDetailsExtend
<SelectContent><SelectItem value="10">Outstanding (9-10)</SelectItem><SelectItem value="8">Excellent (7-8)</SelectItem><SelectItem value="6">Good (5-6)</SelectItem><SelectItem value="4">Average (3-4)</SelectItem><SelectItem value="2">Below Average (1-2)</SelectItem></SelectContent> <SelectContent><SelectItem value="10">Outstanding (9-10)</SelectItem><SelectItem value="8">Excellent (7-8)</SelectItem><SelectItem value="6">Good (5-6)</SelectItem><SelectItem value="4">Average (3-4)</SelectItem><SelectItem value="2">Below Average (1-2)</SelectItem></SelectContent>
</Select> </Select>
</div> </div>
<div>
<Label>Recommendation <span className="text-red-500">*</span></Label>
<Select value={level2Recommendation} onValueChange={setLevel2Recommendation}>
<SelectTrigger className="mt-2" data-testid="onboarding-level2-recommendation-select">
<SelectValue placeholder="Select recommendation" />
</SelectTrigger>
<SelectContent>
<SelectItem value="Approve">Approve</SelectItem>
<SelectItem value="Reject">Reject</SelectItem>
<SelectItem value="Hold">Hold</SelectItem>
</SelectContent>
</Select>
</div>
<Separator /> <Separator />
{l2Fields.length === 0 && ( {l2Fields.length === 0 && (
<div className="rounded-md border border-amber-200 bg-amber-50 px-3 py-2 text-sm text-amber-800"> <div className="rounded-md border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-800">
Level 2 feedback configuration is not available. Configure it in Master &gt; Interview Configurations. Level 2 feedback configuration is not available. Configure it in Master &gt; Interview Configurations.
</div> </div>
)} )}
@ -255,6 +240,19 @@ export function ApplicationDetailsExtendedModals(props: ApplicationDetailsExtend
)} )}
</div> </div>
))} ))}
<div>
<Label>Recommendation <span className="text-red-500">*</span></Label>
<Select value={level2Recommendation} onValueChange={setLevel2Recommendation}>
<SelectTrigger className="mt-2" data-testid="onboarding-level2-recommendation-select">
<SelectValue placeholder="Select recommendation" />
</SelectTrigger>
<SelectContent>
<SelectItem value="Approve">Approve</SelectItem>
<SelectItem value="Reject">Reject</SelectItem>
<SelectItem value="Hold">Hold</SelectItem>
</SelectContent>
</Select>
</div>
<div className="flex gap-3"> <div className="flex gap-3">
<Button variant="outline" className="flex-1" onClick={() => setShowLevel2FeedbackModal(false)} data-testid="onboarding-level2-feedback-cancel">Cancel</Button> <Button variant="outline" className="flex-1" onClick={() => setShowLevel2FeedbackModal(false)} data-testid="onboarding-level2-feedback-cancel">Cancel</Button>
<Button className="flex-1 bg-black hover:bg-zinc-800 text-white" onClick={handleSubmitLevel2Feedback} disabled={isSubmittingLevel2 || l2Fields.length === 0} 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>
@ -311,22 +309,10 @@ export function ApplicationDetailsExtendedModals(props: ApplicationDetailsExtend
<SelectContent><SelectItem value="10">Outstanding (9-10)</SelectItem><SelectItem value="8">Excellent (7-8)</SelectItem><SelectItem value="6">Good (5-6)</SelectItem><SelectItem value="4">Average (3-4)</SelectItem><SelectItem value="2">Below Average (1-2)</SelectItem></SelectContent> <SelectContent><SelectItem value="10">Outstanding (9-10)</SelectItem><SelectItem value="8">Excellent (7-8)</SelectItem><SelectItem value="6">Good (5-6)</SelectItem><SelectItem value="4">Average (3-4)</SelectItem><SelectItem value="2">Below Average (1-2)</SelectItem></SelectContent>
</Select> </Select>
</div> </div>
<div>
<Label>Recommendation <span className="text-red-500">*</span></Label>
<Select value={level3Recommendation} onValueChange={setLevel3Recommendation}>
<SelectTrigger className="mt-2" data-testid="onboarding-level3-recommendation-select">
<SelectValue placeholder="Select recommendation" />
</SelectTrigger>
<SelectContent>
<SelectItem value="Approve">Approve</SelectItem>
<SelectItem value="Reject">Reject</SelectItem>
<SelectItem value="Hold">Hold</SelectItem>
</SelectContent>
</Select>
</div>
<Separator /> <Separator />
{l3Fields.length === 0 && ( {l3Fields.length === 0 && (
<div className="rounded-md border border-amber-200 bg-amber-50 px-3 py-2 text-sm text-amber-800"> <div className="rounded-md border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-800">
Level 3 feedback configuration is not available. Configure it in Master &gt; Interview Configurations. Level 3 feedback configuration is not available. Configure it in Master &gt; Interview Configurations.
</div> </div>
)} )}
@ -358,6 +344,19 @@ export function ApplicationDetailsExtendedModals(props: ApplicationDetailsExtend
)} )}
</div> </div>
))} ))}
<div>
<Label>Recommendation <span className="text-red-500">*</span></Label>
<Select value={level3Recommendation} onValueChange={setLevel3Recommendation}>
<SelectTrigger className="mt-2" data-testid="onboarding-level3-recommendation-select">
<SelectValue placeholder="Select recommendation" />
</SelectTrigger>
<SelectContent>
<SelectItem value="Approve">Approve</SelectItem>
<SelectItem value="Reject">Reject</SelectItem>
<SelectItem value="Hold">Hold</SelectItem>
</SelectContent>
</Select>
</div>
<div className="flex gap-3"> <div className="flex gap-3">
<Button variant="outline" className="flex-1" onClick={() => setShowLevel3FeedbackModal(false)} data-testid="onboarding-level3-feedback-cancel">Cancel</Button> <Button variant="outline" className="flex-1" onClick={() => setShowLevel3FeedbackModal(false)} data-testid="onboarding-level3-feedback-cancel">Cancel</Button>
<Button className="flex-1 bg-black hover:bg-zinc-800 text-white" onClick={handleSubmitLevel3Feedback} disabled={isSubmittingLevel3 || l3Fields.length === 0} 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>
@ -369,13 +368,13 @@ export function ApplicationDetailsExtendedModals(props: ApplicationDetailsExtend
<Dialog open={showDocumentsModal} onOpenChange={(open) => { setShowDocumentsModal(open); if (!open) setShowUploadForm(false); }}> <Dialog open={showDocumentsModal} onOpenChange={(open) => { setShowDocumentsModal(open); if (!open) setShowUploadForm(false); }}>
<DialogContent className="max-w-[95vw] sm:max-w-2xl md:max-w-3xl lg:max-w-4xl max-h-[90vh] overflow-hidden flex flex-col p-4 sm:p-6" data-testid="onboarding-documents-modal"> <DialogContent className="max-w-[95vw] sm:max-w-2xl md:max-w-3xl lg:max-w-4xl max-h-[90vh] overflow-hidden flex flex-col p-4 sm:p-6" data-testid="onboarding-documents-modal">
<DialogHeader className="pb-4"> <DialogHeader className="pb-4">
<DialogTitle className="text-xl font-bold flex items-center gap-2"><FileText className="w-5 h-5 text-amber-600" />Documents - {selectedStage || 'General'}</DialogTitle> <DialogTitle className="text-xl font-bold flex items-center gap-2"><FileText className="w-5 h-5 text-re-red" />Documents - {selectedStage || 'General'}</DialogTitle>
<DialogDescription className="text-slate-500">View and manage documents uploaded for this stage.</DialogDescription> <DialogDescription className="text-slate-500">View and manage documents uploaded for this stage.</DialogDescription>
</DialogHeader> </DialogHeader>
{!showUploadForm ? ( {!showUploadForm ? (
<div className="flex-1 flex flex-col min-h-0 space-y-4"> <div className="flex-1 flex flex-col min-h-0 space-y-4">
{getDocumentsForStage(selectedStage || '').length > 0 ? ( {getDocumentsForStage(selectedStage || '').length > 0 ? (
<div className="flex-1 overflow-auto border rounded-lg border-slate-200" data-testid="onboarding-documents-table-container"> <div className="custom-scrollbar-x-slim flex-1 overflow-auto border rounded-lg border-slate-200" data-testid="onboarding-documents-table-container">
<Table className="w-full table-auto"> <Table className="w-full table-auto">
<TableHeader className="bg-slate-50/80 sticky top-0 z-10"> <TableHeader className="bg-slate-50/80 sticky top-0 z-10">
<TableRow className="hover:bg-transparent border-b"> <TableRow className="hover:bg-transparent border-b">
@ -396,7 +395,7 @@ export function ApplicationDetailsExtendedModals(props: ApplicationDetailsExtend
<TableCell className="text-right py-3"> <TableCell className="text-right py-3">
<div className="flex gap-1 justify-end"> <div className="flex gap-1 justify-end">
<Button variant="ghost" size="icon" className="h-8 w-8 text-slate-400 hover:text-indigo-600 hover:bg-indigo-50 rounded-full" onClick={() => { setPreviewDoc(doc); setShowPreviewModal(true); }} data-testid={`onboarding-document-preview-${index}`}><Eye className="w-4 h-4" /></Button> <Button variant="ghost" size="icon" className="h-8 w-8 text-slate-400 hover:text-indigo-600 hover:bg-indigo-50 rounded-full" onClick={() => { setPreviewDoc(doc); setShowPreviewModal(true); }} data-testid={`onboarding-document-preview-${index}`}><Eye className="w-4 h-4" /></Button>
<Button variant="ghost" size="icon" className="h-8 w-8 text-slate-400 hover:text-amber-600 hover:bg-amber-50 rounded-full" onClick={() => { const baseUrl = (import.meta as any).env?.VITE_API_URL || 'http://localhost:5000'; window.open(`${baseUrl}/${doc.filePath}`, '_blank'); }} data-testid={`onboarding-document-download-${index}`}><Download className="w-4 h-4" /></Button> <Button variant="ghost" size="icon" className="h-8 w-8 text-slate-400 hover:text-re-red hover:bg-red-50 rounded-full" onClick={() => { const baseUrl = (import.meta as any).env?.VITE_API_URL || 'http://localhost:5000'; window.open(`${baseUrl}/${doc.filePath}`, '_blank'); }} data-testid={`onboarding-document-download-${index}`}><Download className="w-4 h-4" /></Button>
</div> </div>
</TableCell> </TableCell>
</TableRow> </TableRow>
@ -408,66 +407,46 @@ export function ApplicationDetailsExtendedModals(props: ApplicationDetailsExtend
<div className="flex-1 flex flex-col items-center justify-center py-12 text-center border rounded-lg bg-slate-50/30" data-testid="onboarding-documents-empty"><div className="w-16 h-16 rounded-full bg-slate-100 flex items-center justify-center mb-4"><FileText className="w-8 h-8 text-slate-300" /></div><h3 className="text-slate-900 font-semibold mb-2">No Documents Found</h3><p className="text-slate-600 text-sm max-w-[250px]">No documents have been uploaded for this stage yet.</p></div> <div className="flex-1 flex flex-col items-center justify-center py-12 text-center border rounded-lg bg-slate-50/30" data-testid="onboarding-documents-empty"><div className="w-16 h-16 rounded-full bg-slate-100 flex items-center justify-center mb-4"><FileText className="w-8 h-8 text-slate-300" /></div><h3 className="text-slate-900 font-semibold mb-2">No Documents Found</h3><p className="text-slate-600 text-sm max-w-[250px]">No documents have been uploaded for this stage yet.</p></div>
)} )}
<div className="flex flex-col sm:flex-row gap-3 pt-2 mt-auto"> <div className="flex flex-col sm:flex-row gap-3 pt-2 mt-auto">
<Button className="flex-1 bg-amber-600 hover:bg-amber-700 text-white font-bold py-3 sm:py-5 rounded-xl shadow-lg shadow-amber-600/15 transition-all hover:scale-[1.01] active:scale-[0.99]" onClick={() => setShowUploadForm(true)} data-testid="onboarding-documents-upload-button"><Upload className="w-5 h-5 mr-3" />Upload Document</Button> <Button className="flex-1 bg-re-red hover:bg-re-red-hover text-white font-bold py-3 sm:py-5 rounded-xl shadow-lg shadow-re-red/15 transition-all hover:scale-[1.01] active:scale-[0.99]" onClick={() => setShowUploadForm(true)} data-testid="onboarding-documents-upload-button"><Upload className="w-5 h-5 mr-3" />Upload Document</Button>
<Button variant="outline" className="flex-1 sm:flex-none py-3 sm:py-5 px-8 rounded-xl border-slate-200 font-semibold text-slate-600 hover:bg-slate-50" onClick={() => setShowDocumentsModal(false)} data-testid="onboarding-documents-close-button">Close</Button> <Button variant="outline" className="flex-1 sm:flex-none py-3 sm:py-5 px-8 rounded-xl border-slate-200 font-semibold text-slate-600 hover:bg-slate-50" onClick={() => setShowDocumentsModal(false)} data-testid="onboarding-documents-close-button">Close</Button>
</div> </div>
</div> </div>
) : ( ) : (
<div className="space-y-6 py-4" data-testid="onboarding-documents-upload-form"> <div className="space-y-6 py-4" data-testid="onboarding-documents-upload-form">
<div className="grid gap-6 bg-slate-50/50 p-4 sm:p-6 rounded-2xl border border-slate-200"> <div className="bg-slate-50/50 p-4 sm:p-6 rounded-2xl border border-slate-200">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-6"> <div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div className="space-y-2"> <div className="space-y-2">
<Label className="text-slate-700 font-semibold px-1">Stage context <span className="text-red-500">*</span></Label> <Label className="text-slate-700 font-semibold px-1">Document Name <span className="text-red-500">*</span></Label>
<Select value={selectedStage || 'null'} onValueChange={(val) => setSelectedStage(val === 'null' ? null : val)}> <Input
<SelectTrigger className="bg-white border-slate-200 h-11 rounded-xl focus:ring-amber-500 shadow-sm" data-testid="onboarding-documents-stage-select"><SelectValue placeholder="Select stage" /></SelectTrigger> type="text"
<SelectContent> placeholder="Enter document name"
<SelectItem value="null">General / No Stage</SelectItem> value={uploadDocType}
{flattenedStages.map((s: any, idx: number) => <SelectItem key={`${s.name}-${idx}`} value={s.name}>{s.parentBranch ? `${s.parentBranch}: ${s.name}` : s.name}</SelectItem>)} onChange={(e) => setUploadDocType(e.target.value)}
</SelectContent> className="bg-white border-slate-200 h-12 rounded-xl focus:ring-re-red shadow-sm"
</Select> data-testid="onboarding-documents-name-input"
/>
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label className="text-slate-700 font-semibold px-1">Document Type <span className="text-red-500">*</span></Label> <Label className="text-slate-700 font-semibold px-1">Select File <span className="text-red-500">*</span></Label>
<Select value={uploadDocType} onValueChange={setUploadDocType}> <Input
<SelectTrigger className="bg-white border-slate-200 h-11 rounded-xl focus:ring-amber-500 shadow-sm" data-testid="onboarding-documents-type-select"><SelectValue placeholder="Select type" /></SelectTrigger> type="file"
<SelectContent> className="bg-white border-slate-200 h-12 rounded-xl focus:ring-re-red shadow-sm file:mr-4 file:py-2 file:px-4 file:rounded-full file:border-0 file:text-sm file:font-semibold file:bg-red-50 file:text-re-red-hover hover:file:bg-red-50 cursor-pointer"
{(() => { onChange={(e) => {
const baseDocs = ['Other']; const file = e.target.files ? e.target.files[0] : null;
const stageConfigs = documentConfigs.filter((c: any) => { setUploadFile(file);
const cfgStage = c.stageCode?.trim(); if (file) {
const selStage = (selectedStage || 'General').trim(); const baseName = file.name.replace(/\.[^/.]+$/, '');
if (cfgStage === selStage) return true; setUploadDocType(baseName);
if (selStage.startsWith('EOR:') && cfgStage === 'EOR') return true; }
if (!selectedStage && cfgStage === 'General') return true; }}
return false; data-testid="onboarding-documents-file-input"
}); />
let filteredDocs: string[] = [];
if (stageConfigs.length > 0) filteredDocs = stageConfigs.map((c: any) => c.documentType);
else if (!selectedStage || selectedStage === 'General') {
filteredDocs = ['PAN Card', 'GST Certificate', 'Aadhaar Card', 'Passport Size Photograph', 'Partnership Deed', 'LLP Agreement', 'Certificate of Incorporation', 'Board Resolution', 'Firm Registration Certificate', 'Cancelled Check', 'Bank Statement', 'Other'];
} else if (selectedStage?.toLowerCase().includes('architecture')) {
filteredDocs = ['Architecture Blueprint', 'Site Plan', 'Proposed Site City Map', 'Site Readiness Report', 'Architecture Completion Certificate', 'Other'];
} else if (selectedStage?.toLowerCase().includes('fdd')) {
filteredDocs = ['FDD Final Audit Report', 'Bank Statement', 'Income Tax Returns (ITR)', 'CIBIL Report', 'Other'];
} else filteredDocs = baseDocs;
if (selectedStage?.startsWith('EOR: ')) {
const eorItem = selectedStage.replace('EOR: ', '');
if (!filteredDocs.includes(eorItem)) filteredDocs = [eorItem, ...filteredDocs];
}
return Array.from(new Set(filteredDocs)).map((doc, idx) => <SelectItem key={`${doc}-${idx}`} value={doc} data-testid={`onboarding-documents-type-option-${idx}`}>{doc}</SelectItem>);
})()}
</SelectContent>
</Select>
</div> </div>
</div> </div>
<div className="space-y-2">
<Label className="text-slate-700 font-semibold px-1">Select File <span className="text-red-500">*</span></Label>
<Input type="file" className="bg-white border-slate-200 h-12 rounded-xl focus:ring-amber-500 shadow-sm file:mr-4 file:py-2 file:px-4 file:rounded-full file:border-0 file:text-sm file:font-semibold file:bg-amber-50 file:text-amber-700 hover:file:bg-amber-100 cursor-pointer" onChange={(e) => setUploadFile(e.target.files ? e.target.files[0] : null)} data-testid="onboarding-documents-file-input" />
</div>
</div> </div>
<div className="flex flex-col sm:flex-row gap-3 pt-4"> <div className="flex flex-col sm:flex-row gap-3 pt-4">
<Button className="flex-1 order-2 sm:order-1 py-3 sm:py-5 rounded-xl border-slate-200 font-semibold text-slate-600 hover:bg-slate-50" variant="outline" onClick={() => setShowUploadForm(false)} disabled={isUploading} data-testid="onboarding-documents-upload-cancel">Cancel</Button> <Button className="flex-1 order-2 sm:order-1 py-3 sm:py-5 rounded-xl border-slate-200 font-semibold text-slate-600 hover:bg-slate-50" variant="outline" onClick={() => setShowUploadForm(false)} disabled={isUploading} data-testid="onboarding-documents-upload-cancel">Cancel</Button>
<Button className="flex-1 order-1 sm:order-2 bg-amber-600 hover:bg-amber-700 text-white font-bold py-3 sm:py-5 rounded-xl shadow-lg shadow-amber-600/15 transition-all hover:scale-[1.01] active:scale-[0.99]" onClick={async () => { await handleUpload(); setShowUploadForm(false); }} disabled={!uploadFile || !uploadDocType || isUploading} data-testid="onboarding-documents-upload-submit"> <Button className="flex-1 order-1 sm:order-2 bg-re-red hover:bg-re-red-hover text-white font-bold py-3 sm:py-5 rounded-xl shadow-lg shadow-re-red/15 transition-all hover:scale-[1.01] active:scale-[0.99]" onClick={async () => { await handleUpload(); setShowUploadForm(false); }} disabled={!uploadFile || !uploadDocType || isUploading} data-testid="onboarding-documents-upload-submit">
{isUploading ? <span className="flex items-center gap-2"><div className="w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin" />Uploading...</span> : <span className="flex items-center gap-2"><Upload className="w-5 h-5" />Confirm Upload</span>} {isUploading ? <span className="flex items-center gap-2"><div className="w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin" />Uploading...</span> : <span className="flex items-center gap-2"><Upload className="w-5 h-5" />Confirm Upload</span>}
</Button> </Button>
</div> </div>
@ -479,11 +458,11 @@ export function ApplicationDetailsExtendedModals(props: ApplicationDetailsExtend
<Dialog open={showFddFinalizeModal} onOpenChange={setShowFddFinalizeModal}> <Dialog open={showFddFinalizeModal} onOpenChange={setShowFddFinalizeModal}>
<DialogContent className="max-w-md p-0 overflow-hidden border-none shadow-2xl rounded-3xl" data-testid="onboarding-fdd-finalize-modal"> <DialogContent className="max-w-md p-0 overflow-hidden border-none shadow-2xl rounded-3xl" data-testid="onboarding-fdd-finalize-modal">
<div className="bg-slate-950 p-8 flex items-center justify-center relative overflow-hidden"><div className="absolute inset-0 bg-gradient-to-br from-amber-600/20 to-transparent" /><div className="w-20 h-20 bg-amber-600/20 rounded-full flex items-center justify-center animate-pulse relative z-10 shadow-[0_0_40px_rgba(245,158,11,0.2)]"><ShieldCheck className="w-10 h-10 text-amber-500" /></div></div> <div className="bg-slate-950 p-8 flex items-center justify-center relative overflow-hidden"><div className="absolute inset-0 bg-gradient-to-br from-re-red/20 to-transparent" /><div className="w-20 h-20 bg-re-red/20 rounded-full flex items-center justify-center animate-pulse relative z-10 shadow-[0_0_40px_rgba(218,41,28,0.2)]"><ShieldCheck className="w-10 h-10 text-re-red" /></div></div>
<div className="p-8 space-y-6 bg-white"> <div className="p-8 space-y-6 bg-white">
<DialogHeader> <DialogHeader>
<DialogTitle className="text-2xl font-black text-slate-900 text-center tracking-tight">Finalize FDD Audit</DialogTitle> <DialogTitle className="text-2xl font-black text-slate-900 text-center tracking-tight">Finalize FDD Audit</DialogTitle>
<DialogDescription className="text-slate-500 text-center pt-2 leading-relaxed text-sm font-medium">You are about to submit your final findings. This action will <span className="font-bold text-slate-900 underline decoration-amber-500 decoration-2">lock the audit session</span> and trigger the LOI approval workflow.</DialogDescription> <DialogDescription className="text-slate-500 text-center pt-2 leading-relaxed text-sm font-medium">You are about to submit your final findings. This action will <span className="font-bold text-slate-900 underline decoration-re-red decoration-2">lock the audit session</span> and trigger the LOI approval workflow.</DialogDescription>
</DialogHeader> </DialogHeader>
<div className="space-y-4"> <div className="space-y-4">
{(currentUser?.role !== 'FDD' && currentUser?.roleCode !== 'FDD') && ( {(currentUser?.role !== 'FDD' && currentUser?.roleCode !== 'FDD') && (
@ -491,7 +470,7 @@ export function ApplicationDetailsExtendedModals(props: ApplicationDetailsExtend
<Label className="text-[10px] font-black uppercase tracking-widest text-slate-400">Auditor Recommendation <span className="text-red-500">*</span></Label> <Label className="text-[10px] font-black uppercase tracking-widest text-slate-400">Auditor Recommendation <span className="text-red-500">*</span></Label>
<div className="flex gap-2"> <div className="flex gap-2">
{['Recommended', 'Qualified with Observations', 'Not Recommended'].map((rec) => ( {['Recommended', 'Qualified with Observations', 'Not Recommended'].map((rec) => (
<Button key={rec} variant={fddAuditRecommendation === rec ? 'default' : 'outline'} className={cn("flex-1 h-10 font-bold text-[9px] uppercase tracking-wider rounded-xl transition-all", fddAuditRecommendation === rec && rec === 'Recommended' && "bg-emerald-600 hover:bg-emerald-700", fddAuditRecommendation === rec && rec === 'Qualified with Observations' && "bg-amber-500 hover:bg-amber-600", fddAuditRecommendation === rec && rec === 'Not Recommended' && "bg-red-600 hover:bg-red-700")} onClick={() => setFddAuditRecommendation(rec)} data-testid={`onboarding-fdd-recommendation-${rec.replace(/\s+/g, '-').toLowerCase()}`}>{rec}</Button> <Button key={rec} variant={fddAuditRecommendation === rec ? 'default' : 'outline'} className={cn("flex-1 h-10 font-bold text-[9px] uppercase tracking-wider rounded-xl transition-all", fddAuditRecommendation === rec && rec === 'Recommended' && "bg-emerald-600 hover:bg-emerald-700", fddAuditRecommendation === rec && rec === 'Qualified with Observations' && "bg-red-500 hover:bg-re-red", fddAuditRecommendation === rec && rec === 'Not Recommended' && "bg-red-600 hover:bg-red-700")} onClick={() => setFddAuditRecommendation(rec)} data-testid={`onboarding-fdd-recommendation-${rec.replace(/\s+/g, '-').toLowerCase()}`}>{rec}</Button>
))} ))}
</div> </div>
</div> </div>
@ -500,18 +479,18 @@ export function ApplicationDetailsExtendedModals(props: ApplicationDetailsExtend
<Label className="text-[10px] font-black uppercase tracking-widest text-slate-400">Findings Summary</Label> <Label className="text-[10px] font-black uppercase tracking-widest text-slate-400">Findings Summary</Label>
<Textarea <Textarea
placeholder="Summarize key financial findings or discrepancies..." placeholder="Summarize key financial findings or discrepancies..."
className="min-h-[100px] rounded-xl border-slate-200 focus:ring-amber-500 text-sm" className="min-h-[100px] rounded-xl border-slate-200 focus:ring-re-red text-sm"
value={fddAuditFindings} value={fddAuditFindings}
onChange={(e) => setFddAuditFindings(e.target.value)} onChange={(e) => setFddAuditFindings(e.target.value)}
data-testid="onboarding-fdd-findings-textarea" data-testid="onboarding-fdd-findings-textarea"
/> />
</div> </div>
</div> </div>
<div className="bg-amber-50 p-4 rounded-2xl flex gap-3 border border-amber-100"><Info className="w-5 h-5 text-amber-600 shrink-0 mt-0.5" /><p className="text-[11px] text-amber-800 font-medium italic">Ensure the final PDF report is uploaded first. This satisfies the FDD statutory requirement.</p></div> <div className="bg-red-50 p-4 rounded-2xl flex gap-3 border border-red-100"><Info className="w-5 h-5 text-re-red shrink-0 mt-0.5" /><p className="text-[11px] text-red-800 font-medium italic">Ensure the final PDF report is uploaded first. This satisfies the FDD statutory requirement.</p></div>
<div className="flex flex-col sm:flex-row gap-3 pt-2"> <div className="flex flex-col sm:flex-row gap-3 pt-2">
<Button variant="outline" className="w-full sm:flex-1 h-12 rounded-2xl font-bold text-slate-600 hover:bg-slate-50 border-slate-200" onClick={() => setShowFddFinalizeModal(false)} disabled={isFinalizingFdd} data-testid="onboarding-fdd-finalize-cancel">Cancel</Button> <Button variant="outline" className="w-full sm:flex-1 h-12 rounded-2xl font-bold text-slate-600 hover:bg-slate-50 border-slate-200" onClick={() => setShowFddFinalizeModal(false)} disabled={isFinalizingFdd} data-testid="onboarding-fdd-finalize-cancel">Cancel</Button>
<Button <Button
className="w-full sm:flex-1 h-12 rounded-2xl font-bold bg-slate-950 hover:bg-slate-900 text-white shadow-lg shadow-slate-200 transition-all active:scale-95 border-b-4 border-amber-500" className="w-full sm:flex-1 h-12 rounded-2xl font-bold bg-slate-950 hover:bg-slate-900 text-white shadow-lg shadow-slate-200 transition-all active:scale-95 border-b-4 border-re-red"
disabled={isFinalizingFdd || !fddAuditFindings} disabled={isFinalizingFdd || !fddAuditFindings}
data-testid="onboarding-fdd-finalize-submit" data-testid="onboarding-fdd-finalize-submit"
onClick={async () => { onClick={async () => {
@ -587,16 +566,16 @@ export function ApplicationDetailsExtendedModals(props: ApplicationDetailsExtend
<Dialog open={showFirmTypeModal} onOpenChange={setShowFirmTypeModal}> <Dialog open={showFirmTypeModal} onOpenChange={setShowFirmTypeModal}>
<DialogContent className="max-w-md p-0 overflow-hidden rounded-3xl border-none shadow-2xl" data-testid="onboarding-firm-type-modal"> <DialogContent className="max-w-md p-0 overflow-hidden rounded-3xl border-none shadow-2xl" data-testid="onboarding-firm-type-modal">
<div className="bg-amber-600 p-8 text-white"> <div className="bg-re-red p-8 text-white">
<div className="w-16 h-16 rounded-2xl bg-white/20 flex items-center justify-center mb-6 backdrop-blur-sm border border-white/30 shadow-inner"><Building2 className="w-8 h-8 text-white" /></div> <div className="w-16 h-16 rounded-2xl bg-white/20 flex items-center justify-center mb-6 backdrop-blur-sm border border-white/30 shadow-inner"><Building2 className="w-8 h-8 text-white" /></div>
<h3 className="text-2xl font-black tracking-tight mb-2">Update Firm Type</h3> <h3 className="text-2xl font-black tracking-tight mb-2">Update Firm Type</h3>
<p className="text-amber-100/80 text-sm font-medium leading-relaxed">Select the proposed legal constitution for this dealership application.</p> <p className="text-red-100/80 text-sm font-medium leading-relaxed">Select the proposed legal constitution for this dealership application.</p>
</div> </div>
<div className="p-8 space-y-6 bg-white"> <div className="p-8 space-y-6 bg-white">
<div className="space-y-2"> <div className="space-y-2">
<Label className="text-[10px] text-slate-400 uppercase tracking-widest font-black">Proposed Legal Constitution <span className="text-red-500">*</span></Label> <Label className="text-[10px] text-slate-400 uppercase tracking-widest font-black">Proposed Legal Constitution <span className="text-red-500">*</span></Label>
<Select value={tempFirmType} onValueChange={setTempFirmType}> <Select value={tempFirmType} onValueChange={setTempFirmType}>
<SelectTrigger className="h-12 rounded-xl border-slate-200 focus:ring-amber-500" data-testid="onboarding-firm-type-select"><SelectValue placeholder="Select Firm Type" /></SelectTrigger> <SelectTrigger className="h-12 rounded-xl border-slate-200 focus:ring-re-red" data-testid="onboarding-firm-type-select"><SelectValue placeholder="Select Firm Type" /></SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value="Proprietorship" data-testid="onboarding-firm-type-proprietorship">Proprietorship</SelectItem> <SelectItem value="Proprietorship" data-testid="onboarding-firm-type-proprietorship">Proprietorship</SelectItem>
<SelectItem value="Partnership" data-testid="onboarding-firm-type-partnership">Partnership</SelectItem> <SelectItem value="Partnership" data-testid="onboarding-firm-type-partnership">Partnership</SelectItem>
@ -608,7 +587,7 @@ export function ApplicationDetailsExtendedModals(props: ApplicationDetailsExtend
</div> </div>
<div className="flex gap-3 pt-2"> <div className="flex gap-3 pt-2">
<Button variant="outline" className="flex-1 h-12 rounded-xl font-bold text-slate-600 border-slate-200" onClick={() => setShowFirmTypeModal(false)} disabled={updatingFirmType} data-testid="onboarding-firm-type-cancel">Cancel</Button> <Button variant="outline" className="flex-1 h-12 rounded-xl font-bold text-slate-600 border-slate-200" onClick={() => setShowFirmTypeModal(false)} disabled={updatingFirmType} data-testid="onboarding-firm-type-cancel">Cancel</Button>
<Button className="flex-1 h-12 rounded-xl font-bold bg-amber-600 hover:bg-amber-700 text-white shadow-lg shadow-amber-200 transition-all active:scale-95" disabled={updatingFirmType || !tempFirmType} onClick={handleUpdateFirmType} data-testid="onboarding-firm-type-submit">{updatingFirmType ? <Loader2 className="w-5 h-5 animate-spin" /> : 'Update Type'}</Button> <Button className="flex-1 h-12 rounded-xl font-bold bg-re-red hover:bg-re-red-hover text-white shadow-lg shadow-red-200 transition-all active:scale-95" disabled={updatingFirmType || !tempFirmType} onClick={handleUpdateFirmType} data-testid="onboarding-firm-type-submit">{updatingFirmType ? <Loader2 className="w-5 h-5 animate-spin" /> : 'Update Type'}</Button>
</div> </div>
</div> </div>
</DialogContent> </DialogContent>

View File

@ -1,4 +1,4 @@
import { AlertCircle, CheckCircle, ClipboardList, Download, Eye, FileText, ShieldAlert, ShieldCheck, Upload } from 'lucide-react'; import { AlertCircle, CheckCircle, ClipboardList, Download, Eye, FileText, ShieldCheck, Upload } from 'lucide-react';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { onboardingService } from '@/services/onboarding.service'; import { onboardingService } from '@/services/onboarding.service';
import { cn, formatDateTime } from '@/components/ui/utils'; import { cn, formatDateTime } from '@/components/ui/utils';
@ -8,13 +8,7 @@ import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
interface Props { interface Props {
application: any; application: any;
currentUser: any;
documents: any[]; documents: any[];
fddAgencies: any[];
selectedAgencyId: string;
setSelectedAgencyId: (v: string) => void;
isAssigningAgency: boolean;
handleAssignAgency: () => void;
setPreviewDoc: (d: any) => void; setPreviewDoc: (d: any) => void;
setShowPreviewModal: (v: boolean) => void; setShowPreviewModal: (v: boolean) => void;
setIsUploading: (v: boolean) => void; setIsUploading: (v: boolean) => void;
@ -24,13 +18,7 @@ interface Props {
export function ApplicationDetailsFddAuditContent({ export function ApplicationDetailsFddAuditContent({
application, application,
currentUser,
documents, documents,
fddAgencies,
selectedAgencyId,
setSelectedAgencyId,
isAssigningAgency,
handleAssignAgency,
setPreviewDoc, setPreviewDoc,
setShowPreviewModal, setShowPreviewModal,
setIsUploading, setIsUploading,
@ -82,47 +70,6 @@ export function ApplicationDetailsFddAuditContent({
The Financial Due Diligence process has not been initiated for this application yet. The Financial Due Diligence process has not been initiated for this application yet.
</p> </p>
</div> </div>
{(currentUser?.role === 'DD Admin' || currentUser?.role === 'Super Admin') && (
<Card className="border-amber-100 bg-amber-50/30 overflow-hidden rounded-2xl" data-testid="onboarding-fdd-initiate-card">
<CardHeader className="pb-2">
<CardTitle className="text-xs font-black uppercase tracking-widest text-amber-800 flex items-center gap-2">
<ShieldAlert className="w-4 h-4" />
Initiate FDD Audit
</CardTitle>
</CardHeader>
<CardContent>
<div className="flex flex-col md:flex-row gap-4">
<div className="flex-1">
<label className="text-[10px] font-black uppercase tracking-widest text-slate-400 mb-1.5 block">Select FDD Agency <span className="text-red-500">*</span></label>
<select
className="w-full h-11 bg-white border border-slate-200 rounded-xl px-4 text-sm font-medium focus:ring-2 focus:ring-amber-500/20 focus:border-amber-500 outline-none transition-all shadow-sm"
value={selectedAgencyId}
onChange={(e) => setSelectedAgencyId(e.target.value)}
data-testid="onboarding-fdd-agency-select"
>
<option value="">Choose partner agency...</option>
{(fddAgencies || []).map((agency: any) => (
<option key={agency.id} value={agency.id} data-testid={`onboarding-fdd-agency-option-${agency.id}`}>
{agency.fullName || agency.name} ({agency.email})
</option>
))}
</select>
</div>
<div className="flex items-end">
<Button
className="bg-slate-900 text-white hover:bg-slate-800 font-black text-[10px] uppercase tracking-widest px-8 h-11 border-none shadow-lg shadow-slate-900/10 transition-all active:scale-[0.98]"
onClick={handleAssignAgency}
disabled={isAssigningAgency || !selectedAgencyId}
data-testid="onboarding-fdd-assign-button"
>
{isAssigningAgency ? 'Assigning...' : 'Assign & Start Audit'}
</Button>
</div>
</div>
</CardContent>
</Card>
)}
</div> </div>
); );
} }
@ -132,8 +79,8 @@ export function ApplicationDetailsFddAuditContent({
{hasAssignment && ( {hasAssignment && (
<div className="flex items-center justify-between p-4 bg-slate-50 border border-slate-200 rounded-xl mb-6" data-testid="onboarding-fdd-assignment-banner"> <div className="flex items-center justify-between p-4 bg-slate-50 border border-slate-200 rounded-xl mb-6" data-testid="onboarding-fdd-assignment-banner">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="p-2 bg-amber-100 rounded-lg"> <div className="p-2 bg-red-50 rounded-lg">
<ShieldCheck className="w-5 h-5 text-amber-600" /> <ShieldCheck className="w-5 h-5 text-re-red" />
</div> </div>
<div> <div>
<h4 className="text-sm font-bold text-slate-900">FDD Assignment Active</h4> <h4 className="text-sm font-bold text-slate-900">FDD Assignment Active</h4>
@ -230,7 +177,7 @@ export function ApplicationDetailsFddAuditContent({
</div> </div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4" data-testid="onboarding-fdd-support-docs-grid"> <div className="grid grid-cols-1 md:grid-cols-2 gap-4" data-testid="onboarding-fdd-support-docs-grid">
{(documents || []).filter(isFddSupportDoc).map((doc: any, index: number) => ( {(documents || []).filter(isFddSupportDoc).map((doc: any, index: number) => (
<div key={doc.id} className="group bg-white border border-slate-200 rounded-xl p-4 flex items-center justify-between hover:border-amber-400 transition-all hover:shadow-md" data-testid={`onboarding-fdd-support-doc-${index}`}> <div key={doc.id} className="group bg-white border border-slate-200 rounded-xl p-4 flex items-center justify-between hover:border-red-300 transition-all hover:shadow-md" data-testid={`onboarding-fdd-support-doc-${index}`}>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-lg bg-slate-50 flex items-center justify-center"><FileText className="w-5 h-5 text-slate-400" /></div> <div className="w-10 h-10 rounded-lg bg-slate-50 flex items-center justify-center"><FileText className="w-5 h-5 text-slate-400" /></div>
<div className="overflow-hidden"> <div className="overflow-hidden">
@ -239,13 +186,13 @@ export function ApplicationDetailsFddAuditContent({
</div> </div>
</div> </div>
<div className="flex gap-1"> <div className="flex gap-1">
<Button variant="ghost" size="icon" className="h-8 w-8 text-slate-400 hover:text-amber-600 hover:bg-amber-50" onClick={() => { <Button variant="ghost" size="icon" className="h-8 w-8 text-slate-400 hover:text-re-red hover:bg-red-50" onClick={() => {
const baseUrl = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:5000'; const baseUrl = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:5000';
window.open(`${baseUrl}/${doc.filePath}`, '_blank'); window.open(`${baseUrl}/${doc.filePath}`, '_blank');
}} data-testid={`onboarding-fdd-support-doc-download-${index}`}> }} data-testid={`onboarding-fdd-support-doc-download-${index}`}>
<Download className="w-4 h-4" /> <Download className="w-4 h-4" />
</Button> </Button>
<Button variant="ghost" size="icon" className="h-8 w-8 text-slate-400 hover:text-amber-600 hover:bg-amber-50" onClick={() => { setPreviewDoc(doc); setShowPreviewModal(true); }} data-testid={`onboarding-fdd-support-doc-preview-${index}`}> <Button variant="ghost" size="icon" className="h-8 w-8 text-slate-400 hover:text-re-red hover:bg-red-50" onClick={() => { setPreviewDoc(doc); setShowPreviewModal(true); }} data-testid={`onboarding-fdd-support-doc-preview-${index}`}>
<Eye className="w-4 h-4" /> <Eye className="w-4 h-4" />
</Button> </Button>
</div> </div>

View File

@ -1,9 +1,12 @@
import { ArrowLeft, MessageSquare, ShieldAlert } from 'lucide-react'; import { ArrowLeft, MessageSquare, ShieldAlert } from 'lucide-react';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Application } from '@/lib/mock-data'; import { Application } from '@/lib/mock-data';
import { SlaBadge } from '@/components/sla/SlaBadge';
import { SlaStatusSnapshot } from '@/services/sla.service';
interface ApplicationDetailsHeaderProps { interface ApplicationDetailsHeaderProps {
application: Application; application: Application;
slaStatus?: SlaStatusSnapshot | null;
isNonResponsive: boolean; isNonResponsive: boolean;
isAdmin: boolean; isAdmin: boolean;
onBack: () => void; onBack: () => void;
@ -12,6 +15,7 @@ interface ApplicationDetailsHeaderProps {
export function ApplicationDetailsHeader({ export function ApplicationDetailsHeader({
application, application,
slaStatus,
isNonResponsive, isNonResponsive,
isAdmin, isAdmin,
onBack, onBack,
@ -58,12 +62,17 @@ export function ApplicationDetailsHeader({
<div className="truncate"> <div className="truncate">
<h1 className="text-slate-900 truncate leading-tight" data-testid="onboarding-details-application-name">{application.name}</h1> <h1 className="text-slate-900 truncate leading-tight" data-testid="onboarding-details-application-name">{application.name}</h1>
<p className="text-slate-600 truncate text-sm" data-testid="onboarding-details-registration-number">{application.registrationNumber}</p> <p className="text-slate-600 truncate text-sm" data-testid="onboarding-details-registration-number">{application.registrationNumber}</p>
{slaStatus && (
<div className="mt-1">
<SlaBadge status={slaStatus} />
</div>
)}
</div> </div>
</div> </div>
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
<Button <Button
variant="outline" variant="outline"
className="relative hover:bg-amber-50 hover:border-amber-300 hover:text-amber-700 transition-all shadow-sm" className="relative hover:bg-red-50 hover:border-red-300 hover:text-re-red-hover transition-all shadow-sm"
onClick={onOpenWorknotes} onClick={onOpenWorknotes}
data-testid="onboarding-details-view-work-notes" data-testid="onboarding-details-view-work-notes"
> >

View File

@ -1,9 +1,12 @@
import { useState } from 'react';
import { import {
AlertCircle, AlertCircle,
Calendar, Calendar,
CheckCircle, CheckCircle,
ChevronDown, ChevronDown,
ClipboardCheck,
Clock, Clock,
FileText,
GitBranch, GitBranch,
Info, Info,
Lock, Lock,
@ -14,7 +17,12 @@ import {
XCircle, XCircle,
Zap, Zap,
} from 'lucide-react'; } from 'lucide-react';
import { RequestDocumentsModal } from './RequestDocumentsModal';
import { cn, formatDateTime } from '@/components/ui/utils'; import { cn, formatDateTime } from '@/components/ui/utils';
import {
getRequestStatusBadgeSolidClass,
getStatusProgressBarClass,
} from '@/lib/statusProgressTheme';
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
@ -46,6 +54,14 @@ interface ApplicationDetailsSidebarProps {
currentUser: any; currentUser: any;
handleGenerateDealerCodes: () => void; handleGenerateDealerCodes: () => void;
onOpenAssignArchitectureModal: () => void; onOpenAssignArchitectureModal: () => void;
onOpenAssignFdd: () => void;
showAssignFddModal: boolean;
setShowAssignFddModal: (value: boolean) => void;
fddAgencies: any[];
selectedAgencyId: string;
setSelectedAgencyId: (value: string) => void;
isAssigningAgency: boolean;
handleAssignAgency: () => void;
activeInterviewForUser: any; activeInterviewForUser: any;
hasSubmittedFeedback: boolean; hasSubmittedFeedback: boolean;
setSelectedInterviewForFeedback: (value: any) => void; setSelectedInterviewForFeedback: (value: any) => void;
@ -62,8 +78,41 @@ interface ApplicationDetailsSidebarProps {
setParticipantType: (value: string) => void; setParticipantType: (value: string) => void;
handleAddParticipant: () => void; handleAddParticipant: () => void;
isAssigningParticipant: boolean; isAssigningParticipant: boolean;
documents?: any[];
documentConfigs?: any[];
} }
// Statuses where the admin can request supporting documents from the prospect (post-LOI approval).
const REQUEST_DOCUMENTS_ALLOWED_STATUSES = new Set<string>([
'Security Deposit',
'Security Details',
'Payment Pending',
'LOI Issuance Pending',
'LOI Issued',
'Dealer Code Generation',
'Architecture Team Assigned',
'Architecture Document Upload',
'Architecture Team Completion',
'Statutory GST',
'Statutory PAN',
'Statutory Nodal',
'Statutory Check',
'Statutory Partnership',
'Statutory Firm Reg',
'Statutory Virtual Code',
'Statutory Domain',
'Statutory MSD',
'Statutory LOI Ack',
'LOA Pending',
]);
const REQUEST_DOCUMENTS_ALLOWED_ROLES = new Set<string>([
'DD Admin',
'Super Admin',
'DD Lead',
'DD Head',
]);
export function ApplicationDetailsSidebar(props: ApplicationDetailsSidebarProps) { export function ApplicationDetailsSidebar(props: ApplicationDetailsSidebarProps) {
const { const {
application, application,
@ -80,6 +129,14 @@ export function ApplicationDetailsSidebar(props: ApplicationDetailsSidebarProps)
currentUser, currentUser,
handleGenerateDealerCodes, handleGenerateDealerCodes,
onOpenAssignArchitectureModal, onOpenAssignArchitectureModal,
onOpenAssignFdd,
showAssignFddModal,
setShowAssignFddModal,
fddAgencies,
selectedAgencyId,
setSelectedAgencyId,
isAssigningAgency,
handleAssignAgency,
activeInterviewForUser, activeInterviewForUser,
hasSubmittedFeedback, hasSubmittedFeedback,
setSelectedInterviewForFeedback, setSelectedInterviewForFeedback,
@ -96,8 +153,18 @@ export function ApplicationDetailsSidebar(props: ApplicationDetailsSidebarProps)
setParticipantType, setParticipantType,
handleAddParticipant, handleAddParticipant,
isAssigningParticipant, isAssigningParticipant,
documents = [],
documentConfigs = [],
} = props; } = props;
const [showRequestDocsModal, setShowRequestDocsModal] = useState(false);
const userRoleResolved =
currentUser?.roleCode || currentUser?.role || '';
const canRequestDocuments =
REQUEST_DOCUMENTS_ALLOWED_ROLES.has(userRoleResolved) &&
REQUEST_DOCUMENTS_ALLOWED_STATUSES.has(application?.status || '');
return ( return (
<div className="space-y-6"> <div className="space-y-6">
<Card data-testid="onboarding-details-summary-card"> <Card data-testid="onboarding-details-summary-card">
@ -111,13 +178,8 @@ export function ApplicationDetailsSidebar(props: ApplicationDetailsSidebarProps)
</div> </div>
<div> <div>
<p className="text-slate-600">Current Status</p> <p className="text-slate-600">Current Status</p>
<Badge <Badge
className={cn( className={cn('mt-1', getRequestStatusBadgeSolidClass(application.status))}
"mt-1",
application.status === 'Onboarded' ? "bg-green-600 hover:bg-green-700 text-white" :
application.status === 'Rejected' ? "bg-red-600" :
"bg-amber-600"
)}
data-testid="onboarding-details-summary-status" data-testid="onboarding-details-summary-status"
> >
{application.status} {application.status}
@ -135,7 +197,12 @@ export function ApplicationDetailsSidebar(props: ApplicationDetailsSidebarProps)
<div> <div>
<p className="text-slate-600">Progress</p> <p className="text-slate-600">Progress</p>
<div className="flex items-center gap-2 mt-2"> <div className="flex items-center gap-2 mt-2">
<Progress value={application.progress} className="flex-1" data-testid="onboarding-details-summary-progress-bar" /> <Progress
value={application.progress}
className="flex-1 bg-red-50"
indicatorClassName={getStatusProgressBarClass(application.status)}
data-testid="onboarding-details-summary-progress-bar"
/>
<span className="text-slate-900" data-testid="onboarding-details-summary-progress-text">{application.progress}%</span> <span className="text-slate-900" data-testid="onboarding-details-summary-progress-text">{application.progress}%</span>
</div> </div>
</div> </div>
@ -155,12 +222,12 @@ export function ApplicationDetailsSidebar(props: ApplicationDetailsSidebarProps)
</CardHeader> </CardHeader>
<CardContent className="space-y-3"> <CardContent className="space-y-3">
{permissions.isLoaLocked && ( {permissions.isLoaLocked && (
<Alert variant="destructive" className="mb-4 bg-amber-50 border-amber-200 text-amber-800" data-testid="onboarding-details-loa-locked-alert"> <Alert variant="destructive" className="mb-4 bg-red-50 border-red-200 text-red-800" data-testid="onboarding-details-loa-locked-alert">
<Lock className="w-4 h-4 text-amber-600" /> <Lock className="w-4 h-4 text-re-red" />
<AlertTitle className="text-amber-900 font-semibold">LOA approval locked</AlertTitle> <AlertTitle className="text-red-900 font-semibold">LOA approval locked</AlertTitle>
<AlertDescription className="text-amber-800"> <AlertDescription className="text-red-800">
<span className="font-medium">First Fill</span> (later-stage payment) must be verified by Finance <span className="font-medium">First Fill</span> (later-stage payment) must be verified by Finance
before LOA approval can proceed. This is separate from the initial security deposit before LOI Issued. before LOA approval can proceed. This is separate from the initial Security Deposit before LOI Issued.
</AlertDescription> </AlertDescription>
</Alert> </Alert>
)} )}
@ -177,29 +244,29 @@ export function ApplicationDetailsSidebar(props: ApplicationDetailsSidebarProps)
Finance has verified the <span className="font-medium">First Fill</span> payment. The application Finance has verified the <span className="font-medium">First Fill</span> payment. The application
status was <span className="font-medium">not</span> changed until you reach{' '} status was <span className="font-medium">not</span> changed until you reach{' '}
<span className="font-medium">LOA Pending</span>. When you get there, LOA approval will not be <span className="font-medium">LOA Pending</span>. When you get there, LOA approval will not be
blocked by payment (same pattern as recording the initial security deposit before the LOI blocked by payment (same pattern as recording the initial Security Deposit before the LOI
security step). security step).
</AlertDescription> </AlertDescription>
</Alert> </Alert>
)} )}
{permissions.isSecurityDetailsLocked && ( {permissions.isSecurityDetailsLocked && (
<Alert variant="destructive" className="mb-4 bg-amber-50 border-amber-200 text-amber-800" data-testid="onboarding-details-security-locked-alert"> <Alert variant="destructive" className="mb-4 bg-red-50 border-red-200 text-red-800" data-testid="onboarding-details-security-locked-alert">
<Lock className="w-4 h-4 text-amber-600" /> <Lock className="w-4 h-4 text-re-red" />
<AlertTitle className="text-amber-900 font-semibold">Security Details approval locked</AlertTitle> <AlertTitle className="text-red-900 font-semibold">Security Deposit approval locked</AlertTitle>
<AlertDescription className="text-amber-800"> <AlertDescription className="text-red-800">
Finance must verify the <span className="font-medium">Security Deposit</span> before this stage can be approved. Finance must verify the <span className="font-medium">Security Deposit</span> before this stage can be approved.
You can still use <span className="font-medium">Reject</span> if needed. You can still use <span className="font-medium">Reject</span> if needed.
</AlertDescription> </AlertDescription>
</Alert> </Alert>
)} )}
{['Security Details', 'Payment Pending'].includes(application.status) && ( {['Security Deposit', 'Security Details', 'Payment Pending'].includes(application.status) && (
<Alert className="mb-4 border-sky-200 bg-sky-50/90 text-sky-900" data-testid="onboarding-details-security-review-alert"> <Alert className="mb-4 border-sky-200 bg-sky-50/90 text-sky-900" data-testid="onboarding-details-security-review-alert">
<Info className="h-4 w-4 text-sky-700" /> <Info className="h-4 w-4 text-sky-700" />
<AlertTitle className="text-sky-950 font-semibold">Security Details review</AlertTitle> <AlertTitle className="text-sky-950 font-semibold">Security Deposit review</AlertTitle>
<AlertDescription className="text-sm text-sky-900/90 leading-relaxed"> <AlertDescription className="text-sm text-sky-900/90 leading-relaxed">
Check the initial security deposit on the <span className="font-medium">Payments</span> tab (Finance Check the initial Security Deposit on the <span className="font-medium">Payments</span> tab (Finance
may have already marked it verified). When satisfied, use <span className="font-medium">Approve</span>{' '} may have already marked it verified). When satisfied, use <span className="font-medium">Approve</span>{' '}
to move to <span className="font-medium">LOI Issued</span>. to move to <span className="font-medium">LOI Issued</span>.
</AlertDescription> </AlertDescription>
@ -217,10 +284,10 @@ export function ApplicationDetailsSidebar(props: ApplicationDetailsSidebarProps)
)} )}
{isAdmin && (application.status === 'Level 3 Approved' || application.status === 'FDD Verification') && (!application.fddAssignments || application.fddAssignments.length === 0) && ( {isAdmin && (application.status === 'Level 3 Approved' || application.status === 'FDD Verification') && (!application.fddAssignments || application.fddAssignments.length === 0) && (
<Alert className="mb-4 bg-amber-50 border-amber-200 text-amber-800" data-testid="onboarding-details-fdd-assignment-alert"> <Alert className="mb-4 bg-red-50 border-red-200 text-red-800" data-testid="onboarding-details-fdd-assignment-alert">
<AlertCircle className="w-4 h-4 text-amber-600" /> <AlertCircle className="w-4 h-4 text-re-red" />
<AlertTitle className="text-amber-900 font-bold">FDD Assignment Required</AlertTitle> <AlertTitle className="text-red-900 font-bold">FDD Assignment Required</AlertTitle>
<AlertDescription className="text-amber-800 font-medium"> <AlertDescription className="text-red-800 font-medium">
This application is pending financial due diligence. Please assign an FDD Agency to proceed with the audit. This application is pending financial due diligence. Please assign an FDD Agency to proceed with the audit.
</AlertDescription> </AlertDescription>
</Alert> </Alert>
@ -263,11 +330,23 @@ export function ApplicationDetailsSidebar(props: ApplicationDetailsSidebarProps)
</Button> </Button>
)} )}
{canRequestDocuments && (
<Button
variant="outline"
className="w-full border-amber-300 hover:bg-amber-50 text-amber-700"
onClick={() => setShowRequestDocsModal(true)}
data-testid="onboarding-details-request-documents"
>
<FileText className="w-4 h-4 mr-2" />
Request Documents
</Button>
)}
{currentUser && ['DD Admin', 'Super Admin'].includes(currentUser.role) && {currentUser && ['DD Admin', 'Super Admin'].includes(currentUser.role) &&
['Dealer Code Generation', 'LOA Pending', 'Architecture Team Assigned', 'Architecture Document Upload', 'Architecture Team Completion'].includes(application.status) && ( ['Dealer Code Generation', 'LOA Pending', 'Architecture Team Assigned', 'Architecture Document Upload', 'Architecture Team Completion'].includes(application.status) && (
<> <>
{!application.dealerCode && ( {!application.dealerCode && (
<Button className="w-full bg-blue-600 hover:bg-blue-700" onClick={handleGenerateDealerCodes} data-testid="onboarding-details-generate-dealer-codes"> <Button className="w-full bg-re-red hover:bg-re-red-hover" onClick={handleGenerateDealerCodes} data-testid="onboarding-details-generate-dealer-codes">
<Zap className="w-4 h-4 mr-2" /> <Zap className="w-4 h-4 mr-2" />
Generate Dealer Codes Generate Dealer Codes
</Button> </Button>
@ -276,7 +355,7 @@ export function ApplicationDetailsSidebar(props: ApplicationDetailsSidebarProps)
{application.dealerCode && !application.architectureAssignedTo && ( {application.dealerCode && !application.architectureAssignedTo && (
<Button <Button
variant="outline" variant="outline"
className="w-full border-blue-200 hover:bg-blue-50 text-blue-700" className="w-full border-red-200 hover:bg-red-50 text-re-red"
onClick={onOpenAssignArchitectureModal} onClick={onOpenAssignArchitectureModal}
data-testid="onboarding-details-assign-architecture" data-testid="onboarding-details-assign-architecture"
> >
@ -287,6 +366,55 @@ export function ApplicationDetailsSidebar(props: ApplicationDetailsSidebarProps)
</> </>
)} )}
{isAdmin &&
['Level 3 Approved', 'Level 3 Recommended', 'FDD Verification', 'FDD In Progress'].includes(application.status) &&
(!application.fddAssignments || application.fddAssignments.length === 0) && (
<Dialog open={showAssignFddModal} onOpenChange={setShowAssignFddModal}>
<Button
variant="outline"
className="w-full border-purple-200 hover:bg-purple-50 text-purple-700"
onClick={onOpenAssignFdd}
data-testid="onboarding-details-assign-fdd"
>
<ClipboardCheck className="w-4 h-4 mr-2" />
Assign FDD
</Button>
<DialogContent data-testid="onboarding-details-assign-fdd-modal">
<DialogHeader>
<DialogTitle>Assign FDD Agency</DialogTitle>
<DialogDescription>
Select an FDD partner agency to perform the financial due diligence audit for this application.
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div>
<Label>FDD Agency</Label>
<Select value={selectedAgencyId} onValueChange={setSelectedAgencyId}>
<SelectTrigger className="mt-2" data-testid="onboarding-details-assign-fdd-select">
<SelectValue placeholder={fddAgencies?.length ? 'Choose partner agency...' : 'No agencies available'} />
</SelectTrigger>
<SelectContent>
{(fddAgencies || []).map((agency: any) => (
<SelectItem key={agency.id} value={agency.id}>
{agency.fullName || agency.name} ({agency.email})
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<Button
className="w-full bg-re-red hover:bg-re-red-hover font-bold h-11"
onClick={handleAssignAgency}
disabled={isAssigningAgency || !selectedAgencyId}
data-testid="onboarding-details-assign-fdd-submit"
>
{isAssigningAgency ? 'Assigning...' : 'Assign Agency'}
</Button>
</div>
</DialogContent>
</Dialog>
)}
{activeInterviewForUser && !hasSubmittedFeedback && ( {activeInterviewForUser && !hasSubmittedFeedback && (
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
@ -394,7 +522,7 @@ export function ApplicationDetailsSidebar(props: ApplicationDetailsSidebarProps)
</Select> </Select>
</div> </div>
<Button <Button
className="w-full bg-amber-600 hover:bg-amber-700 font-bold h-11" className="w-full bg-re-red hover:bg-re-red-hover font-bold h-11"
onClick={handleAddParticipant} onClick={handleAddParticipant}
disabled={isAssigningParticipant} disabled={isAssigningParticipant}
data-testid="onboarding-details-assign-user-submit" data-testid="onboarding-details-assign-user-submit"
@ -408,6 +536,15 @@ export function ApplicationDetailsSidebar(props: ApplicationDetailsSidebarProps)
</CardContent> </CardContent>
</Card> </Card>
)} )}
<RequestDocumentsModal
open={showRequestDocsModal}
onClose={() => setShowRequestDocsModal(false)}
applicationId={application?.id || ''}
applicantName={application?.name || application?.applicantName || 'the prospect'}
documentConfigs={documentConfigs}
uploadedDocuments={documents}
/>
</div> </div>
); );
} }

View File

@ -1,3 +1,4 @@
import { useEffect } from 'react';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { import {
AlertCircle, AlertCircle,
@ -9,6 +10,7 @@ import {
Clock, Clock,
ClipboardList, ClipboardList,
Download, Download,
Eye,
FileText, FileText,
GitBranch, GitBranch,
Lock, Lock,
@ -18,6 +20,12 @@ import {
User, User,
} from 'lucide-react'; } from 'lucide-react';
import { cn, formatDateTime } from '@/components/ui/utils'; import { cn, formatDateTime } from '@/components/ui/utils';
import {
getPercentProgressBadgeSolidClass,
getPercentProgressBarClass,
getStatusProgressBadgeSolidClass,
getStatusProgressBarClass,
} from '@/lib/statusProgressTheme';
import QuestionnaireResponseView from '../QuestionnaireResponseView'; import QuestionnaireResponseView from '../QuestionnaireResponseView';
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'; import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
@ -49,7 +57,7 @@ interface ApplicationDetailsTabsProps {
setShowDocumentsModal: (value: boolean) => void; setShowDocumentsModal: (value: boolean) => void;
setShowUploadForm: (value: boolean) => void; setShowUploadForm: (value: boolean) => void;
handleRetriggerEvaluators: () => void; handleRetriggerEvaluators: () => void;
handleCancelInterview: (interviewId: any) => void; handleRescheduleInterview: (interview: any) => void;
setSelectedEvaluationForView: (value: any) => void; setSelectedEvaluationForView: (value: any) => void;
setShowFeedbackDetailsModal: (value: boolean) => void; setShowFeedbackDetailsModal: (value: boolean) => void;
renderFddAuditContent: () => React.ReactNode; renderFddAuditContent: () => React.ReactNode;
@ -58,6 +66,11 @@ interface ApplicationDetailsTabsProps {
eorChecklist: any[]; eorChecklist: any[];
setUploadDocType: (value: string) => void; setUploadDocType: (value: string) => void;
isAdmin: boolean; isAdmin: boolean;
/**
* Whether the viewer may see finance/process-sensitive tabs (FDD Audit
* and Payments). Restricted to DD-Admin / Super Admin by policy.
*/
canViewFinanceTabs: boolean;
fetchApplication: () => void; fetchApplication: () => void;
fetchEorData: () => void; fetchEorData: () => void;
deposits: any[]; deposits: any[];
@ -84,7 +97,7 @@ export function ApplicationDetailsTabs(props: ApplicationDetailsTabsProps) {
setShowDocumentsModal, setShowDocumentsModal,
setShowUploadForm, setShowUploadForm,
handleRetriggerEvaluators, handleRetriggerEvaluators,
handleCancelInterview, handleRescheduleInterview,
setSelectedEvaluationForView, setSelectedEvaluationForView,
setShowFeedbackDetailsModal, setShowFeedbackDetailsModal,
renderFddAuditContent, renderFddAuditContent,
@ -93,6 +106,7 @@ export function ApplicationDetailsTabs(props: ApplicationDetailsTabsProps) {
eorChecklist, eorChecklist,
setUploadDocType, setUploadDocType,
isAdmin, isAdmin,
canViewFinanceTabs,
fetchApplication, fetchApplication,
fetchEorData, fetchEorData,
deposits, deposits,
@ -105,6 +119,15 @@ export function ApplicationDetailsTabs(props: ApplicationDetailsTabsProps) {
auditLogActionBadgeClass, auditLogActionBadgeClass,
} = props; } = props;
// If the viewer loses (or never had) access to FDD/Payments tabs but the
// active tab is one of them — e.g. via direct deep-link or stale state —
// bounce them back to the Progress tab so they don't see an empty pane.
useEffect(() => {
if (!canViewFinanceTabs && (activeTab === 'fdd' || activeTab === 'payments')) {
setActiveTab('progress');
}
}, [canViewFinanceTabs, activeTab, setActiveTab]);
const normalizeRole = (value: unknown): string => const normalizeRole = (value: unknown): string =>
String(value || '') String(value || '')
.trim() .trim()
@ -125,15 +148,19 @@ export function ApplicationDetailsTabs(props: ApplicationDetailsTabsProps) {
<Card data-testid="onboarding-details-tabs-container"> <Card data-testid="onboarding-details-tabs-container">
<Tabs value={activeTab} onValueChange={setActiveTab}> <Tabs value={activeTab} onValueChange={setActiveTab}>
<CardHeader className="pb-4 px-4 sm:px-6"> <CardHeader className="pb-4 px-4 sm:px-6">
<div className="overflow-x-auto custom-scrollbar-x -mx-4 px-4 sm:-mx-6 sm:px-6"> <div className="overflow-x-auto custom-scrollbar-x-slim -mx-4 px-4 sm:-mx-6 sm:px-6">
<TabsList className="w-max min-w-full justify-start h-11 bg-slate-100/80 p-1" data-testid="onboarding-tabs-list"> <TabsList className="w-max min-w-full justify-start h-11 bg-slate-100/80 p-1" data-testid="onboarding-tabs-list">
<TabsTrigger value="questionnaire" className="min-w-[120px]" data-testid="onboarding-tab-trigger-questionnaire">Questionnaire</TabsTrigger> <TabsTrigger value="questionnaire" className="min-w-[120px]" data-testid="onboarding-tab-trigger-questionnaire">Questionnaire</TabsTrigger>
<TabsTrigger value="progress" className="min-w-[80px]" data-testid="onboarding-tab-trigger-progress">Progress</TabsTrigger> <TabsTrigger value="progress" className="min-w-[80px]" data-testid="onboarding-tab-trigger-progress">Progress</TabsTrigger>
<TabsTrigger value="documents" className="min-w-[100px]" data-testid="onboarding-tab-trigger-documents">Documents</TabsTrigger> <TabsTrigger value="documents" className="min-w-[100px]" data-testid="onboarding-tab-trigger-documents">Documents</TabsTrigger>
<TabsTrigger value="interviews" className="min-w-[100px]" data-testid="onboarding-tab-trigger-interviews">Interviews</TabsTrigger> <TabsTrigger value="interviews" className="min-w-[100px]" data-testid="onboarding-tab-trigger-interviews">Interviews</TabsTrigger>
<TabsTrigger value="fdd" className="min-w-[120px]" data-testid="onboarding-tab-trigger-fdd">FDD Audit</TabsTrigger> {canViewFinanceTabs && (
<TabsTrigger value="fdd" className="min-w-[120px]" data-testid="onboarding-tab-trigger-fdd">FDD Audit</TabsTrigger>
)}
<TabsTrigger value="eor" className="min-w-[120px]" data-testid="onboarding-tab-trigger-eor">EOR Checklist</TabsTrigger> <TabsTrigger value="eor" className="min-w-[120px]" data-testid="onboarding-tab-trigger-eor">EOR Checklist</TabsTrigger>
<TabsTrigger value="payments" className="min-w-[100px]" data-testid="onboarding-tab-trigger-payments">Payments</TabsTrigger> {canViewFinanceTabs && (
<TabsTrigger value="payments" className="min-w-[100px]" data-testid="onboarding-tab-trigger-payments">Payments</TabsTrigger>
)}
<TabsTrigger value="audit" className="min-w-[100px]" data-testid="onboarding-tab-trigger-audit">Audit Trail</TabsTrigger> <TabsTrigger value="audit" className="min-w-[100px]" data-testid="onboarding-tab-trigger-audit">Audit Trail</TabsTrigger>
</TabsList> </TabsList>
</div> </div>
@ -148,12 +175,17 @@ export function ApplicationDetailsTabs(props: ApplicationDetailsTabsProps) {
<div> <div>
<div className="flex items-center justify-between mb-4"> <div className="flex items-center justify-between mb-4">
<h3 className="text-slate-900">Application Journey</h3> <h3 className="text-slate-900">Application Journey</h3>
<Badge className="bg-amber-600" data-testid="onboarding-progress-percentage-badge">{application.progress}% Complete</Badge> <Badge className={getStatusProgressBadgeSolidClass(application.status)} data-testid="onboarding-progress-percentage-badge">{application.progress}% Complete</Badge>
</div> </div>
<Progress value={application.progress} className="h-3 mb-6" data-testid="onboarding-progress-bar" /> <Progress
value={application.progress}
className="h-3 mb-6 bg-status-progress-soft"
indicatorClassName={getStatusProgressBarClass(application.status)}
data-testid="onboarding-progress-bar"
/>
</div> </div>
<div className="relative" data-testid="onboarding-progress-stages-container"> <div className="relative status-progress-ui" data-testid="onboarding-progress-stages-container">
{(() => { {(() => {
const interviewRoleMap: Record<number, string[]> = { const interviewRoleMap: Record<number, string[]> = {
1: ['DD-ZM', 'RBM'], 1: ['DD-ZM', 'RBM'],
@ -235,7 +267,7 @@ export function ApplicationDetailsTabs(props: ApplicationDetailsTabsProps) {
<div className={cn( <div className={cn(
"absolute -top-0.5 -right-0.5 w-2 h-2 rounded-full border border-white", "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" approver.status === 'approved' ? "bg-green-500" : approver.status === 'rejected' ? "bg-red-500" : "bg-status-progress"
)} data-testid={`onboarding-stage-approver-status-dot-${stageIndex}-${i}`} /> )} data-testid={`onboarding-stage-approver-status-dot-${stageIndex}-${i}`} />
<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"> <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">
@ -254,7 +286,7 @@ export function ApplicationDetailsTabs(props: ApplicationDetailsTabsProps) {
<div className={`w-10 h-10 rounded-full flex items-center justify-center border-2 z-10 relative ${stage.status === 'completed' <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-md' ? 'bg-green-500 border-green-500 text-white shadow-md'
: stage.status === 'active' : stage.status === 'active'
? stage.isLocked ? 'bg-slate-400 border-slate-400 text-white' : 'bg-amber-500 border-amber-500 text-white animate-pulse-subtle' ? stage.isLocked ? 'bg-slate-400 border-slate-400 text-white' : 'bg-status-workflow-active border-status-workflow-active text-white animate-pulse-subtle'
: 'bg-white border-slate-300 text-slate-400 shadow-none' : 'bg-white border-slate-300 text-slate-400 shadow-none'
}`} data-testid={`onboarding-progress-stage-icon-${index}`}> }`} data-testid={`onboarding-progress-stage-icon-${index}`}>
{stage.isParallel ? ( {stage.isParallel ? (
@ -264,7 +296,7 @@ export function ApplicationDetailsTabs(props: ApplicationDetailsTabsProps) {
<Lock className="w-5 h-5 text-white cursor-help" /> <Lock className="w-5 h-5 text-white cursor-help" />
<div className="absolute bottom-full left-1/2 -translate-x-1/2 mb-2 px-3 py-1.5 bg-slate-900 text-white text-[10px] rounded shadow-xl opacity-0 group-hover:opacity-100 pointer-events-none transition-all duration-200 whitespace-nowrap z-[100] border border-slate-700"> <div className="absolute bottom-full left-1/2 -translate-x-1/2 mb-2 px-3 py-1.5 bg-slate-900 text-white text-[10px] rounded shadow-xl opacity-0 group-hover:opacity-100 pointer-events-none transition-all duration-200 whitespace-nowrap z-[100] border border-slate-700">
<div className="flex flex-col gap-1"> <div className="flex flex-col gap-1">
<span className="font-bold text-amber-400 flex items-center gap-1"> <span className="font-bold text-status-progress flex items-center gap-1">
<AlertCircle className="w-3 h-3" /> Stage Locked <AlertCircle className="w-3 h-3" /> Stage Locked
</span> </span>
<span>{stage.lockMessage}</span> <span>{stage.lockMessage}</span>
@ -292,7 +324,7 @@ export function ApplicationDetailsTabs(props: ApplicationDetailsTabsProps) {
<div className="flex-1 pt-1"> <div className="flex-1 pt-1">
<p className={cn( <p className={cn(
"font-bold transition-colors", "font-bold transition-colors",
stage.status === 'completed' ? "text-green-700" : stage.status === 'active' ? "text-amber-700" : "text-slate-900" stage.status === 'completed' ? "text-green-700" : stage.status === 'active' ? "text-status-progress-muted" : "text-slate-900"
)} data-testid={`onboarding-progress-stage-name-${index}`}>{stage.name}</p> )} data-testid={`onboarding-progress-stage-name-${index}`}>{stage.name}</p>
{stage.description && ( {stage.description && (
<p className="text-slate-600 text-sm mt-0.5 leading-relaxed" data-testid={`onboarding-progress-stage-desc-${index}`}>{stage.description}</p> <p className="text-slate-600 text-sm mt-0.5 leading-relaxed" data-testid={`onboarding-progress-stage-desc-${index}`}>{stage.description}</p>
@ -301,7 +333,7 @@ export function ApplicationDetailsTabs(props: ApplicationDetailsTabsProps) {
{renderApprovers(stage.name as string, index)} {renderApprovers(stage.name as string, index)}
{stage.evaluators && stage.evaluators.length > 0 && !['LOI Approval', 'LOA', '1st Level Interview', '2nd Level Interview', '3rd Level Interview'].includes(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" data-testid={`onboarding-progress-stage-evaluators-${index}`}> <p className="text-status-progress text-xs mt-1.5 flex items-center gap-1 bg-status-progress-soft w-fit px-2 py-0.5 rounded border border-status-progress" data-testid={`onboarding-progress-stage-evaluators-${index}`}>
<User className="w-3 h-3" /> <User className="w-3 h-3" />
Evaluators: {stage.evaluators.join(' + ')} Evaluators: {stage.evaluators.join(' + ')}
</p> </p>
@ -314,7 +346,7 @@ export function ApplicationDetailsTabs(props: ApplicationDetailsTabsProps) {
5: 2, 5: 2,
6: 2, 6: 2,
8: 2, 8: 2,
12: 2 13: 2
}; };
const stageId = Number(stage.id); const stageId = Number(stage.id);
const expectedCount = expectedMap[stageId]; const expectedCount = expectedMap[stageId];
@ -325,7 +357,7 @@ export function ApplicationDetailsTabs(props: ApplicationDetailsTabsProps) {
5: 2, 5: 2,
6: 3, 6: 3,
8: 'LOI_APPROVAL', 8: 'LOI_APPROVAL',
12: 'LOA_APPROVAL', 13: 'LOA_APPROVAL',
}; };
const mappedStageCode = stageCodeById[stageId]; const mappedStageCode = stageCodeById[stageId];
const actualCount = mappedStageCode ? getApproverStatus(mappedStageCode).length : (stage.evaluators?.length || 0); const actualCount = mappedStageCode ? getApproverStatus(mappedStageCode).length : (stage.evaluators?.length || 0);
@ -335,8 +367,8 @@ export function ApplicationDetailsTabs(props: ApplicationDetailsTabsProps) {
if (expectedCount && actualCount < expectedCount && application.status !== 'Rejected' && isEligibleForWarning) { if (expectedCount && actualCount < expectedCount && application.status !== 'Rejected' && isEligibleForWarning) {
return ( return (
<div className="mt-2" data-testid={`onboarding-progress-stage-warning-${index}`}> <div className="mt-2" data-testid={`onboarding-progress-stage-warning-${index}`}>
<Alert variant="destructive" className="py-2 px-3 border-amber-200 bg-amber-50 text-amber-800"> <Alert variant="destructive" className="py-2 px-3 border-red-200 bg-red-50 text-red-800">
<AlertCircle className="h-4 w-4 text-amber-600" /> <AlertCircle className="h-4 w-4 text-re-red" />
<AlertTitle className="text-xs font-semibold">Missing Evaluators</AlertTitle> <AlertTitle className="text-xs font-semibold">Missing Evaluators</AlertTitle>
<AlertDescription className="text-xs"> <AlertDescription className="text-xs">
{actualCount === 0 {actualCount === 0
@ -346,7 +378,7 @@ export function ApplicationDetailsTabs(props: ApplicationDetailsTabsProps) {
<Button <Button
variant="link" variant="link"
size="sm" size="sm"
className="h-auto p-0 ml-1 text-xs text-amber-700 underline" className="h-auto p-0 ml-1 text-xs text-re-red-hover underline"
onClick={handleRetriggerEvaluators} onClick={handleRetriggerEvaluators}
data-testid={`onboarding-progress-stage-retrigger-${index}`} data-testid={`onboarding-progress-stage-retrigger-${index}`}
> >
@ -367,13 +399,20 @@ export function ApplicationDetailsTabs(props: ApplicationDetailsTabsProps) {
(!doc.stage && doc.documentType?.toLowerCase().includes(stage.name.toLowerCase().split(' ')[0])) (!doc.stage && doc.documentType?.toLowerCase().includes(stage.name.toLowerCase().split(' ')[0]))
).length; ).length;
// Upload is allowed only on the currently active (and unlocked) stage.
const canUploadHere = stage.status === 'active' && !stage.isLocked;
if (stageDocsCount === 0 && !canUploadHere) {
return null;
}
return ( return (
<div className="flex items-center gap-2 mt-1"> <div className="flex items-center gap-2 mt-1">
<button <button
onClick={() => { onClick={() => {
setSelectedStage(stage.name); setSelectedStage(stage.name);
setShowDocumentsModal(true); setShowDocumentsModal(true);
if (stageDocsCount === 0) setShowUploadForm(true); if (stageDocsCount === 0 && canUploadHere) setShowUploadForm(true);
}} }}
className="text-xs font-semibold text-blue-600 hover:text-blue-800 flex items-center gap-1.5 px-3 py-1 rounded-full bg-blue-50 border border-blue-100 hover:bg-blue-100 transition-all shadow-sm" className="text-xs font-semibold text-blue-600 hover:text-blue-800 flex items-center gap-1.5 px-3 py-1 rounded-full bg-blue-50 border border-blue-100 hover:bg-blue-100 transition-all shadow-sm"
data-testid={`onboarding-progress-stage-docs-${index}`} data-testid={`onboarding-progress-stage-docs-${index}`}
@ -452,7 +491,7 @@ export function ApplicationDetailsTabs(props: ApplicationDetailsTabsProps) {
<div className={`w-8 h-8 rounded-full flex items-center justify-center border-2 ${isDone <div className={`w-8 h-8 rounded-full flex items-center justify-center border-2 ${isDone
? `${branchColor === 'blue' ? 'bg-blue-500 border-blue-500' : 'bg-green-500 border-green-500'}` ? `${branchColor === 'blue' ? 'bg-blue-500 border-blue-500' : 'bg-green-500 border-green-500'}`
: branchStage.status === 'active' : branchStage.status === 'active'
? 'bg-amber-500 border-amber-500 text-white shadow-sm' ? 'bg-status-workflow-active border-status-workflow-active text-white shadow-sm'
: 'bg-white border-slate-300 text-slate-400' : 'bg-white border-slate-300 text-slate-400'
}`} data-testid={`onboarding-progress-branch-stage-icon-${branchKey}-${bsIdx}`}> }`} data-testid={`onboarding-progress-branch-stage-icon-${branchKey}-${bsIdx}`}>
{isDone ? ( {isDone ? (
@ -470,25 +509,34 @@ export function ApplicationDetailsTabs(props: ApplicationDetailsTabsProps) {
<p className="text-slate-500 text-xs mt-0.5">{branchStage.description}</p> <p className="text-slate-500 text-xs mt-0.5">{branchStage.description}</p>
)} )}
<div className="flex items-center gap-2 mt-1"> {(() => {
<button // Upload is allowed only on the currently active branch stage.
onClick={() => { const canUploadHere = branchStage.status === 'active';
setSelectedStage(branchStage.name); if (stageDocs.length === 0 && !canUploadHere) {
setShowDocumentsModal(true); return null;
if (stageDocs.length === 0) setShowUploadForm(true); }
}} return (
className={cn( <div className="flex items-center gap-2 mt-1">
"text-[10px] font-medium flex items-center gap-1 transition-colors", <button
branchColor === 'blue' ? "text-blue-600 hover:text-blue-800" : "text-green-600 hover:text-green-800" onClick={() => {
)} setSelectedStage(branchStage.name);
data-testid={`onboarding-progress-branch-stage-docs-${branchKey}-${bsIdx}`} setShowDocumentsModal(true);
> if (stageDocs.length === 0 && canUploadHere) setShowUploadForm(true);
<FileText className="w-2.5 h-2.5" /> }}
{stageDocs.length > 0 ? `${stageDocs.length} Docs` : 'Upload'} className={cn(
</button> "text-[10px] font-medium flex items-center gap-1 transition-colors",
</div> branchColor === 'blue' ? "text-blue-600 hover:text-blue-800" : "text-green-600 hover:text-green-800"
)}
data-testid={`onboarding-progress-branch-stage-docs-${branchKey}-${bsIdx}`}
>
<FileText className="w-2.5 h-2.5" />
{stageDocs.length > 0 ? `${stageDocs.length} Docs` : 'Upload'}
</button>
</div>
);
})()}
<p className="text-slate-400 text-[10px] mt-1" data-testid={`onboarding-progress-branch-stage-status-${branchKey}-${bsIdx}`}> <p className="text-slate-400 text-[10px] mt-1" data-testid={`onboarding-progress-branch-stage-status-${branchKey}-${bsIdx}`}>
{isDone && branchStage.date ? `Done: ${formatDateTime(branchStage.date)}` : isDone && stageDocs.length > 0 ? `Uploaded: ${formatDateTime(stageDocs[0].updatedAt || stageDocs[0].createdAt)}` : branchStage.status === 'active' ? 'Evaluating' : 'Pending'} {isDone && branchStage.date ? `Done: ${formatDateTime(branchStage.date)}` : isDone && stageDocs.length > 0 ? `Uploaded: ${formatDateTime(stageDocs[0].updatedAt || stageDocs[0].createdAt)}` : 'Pending'}
</p> </p>
</div> </div>
</> </>
@ -514,7 +562,7 @@ export function ApplicationDetailsTabs(props: ApplicationDetailsTabsProps) {
<TabsContent value="documents" className="space-y-4" data-testid="onboarding-tab-content-documents"> <TabsContent value="documents" className="space-y-4" data-testid="onboarding-tab-content-documents">
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<h3 className="text-slate-900">Uploaded Documents</h3> <h3 className="text-slate-900">Uploaded Documents</h3>
<Button size="sm" className="bg-amber-600 hover:bg-amber-700" data-testid="onboarding-documents-upload-tab-button" onClick={() => { <Button size="sm" className="bg-re-red hover:bg-re-red-hover" data-testid="onboarding-documents-upload-tab-button" onClick={() => {
setSelectedStage(null); setSelectedStage(null);
setShowDocumentsModal(true); setShowDocumentsModal(true);
setShowUploadForm(true); setShowUploadForm(true);
@ -524,7 +572,7 @@ export function ApplicationDetailsTabs(props: ApplicationDetailsTabsProps) {
</Button> </Button>
</div> </div>
<div className="overflow-x-auto"> <div className="overflow-x-auto custom-scrollbar-x-slim">
<Table data-testid="onboarding-documents-table"> <Table data-testid="onboarding-documents-table">
<TableHeader> <TableHeader>
<TableRow> <TableRow>
@ -556,11 +604,17 @@ export function ApplicationDetailsTabs(props: ApplicationDetailsTabsProps) {
</TableCell> </TableCell>
<TableCell> <TableCell>
<div className="flex justify-end gap-2"> <div className="flex justify-end gap-2">
<Button size="sm" variant="outline" data-testid={`onboarding-document-download-${idx}`} onClick={() => { <Button size="sm" variant="outline" data-testid={`onboarding-document-preview-${idx}`} onClick={() => {
const baseUrl = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:5000'; setPreviewDoc(doc);
window.open(`${baseUrl}/${doc.filePath}`, '_blank'); setShowPreviewModal(true);
}}> }}>
<Download className="w-3 h-3" /> <Eye className="w-3 h-3 text-slate-500" />
</Button>
<Button size="sm" variant="outline" data-testid={`onboarding-document-download-${idx}`} onClick={() => {
const baseUrl = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:5000';
window.open(`${baseUrl}/${doc.filePath}`, '_blank');
}}>
<Download className="w-3 h-3 text-slate-500" />
</Button> </Button>
</div> </div>
</TableCell> </TableCell>
@ -574,7 +628,7 @@ export function ApplicationDetailsTabs(props: ApplicationDetailsTabsProps) {
<TabsContent value="interviews" className="space-y-6" data-testid="onboarding-tab-content-interviews"> <TabsContent value="interviews" className="space-y-6" data-testid="onboarding-tab-content-interviews">
<div> <div>
<h3 className="text-slate-900 mb-4">Scheduled Interviews</h3> <h3 className="text-slate-900 mb-4">Scheduled Interviews</h3>
<div className="overflow-x-auto"> <div className="overflow-x-auto custom-scrollbar-x-slim">
<Table data-testid="onboarding-interviews-scheduled-table"> <Table data-testid="onboarding-interviews-scheduled-table">
<TableHeader> <TableHeader>
<TableRow> <TableRow>
@ -620,11 +674,11 @@ export function ApplicationDetailsTabs(props: ApplicationDetailsTabsProps) {
<Button <Button
variant="ghost" variant="ghost"
size="sm" size="sm"
className="text-red-500 hover:text-red-700 hover:bg-red-50 h-8 px-2" className="text-primary-600 hover:text-primary-700 hover:bg-primary-50 h-8 px-2"
data-testid={`onboarding-interview-cancel-${idx}`} data-testid={`onboarding-interview-reschedule-${idx}`}
onClick={() => handleCancelInterview(interview.id)} onClick={() => handleRescheduleInterview(interview)}
> >
Cancel Reschedule
</Button> </Button>
)} )}
</TableCell> </TableCell>
@ -736,16 +790,23 @@ export function ApplicationDetailsTabs(props: ApplicationDetailsTabsProps) {
)} )}
</TabsContent> </TabsContent>
<TabsContent value="fdd" className="space-y-6" data-testid="onboarding-tab-content-fdd"> {canViewFinanceTabs && (
{renderFddAuditContent()} <TabsContent value="fdd" className="space-y-6" data-testid="onboarding-tab-content-fdd">
</TabsContent> {renderFddAuditContent()}
</TabsContent>
)}
<TabsContent value="eor" className="space-y-4" data-testid="onboarding-tab-content-eor"> <TabsContent value="eor" className="space-y-4 status-progress-ui" data-testid="onboarding-tab-content-eor">
<div className="flex items-center justify-between mb-4"> <div className="flex items-center justify-between mb-4">
<h3 className="text-slate-900">Essential Operating Requirements</h3> <h3 className="text-slate-900">Essential Operating Requirements</h3>
<Badge className="bg-amber-600" data-testid="onboarding-eor-progress-badge">{Math.round(eorProgress)}% Complete</Badge> <Badge className={getPercentProgressBadgeSolidClass(eorProgress)} data-testid="onboarding-eor-progress-badge">{Math.round(eorProgress)}% Complete</Badge>
</div> </div>
<Progress value={eorProgress} className="h-3 mb-6" data-testid="onboarding-eor-progress-bar" /> <Progress
value={eorProgress}
className="h-3 mb-6 bg-status-progress-soft"
indicatorClassName={getPercentProgressBarClass(eorProgress)}
data-testid="onboarding-eor-progress-bar"
/>
<div className="space-y-3" data-testid="onboarding-eor-checklist"> <div className="space-y-3" data-testid="onboarding-eor-checklist">
{(eorData?.items || eorChecklist).map((item: any, idx: number) => { {(eorData?.items || eorChecklist).map((item: any, idx: number) => {
@ -780,7 +841,7 @@ export function ApplicationDetailsTabs(props: ApplicationDetailsTabsProps) {
{docType} {docType}
</span> </span>
{hasDocument && !item.isCompliant && ( {hasDocument && !item.isCompliant && (
<Badge variant="outline" className="text-[10px] h-4 px-1.5 bg-amber-50 text-amber-600 border-amber-200 uppercase tracking-wider font-bold"> <Badge variant="outline" className="text-[10px] h-4 px-1.5 bg-red-50 text-re-red border-red-200 uppercase tracking-wider font-bold">
Needs Verification Needs Verification
</Badge> </Badge>
)} )}
@ -841,7 +902,7 @@ export function ApplicationDetailsTabs(props: ApplicationDetailsTabsProps) {
)} )}
{!hasDocument && ( {!hasDocument && (
<div className="p-2 text-slate-300 group-hover:text-amber-500 transition-colors" data-testid={`onboarding-eor-upload-hint-${idx}`}> <div className="p-2 text-slate-300 group-hover:text-re-red transition-colors" data-testid={`onboarding-eor-upload-hint-${idx}`}>
<Upload className="w-4 h-4" /> <Upload className="w-4 h-4" />
</div> </div>
)} )}
@ -888,6 +949,7 @@ export function ApplicationDetailsTabs(props: ApplicationDetailsTabsProps) {
)} )}
</TabsContent> </TabsContent>
{canViewFinanceTabs && (
<TabsContent value="payments" className="space-y-6" data-testid="onboarding-tab-content-payments"> <TabsContent value="payments" className="space-y-6" data-testid="onboarding-tab-content-payments">
<div className="flex items-center justify-between mb-2"> <div className="flex items-center justify-between mb-2">
<h3 className="text-lg font-semibold text-slate-900">Security Deposits</h3> <h3 className="text-lg font-semibold text-slate-900">Security Deposits</h3>
@ -906,12 +968,12 @@ export function ApplicationDetailsTabs(props: ApplicationDetailsTabsProps) {
<Card className={cn( <Card className={cn(
"border-l-4", "border-l-4",
deposit?.status === 'Verified' ? "border-l-green-500" : deposit?.status === 'Verified' ? "border-l-green-500" :
deposit?.status === 'Rejected' ? "border-l-red-500" : "border-l-amber-500" deposit?.status === 'Rejected' ? "border-l-red-500" : "border-l-re-red"
)} data-testid="onboarding-payment-card-security"> )} data-testid="onboarding-payment-card-security">
<CardContent className="pt-6"> <CardContent className="pt-6">
<div className="flex items-center justify-between mb-4"> <div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="w-8 h-8 rounded bg-amber-50 flex items-center justify-center text-amber-600"> <div className="w-8 h-8 rounded bg-red-50 flex items-center justify-center text-re-red">
<ClipboardList className="w-4 h-4" /> <ClipboardList className="w-4 h-4" />
</div> </div>
<span className="font-semibold text-slate-700">Security Deposit</span> <span className="font-semibold text-slate-700">Security Deposit</span>
@ -919,7 +981,7 @@ export function ApplicationDetailsTabs(props: ApplicationDetailsTabsProps) {
<Badge className={cn( <Badge className={cn(
deposit?.status === 'Verified' ? "bg-green-100 text-green-700 hover:bg-green-100" : deposit?.status === 'Verified' ? "bg-green-100 text-green-700 hover:bg-green-100" :
deposit?.status === 'Rejected' ? "bg-red-100 text-red-700 hover:bg-red-100" : deposit?.status === 'Rejected' ? "bg-red-100 text-red-700 hover:bg-red-100" :
"bg-amber-100 text-amber-700 hover:bg-amber-100" "bg-red-50 text-re-red-hover hover:bg-red-50"
)} data-testid="onboarding-payment-status-security"> )} data-testid="onboarding-payment-status-security">
{deposit?.status || 'Awaiting'} {deposit?.status || 'Awaiting'}
</Badge> </Badge>
@ -960,7 +1022,7 @@ export function ApplicationDetailsTabs(props: ApplicationDetailsTabsProps) {
<Button <Button
variant="ghost" variant="ghost"
size="sm" size="sm"
className="h-6 px-2 text-[10px] text-amber-600 hover:text-amber-700 hover:bg-amber-50" className="h-6 px-2 text-[10px] text-re-red hover:text-re-red-hover hover:bg-red-50"
data-testid={`onboarding-payment-doc-view-security-${idx}`} data-testid={`onboarding-payment-doc-view-security-${idx}`}
onClick={() => { setPreviewDoc(doc); setShowPreviewModal(true); }} onClick={() => { setPreviewDoc(doc); setShowPreviewModal(true); }}
> >
@ -988,7 +1050,7 @@ export function ApplicationDetailsTabs(props: ApplicationDetailsTabsProps) {
<Card className={cn( <Card className={cn(
"border-l-4", "border-l-4",
deposit?.status === 'Verified' ? "border-l-green-500" : deposit?.status === 'Verified' ? "border-l-green-500" :
deposit?.status === 'Rejected' ? "border-l-red-500" : "border-l-amber-500" deposit?.status === 'Rejected' ? "border-l-red-500" : "border-l-re-red"
)} data-testid="onboarding-payment-card-first-fill"> )} data-testid="onboarding-payment-card-first-fill">
<CardContent className="pt-6"> <CardContent className="pt-6">
<div className="flex items-center justify-between mb-4"> <div className="flex items-center justify-between mb-4">
@ -1001,7 +1063,7 @@ export function ApplicationDetailsTabs(props: ApplicationDetailsTabsProps) {
<Badge className={cn( <Badge className={cn(
deposit?.status === 'Verified' ? "bg-green-100 text-green-700 hover:bg-green-100" : deposit?.status === 'Verified' ? "bg-green-100 text-green-700 hover:bg-green-100" :
deposit?.status === 'Rejected' ? "bg-red-100 text-red-700 hover:bg-red-100" : deposit?.status === 'Rejected' ? "bg-red-100 text-red-700 hover:bg-red-100" :
"bg-amber-100 text-amber-700 hover:bg-amber-100" "bg-red-50 text-re-red-hover hover:bg-red-50"
)} data-testid="onboarding-payment-status-first-fill"> )} data-testid="onboarding-payment-status-first-fill">
{deposit?.status || 'Awaiting'} {deposit?.status || 'Awaiting'}
</Badge> </Badge>
@ -1062,13 +1124,14 @@ export function ApplicationDetailsTabs(props: ApplicationDetailsTabsProps) {
})()} })()}
</div> </div>
</TabsContent> </TabsContent>
)}
<TabsContent value="audit" data-testid="onboarding-tab-content-audit"> <TabsContent value="audit" data-testid="onboarding-tab-content-audit">
<ScrollArea className="h-[30rem] rounded-md border border-slate-100 bg-slate-50/50"> <ScrollArea className="h-[30rem] rounded-md border border-slate-100 bg-slate-50/50">
<div className="space-y-2.5 p-3 pr-4" data-testid="onboarding-audit-logs-container"> <div className="space-y-2.5 p-3 pr-4" data-testid="onboarding-audit-logs-container">
{auditLoading ? ( {auditLoading ? (
<div className="flex items-center justify-center py-10" data-testid="onboarding-audit-loading"> <div className="flex items-center justify-center py-10" data-testid="onboarding-audit-loading">
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-amber-600" /> <div className="animate-spin rounded-full h-6 w-6 border-b-2 border-re-red" />
<span className="ml-2 text-sm text-slate-500">Loading audit trail</span> <span className="ml-2 text-sm text-slate-500">Loading audit trail</span>
</div> </div>
) : auditLogs.length === 0 ? ( ) : auditLogs.length === 0 ? (

View File

@ -0,0 +1,271 @@
import { useMemo, useState } from 'react';
import { toast } from 'sonner';
import { CheckCircle2, FileQuestion, Loader2, Mail } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Checkbox } from '@/components/ui/checkbox';
import { Input } from '@/components/ui/input';
import { Textarea } from '@/components/ui/textarea';
import { Label } from '@/components/ui/label';
import { ScrollArea } from '@/components/ui/scroll-area';
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import { onboardingService } from '@/services/onboarding.service';
/**
* Modal where an admin (DD Admin / Super Admin / DD Lead / DD Head) ticks the documents
* still missing for a prospect and dispatches one email per category. Uploaded docs
* are surfaced as disabled rows for transparency.
*/
interface RequestDocumentsModalProps {
open: boolean;
onClose: () => void;
applicationId: string;
applicantName: string;
documentConfigs: any[];
uploadedDocuments: any[];
}
type CategoryKey = 'LOI' | 'Statutory' | 'Architecture' | 'FDD' | 'Other';
const CATEGORY_LABEL: Record<CategoryKey, string> = {
LOI: 'LOI Documents',
Statutory: 'Statutory & Compliance',
Architecture: 'Architecture Inputs',
FDD: 'FDD / Financial',
Other: 'Other',
};
function categorize(stageCode?: string | null): CategoryKey {
const s = String(stageCode || '').toLowerCase();
if (s.startsWith('loi')) return 'LOI';
if (s.startsWith('statutory')) return 'Statutory';
if (s.startsWith('architecture')) return 'Architecture';
if (s.startsWith('fdd')) return 'FDD';
return 'Other';
}
export function RequestDocumentsModal({
open,
onClose,
applicationId,
applicantName,
documentConfigs,
uploadedDocuments,
}: RequestDocumentsModalProps) {
const [selected, setSelected] = useState<Set<string>>(new Set());
const [dueDays, setDueDays] = useState<number>(14);
const [customMessage, setCustomMessage] = useState<string>('');
const [submitting, setSubmitting] = useState(false);
const uploadedSet = useMemo(
() => new Set((uploadedDocuments || []).map((d: any) => d.documentType)),
[uploadedDocuments]
);
const grouped = useMemo(() => {
const buckets: Record<CategoryKey, any[]> = {
LOI: [], Statutory: [], Architecture: [], FDD: [], Other: [],
};
// De-duplicate by documentType — the master may return multiple rows per type if the
// doc is reused across stages. We keep the first occurrence (typically the canonical one).
const seen = new Set<string>();
for (const cfg of documentConfigs || []) {
if (!cfg?.documentType || seen.has(cfg.documentType)) continue;
seen.add(cfg.documentType);
buckets[categorize(cfg.stageCode)].push(cfg);
}
return buckets;
}, [documentConfigs]);
const toggle = (docType: string) => {
setSelected((prev) => {
const next = new Set(prev);
if (next.has(docType)) next.delete(docType);
else next.add(docType);
return next;
});
};
const handleSubmit = async () => {
const documentTypes = Array.from(selected);
if (documentTypes.length === 0) {
toast.warning('Pick at least one document to request');
return;
}
setSubmitting(true);
try {
const result: any = await onboardingService.requestProspectDocuments(applicationId, {
documentTypes,
dueDays,
customMessage: customMessage.trim() || undefined,
});
const emailsSent = (result?.emailsSent || []).filter((e: any) => e.status === 'sent');
const failed = (result?.emailsSent || []).filter((e: any) => e.status === 'failed');
const skipped = result?.skippedAlreadyUploaded || [];
if (emailsSent.length > 0) {
toast.success(
`Sent ${emailsSent.length} email${emailsSent.length === 1 ? '' : 's'} to ${applicantName}` +
(skipped.length ? `${skipped.length} already uploaded, skipped` : '')
);
} else if (skipped.length) {
toast.info(`No email sent — all selected documents were already uploaded.`);
}
if (failed.length) {
toast.error(`Some email categories failed: ${failed.map((f: any) => f.category).join(', ')}`);
}
setSelected(new Set());
setCustomMessage('');
onClose();
} catch (err: any) {
toast.error(err?.message || 'Failed to send document request');
} finally {
setSubmitting(false);
}
};
const totalSelectable = (Object.values(grouped) as any[][]).reduce((sum, list) => sum + list.length, 0);
return (
<Dialog open={open} onOpenChange={(o) => { if (!o) onClose(); }}>
<DialogContent className="sm:max-w-2xl">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<Mail className="w-5 h-5 text-re-red" />
Request Documents from Prospect
</DialogTitle>
<DialogDescription>
Pick the documents you want {applicantName || 'the prospect'} to upload. We'll send one email per
category. Anything already uploaded is shown for context and skipped automatically.
</DialogDescription>
</DialogHeader>
{totalSelectable === 0 ? (
<div className="py-10 text-center text-slate-500 flex flex-col items-center gap-2">
<FileQuestion className="w-8 h-8" />
<p>No document configurations available for this application.</p>
</div>
) : (
<ScrollArea className="max-h-[55vh] pr-3">
<div className="space-y-5">
{(Object.keys(CATEGORY_LABEL) as CategoryKey[]).map((cat) => {
const items = grouped[cat];
if (items.length === 0) return null;
return (
<div key={cat} className="border rounded-lg p-3 bg-slate-50">
<h4 className="font-medium text-slate-700 mb-2 text-sm uppercase tracking-wide">
{CATEGORY_LABEL[cat]}
</h4>
<div className="space-y-2">
{items.map((cfg: any) => {
const isUploaded = uploadedSet.has(cfg.documentType);
return (
<div
key={`${cat}-${cfg.documentType}`}
className={`flex items-start gap-3 p-2 rounded ${
isUploaded ? 'bg-green-50 border border-green-100' : 'bg-white border border-slate-200'
}`}
>
{isUploaded ? (
<CheckCircle2 className="w-5 h-5 text-green-600 mt-0.5 shrink-0" />
) : (
<Checkbox
id={`req-doc-${cfg.documentType}`}
checked={selected.has(cfg.documentType)}
onCheckedChange={() => toggle(cfg.documentType)}
className="mt-0.5"
/>
)}
<Label
htmlFor={`req-doc-${cfg.documentType}`}
className={`flex-1 cursor-pointer text-sm leading-snug ${
isUploaded ? 'text-slate-500 line-through' : 'text-slate-800'
}`}
>
<span className="font-medium">{cfg.documentType}</span>
{cfg.isMandatory && !isUploaded && (
<Badge className="ml-2 bg-red-100 text-red-700 text-[10px]">Mandatory</Badge>
)}
{isUploaded && (
<Badge className="ml-2 bg-green-100 text-green-700 text-[10px]">Uploaded</Badge>
)}
{cfg.description && (
<div className="text-xs text-slate-500 mt-0.5">{cfg.description}</div>
)}
</Label>
</div>
);
})}
</div>
</div>
);
})}
</div>
</ScrollArea>
)}
<div className="grid grid-cols-1 sm:grid-cols-3 gap-3 pt-2 border-t">
<div className="sm:col-span-1">
<Label htmlFor="req-doc-due-days" className="text-xs text-slate-600">
Due in (days)
</Label>
<Input
id="req-doc-due-days"
type="number"
min={1}
max={60}
value={dueDays}
onChange={(e) => setDueDays(Math.max(1, Number(e.target.value) || 14))}
className="mt-1"
/>
</div>
<div className="sm:col-span-2">
<Label htmlFor="req-doc-message" className="text-xs text-slate-600">
Custom message (optional)
</Label>
<Textarea
id="req-doc-message"
placeholder="Add a short note for the prospect…"
value={customMessage}
onChange={(e) => setCustomMessage(e.target.value)}
rows={2}
className="mt-1"
/>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={onClose} disabled={submitting}>
Cancel
</Button>
<Button
onClick={handleSubmit}
disabled={submitting || selected.size === 0}
className="bg-re-red hover:bg-re-red/90 text-white"
>
{submitting ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
Sending
</>
) : (
<>
<Mail className="w-4 h-4 mr-2" />
Send Request ({selected.size})
</>
)}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}

View File

@ -171,6 +171,6 @@ export function auditLogActionBadgeClass(action: string): string {
if (a === 'CREATED' || a.includes('APPROV') || a.includes('COMPLETE')) return 'border-emerald-200 bg-emerald-50/90 text-emerald-900'; if (a === 'CREATED' || a.includes('APPROV') || a.includes('COMPLETE')) return 'border-emerald-200 bg-emerald-50/90 text-emerald-900';
if (a.includes('DOCUMENT') || a.includes('UPLOAD') || a.includes('ATTACHMENT')) return 'border-sky-200 bg-sky-50/80 text-sky-900'; if (a.includes('DOCUMENT') || a.includes('UPLOAD') || a.includes('ATTACHMENT')) return 'border-sky-200 bg-sky-50/80 text-sky-900';
if (a.includes('PAYMENT') || a.includes('SECURITY') || a.includes('DEPOSIT')) return 'border-violet-200 bg-violet-50/80 text-violet-900'; if (a.includes('PAYMENT') || a.includes('SECURITY') || a.includes('DEPOSIT')) return 'border-violet-200 bg-violet-50/80 text-violet-900';
if (a.includes('FDD') || a.includes('QUESTIONNAIRE') || a.includes('INTERVIEW')) return 'border-amber-200 bg-amber-50/80 text-amber-900'; if (a.includes('FDD') || a.includes('QUESTIONNAIRE') || a.includes('INTERVIEW')) return 'border-red-200 bg-red-50/80 text-red-900';
return 'border-slate-200 bg-slate-50 text-slate-700'; return 'border-slate-200 bg-slate-50 text-slate-700';
} }

View File

@ -17,10 +17,15 @@ interface UseApplicationDetailsAdminActionsParams {
participantType: string; participantType: string;
users: any[]; users: any[];
interviewDate: string; interviewDate: string;
setInterviewDate: Dispatch<SetStateAction<string>>;
interviewType: string; interviewType: string;
setInterviewType: Dispatch<SetStateAction<string>>;
interviewMode: string; interviewMode: string;
setInterviewMode: Dispatch<SetStateAction<string>>;
meetingLink: string; meetingLink: string;
setMeetingLink: Dispatch<SetStateAction<string>>;
location: string; location: string;
setLocation: Dispatch<SetStateAction<string>>;
scheduledInterviewParticipants: any[]; scheduledInterviewParticipants: any[];
uploadFile: File | null; uploadFile: File | null;
uploadDocType: string; uploadDocType: string;
@ -45,6 +50,8 @@ interface UseApplicationDetailsAdminActionsParams {
setShowCancelInterviewModal: Dispatch<SetStateAction<boolean>>; setShowCancelInterviewModal: Dispatch<SetStateAction<boolean>>;
interviewIdToCancel: string; interviewIdToCancel: string;
setInterviewIdToCancel: Dispatch<SetStateAction<string>>; setInterviewIdToCancel: Dispatch<SetStateAction<string>>;
interviewToReschedule: any;
setInterviewToReschedule: Dispatch<SetStateAction<any>>;
setIsCancellingInterview: Dispatch<SetStateAction<boolean>>; setIsCancellingInterview: Dispatch<SetStateAction<boolean>>;
setIsUploading: Dispatch<SetStateAction<boolean>>; setIsUploading: Dispatch<SetStateAction<boolean>>;
setShowUploadForm: Dispatch<SetStateAction<boolean>>; setShowUploadForm: Dispatch<SetStateAction<boolean>>;
@ -79,10 +86,15 @@ export function useApplicationDetailsAdminActions(params: UseApplicationDetailsA
participantType, participantType,
users, users,
interviewDate, interviewDate,
setInterviewDate,
interviewType, interviewType,
setInterviewType,
interviewMode, interviewMode,
setInterviewMode,
meetingLink, meetingLink,
setMeetingLink,
location, location,
setLocation,
scheduledInterviewParticipants, scheduledInterviewParticipants,
uploadFile, uploadFile,
uploadDocType, uploadDocType,
@ -107,6 +119,8 @@ export function useApplicationDetailsAdminActions(params: UseApplicationDetailsA
setShowCancelInterviewModal, setShowCancelInterviewModal,
interviewIdToCancel, interviewIdToCancel,
setInterviewIdToCancel, setInterviewIdToCancel,
interviewToReschedule,
setInterviewToReschedule,
setIsCancellingInterview, setIsCancellingInterview,
setIsUploading, setIsUploading,
setShowUploadForm, setShowUploadForm,
@ -176,7 +190,7 @@ export function useApplicationDetailsAdminActions(params: UseApplicationDetailsA
}, [currentUser, application, setUsers]); }, [currentUser, application, setUsers]);
const prefillInterviewParticipants = useCallback(() => { const prefillInterviewParticipants = useCallback(() => {
if (!showScheduleModal || !application) return; if (!showScheduleModal || !application || interviewToReschedule) return;
const levelNum = parseInt(interviewType.replace('level', '')) || 1; const levelNum = parseInt(interviewType.replace('level', '')) || 1;
const requiredRolesByLevel: Record<number, string[]> = { const requiredRolesByLevel: Record<number, string[]> = {
1: ['DD-ZM', 'RBM'], 1: ['DD-ZM', 'RBM'],
@ -233,7 +247,7 @@ export function useApplicationDetailsAdminActions(params: UseApplicationDetailsA
} }
}); });
setScheduledInterviewParticipants(unique); setScheduledInterviewParticipants(unique);
}, [showScheduleModal, application, interviewType, setScheduledInterviewParticipants]); }, [showScheduleModal, application, interviewType, interviewToReschedule, setScheduledInterviewParticipants]);
const handleScheduleInterview = async () => { const handleScheduleInterview = async () => {
if (!interviewDate) { if (!interviewDate) {
@ -242,20 +256,32 @@ export function useApplicationDetailsAdminActions(params: UseApplicationDetailsA
} }
try { try {
setIsScheduling(true); setIsScheduling(true);
await onboardingService.scheduleInterview({ const payload = {
applicationId: application?.id, applicationId: application?.id,
level: interviewType, level: interviewType,
scheduledAt: interviewDate, scheduledAt: interviewDate,
type: interviewMode === 'virtual' ? 'Virtual Interview' : 'Physical Interview', type: interviewMode === 'virtual' ? 'Virtual Interview' : 'Physical Interview',
location: interviewMode === 'virtual' ? meetingLink : location, location: interviewMode === 'virtual' ? meetingLink : location,
participants: scheduledInterviewParticipants.map((p) => p.id), participants: scheduledInterviewParticipants.map((p) => p.id),
}); };
toast.success('Interview scheduled successfully');
if (interviewToReschedule) {
await onboardingService.updateInterview(interviewToReschedule.id, {
...payload,
status: 'Scheduled',
});
toast.success('Interview rescheduled successfully');
} else {
await onboardingService.scheduleInterview(payload);
toast.success('Interview scheduled successfully');
}
setShowScheduleModal(false); setShowScheduleModal(false);
setInterviewToReschedule(null);
await fetchInterviews(); await fetchInterviews();
await fetchApplication(); await fetchApplication();
} catch { } catch {
toast.error('Failed to schedule interview'); toast.error(interviewToReschedule ? 'Failed to reschedule interview' : 'Failed to schedule interview');
} finally { } finally {
setIsScheduling(false); setIsScheduling(false);
} }
@ -266,6 +292,24 @@ export function useApplicationDetailsAdminActions(params: UseApplicationDetailsA
setShowCancelInterviewModal(true); setShowCancelInterviewModal(true);
}; };
const handleRescheduleInterview = async (interview: any) => {
setInterviewToReschedule(interview);
setInterviewType(`level${interview.level}`);
setInterviewMode(interview.interviewType?.toLowerCase().includes('virtual') ? 'virtual' : 'physical');
setInterviewDate(interview.scheduleDate ? (() => {
const d = new Date(interview.scheduleDate);
return new Date(d.getTime() - d.getTimezoneOffset() * 60000).toISOString().slice(0, 16);
})() : '');
if (interview.interviewType?.toLowerCase().includes('virtual')) {
setMeetingLink(interview.linkOrLocation || '');
} else {
setLocation(interview.linkOrLocation || '');
}
const participants = (interview.participants || []).map((p: any) => p.user || p).filter(Boolean);
setScheduledInterviewParticipants(participants);
setShowScheduleModal(true);
};
const handleConfirmCancelInterview = async () => { const handleConfirmCancelInterview = async () => {
if (!interviewIdToCancel) return; if (!interviewIdToCancel) return;
try { try {
@ -284,14 +328,26 @@ export function useApplicationDetailsAdminActions(params: UseApplicationDetailsA
const handleUpload = async () => { const handleUpload = async () => {
if (!uploadFile || !uploadDocType) { if (!uploadFile || !uploadDocType) {
toast.warning('Please select a file and document type'); toast.warning('Please enter a document name and select a file');
return; return;
} }
try { try {
setIsUploading(true); setIsUploading(true);
const formData = new FormData(); const formData = new FormData();
formData.append('file', uploadFile); const originalExt = uploadFile.name.match(/\.[^/.]+$/)?.[0] || '';
formData.append('documentType', uploadDocType); const typedName = uploadDocType.trim();
const customFileName = typedName.toLowerCase().endsWith(originalExt.toLowerCase())
? typedName
: `${typedName}${originalExt}`;
formData.append('file', uploadFile, customFileName);
// Document type is owned by the entry point, not a user-facing dropdown.
// For checklist-driven entry points (e.g. EOR items), use the checklist item's name
// so backend auto-linking (EOR compliance, architecture date, etc.) still works.
// Everything else uploads as a generic 'Other' document.
const checklistDocType = selectedStage?.startsWith('EOR: ')
? selectedStage.replace(/^EOR:\s*/, '')
: null;
formData.append('documentType', checklistDocType || 'Other');
if (selectedStage) formData.append('stage', selectedStage); if (selectedStage) formData.append('stage', selectedStage);
await onboardingService.uploadDocument(applicationId, formData); await onboardingService.uploadDocument(applicationId, formData);
toast.success('Document uploaded successfully'); toast.success('Document uploaded successfully');
@ -371,7 +427,8 @@ export function useApplicationDetailsAdminActions(params: UseApplicationDetailsA
case 'Level 3 Interview Pending': newStatus = 'Level 3 Approved'; break; case 'Level 3 Interview Pending': newStatus = 'Level 3 Approved'; break;
case 'Level 3 Approved': newStatus = 'FDD Verification'; break; case 'Level 3 Approved': newStatus = 'FDD Verification'; break;
case 'FDD Verification': newStatus = 'LOI In Progress'; break; case 'FDD Verification': newStatus = 'LOI In Progress'; break;
case 'LOI In Progress': newStatus = 'Security Details'; break; case 'LOI In Progress': newStatus = 'Security Deposit'; break;
case 'Security Deposit':
case 'Security Details': case 'Security Details':
case 'Payment Pending': newStatus = 'LOI Issued'; break; case 'Payment Pending': newStatus = 'LOI Issued'; break;
case 'LOI Issued': newStatus = 'Dealer Code Generation'; break; case 'LOI Issued': newStatus = 'Dealer Code Generation'; break;
@ -606,6 +663,7 @@ export function useApplicationDetailsAdminActions(params: UseApplicationDetailsA
fetchUsers, fetchUsers,
maybeFetchUsersForModal, maybeFetchUsersForModal,
handleScheduleInterview, handleScheduleInterview,
handleRescheduleInterview,
handleCancelInterview, handleCancelInterview,
handleConfirmCancelInterview, handleConfirmCancelInterview,
handleUpload, handleUpload,

View File

@ -84,7 +84,9 @@ export function useApplicationDetailsData({ applicationId }: UseApplicationDetai
level3InterviewDate: getStageDate('3rd Level Interview', 'Level 3 Approved'), level3InterviewDate: getStageDate('3rd Level Interview', 'Level 3 Approved'),
fddDate: getStageDate('FDD', 'FDD Verification'), fddDate: getStageDate('FDD', 'FDD Verification'),
loiApprovalDate: getStageDate('LOI Approval', 'LOI In Progress'), loiApprovalDate: getStageDate('LOI Approval', 'LOI In Progress'),
securityDetailsDate: getStageDate('Security Details', 'Security Details'), securityDetailsDate:
getStageDate('Security Deposit', 'Security Deposit') ||
getStageDate('Security Details', 'Security Details'),
loiIssueDate: getStageDate('LOI Issue', 'LOI Issued'), loiIssueDate: getStageDate('LOI Issue', 'LOI Issued'),
dealerCodeDate: getStageDate('Dealer Code Generation', 'Dealer Code Generation'), dealerCodeDate: getStageDate('Dealer Code Generation', 'Dealer Code Generation'),
architectureAssignedDate: getStageDate('Architecture Team Assigned', 'Architecture Team Assigned'), architectureAssignedDate: getStageDate('Architecture Team Assigned', 'Architecture Team Assigned'),

View File

@ -6,6 +6,106 @@ interface UseApplicationDetailsPermissionsParams {
eorProgress: number; eorProgress: number;
} }
function sameUserId(a: unknown, b: unknown): boolean {
if (a == null || b == null) return false;
return String(a).trim() === String(b).trim();
}
/** Backend / DB may use different casing (e.g. default `scheduled` vs created `Scheduled`). */
function normalizeInterviewStatus(status: unknown): string {
return String(status ?? '')
.trim()
.toLowerCase()
.replace(/\s+/g, ' ');
}
function isActiveInterviewStatus(status: unknown): boolean {
const n = normalizeInterviewStatus(status);
if (!n) return false;
return (
n === 'scheduled' ||
n === 'rescheduled' ||
n === 'pending' ||
n === 'in progress' ||
n === 'inprogress'
);
}
function isCompletedInterviewStatus(status: unknown): boolean {
return normalizeInterviewStatus(status) === 'completed';
}
function userIsInterviewParticipant(interview: any, userId: unknown): boolean {
if (!userId || !interview?.participants?.length) return false;
return interview.participants.some(
(p: any) => sameUserId(p.userId, userId) || sameUserId(p.user?.id, userId),
);
}
/** Which interview level the application is currently in (for feedback UI). */
function inferInterviewLevelFromApplicationStatus(status: unknown): number | undefined {
const s = String(status ?? '').trim();
const map: Record<string, number> = {
'Level 1 Interview Pending': 1,
'Level 1 Recommended': 1,
'Level 2 Interview Pending': 2,
'Level 2 Recommended': 2,
'Level 3 Interview Pending': 3,
'Level 3 Recommended': 3,
};
return map[s];
}
function normalizeRoleToken(value: unknown): string {
return String(value ?? '')
.trim()
.toLowerCase()
.replace(/[_\s-]+/g, '');
}
/**
* Allow feedback when scheduling forgot to attach this user as InterviewParticipant but they are a
* designated evaluator for this level (matches backend interview policy / schedule prefill roles).
*/
function userRoleEligibleForInterviewLevel(user: any, level: number): boolean {
if (!user) return false;
const privilegedRoles = ['Super Admin', 'DD Admin'];
if (
privilegedRoles.includes(String(user.role ?? '')) ||
privilegedRoles.includes(String(user.roleCode ?? ''))
) {
return true;
}
const rolesByLevel: Record<number, string[]> = {
1: ['DD-ZM', 'DD ZM', 'RBM'],
2: ['DD Lead', 'ZBH'],
3: ['NBH', 'DD Head'],
};
const allowedRaw = rolesByLevel[level];
if (!allowedRaw?.length) return false;
const allowed = allowedRaw.map(normalizeRoleToken);
const candidates = [
user.role,
user.roleCode,
user.roleName,
user.role?.roleCode,
user.role?.roleName,
]
.filter(Boolean)
.map(normalizeRoleToken);
for (const c of candidates) {
if (!c) continue;
for (const a of allowed) {
if (!a) continue;
if (c === a || c.includes(a) || a.includes(c)) return true;
}
}
return false;
}
export function useApplicationDetailsPermissions({ export function useApplicationDetailsPermissions({
application, application,
interviews, interviews,
@ -15,24 +115,46 @@ export function useApplicationDetailsPermissions({
}: UseApplicationDetailsPermissionsParams) { }: UseApplicationDetailsPermissionsParams) {
const interviewsList = Array.isArray(interviews) ? interviews : []; const interviewsList = Array.isArray(interviews) ? interviews : [];
const activeInterviewForUser = interviewsList.find((i: any) => const stageInterviewLevel = inferInterviewLevelFromApplicationStatus(application?.status);
['Scheduled', 'Rescheduled', 'Pending', 'In Progress'].includes(i.status) &&
i.participants?.some((p: any) => p.userId === currentUser?.id)
);
const lastInterviewForUser = [...interviewsList].reverse().find((i: any) => /** Prefer participant match on the interview row that matches current application stage when possible. */
i.participants?.some((p: any) => p.userId === currentUser?.id) const participantActiveInterview =
); (stageInterviewLevel != null
? interviewsList.find(
(i: any) =>
isActiveInterviewStatus(i.status) &&
userIsInterviewParticipant(i, currentUser?.id) &&
Number(i.level) === stageInterviewLevel,
)
: undefined) ??
interviewsList.find(
(i: any) => isActiveInterviewStatus(i.status) && userIsInterviewParticipant(i, currentUser?.id),
);
const currentUserEvaluation = (activeInterviewForUser || lastInterviewForUser)?.evaluations?.find( /** Same stage + active interview + evaluator role — covers missing / partial participant rows. */
(e: any) => e.evaluatorId === currentUser?.id const roleFallbackActiveInterview =
); stageInterviewLevel != null &&
currentUser &&
userRoleEligibleForInterviewLevel(currentUser, stageInterviewLevel)
? interviewsList.find(
(i: any) =>
Number(i.level) === stageInterviewLevel && isActiveInterviewStatus(i.status),
)
: undefined;
const activeInterviewForUser = participantActiveInterview ?? roleFallbackActiveInterview;
const lastInterviewForUser = interviewsList.find((i: any) => userIsInterviewParticipant(i, currentUser?.id));
const currentUserEvaluation =
activeInterviewForUser?.evaluations?.find((e: any) => sameUserId(e.evaluatorId, currentUser?.id)) ??
lastInterviewForUser?.evaluations?.find((e: any) => sameUserId(e.evaluatorId, currentUser?.id));
const isInterviewCompleted = (level: number) => const isInterviewCompleted = (level: number) =>
interviewsList.some((i: any) => Number(i.level) === level && i.status === 'Completed'); interviewsList.some((i: any) => Number(i.level) === level && isCompletedInterviewStatus(i.status));
const isInterviewActive = (level: number) => const isInterviewActive = (level: number) =>
interviewsList.some((i: any) => Number(i.level) === level && i.status === 'Scheduled'); interviewsList.some((i: any) => Number(i.level) === level && isActiveInterviewStatus(i.status));
const hasSubmittedFeedback = !!currentUserEvaluation; const hasSubmittedFeedback = !!currentUserEvaluation;
@ -66,7 +188,7 @@ export function useApplicationDetailsPermissions({
const isAdminRole = ['DD Admin', 'Super Admin', 'NBH', 'DD Lead', 'DD Head', 'Finance', 'Finance Admin', 'FDD', 'ZBH', 'RBM'].includes(currentUser.role); const isAdminRole = ['DD Admin', 'Super Admin', 'NBH', 'DD Lead', 'DD Head', 'Finance', 'Finance Admin', 'FDD', 'ZBH', 'RBM'].includes(currentUser.role);
const isAdministrativeStage = [ const isAdministrativeStage = [
'Level 3 Approved', 'FDD Verification', 'Level 3 Approved', 'FDD Verification',
'LOI In Progress', 'Security Details', 'Payment Pending', 'LOI Issued', 'Statutory LOI Ack', 'LOI In Progress', 'Security Deposit', 'Security Details', 'Payment Pending', 'LOI Issued', 'Statutory LOI Ack',
'Architecture Team Assigned', 'Architecture Document Upload', 'Architecture Team Completion', 'Architecture Team Assigned', 'Architecture Document Upload', 'Architecture Team Completion',
'Statutory GST', 'Statutory PAN', 'Statutory Nodal', 'Statutory Check', 'Statutory GST', 'Statutory PAN', 'Statutory Nodal', 'Statutory Check',
'Statutory Partnership', 'Statutory Firm Reg', 'Statutory Rental', 'Statutory Partnership', 'Statutory Firm Reg', 'Statutory Rental',
@ -76,7 +198,7 @@ export function useApplicationDetailsPermissions({
const isLoaLocked = application.status === 'LOA Pending' && getDeposit('FIRST_FILL')?.status !== 'Verified'; const isLoaLocked = application.status === 'LOA Pending' && getDeposit('FIRST_FILL')?.status !== 'Verified';
const isSecurityDetailsLocked = const isSecurityDetailsLocked =
['Security Details', 'Payment Pending'].includes(application.status) && ['Security Deposit', 'Security Details', 'Payment Pending'].includes(application.status) &&
getDeposit('SECURITY_DEPOSIT')?.status !== 'Verified'; getDeposit('SECURITY_DEPOSIT')?.status !== 'Verified';
const isFinalState = application.status === 'Onboarded' || application.status === 'Rejected'; const isFinalState = application.status === 'Onboarded' || application.status === 'Rejected';

View File

@ -6,14 +6,16 @@ interface UseApplicationDetailsStageDataParams {
interviews: any[]; interviews: any[];
eorData: any; eorData: any;
getDeposit: (type: string) => any; getDeposit: (type: string) => any;
documentConfigs?: any[];
} }
export function useApplicationDetailsStageData({ export function useApplicationDetailsStageData({
application, application,
documents, documents,
interviews, interviews: _interviews,
eorData, eorData,
getDeposit, getDeposit,
documentConfigs = [],
}: UseApplicationDetailsStageDataParams) { }: UseApplicationDetailsStageDataParams) {
const normalizeRole = (value: unknown): string => const normalizeRole = (value: unknown): string =>
String(value || '') String(value || '')
@ -38,39 +40,39 @@ export function useApplicationDetailsStageData({
return (documents || []).some((d) => d.documentType === docType); return (documents || []).some((d) => d.documentType === docType);
}; };
const isInterviewScheduled = (level: number | string) => { const getSecurityDepositStageStatus = (): ProcessStage['status'] => {
return (interviews || []).some((i) => (i.level === level || i.level === level.toString()) && i.status?.toLowerCase() === 'scheduled'); const rows = application.progressTracking || [];
const row =
rows.find((ps: any) => ps.stageName === 'Security Deposit') ||
rows.find((ps: any) => ps.stageName === 'Security Details');
return row?.status ? (row.status as ProcessStage['status']) : 'pending';
}; };
const getStageStatus = (stageName: string, fallbackLogic: () => ProcessStage['status']): ProcessStage['status'] => { const getStageStatus = (stageName: string, fallbackStatus: ProcessStage['status'] = 'pending'): ProcessStage['status'] => {
const backendStage = (application.progressTracking || []).find((ps: any) => ps.stageName === stageName); const backendStage = (application.progressTracking || []).find((ps: any) => ps.stageName === stageName);
if (backendStage && (backendStage.status === 'completed' || backendStage.status === 'active')) { return backendStage?.status ? (backendStage.status as any) : fallbackStatus;
return backendStage.status as any;
}
return fallbackLogic();
}; };
const processStages: ProcessStage[] = [ const processStages: ProcessStage[] = [
{ id: 1, name: 'Submitted', status: 'completed', date: application.submissionDate, description: 'Application submitted', documentsUploaded: 3 }, { id: 1, name: 'Submitted', status: 'completed', date: application.submissionDate, description: 'Application submitted', documentsUploaded: 3 },
{ {
id: 2, name: 'Questionnaire', status: getStageStatus('Questionnaire', () => id: 2, name: 'Questionnaire', status: getStageStatus('Questionnaire'),
['Questionnaire Completed', 'Shortlisted', 'Level 1 Interview Pending', '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 === 'Questionnaire Pending' ? 'active' : 'pending'),
date: application.questionnaireDate, description: 'Questionnaire completed', documentsUploaded: 0 date: application.questionnaireDate, description: 'Questionnaire completed', documentsUploaded: 0
}, },
{ {
id: 3, name: 'Shortlist', status: getStageStatus('Shortlist', () => ['Shortlisted', 'Level 1 Interview Pending', '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', 'Rejected', 'Onboarded'].includes(application.status) ? 'completed' : 'pending'), id: 3, name: 'Shortlist', status: getStageStatus('Shortlist'),
date: application.shortlistDate, description: 'Application shortlisted by DD', date: application.shortlistDate, description: 'Application shortlisted by DD',
evaluators: Array.from(new Set((application.participants || []).filter((p: any) => p.participantType === 'assignee').map((p: any) => `${p.user?.fullName || p.user?.name || 'User'} (${p.user?.roleCode || p.participantType})`))), evaluators: Array.from(new Set((application.participants || []).filter((p: any) => p.participantType === 'assignee').map((p: any) => `${p.user?.fullName || p.user?.name || 'User'} (${p.user?.roleCode || p.participantType})`))),
documentsUploaded: 2 documentsUploaded: 2
}, },
{ {
id: 4, name: '1st Level Interview', status: getStageStatus('1st Level Interview', () => ['Level 1 Approved', 'Level 2 Interview Pending', 'Level 2 Approved', 'Level 2 Recommended', 'Level 3 Interview Pending', 'Level 3 Approved', 'FDD Verification', 'LOI In Progress', 'Payment Pending', 'LOI Issued', 'Statutory LOI Ack', 'Dealer Code Generation', 'Architecture Work', 'Statutory Work', 'LOA Pending', 'LOA Issued', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Onboarded'].includes(application.status) ? 'completed' : (application.status === 'Level 1 Interview Pending' && isInterviewScheduled(1)) ? 'active' : 'pending'), id: 4, name: '1st Level Interview', status: getStageStatus('1st Level Interview'),
date: application.level1InterviewDate, description: 'DD-ZM + RBM evaluation', date: application.level1InterviewDate, description: 'DD-ZM + RBM evaluation',
evaluators: Array.from(new Set( evaluators: Array.from(new Set(
(application.participants || []) (application.participants || [])
.filter((p: any) => .filter((p: any) =>
p.metadata?.interviewLevel === 1 || p.metadata?.interviewLevel === 1 ||
p.metadata?.interviewLevel === '1' || p.metadata?.interviewLevel === '1' ||
p.metadata?.allAssignments?.includes(1) || p.metadata?.allAssignments?.includes(1) ||
p.metadata?.allAssignments?.includes('1') || p.metadata?.allAssignments?.includes('1') ||
hasAnyRole(p, ['DD-ZM', 'RBM']) hasAnyRole(p, ['DD-ZM', 'RBM'])
@ -80,13 +82,13 @@ export function useApplicationDetailsStageData({
documentsUploaded: 1 documentsUploaded: 1
}, },
{ {
id: 5, name: '2nd Level Interview', status: getStageStatus('2nd Level Interview', () => ['Level 2 Approved', 'Level 2 Recommended', 'Level 3 Interview Pending', 'Level 3 Approved', 'FDD Verification', 'LOI In Progress', 'Payment Pending', 'LOI Issued', 'Statutory LOI Ack', 'Dealer Code Generation', 'Architecture Work', 'Statutory Work', 'LOA Pending', 'LOA Issued', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Onboarded'].includes(application.status) ? 'completed' : (application.status === 'Level 2 Interview Pending' && isInterviewScheduled(2)) ? 'active' : 'pending'), id: 5, name: '2nd Level Interview', status: getStageStatus('2nd Level Interview'),
date: application.level2InterviewDate, description: 'DD Lead + ZBH evaluation', date: application.level2InterviewDate, description: 'DD Lead + ZBH evaluation',
evaluators: Array.from(new Set( evaluators: Array.from(new Set(
(application.participants || []) (application.participants || [])
.filter((p: any) => .filter((p: any) =>
p.metadata?.interviewLevel === 2 || p.metadata?.interviewLevel === 2 ||
p.metadata?.interviewLevel === '2' || p.metadata?.interviewLevel === '2' ||
p.metadata?.allAssignments?.includes(2) || p.metadata?.allAssignments?.includes(2) ||
p.metadata?.allAssignments?.includes('2') || p.metadata?.allAssignments?.includes('2') ||
hasAnyRole(p, ['DD Lead', 'ZBH']) hasAnyRole(p, ['DD Lead', 'ZBH'])
@ -96,13 +98,13 @@ export function useApplicationDetailsStageData({
documentsUploaded: 1 documentsUploaded: 1
}, },
{ {
id: 6, name: '3rd Level Interview', status: getStageStatus('3rd Level Interview', () => ['Level 3 Approved', 'FDD Verification', 'LOI In Progress', 'Payment Pending', 'LOI Issued', 'Statutory LOI Ack', 'Dealer Code Generation', 'Architecture Work', 'Statutory Work', 'LOA Pending', 'LOA Issued', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Onboarded'].includes(application.status) ? 'completed' : (application.status === 'Level 3 Interview Pending' && isInterviewScheduled(3)) ? 'active' : 'pending'), id: 6, name: '3rd Level Interview', status: getStageStatus('3rd Level Interview'),
date: application.level3InterviewDate, description: 'NBH + DD Head evaluation', date: application.level3InterviewDate, description: 'NBH + DD Head evaluation',
evaluators: Array.from(new Set( evaluators: Array.from(new Set(
(application.participants || []) (application.participants || [])
.filter((p: any) => .filter((p: any) =>
p.metadata?.interviewLevel === 3 || p.metadata?.interviewLevel === 3 ||
p.metadata?.interviewLevel === '3' || p.metadata?.interviewLevel === '3' ||
p.metadata?.allAssignments?.includes(3) || p.metadata?.allAssignments?.includes(3) ||
p.metadata?.allAssignments?.includes('3') || p.metadata?.allAssignments?.includes('3') ||
hasAnyRole(p, ['NBH', 'DD Head']) hasAnyRole(p, ['NBH', 'DD Head'])
@ -111,55 +113,91 @@ export function useApplicationDetailsStageData({
)), )),
documentsUploaded: 2 documentsUploaded: 2
}, },
{ id: 7, name: 'FDD', status: getStageStatus('FDD', () => ['LOI In Progress', 'Payment Pending', 'LOI Issued', 'Statutory LOI Ack', 'Dealer Code Generation', 'Architecture Work', 'Statutory Work', 'LOA Pending', 'LOA Issued', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Onboarded'].includes(application.status) ? 'completed' : application.status === 'FDD Verification' ? 'active' : 'pending'), date: application.fddDate, description: 'Financial Due Diligence', documentsUploaded: 5 }, { id: 7, name: 'FDD', status: getStageStatus('FDD'), date: application.fddDate, description: 'Financial Due Diligence', documentsUploaded: 5 },
{ {
id: 8, name: 'LOI Approval', status: getStageStatus('LOI Approval', () => ['Security Details', 'Payment Pending', 'LOI Issued', 'Statutory LOI Ack', 'Dealer Code Generation', 'Architecture Work', 'Statutory Work', 'LOA Pending', 'LOA Issued', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Onboarded'].includes(application.status) ? 'completed' : application.status === 'LOI In Progress' ? 'active' : 'pending'), id: 8, name: 'LOI Approval', status: getStageStatus('LOI Approval'),
date: application.loiApprovalDate, description: 'Letter of Intent approval', date: application.loiApprovalDate, description: 'Letter of Intent approval',
evaluators: Array.from(new Set((application.participants || []).filter((p: any) => p.metadata?.stageCode === 'LOI_APPROVAL' || p.metadata?.allAssignments?.includes('LOI_APPROVAL')).map(participantLabel))), evaluators: Array.from(new Set((application.participants || []).filter((p: any) => p.metadata?.stageCode === 'LOI_APPROVAL' || p.metadata?.allAssignments?.includes('LOI_APPROVAL')).map(participantLabel))),
documentsUploaded: 1 documentsUploaded: 1
}, },
{ {
id: 9, name: 'Security Details', status: getStageStatus('Security Details', () => ['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 === 'Security Details' || application.status === 'Payment Pending' ? 'active' : 'pending'), id: 9, name: 'Security Deposit', status: getSecurityDepositStageStatus(),
date: application.securityDetailsDate, description: 'Security verification', documentsUploaded: 3 date: application.securityDetailsDate, description: 'Security Deposit verification', documentsUploaded: 3
},
(() => {
const loiDocConfigs = documentConfigs.filter((c: any) => c.stageCode === 'LOI Issue' || c.stageCode === 'LOI Documents');
const loiDocList = loiDocConfigs.length
? loiDocConfigs.map((c: any) => c.documentType)
: ['Letter of Intent', 'Signed LOI'];
const allUploaded = loiDocList.every((dt: string) =>
isDocumentUploaded(dt) || (dt === 'Letter of Intent' && isDocumentUploaded('LOI')) || (dt === 'Signed LOI' && isDocumentUploaded('LOI Signed Copy'))
);
const loiApprovalDone = getStageStatus('LOI Approval') === 'completed';
const status: ProcessStage['status'] = allUploaded ? 'completed' : loiApprovalDone ? 'active' : 'pending';
return {
id: 10, name: 'LOI Documents', status,
description: 'Upload Letter of Intent documents before issuance', isParallel: true,
branches: [
{
name: 'Documents Required', color: 'green', stages:
loiDocConfigs.length
? loiDocConfigs.map((c: any, i: number) => ({
id: `10a-${i}`,
name: c.documentType,
status: isDocumentUploaded(c.documentType) ? 'completed' : 'active',
description: c.isMandatory ? `Upload ${c.documentType} (Mandatory)` : `Upload ${c.documentType}`
}))
: [
{ id: '10a-1', name: 'Letter of Intent', status: isDocumentUploaded('Letter of Intent') || isDocumentUploaded('LOI') ? 'completed' : 'active', description: 'Letter of Intent document' },
{ id: '10a-2', name: 'Signed LOI', status: isDocumentUploaded('Signed LOI') || isDocumentUploaded('LOI Signed Copy') ? 'completed' : 'active', description: 'Signed Letter of Intent' },
]
}
]
} satisfies ProcessStage;
})(),
{
id: 11, name: 'LOI Issue', status: getStageStatus('LOI Issue'),
date: application.loiIssueDate, description: 'Letter of Intent issued'
}, },
{ {
id: 10, name: 'LOI Issue', status: getStageStatus('LOI Issue', () => ['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 Issued' ? 'active' : 'pending'), id: 12, name: 'Dealer Code Generation', status: getStageStatus('Dealer Code Generation'),
date: application.loiIssueDate, description: 'Letter of Intent issued', documentsUploaded: 1
},
{
id: 11, name: 'Dealer Code Generation', status: getStageStatus('Dealer Code Generation', () => (application.dealerCode || ['Dealer Code Generation', 'Architecture Work', 'Statutory Work', 'LOA Pending', 'LOA Issued', 'EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Onboarded'].includes(application.status)) ? 'completed' : 'pending'),
date: application.dealerCodeDate, description: 'Dealer code generated and assigned', isParallel: true, date: application.dealerCodeDate, description: 'Dealer code generated and assigned', isParallel: true,
branches: [ branches: [
{ name: 'Architectural Work', color: 'green', stages: [ {
{ id: '11a-1', name: 'Architecture Assignment', status: application.architectureAssignedTo ? 'completed' : application.status === 'Architecture Team Assigned' ? 'active' : 'pending', description: 'Assigned to architecture team' }, name: 'Architectural Work', color: 'green', stages: [
{ id: '11a-2', name: 'Site Plan Blueprint', status: isDocumentUploaded('Architecture Blueprint') ? 'completed' : application.architectureAssignedTo ? 'active' : 'pending', description: 'Blueprints and site plans' }, { id: '12a-1', name: 'Architecture Assignment', status: application.architectureAssignedTo ? 'completed' : application.status === 'Architecture Team Assigned' ? 'active' : 'pending', description: 'Assigned to architecture team' },
{ id: '11a-3', name: 'Architecture Work', status: application.architectureStatus === 'COMPLETED' ? 'completed' : (application.architectureStatus === 'IN_PROGRESS' || isDocumentUploaded('Architecture Blueprint')) ? 'active' : 'pending', description: 'Final architecture approval' }, { id: '12a-2', name: 'Site Plan Blueprint', status: isDocumentUploaded('Architecture Blueprint') ? 'completed' : application.architectureAssignedTo ? 'active' : 'pending', description: 'Blueprints and site plans' },
]}, { id: '12a-3', name: 'Architecture Work', status: application.architectureStatus === 'COMPLETED' ? 'completed' : (application.architectureStatus === 'IN_PROGRESS' || isDocumentUploaded('Architecture Blueprint')) ? 'active' : 'pending', description: 'Final architecture approval' },
{ name: 'Statutory Documents', color: 'green', stages: [ ]
{ id: '11b-1', name: 'GST', status: isDocumentUploaded('GST Certificate') || isDocumentUploaded('GST') ? 'completed' : 'active', description: 'GST certificate' }, },
{ id: '11b-2', name: 'PAN', status: isDocumentUploaded('PAN Card') || isDocumentUploaded('PAN') ? 'completed' : 'active', description: 'PAN card' }, {
{ id: '11b-3', name: 'Nodal Agreement', status: isDocumentUploaded('Nodal Agreement') ? 'completed' : 'active', description: 'Nodal agreement document' }, name: 'Statutory Documents', color: 'green', stages: [
{ id: '11b-4', name: 'Cancelled Check', status: isDocumentUploaded('Cancelled Check') ? 'completed' : 'active', description: 'Cancelled check copy' }, { id: '12b-1', name: 'GST', status: isDocumentUploaded('GST Certificate') || isDocumentUploaded('GST') ? 'completed' : 'active', description: 'GST certificate' },
{ id: '11b-5', name: 'Partnership Deed/LLP/MOA/AOA/COI', status: isDocumentUploaded('Partnership Deed/LLP/MOA/AOA/COI') || isDocumentUploaded('Partnership Deed') ? 'completed' : 'active', description: 'Business entity documents' }, { id: '12b-2', name: 'PAN', status: isDocumentUploaded('PAN Card') || isDocumentUploaded('PAN') ? 'completed' : 'active', description: 'PAN card' },
{ id: '11b-6', name: 'Firm Registration Certificate', status: isDocumentUploaded('Firm Registration Certificate') || isDocumentUploaded('Firm Registration') ? 'completed' : 'active', description: 'Firm registration certificate' }, { id: '12b-3', name: 'Nodal Agreement', status: isDocumentUploaded('Nodal Agreement') ? 'completed' : 'active', description: 'Nodal agreement document' },
{ id: '11b-7', name: 'Rental agreement/ Lease agreement / Own/ Land agreement', status: isDocumentUploaded('Rental agreement/ Lease agreement / Own/ Land agreement') || isDocumentUploaded('Property Document') ? 'completed' : 'active', description: 'Property agreement document' }, { id: '12b-4', name: 'Cancelled Check', status: isDocumentUploaded('Cancelled Check') ? 'completed' : 'active', description: 'Cancelled check copy' },
{ id: '11b-8', name: 'Virtual Code', status: isDocumentUploaded('Virtual Code') || isDocumentUploaded('Virtual Code Confirmation') ? 'completed' : 'active', description: 'Virtual code availability' }, { id: '12b-5', name: 'Partnership Deed/LLP/MOA/AOA/COI', status: isDocumentUploaded('Partnership Deed/LLP/MOA/AOA/COI') || isDocumentUploaded('Partnership Deed') ? 'completed' : 'active', description: 'Business entity documents' },
{ id: '11b-9', name: 'Domain ID', status: isDocumentUploaded('Domain ID') || isDocumentUploaded('Domain ID Setup') ? 'completed' : 'active', description: 'Domain ID setup' }, { id: '12b-6', name: 'Firm Registration Certificate', status: isDocumentUploaded('Firm Registration Certificate') || isDocumentUploaded('Firm Registration') ? 'completed' : 'active', description: 'Firm registration certificate' },
{ id: '11b-10', name: 'MSD Configuration', status: isDocumentUploaded('MSD Configuration') ? 'completed' : 'active', description: 'Microsoft Dynamics configuration' }, { id: '12b-7', name: 'Rental agreement/ Lease agreement / Own/ Land agreement', status: isDocumentUploaded('Rental agreement/ Lease agreement / Own/ Land agreement') || isDocumentUploaded('Property Document') ? 'completed' : 'active', description: 'Property agreement document' },
{ id: '11b-11', name: 'LOI Acknowledgement Copy', status: isDocumentUploaded('LOI Acknowledgement Copy') || isDocumentUploaded('LOI Acknowledgement') ? 'completed' : 'active', description: 'LOI acknowledgement copy' }, { id: '12b-8', name: 'Virtual Code', status: isDocumentUploaded('Virtual Code') || isDocumentUploaded('Virtual Code Confirmation') ? 'completed' : 'active', description: 'Virtual code availability' },
]}, { id: '12b-9', name: 'Domain ID', status: isDocumentUploaded('Domain ID') || isDocumentUploaded('Domain ID Setup') ? 'completed' : 'active', description: 'Domain ID setup' },
{ id: '12b-10', name: 'MSD Configuration', status: isDocumentUploaded('MSD Configuration') ? 'completed' : 'active', description: 'Microsoft Dynamics configuration' },
{ id: '12b-11', name: 'LOI Acknowledgement Copy', status: isDocumentUploaded('LOI Acknowledgement Copy') || isDocumentUploaded('LOI Acknowledgement') ? 'completed' : 'active', description: 'LOI acknowledgement copy' },
]
},
] ]
}, },
{ {
id: 12, name: 'LOA', status: getStageStatus('LOA', () => ['EOR In Progress', 'EOR Complete', 'Inauguration', 'Approved', 'Onboarded'].includes(application.status) ? 'completed' : application.status === 'LOA Pending' ? 'active' : 'pending'), id: 13, name: 'LOA', status: getStageStatus('LOA'),
isLocked: application.status === 'LOA Pending' && getDeposit('FIRST_FILL')?.status !== 'Verified', isLocked: application.status === 'LOA Pending' && getDeposit('FIRST_FILL')?.status !== 'Verified',
lockMessage: 'First Fill (₹15L) must be verified by Finance before LOA Approval.', lockMessage: 'First Fill (₹15L) must be verified by Finance before LOA Approval.',
evaluators: Array.from(new Set((application.participants || []).filter((p: any) => p.metadata?.stageCode === 'LOA_APPROVAL' || p.metadata?.allAssignments?.includes('LOA_APPROVAL')).map(participantLabel))), evaluators: Array.from(new Set((application.participants || []).filter((p: any) => p.metadata?.stageCode === 'LOA_APPROVAL' || p.metadata?.allAssignments?.includes('LOA_APPROVAL')).map(participantLabel))),
description: 'Letter of Authorization' description: 'Letter of Agreement'
}, },
{ id: 13, name: 'EOR Complete', status: getStageStatus('EOR Complete', () => ['Inauguration', 'Approved', 'Onboarded'].includes(application.status) ? 'completed' : application.status === 'EOR Complete' ? 'active' : 'pending'), description: 'Essential Operating Requirements' }, { id: 14, name: 'EOR Complete', status: getStageStatus('EOR Complete'), description: 'Essential Operating Requirements' },
{ id: 14, name: 'Inauguration', status: getStageStatus('Inauguration', () => ['Approved', 'Onboarded'].includes(application.status) ? 'completed' : application.status === 'Inauguration' ? 'active' : 'pending'), description: 'Dealership inauguration' }, { id: 15, name: 'Inauguration', status: getStageStatus('Inauguration'), description: 'Dealership inauguration' },
{ id: 15, name: 'Dealership Active', status: getStageStatus('Onboarded', () => application.status === 'Onboarded' ? 'completed' : ['Inauguration', 'Approved'].includes(application.status) ? 'active' : 'pending'), description: 'Dealer profile active' }, { id: 16, name: 'Dealership Active', status: getStageStatus('Onboarded'), description: 'Dealer profile active' },
]; ];
const eorChecklist = [ const eorChecklist = [

View File

@ -19,6 +19,7 @@ export function useApplicationDetailsUIState({ initialTab = 'questionnaire' }: U
const [showScheduleModal, setShowScheduleModal] = useState(false); const [showScheduleModal, setShowScheduleModal] = useState(false);
const [showCancelInterviewModal, setShowCancelInterviewModal] = useState(false); const [showCancelInterviewModal, setShowCancelInterviewModal] = useState(false);
const [interviewIdToCancel, setInterviewIdToCancel] = useState(''); const [interviewIdToCancel, setInterviewIdToCancel] = useState('');
const [interviewToReschedule, setInterviewToReschedule] = useState<any>(null);
const [showKTMatrixModal, setShowKTMatrixModal] = useState(false); const [showKTMatrixModal, setShowKTMatrixModal] = useState(false);
const [showLevel2FeedbackModal, setShowLevel2FeedbackModal] = useState(false); const [showLevel2FeedbackModal, setShowLevel2FeedbackModal] = useState(false);
const [showLevel3FeedbackModal, setShowLevel3FeedbackModal] = useState(false); const [showLevel3FeedbackModal, setShowLevel3FeedbackModal] = useState(false);
@ -61,6 +62,7 @@ export function useApplicationDetailsUIState({ initialTab = 'questionnaire' }: U
const [fddAgencies, setFddAgencies] = useState<any[]>([]); const [fddAgencies, setFddAgencies] = useState<any[]>([]);
const [selectedAgencyId, setSelectedAgencyId] = useState(''); const [selectedAgencyId, setSelectedAgencyId] = useState('');
const [isAssigningAgency, setIsAssigningAgency] = useState(false); const [isAssigningAgency, setIsAssigningAgency] = useState(false);
const [showAssignFddModal, setShowAssignFddModal] = useState(false);
const [isApproving, setIsApproving] = useState(false); const [isApproving, setIsApproving] = useState(false);
const [isRejecting, setIsRejecting] = useState(false); const [isRejecting, setIsRejecting] = useState(false);
const [ktMatrixScores, setKtMatrixScores] = useState<Record<string, number>>({}); const [ktMatrixScores, setKtMatrixScores] = useState<Record<string, number>>({});
@ -98,6 +100,7 @@ export function useApplicationDetailsUIState({ initialTab = 'questionnaire' }: U
showScheduleModal, setShowScheduleModal, showScheduleModal, setShowScheduleModal,
showCancelInterviewModal, setShowCancelInterviewModal, showCancelInterviewModal, setShowCancelInterviewModal,
interviewIdToCancel, setInterviewIdToCancel, interviewIdToCancel, setInterviewIdToCancel,
interviewToReschedule, setInterviewToReschedule,
showKTMatrixModal, setShowKTMatrixModal, showKTMatrixModal, setShowKTMatrixModal,
showLevel2FeedbackModal, setShowLevel2FeedbackModal, showLevel2FeedbackModal, setShowLevel2FeedbackModal,
showLevel3FeedbackModal, setShowLevel3FeedbackModal, showLevel3FeedbackModal, setShowLevel3FeedbackModal,
@ -140,6 +143,7 @@ export function useApplicationDetailsUIState({ initialTab = 'questionnaire' }: U
fddAgencies, setFddAgencies, fddAgencies, setFddAgencies,
selectedAgencyId, setSelectedAgencyId, selectedAgencyId, setSelectedAgencyId,
isAssigningAgency, setIsAssigningAgency, isAssigningAgency, setIsAssigningAgency,
showAssignFddModal, setShowAssignFddModal,
isApproving, setIsApproving, isApproving, setIsApproving,
isRejecting, setIsRejecting, isRejecting, setIsRejecting,
ktMatrixScores, setKtMatrixScores, ktMatrixScores, setKtMatrixScores,

View File

@ -26,7 +26,6 @@ import {
Download, Download,
Grid3x3, Grid3x3,
List, List,
Mail,
CheckCircle, CheckCircle,
AlertCircle, AlertCircle,
Loader2 Loader2
@ -41,7 +40,7 @@ import {
TableHeader, TableHeader,
TableRow, TableRow,
} from '@/components/ui/table'; } from '@/components/ui/table';
import { Progress } from '@/components/ui/progress'; import { ApplicationProgressBar } from '@/features/onboarding/components/ApplicationProgressBar';
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog'; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { Label } from '@/components/ui/label'; import { Label } from '@/components/ui/label';
import { Textarea } from '@/components/ui/textarea'; import { Textarea } from '@/components/ui/textarea';
@ -193,9 +192,6 @@ export function AllApplicationsPage({ onViewDetails, initialFilter = 'all' }: Al
} }
}; };
const handleBulkReminders = () => {
toast.success(`Reminder emails sent to ${selectedIds.length} applicant(s)`);
};
// For DD's All Applications page, only show initial statuses // For DD's All Applications page, only show initial statuses
const statusOptions: ApplicationStatus[] = [ const statusOptions: ApplicationStatus[] = [
@ -219,7 +215,7 @@ export function AllApplicationsPage({ onViewDetails, initialFilter = 'all' }: Al
'In Review': 'bg-slate-100 text-slate-800', 'In Review': 'bg-slate-100 text-slate-800',
'Level 3 Approved': 'bg-green-100 text-green-800', 'Level 3 Approved': 'bg-green-100 text-green-800',
'FDD Verification': 'bg-indigo-100 text-indigo-800', 'FDD Verification': 'bg-indigo-100 text-indigo-800',
'Payment Pending': 'bg-amber-100 text-amber-800', 'Payment Pending': 'bg-red-50 text-red-800',
'LOI In Progress': 'bg-sky-100 text-sky-800', 'LOI In Progress': 'bg-sky-100 text-sky-800',
'LOI Issued': 'bg-sky-100 text-sky-800', 'LOI Issued': 'bg-sky-100 text-sky-800',
'Dealer Code Generation': 'bg-purple-100 text-purple-800', 'Dealer Code Generation': 'bg-purple-100 text-purple-800',
@ -240,15 +236,19 @@ export function AllApplicationsPage({ onViewDetails, initialFilter = 'all' }: Al
'EOR In Progress': 'bg-violet-100 text-violet-800', 'EOR In Progress': 'bg-violet-100 text-violet-800',
'EOR Complete': 'bg-violet-100 text-violet-800', 'EOR Complete': 'bg-violet-100 text-violet-800',
'LOA Pending': 'bg-pink-100 text-pink-800', 'LOA Pending': 'bg-pink-100 text-pink-800',
'Inauguration': 'bg-amber-100 text-amber-800', 'Inauguration': 'bg-red-50 text-red-800',
'Approved': 'bg-green-100 text-green-800', 'Approved': 'bg-green-100 text-green-800',
'Rejected': 'bg-red-100 text-red-800', 'Rejected': 'bg-red-100 text-red-800',
'Disqualified': 'bg-gray-100 text-gray-800', 'Disqualified': 'bg-gray-100 text-gray-800',
'Onboarded': 'bg-emerald-100 text-emerald-800', 'Onboarded': 'bg-emerald-100 text-emerald-800',
'LOI Approved': 'bg-sky-100 text-sky-800', 'LOI Approved': 'bg-sky-100 text-sky-800',
'Security Details In Progress': 'bg-amber-100 text-amber-800', 'Security Deposit In Progress': 'bg-red-50 text-red-800',
'Security Deposit Approved': 'bg-green-100 text-green-800',
'Security Deposit': 'bg-red-50 text-red-800',
/** Legacy overallStatus until DB migrated */
'Security Details In Progress': 'bg-red-50 text-red-800',
'Security Details Approved': 'bg-green-100 text-green-800', 'Security Details Approved': 'bg-green-100 text-green-800',
'Security Details': 'bg-amber-100 text-amber-800', 'Security Details': 'bg-red-50 text-red-800',
'LOA Issued': 'bg-pink-100 text-pink-800', 'LOA Issued': 'bg-pink-100 text-pink-800',
}; };
return colors[status] || 'bg-gray-100 text-gray-800'; return colors[status] || 'bg-gray-100 text-gray-800';
@ -257,12 +257,12 @@ export function AllApplicationsPage({ onViewDetails, initialFilter = 'all' }: Al
return ( return (
<div className="space-y-6"> <div className="space-y-6">
{/* Info Banner */} {/* Info Banner */}
<div className="bg-amber-50 border border-amber-200 rounded-lg p-4" data-testid="onboarding-all-apps-banner"> <div className="bg-red-50 border border-red-200 rounded-lg p-4" data-testid="onboarding-all-apps-banner">
<div className="flex items-start gap-3"> <div className="flex items-start gap-3">
<AlertCircle className="w-5 h-5 text-amber-600 flex-shrink-0 mt-0.5" /> <AlertCircle className="w-5 h-5 text-re-red flex-shrink-0 mt-0.5" />
<div> <div>
<h3 className="text-amber-900 mb-1">DD Workflow - Initial Application Review</h3> <h3 className="text-red-900 mb-1">DD Workflow - Initial Application Review</h3>
<p className="text-amber-800"> <p className="text-red-800">
This page shows <strong>only applications that haven't been shortlisted yet</strong>. Review and select promising candidates using the <strong>Shortlist</strong> button. This page shows <strong>only applications that haven't been shortlisted yet</strong>. Review and select promising candidates using the <strong>Shortlist</strong> button.
Once shortlisted, applications will be removed from here and moved to the <strong>Dealership Requests</strong> page for further processing. Once shortlisted, applications will be removed from here and moved to the <strong>Dealership Requests</strong> page for further processing.
</p> </p>
@ -331,7 +331,7 @@ export function AllApplicationsPage({ onViewDetails, initialFilter = 'all' }: Al
variant={viewMode === 'grid' ? 'default' : 'outline'} variant={viewMode === 'grid' ? 'default' : 'outline'}
size="sm" size="sm"
onClick={() => setViewMode('grid')} onClick={() => setViewMode('grid')}
className={viewMode === 'grid' ? 'bg-amber-600 hover:bg-amber-700' : ''} className={viewMode === 'grid' ? 'bg-re-red hover:bg-re-red-hover' : ''}
data-testid="onboarding-all-apps-grid-view-btn" data-testid="onboarding-all-apps-grid-view-btn"
> >
<Grid3x3 className="w-4 h-4 mr-2" /> <Grid3x3 className="w-4 h-4 mr-2" />
@ -341,7 +341,7 @@ export function AllApplicationsPage({ onViewDetails, initialFilter = 'all' }: Al
variant={viewMode === 'table' ? 'default' : 'outline'} variant={viewMode === 'table' ? 'default' : 'outline'}
size="sm" size="sm"
onClick={() => setViewMode('table')} onClick={() => setViewMode('table')}
className={viewMode === 'table' ? 'bg-amber-600 hover:bg-amber-700' : ''} className={viewMode === 'table' ? 'bg-re-red hover:bg-re-red-hover' : ''}
data-testid="onboarding-all-apps-table-view-btn" data-testid="onboarding-all-apps-table-view-btn"
> >
<List className="w-4 h-4 mr-2" /> <List className="w-4 h-4 mr-2" />
@ -356,16 +356,6 @@ export function AllApplicationsPage({ onViewDetails, initialFilter = 'all' }: Al
{selectedIds.length > 0 && ( {selectedIds.length > 0 && (
<> <>
<Button
variant="outline"
size="sm"
onClick={handleBulkReminders}
data-testid="onboarding-all-apps-reminders-btn"
>
<Mail className="w-4 h-4 mr-2" />
Send Reminders ({selectedIds.length})
</Button>
<Button <Button
size="sm" size="sm"
onClick={handleShortlist} onClick={handleShortlist}
@ -390,7 +380,7 @@ export function AllApplicationsPage({ onViewDetails, initialFilter = 'all' }: Al
{/* Applications Grid/Table */} {/* Applications Grid/Table */}
{loading ? ( {loading ? (
<div className="flex justify-center items-center h-96 bg-white rounded-lg border border-slate-200"> <div className="flex justify-center items-center h-96 bg-white rounded-lg border border-slate-200">
<Loader2 className="w-8 h-8 animate-spin text-amber-600" /> <Loader2 className="w-8 h-8 animate-spin text-re-red" />
</div> </div>
) : viewMode === 'grid' ? ( ) : viewMode === 'grid' ? (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4" data-testid="onboarding-all-apps-grid-container"> <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4" data-testid="onboarding-all-apps-grid-container">
@ -475,8 +465,12 @@ export function AllApplicationsPage({ onViewDetails, initialFilter = 'all' }: Al
</TableCell> </TableCell>
<TableCell> <TableCell>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Progress value={app.progress} className="w-20" data-testid={`onboarding-all-apps-progress-bar-${idx}`} /> <ApplicationProgressBar
<span className="text-slate-600" data-testid={`onboarding-all-apps-progress-text-${idx}`}>{app.progress}%</span> value={app.progress}
status={app.status}
showPercent
data-testid={`onboarding-all-apps-progress-bar-${idx}`}
/>
</div> </div>
</TableCell> </TableCell>
<TableCell> <TableCell>

View File

@ -1,4 +1,5 @@
import { useEffect } from 'react'; import { useEffect, useState } from 'react';
import { slaService, SlaStatusSnapshot } from '@/services/sla.service';
import { useParams, useNavigate, useLocation } from 'react-router-dom'; import { useParams, useNavigate, useLocation } from 'react-router-dom';
import { onboardingService } from '@/services/onboarding.service'; import { onboardingService } from '@/services/onboarding.service';
import { ApplicationDetailsHeader } from '@/features/onboarding/components/application-details/ApplicationDetailsHeader'; import { ApplicationDetailsHeader } from '@/features/onboarding/components/application-details/ApplicationDetailsHeader';
@ -27,6 +28,19 @@ export const ApplicationDetails = () => {
const { user: currentUser } = useSelector((state: RootState) => state.auth); const { user: currentUser } = useSelector((state: RootState) => state.auth);
const applicationId = id || ''; const applicationId = id || '';
const onBack = () => navigate(-1); const onBack = () => navigate(-1);
const [slaStatus, setSlaStatus] = useState<SlaStatusSnapshot | null>(null);
useEffect(() => {
if (!applicationId) return;
slaService
.getBatchStatus([{ entityType: 'application', entityId: applicationId }])
.then((res) => {
if (res?.success) {
setSlaStatus(res.data[`application:${applicationId}`] ?? null);
}
})
.catch(() => setSlaStatus(null));
}, [applicationId]);
const { const {
application, application,
@ -62,6 +76,7 @@ export const ApplicationDetails = () => {
showScheduleModal, setShowScheduleModal, showScheduleModal, setShowScheduleModal,
showCancelInterviewModal, setShowCancelInterviewModal, showCancelInterviewModal, setShowCancelInterviewModal,
interviewIdToCancel, setInterviewIdToCancel, interviewIdToCancel, setInterviewIdToCancel,
interviewToReschedule, setInterviewToReschedule,
showKTMatrixModal, setShowKTMatrixModal, showKTMatrixModal, setShowKTMatrixModal,
showLevel2FeedbackModal, setShowLevel2FeedbackModal, showLevel2FeedbackModal, setShowLevel2FeedbackModal,
showLevel3FeedbackModal, setShowLevel3FeedbackModal, showLevel3FeedbackModal, setShowLevel3FeedbackModal,
@ -104,6 +119,7 @@ export const ApplicationDetails = () => {
fddAgencies, setFddAgencies, fddAgencies, setFddAgencies,
selectedAgencyId, setSelectedAgencyId, selectedAgencyId, setSelectedAgencyId,
isAssigningAgency, setIsAssigningAgency, isAssigningAgency, setIsAssigningAgency,
showAssignFddModal, setShowAssignFddModal,
isApproving, setIsApproving, isApproving, setIsApproving,
isRejecting, setIsRejecting, isRejecting, setIsRejecting,
ktMatrixScores, setKtMatrixScores, ktMatrixScores, setKtMatrixScores,
@ -162,6 +178,18 @@ export const ApplicationDetails = () => {
currentUser?.role === 'Super Admin' || currentUser?.role === 'DD Admin' || currentUser?.role === 'Super Admin' || currentUser?.role === 'DD Admin' ||
currentUser?.role === 'NBH' || currentUser?.role === 'DD Head' || currentUser?.role === 'NBH' || currentUser?.role === 'DD Head' ||
currentUser?.roleCode === 'NBH' || currentUser?.roleCode === 'DD_HEAD'; currentUser?.roleCode === 'NBH' || currentUser?.roleCode === 'DD_HEAD';
// FDD Audit and Payments tabs are finance-/process-sensitive. Per ops policy,
// only DD-Admin, Super Admin and Finance roles should see them — the broader
// `isAdmin` bucket (which also includes NBH and DD-Head) is too wide here.
const canViewFinanceTabs =
currentUser?.roleCode === 'Super Admin' ||
currentUser?.roleCode === 'DD Admin' ||
currentUser?.roleCode === 'Finance' ||
currentUser?.roleCode === 'Finance Admin' ||
currentUser?.role === 'Super Admin' ||
currentUser?.role === 'DD Admin' ||
currentUser?.role === 'Finance' ||
currentUser?.role === 'Finance Admin';
useEffect(() => { useEffect(() => {
const fetchConfigs = async () => { const fetchConfigs = async () => {
@ -266,7 +294,7 @@ export const ApplicationDetails = () => {
handleRemoveInterviewer, handleRemoveInterviewer,
maybeFetchUsersForModal, maybeFetchUsersForModal,
handleScheduleInterview, handleScheduleInterview,
handleCancelInterview, handleRescheduleInterview,
handleConfirmCancelInterview, handleConfirmCancelInterview,
handleUpload, handleUpload,
handleApprove, handleApprove,
@ -291,10 +319,15 @@ export const ApplicationDetails = () => {
participantType, participantType,
users, users,
interviewDate, interviewDate,
setInterviewDate,
interviewType, interviewType,
setInterviewType,
interviewMode, interviewMode,
setInterviewMode,
meetingLink, meetingLink,
setMeetingLink,
location, location,
setLocation,
scheduledInterviewParticipants, scheduledInterviewParticipants,
uploadFile, uploadFile,
uploadDocType, uploadDocType,
@ -319,6 +352,8 @@ export const ApplicationDetails = () => {
setShowCancelInterviewModal, setShowCancelInterviewModal,
interviewIdToCancel, interviewIdToCancel,
setInterviewIdToCancel, setInterviewIdToCancel,
interviewToReschedule,
setInterviewToReschedule,
setIsCancellingInterview, setIsCancellingInterview,
setIsUploading, setIsUploading,
setShowUploadForm, setShowUploadForm,
@ -344,7 +379,7 @@ export const ApplicationDetails = () => {
if (loading && !application) { if (loading && !application) {
return ( return (
<div className="flex items-center justify-center min-h-[60vh]"> <div className="flex items-center justify-center min-h-[60vh]">
<Loader2 className="w-10 h-10 animate-spin text-amber-600" /> <Loader2 className="w-10 h-10 animate-spin text-re-red" />
</div> </div>
); );
} }
@ -359,6 +394,7 @@ export const ApplicationDetails = () => {
interviews, interviews,
eorData, eorData,
getDeposit, getDeposit,
documentConfigs,
}); });
const { const {
@ -380,13 +416,7 @@ export const ApplicationDetails = () => {
const renderFddAuditContent = () => ( const renderFddAuditContent = () => (
<ApplicationDetailsFddAuditContent <ApplicationDetailsFddAuditContent
application={application} application={application}
currentUser={currentUser}
documents={documents} documents={documents}
fddAgencies={fddAgencies}
selectedAgencyId={selectedAgencyId}
setSelectedAgencyId={setSelectedAgencyId}
isAssigningAgency={isAssigningAgency}
handleAssignAgency={handleAssignAgency}
setPreviewDoc={setPreviewDoc} setPreviewDoc={setPreviewDoc}
setShowPreviewModal={setShowPreviewModal} setShowPreviewModal={setShowPreviewModal}
setIsUploading={setIsUploading} setIsUploading={setIsUploading}
@ -399,6 +429,7 @@ export const ApplicationDetails = () => {
<div className="space-y-6"> <div className="space-y-6">
<ApplicationDetailsHeader <ApplicationDetailsHeader
application={application} application={application}
slaStatus={slaStatus}
isNonResponsive={isNonResponsive} isNonResponsive={isNonResponsive}
isAdmin={isAdmin} isAdmin={isAdmin}
onBack={onBack} onBack={onBack}
@ -443,7 +474,7 @@ export const ApplicationDetails = () => {
setShowDocumentsModal={setShowDocumentsModal} setShowDocumentsModal={setShowDocumentsModal}
setShowUploadForm={setShowUploadForm} setShowUploadForm={setShowUploadForm}
handleRetriggerEvaluators={handleRetriggerEvaluators} handleRetriggerEvaluators={handleRetriggerEvaluators}
handleCancelInterview={handleCancelInterview} handleRescheduleInterview={handleRescheduleInterview}
setSelectedEvaluationForView={setSelectedEvaluationForView} setSelectedEvaluationForView={setSelectedEvaluationForView}
setShowFeedbackDetailsModal={setShowFeedbackDetailsModal} setShowFeedbackDetailsModal={setShowFeedbackDetailsModal}
renderFddAuditContent={renderFddAuditContent} renderFddAuditContent={renderFddAuditContent}
@ -452,6 +483,7 @@ export const ApplicationDetails = () => {
eorChecklist={eorChecklist} eorChecklist={eorChecklist}
setUploadDocType={setUploadDocType} setUploadDocType={setUploadDocType}
isAdmin={isAdmin} isAdmin={isAdmin}
canViewFinanceTabs={canViewFinanceTabs}
fetchApplication={fetchApplication} fetchApplication={fetchApplication}
fetchEorData={fetchEorData} fetchEorData={fetchEorData}
deposits={deposits} deposits={deposits}
@ -487,6 +519,21 @@ export const ApplicationDetails = () => {
currentUser={currentUser} currentUser={currentUser}
handleGenerateDealerCodes={handleGenerateDealerCodes} handleGenerateDealerCodes={handleGenerateDealerCodes}
onOpenAssignArchitectureModal={() => setShowAssignArchitectureModal(true)} onOpenAssignArchitectureModal={() => setShowAssignArchitectureModal(true)}
onOpenAssignFdd={() => {
setSelectedAgencyId('');
fetchFddAgencies();
setShowAssignFddModal(true);
}}
showAssignFddModal={showAssignFddModal}
setShowAssignFddModal={setShowAssignFddModal}
fddAgencies={fddAgencies}
selectedAgencyId={selectedAgencyId}
setSelectedAgencyId={setSelectedAgencyId}
isAssigningAgency={isAssigningAgency}
handleAssignAgency={async () => {
await handleAssignAgency();
setShowAssignFddModal(false);
}}
activeInterviewForUser={activeInterviewForUser} activeInterviewForUser={activeInterviewForUser}
hasSubmittedFeedback={hasSubmittedFeedback} hasSubmittedFeedback={hasSubmittedFeedback}
setSelectedInterviewForFeedback={setSelectedInterviewForFeedback} setSelectedInterviewForFeedback={setSelectedInterviewForFeedback}
@ -503,6 +550,8 @@ export const ApplicationDetails = () => {
setParticipantType={setParticipantType} setParticipantType={setParticipantType}
handleAddParticipant={handleAddParticipant} handleAddParticipant={handleAddParticipant}
isAssigningParticipant={isAssigningParticipant} isAssigningParticipant={isAssigningParticipant}
documents={documents}
documentConfigs={documentConfigs}
/> />
<ApplicationDetailsActionModals <ApplicationDetailsActionModals
@ -532,6 +581,8 @@ export const ApplicationDetails = () => {
setInterviewIdToCancel={setInterviewIdToCancel} setInterviewIdToCancel={setInterviewIdToCancel}
isCancellingInterview={isCancellingInterview} isCancellingInterview={isCancellingInterview}
handleConfirmCancelInterview={handleConfirmCancelInterview} handleConfirmCancelInterview={handleConfirmCancelInterview}
interviewToReschedule={interviewToReschedule}
setInterviewToReschedule={setInterviewToReschedule}
interviewType={interviewType} interviewType={interviewType}
setInterviewType={setInterviewType} setInterviewType={setInterviewType}
interviewMode={interviewMode} interviewMode={interviewMode}

View File

@ -1,7 +1,10 @@
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { toast } from 'sonner';
import { ApplicationStatus, Application } from '@/lib/mock-data'; import { ApplicationStatus, Application } from '@/lib/mock-data';
import { formatDateTime } from '@/components/ui/utils'; import { formatDateTime } from '@/components/ui/utils';
import { onboardingService } from '@/services/onboarding.service'; import { onboardingService } from '@/services/onboarding.service';
import { slaService, SlaStatusSnapshot } from '@/services/sla.service';
import { SlaBadge } from '@/components/sla/SlaBadge';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
import { import {
@ -14,7 +17,8 @@ import {
import { import {
Search, Search,
Download, Download,
Mail Mail,
Loader2
} from 'lucide-react'; } from 'lucide-react';
import { Badge } from '@/components/ui/badge'; import { Badge } from '@/components/ui/badge';
import { Checkbox } from '@/components/ui/checkbox'; import { Checkbox } from '@/components/ui/checkbox';
@ -26,7 +30,7 @@ import {
TableHeader, TableHeader,
TableRow, TableRow,
} from '@/components/ui/table'; } from '@/components/ui/table';
import { Progress } from '@/components/ui/progress'; import { ApplicationProgressBar } from '@/features/onboarding/components/ApplicationProgressBar';
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'; import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { Label } from '@/components/ui/label'; import { Label } from '@/components/ui/label';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
@ -53,12 +57,14 @@ export function ApplicationsPage({ onViewDetails, initialFilter }: ApplicationsP
const [statusFilter, setStatusFilter] = useState<string>(initialFilter || 'all'); const [statusFilter, setStatusFilter] = useState<string>(initialFilter || 'all');
const [selectedIds, setSelectedIds] = useState<string[]>([]); const [selectedIds, setSelectedIds] = useState<string[]>([]);
const [sortBy, setSortBy] = useState<'date'>('date'); const [isSendingReminders, setIsSendingReminders] = useState(false);
const [sortBy] = useState<'date'>('date');
const [showNewApplicationModal, setShowNewApplicationModal] = useState(false); const [showNewApplicationModal, setShowNewApplicationModal] = useState(false);
const [showMyAssignments, setShowMyAssignments] = useState(false); const [showMyAssignments, setShowMyAssignments] = useState(false);
// Real Data Integration // Real Data Integration
const [applications, setApplications] = useState<Application[]>([]); const [applications, setApplications] = useState<Application[]>([]);
const [slaByAppId, setSlaByAppId] = useState<Record<string, SlaStatusSnapshot | null>>({});
const [locations, setLocations] = useState<string[]>([]); const [locations, setLocations] = useState<string[]>([]);
const [currentPage, setCurrentPage] = useState(1); const [currentPage, setCurrentPage] = useState(1);
const [paginationMeta, setPaginationMeta] = useState<any>(null); const [paginationMeta, setPaginationMeta] = useState<any>(null);
@ -84,7 +90,6 @@ export function ApplicationsPage({ onViewDetails, initialFilter }: ApplicationsP
const applicationsData = response.data || []; const applicationsData = response.data || [];
setPaginationMeta(response.meta); setPaginationMeta(response.meta);
// Map backend data to frontend Application interface
const mappedApps = applicationsData.map((app: any) => ({ const mappedApps = applicationsData.map((app: any) => ({
id: app.id, id: app.id,
registrationNumber: app.applicationId || 'N/A', registrationNumber: app.applicationId || 'N/A',
@ -119,6 +124,23 @@ export function ApplicationsPage({ onViewDetails, initialFilter }: ApplicationsP
address: app.address address: app.address
})); }));
setApplications(mappedApps); setApplications(mappedApps);
if (mappedApps.length > 0) {
slaService
.getBatchStatus(mappedApps.map((app: Application) => ({ entityType: 'application', entityId: app.id })))
.then((slaRes) => {
if (slaRes?.success) {
const map: Record<string, SlaStatusSnapshot | null> = {};
mappedApps.forEach((app: Application) => {
map[app.id] = slaRes.data[`application:${app.id}`] ?? null;
});
setSlaByAppId(map);
}
})
.catch(() => setSlaByAppId({}));
} else {
setSlaByAppId({});
}
// Extract unique locations for filtering - could be optimized to fetch once // Extract unique locations for filtering - could be optimized to fetch once
if (locations.length === 0) { if (locations.length === 0) {
@ -153,51 +175,45 @@ export function ApplicationsPage({ onViewDetails, initialFilter }: ApplicationsP
} }
}; };
const handleBulkReminders = () => { const handleBulkReminders = async () => {
alert(`Sending reminders to ${selectedIds.length} applicants`); if (selectedIds.length === 0) return;
setSelectedIds([]);
try {
setIsSendingReminders(true);
const res = await onboardingService.sendBulkReminders(selectedIds);
if (res.success) {
toast.success(res.message || `Reminder emails sent to ${selectedIds.length} applicant(s)`);
setSelectedIds([]);
}
} catch (error: any) {
console.error('Failed to send reminders:', error);
toast.error(error.message || 'Failed to send reminders');
} finally {
setIsSendingReminders(false);
}
}; };
const handleExport = () => { const handleExport = () => {
alert('Exporting applications to CSV...'); alert('Exporting applications to CSV...');
}; };
/**
* Status badge classes keep the palette restrained to the brand
* (re-red + black + slate). A rainbow palette across status pills
* fights the rest of the UI, so we collapse to three buckets:
* - positive end-states (Approved / Onboarded / Completed) black
* - terminal negative (Rejected / Disqualified) muted brand red
* - everything else (in-progress / pending / default) light slate
*/
const getStatusColor = (status: string) => { const getStatusColor = (status: string) => {
const statusColors: Record<string, string> = { const s = String(status || '');
'Submitted': 'bg-slate-500', if (s === 'Rejected' || s === 'Disqualified' || s.includes('Rejected')) {
'Questionnaire Pending': 'bg-orange-500', return 'bg-red-50 text-re-red-hover border border-red-200';
'Questionnaire Completed': 'bg-blue-500', }
'Shortlisted': 'bg-cyan-500', if (s === 'Approved' || s === 'Onboarded' || s === 'Completed') {
'Level 1 Pending': 'bg-amber-500', return 'bg-slate-900 text-white border border-transparent';
'Level 1 Approved': 'bg-green-500', }
'Level 2 Pending': 'bg-purple-500', return 'bg-slate-200 text-slate-800 border border-slate-300';
'Level 2 Approved': 'bg-green-600',
'Level 2 Recommended': 'bg-teal-500',
'Level 3 Pending': 'bg-indigo-500',
'FDD Verification': 'bg-violet-500',
'Payment Pending': 'bg-yellow-500',
'LOI Issued': 'bg-lime-500',
'Dealer Code Generation': 'bg-fuchsia-500',
'Architecture Team Assigned': 'bg-blue-500',
'Architecture Document Upload': 'bg-blue-500',
'Architecture Team Completion': 'bg-blue-500',
'Statutory GST': 'bg-emerald-500',
'Statutory PAN': 'bg-emerald-500',
'Statutory Nodal': 'bg-emerald-500',
'Statutory Check': 'bg-emerald-500',
'Statutory Partnership': 'bg-emerald-500',
'Statutory Firm Reg': 'bg-emerald-500',
'Statutory Virtual Code': 'bg-emerald-500',
'Statutory Domain': 'bg-emerald-500',
'Statutory MSD': 'bg-emerald-500',
'Statutory LOI Ack': 'bg-emerald-500',
'EOR In Progress': 'bg-sky-500',
'LOA Pending': 'bg-emerald-500',
'Approved': 'bg-green-700',
'Rejected': 'bg-red-500',
'Disqualified': 'bg-red-700'
};
return statusColors[status] || 'bg-slate-500';
}; };
return ( return (
@ -257,14 +273,6 @@ export function ApplicationsPage({ onViewDetails, initialFilter }: ApplicationsP
<Label htmlFor="my-assignments" data-testid="onboarding-applications-assignments-label">My Assignments Only</Label> <Label htmlFor="my-assignments" data-testid="onboarding-applications-assignments-label">My Assignments Only</Label>
</div> </div>
<Select value={sortBy} onValueChange={(v) => setSortBy(v as any)}>
<SelectTrigger className="w-full lg:w-40" data-testid="onboarding-applications-sort-select">
<SelectValue placeholder="Sort By" />
</SelectTrigger>
<SelectContent>
<SelectItem value="date">Date</SelectItem>
</SelectContent>
</Select>
</div> </div>
<div className="flex flex-wrap items-center gap-3 mt-4"> <div className="flex flex-wrap items-center gap-3 mt-4">
@ -283,9 +291,14 @@ export function ApplicationsPage({ onViewDetails, initialFilter }: ApplicationsP
variant="outline" variant="outline"
size="sm" size="sm"
onClick={handleBulkReminders} onClick={handleBulkReminders}
disabled={isSendingReminders}
data-testid="onboarding-applications-reminders-button" data-testid="onboarding-applications-reminders-button"
> >
<Mail className="w-4 h-4 mr-2" /> {isSendingReminders ? (
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
) : (
<Mail className="w-4 h-4 mr-2" />
)}
Send Reminders ({selectedIds.length}) Send Reminders ({selectedIds.length})
</Button> </Button>
)} )}
@ -311,6 +324,7 @@ export function ApplicationsPage({ onViewDetails, initialFilter }: ApplicationsP
<TableHead>Name</TableHead> <TableHead>Name</TableHead>
<TableHead>Preferred Location</TableHead> <TableHead>Preferred Location</TableHead>
<TableHead>Status</TableHead> <TableHead>Status</TableHead>
<TableHead>SLA</TableHead>
<TableHead>Applicant Location</TableHead> <TableHead>Applicant Location</TableHead>
<TableHead>Progress</TableHead> <TableHead>Progress</TableHead>
<TableHead>Applied On</TableHead> <TableHead>Applied On</TableHead>
@ -335,14 +349,19 @@ export function ApplicationsPage({ onViewDetails, initialFilter }: ApplicationsP
{app.status} {app.status}
</Badge> </Badge>
</TableCell> </TableCell>
<TableCell>
<SlaBadge status={slaByAppId[app.id]} compact />
</TableCell>
<TableCell className="text-slate-600 max-w-xs truncate" data-testid={`onboarding-application-addr-${idx}`}> <TableCell className="text-slate-600 max-w-xs truncate" data-testid={`onboarding-application-addr-${idx}`}>
{app.residentialAddress} {app.residentialAddress}
</TableCell> </TableCell>
<TableCell> <TableCell>
<div className="flex items-center gap-2"> <ApplicationProgressBar
<Progress value={app.progress} className="h-2 w-20" data-testid={`onboarding-application-progress-bar-${idx}`} /> value={app.progress}
<span className="text-slate-600" data-testid={`onboarding-application-progress-text-${idx}`}>{app.progress}%</span> status={app.status}
</div> showPercent
data-testid={`onboarding-application-progress-bar-${idx}`}
/>
</TableCell> </TableCell>
<TableCell data-testid={`onboarding-application-date-${idx}`}> <TableCell data-testid={`onboarding-application-date-${idx}`}>
{formatDateTime(app.submissionDate)} {formatDateTime(app.submissionDate)}
@ -452,7 +471,7 @@ export function ApplicationsPage({ onViewDetails, initialFilter }: ApplicationsP
<Button variant="outline" onClick={() => setShowNewApplicationModal(false)} data-testid="onboarding-new-app-cancel"> <Button variant="outline" onClick={() => setShowNewApplicationModal(false)} data-testid="onboarding-new-app-cancel">
Cancel Cancel
</Button> </Button>
<Button className="bg-amber-600 hover:bg-amber-700" data-testid="onboarding-new-app-submit"> <Button className="bg-re-red hover:bg-re-red-hover" data-testid="onboarding-new-app-submit">
Create Application Create Application
</Button> </Button>
</div> </div>

View File

@ -408,20 +408,20 @@ export function FDDApplicationDetails() {
{/* SECTION 2: MY SUBMISSIONS */} {/* SECTION 2: MY SUBMISSIONS */}
<div data-testid="onboarding-fdd-details-my-submissions"> <div data-testid="onboarding-fdd-details-my-submissions">
<p className="text-[10px] font-bold text-slate-400 uppercase tracking-wider mb-2 flex items-center gap-2"> <p className="text-[10px] font-bold text-slate-400 uppercase tracking-wider mb-2 flex items-center gap-2">
<div className="w-1.5 h-1.5 rounded-full bg-amber-500"></div> <div className="w-1.5 h-1.5 rounded-full bg-red-500"></div>
My Uploaded Reports My Uploaded Reports
</p> </p>
<div className="space-y-2"> <div className="space-y-2">
{application.uploadedDocuments?.filter((d: any) => d.uploader?.roleCode === 'FDD').map((doc: any, i: number) => ( {application.uploadedDocuments?.filter((d: any) => d.uploader?.roleCode === 'FDD').map((doc: any, i: number) => (
<div key={i} className="p-3 border border-amber-100 bg-amber-50/30 rounded flex items-center justify-between hover:bg-amber-50 transition-all group" data-testid={`onboarding-fdd-details-my-report-row-${i}`}> <div key={i} className="p-3 border border-red-100 bg-red-50/30 rounded flex items-center justify-between hover:bg-red-50 transition-all group" data-testid={`onboarding-fdd-details-my-report-row-${i}`}>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="w-8 h-8 rounded bg-amber-100 flex items-center justify-center text-amber-500"> <div className="w-8 h-8 rounded bg-red-50 flex items-center justify-center text-re-red">
<FileText className="w-4 h-4" /> <FileText className="w-4 h-4" />
</div> </div>
<div> <div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<p className="text-xs font-bold text-slate-900" data-testid={`onboarding-fdd-details-my-report-name-${i}`}>{doc.originalName || doc.fileName}</p> <p className="text-xs font-bold text-slate-900" data-testid={`onboarding-fdd-details-my-report-name-${i}`}>{doc.originalName || doc.fileName}</p>
<span className="text-[8px] bg-amber-500 text-white px-1 py-0.5 rounded uppercase font-bold tracking-tighter">YOUR AUDIT REPORT</span> <span className="text-[8px] bg-red-500 text-white px-1 py-0.5 rounded uppercase font-bold tracking-tighter">YOUR AUDIT REPORT</span>
</div> </div>
<p className="text-[10px] text-slate-400 font-medium" data-testid={`onboarding-fdd-details-my-report-meta-${i}`}> <p className="text-[10px] text-slate-400 font-medium" data-testid={`onboarding-fdd-details-my-report-meta-${i}`}>
{doc.documentType} {formatDateTime(doc.createdAt)} {doc.documentType} {formatDateTime(doc.createdAt)}
@ -433,7 +433,7 @@ export function FDDApplicationDetails() {
<button <button
type="button" type="button"
onClick={() => handlePreview(doc)} onClick={() => handlePreview(doc)}
className="p-1.5 hover:bg-white rounded text-slate-400 hover:text-amber-600 transition-all" className="p-1.5 hover:bg-white rounded text-slate-400 hover:text-re-red transition-all"
data-testid={`onboarding-fdd-details-my-report-preview-${i}`} data-testid={`onboarding-fdd-details-my-report-preview-${i}`}
> >
<Eye className="w-4 h-4" /> <Eye className="w-4 h-4" />
@ -576,22 +576,22 @@ export function FDDApplicationDetails() {
<Dialog open={showFinalizeModal} onOpenChange={setShowFinalizeModal}> <Dialog open={showFinalizeModal} onOpenChange={setShowFinalizeModal}>
<DialogContent className="max-w-md p-0 overflow-hidden border-none shadow-2xl" data-testid="onboarding-fdd-details-finalize-modal"> <DialogContent className="max-w-md p-0 overflow-hidden border-none shadow-2xl" data-testid="onboarding-fdd-details-finalize-modal">
<div className="bg-slate-950 p-6 flex items-center justify-center relative overflow-hidden"> <div className="bg-slate-950 p-6 flex items-center justify-center relative overflow-hidden">
<div className="absolute inset-0 bg-gradient-to-br from-amber-600/20 to-transparent" /> <div className="absolute inset-0 bg-gradient-to-br from-re-red/20 to-transparent" />
<div className="w-16 h-16 bg-amber-600/20 rounded-full flex items-center justify-center animate-pulse relative z-10"> <div className="w-16 h-16 bg-re-red/20 rounded-full flex items-center justify-center animate-pulse relative z-10">
<ShieldCheck className="w-8 h-8 text-amber-500" /> <ShieldCheck className="w-8 h-8 text-re-red" />
</div> </div>
</div> </div>
<div className="p-8 space-y-4"> <div className="p-8 space-y-4">
<DialogHeader> <DialogHeader>
<DialogTitle className="text-2xl font-bold text-slate-900 text-center" data-testid="onboarding-fdd-details-finalize-title">Submit Audit Report</DialogTitle> <DialogTitle className="text-2xl font-bold text-slate-900 text-center" data-testid="onboarding-fdd-details-finalize-title">Submit Audit Report</DialogTitle>
<DialogDescription className="text-slate-500 text-center pt-2 leading-relaxed text-base" data-testid="onboarding-fdd-details-finalize-desc"> <DialogDescription className="text-slate-500 text-center pt-2 leading-relaxed text-base" data-testid="onboarding-fdd-details-finalize-desc">
You are about to submit your final findings. This action will <span className="font-bold text-slate-800 underline decoration-amber-500 decoration-2">notify the Admin</span> for review and approval. You are about to submit your final findings. This action will <span className="font-bold text-slate-800 underline decoration-re-red decoration-2">notify the Admin</span> for review and approval.
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<div className="bg-amber-50 p-4 rounded-xl flex gap-3 border border-amber-100 italic" data-testid="onboarding-fdd-details-finalize-info"> <div className="bg-red-50 p-4 rounded-xl flex gap-3 border border-red-100 italic" data-testid="onboarding-fdd-details-finalize-info">
<Info className="w-5 h-5 text-amber-600 shrink-0 mt-0.5" /> <Info className="w-5 h-5 text-re-red shrink-0 mt-0.5" />
<p className="text-xs text-amber-800 leading-normal"> <p className="text-xs text-red-800 leading-normal">
Once submitted, you cannot edit the findings. Ensure all documents are uploaded. Once submitted, you cannot edit the findings. Ensure all documents are uploaded.
</p> </p>
</div> </div>
@ -602,7 +602,7 @@ export function FDDApplicationDetails() {
<Label className="text-[10px] font-black uppercase tracking-widest text-slate-400">Detailed Audit Findings & Remarks</Label> <Label className="text-[10px] font-black uppercase tracking-widest text-slate-400">Detailed Audit Findings & Remarks</Label>
<Textarea <Textarea
placeholder="Enter detailed financial observations..." placeholder="Enter detailed financial observations..."
className="min-h-[120px] bg-slate-50 border-slate-200 rounded-xl focus:ring-amber-500 text-sm resize-none" className="min-h-[120px] bg-slate-50 border-slate-200 rounded-xl focus:ring-re-red text-sm resize-none"
value={fddAuditFindings} value={fddAuditFindings}
onChange={(e) => setFddAuditFindings(e.target.value)} onChange={(e) => setFddAuditFindings(e.target.value)}
data-testid="onboarding-fdd-details-finalize-remarks" data-testid="onboarding-fdd-details-finalize-remarks"
@ -621,7 +621,7 @@ export function FDDApplicationDetails() {
Cancel Cancel
</Button> </Button>
<Button <Button
className="w-full sm:flex-1 h-12 rounded-xl font-bold bg-slate-950 hover:bg-slate-900 text-white shadow-lg shadow-slate-200 transition-all active:scale-95 border-b-2 border-amber-600" className="w-full sm:flex-1 h-12 rounded-xl font-bold bg-slate-950 hover:bg-slate-900 text-white shadow-lg shadow-slate-200 transition-all active:scale-95 border-b-2 border-re-red"
data-testid="onboarding-fdd-details-finalize-confirm" data-testid="onboarding-fdd-details-finalize-confirm"
onClick={async () => { onClick={async () => {
try { try {

View File

@ -137,7 +137,7 @@ export function FinanceFddDetailPage({ applicationId, onBack }: FinanceFddDetail
if (loading) { if (loading) {
return ( return (
<div className="flex items-center justify-center p-20"> <div className="flex items-center justify-center p-20">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-amber-600"></div> <div className="animate-spin rounded-full h-12 w-12 border-b-2 border-re-red"></div>
</div> </div>
); );
} }
@ -184,7 +184,7 @@ export function FinanceFddDetailPage({ applicationId, onBack }: FinanceFddDetail
<Badge variant="outline" className="bg-slate-50 text-slate-600 border-slate-200 py-1.5 px-3 rounded-full text-[10px] font-black uppercase tracking-widest" data-testid="onboarding-finance-fdd-app-id"> <Badge variant="outline" className="bg-slate-50 text-slate-600 border-slate-200 py-1.5 px-3 rounded-full text-[10px] font-black uppercase tracking-widest" data-testid="onboarding-finance-fdd-app-id">
APP ID: {application.applicationId || application.id} APP ID: {application.applicationId || application.id}
</Badge> </Badge>
<Badge className="bg-amber-100 text-amber-700 hover:bg-amber-100 py-1.5 px-3 rounded-full text-[10px] font-black uppercase tracking-widest border border-amber-200" data-testid="onboarding-finance-fdd-status"> <Badge className="bg-red-50 text-re-red-hover hover:bg-red-50 py-1.5 px-3 rounded-full text-[10px] font-black uppercase tracking-widest border border-red-200" data-testid="onboarding-finance-fdd-status">
{application.status} {application.status}
</Badge> </Badge>
</div> </div>
@ -197,7 +197,7 @@ export function FinanceFddDetailPage({ applicationId, onBack }: FinanceFddDetail
</TabsTrigger> </TabsTrigger>
<TabsTrigger value="worknotes" className="rounded-lg px-6 font-bold data-[state=active]:bg-white data-[state=active]:shadow-sm flex items-center gap-2" data-testid="onboarding-finance-fdd-tab-worknotes"> <TabsTrigger value="worknotes" className="rounded-lg px-6 font-bold data-[state=active]:bg-white data-[state=active]:shadow-sm flex items-center gap-2" data-testid="onboarding-finance-fdd-tab-worknotes">
<MessageSquare className="w-4 h-4" /> Work Notes <MessageSquare className="w-4 h-4" /> Work Notes
{workNotes.length > 0 && <Badge className="ml-1 h-5 w-5 p-0 flex items-center justify-center bg-blue-600 rounded-full text-[10px] text-white font-black" data-testid="onboarding-finance-fdd-worknotes-count">{workNotes.length}</Badge>} {workNotes.length > 0 && <Badge className="ml-1 h-5 w-5 p-0 flex items-center justify-center bg-re-red rounded-full text-[10px] text-white font-black" data-testid="onboarding-finance-fdd-worknotes-count">{workNotes.length}</Badge>}
</TabsTrigger> </TabsTrigger>
<TabsTrigger value="history" className="rounded-lg px-6 font-bold data-[state=active]:bg-white data-[state=active]:shadow-sm flex items-center gap-2" data-testid="onboarding-finance-fdd-tab-history"> <TabsTrigger value="history" className="rounded-lg px-6 font-bold data-[state=active]:bg-white data-[state=active]:shadow-sm flex items-center gap-2" data-testid="onboarding-finance-fdd-tab-history">
<History className="w-4 h-4" /> Audit Trail <History className="w-4 h-4" /> Audit Trail
@ -271,7 +271,7 @@ export function FinanceFddDetailPage({ applicationId, onBack }: FinanceFddDetail
</div> </div>
{doc && ( {doc && (
<div className="flex gap-2"> <div className="flex gap-2">
<Button variant="ghost" size="sm" className="h-8 text-blue-600 font-black text-[10px] uppercase tracking-widest" <Button variant="ghost" size="sm" className="h-8 text-re-red font-black text-[10px] uppercase tracking-widest"
onClick={() => { onClick={() => {
setPreviewDoc(doc); setPreviewDoc(doc);
setShowPreviewModal(true); setShowPreviewModal(true);
@ -293,7 +293,7 @@ export function FinanceFddDetailPage({ applicationId, onBack }: FinanceFddDetail
<div className="space-y-4" data-testid="onboarding-finance-fdd-reports-section"> <div className="space-y-4" data-testid="onboarding-finance-fdd-reports-section">
<div className="flex items-center justify-between px-1"> <div className="flex items-center justify-between px-1">
<h3 className="text-lg font-black text-slate-900 flex items-center gap-2"> <h3 className="text-lg font-black text-slate-900 flex items-center gap-2">
<ShieldCheck className="w-5 h-5 text-amber-600" /> Audit Findings & Reports <ShieldCheck className="w-5 h-5 text-re-red" /> Audit Findings & Reports
</h3> </h3>
<Badge variant="outline" className="bg-white text-slate-500 font-bold border-slate-200" data-testid="onboarding-finance-fdd-reports-count-badge"> <Badge variant="outline" className="bg-white text-slate-500 font-bold border-slate-200" data-testid="onboarding-finance-fdd-reports-count-badge">
{assignments.length} Reports Found {assignments.length} Reports Found
@ -309,12 +309,12 @@ export function FinanceFddDetailPage({ applicationId, onBack }: FinanceFddDetail
) : ( ) : (
<div className="space-y-6"> <div className="space-y-6">
{assignments.map((assignment: any, idx: number) => ( {assignments.map((assignment: any, idx: number) => (
<Card key={assignment.id} className="border-slate-200 shadow-sm overflow-hidden rounded-2xl group hover:border-amber-400 transition-all duration-300" data-testid={`onboarding-finance-fdd-assignment-card-${idx}`}> <Card key={assignment.id} className="border-slate-200 shadow-sm overflow-hidden rounded-2xl group hover:border-red-300 transition-all duration-300" data-testid={`onboarding-finance-fdd-assignment-card-${idx}`}>
<div className="bg-white p-6 border-b border-slate-50"> <div className="bg-white p-6 border-b border-slate-50">
<div className="flex items-center justify-between mb-6"> <div className="flex items-center justify-between mb-6">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="w-12 h-12 bg-amber-50 rounded-2xl flex items-center justify-center border border-amber-100 shadow-inner group-hover:scale-105 transition-transform"> <div className="w-12 h-12 bg-red-50 rounded-2xl flex items-center justify-center border border-red-100 shadow-inner group-hover:scale-105 transition-transform">
<ShieldCheck className="w-6 h-6 text-amber-600" /> <ShieldCheck className="w-6 h-6 text-re-red" />
</div> </div>
<div> <div>
<h4 className="text-slate-900 font-black text-lg leading-none">FDD Audit Assignment</h4> <h4 className="text-slate-900 font-black text-lg leading-none">FDD Audit Assignment</h4>
@ -323,7 +323,7 @@ export function FinanceFddDetailPage({ applicationId, onBack }: FinanceFddDetail
</div> </div>
<Badge className={cn( <Badge className={cn(
"py-1 px-3 rounded-full text-[10px] font-black uppercase tracking-widest", "py-1 px-3 rounded-full text-[10px] font-black uppercase tracking-widest",
assignment.status === 'completed' ? "bg-green-100 text-green-700" : "bg-amber-100 text-amber-700" assignment.status === 'completed' ? "bg-green-100 text-green-700" : "bg-red-50 text-re-red-hover"
)} data-testid={`onboarding-finance-fdd-assignment-status-${idx}`}> )} data-testid={`onboarding-finance-fdd-assignment-status-${idx}`}>
{assignment.status} {assignment.status}
</Badge> </Badge>
@ -344,12 +344,12 @@ export function FinanceFddDetailPage({ applicationId, onBack }: FinanceFddDetail
<div className={cn( <div className={cn(
"inline-flex items-center gap-2 px-4 py-2 rounded-xl border text-xs font-black shadow-sm", "inline-flex items-center gap-2 px-4 py-2 rounded-xl border text-xs font-black shadow-sm",
report.recommendation === 'Green' ? "bg-green-50 border-green-200 text-green-700" : report.recommendation === 'Green' ? "bg-green-50 border-green-200 text-green-700" :
report.recommendation === 'Amber' ? "bg-amber-50 border-amber-200 text-amber-700" : report.recommendation === 'Amber' ? "bg-red-50 border-red-200 text-re-red-hover" :
"bg-red-50 border-red-200 text-red-700" "bg-red-50 border-red-200 text-red-700"
)} data-testid={`onboarding-finance-fdd-report-signal-${idx}-${reportIdx}`}> )} data-testid={`onboarding-finance-fdd-report-signal-${idx}-${reportIdx}`}>
<div className={cn("w-2.5 h-2.5 rounded-full animate-pulse", <div className={cn("w-2.5 h-2.5 rounded-full animate-pulse",
report.recommendation === 'Green' ? "bg-green-500" : report.recommendation === 'Green' ? "bg-green-500" :
report.recommendation === 'Amber' ? "bg-amber-500" : "bg-red-500" report.recommendation === 'Amber' ? "bg-red-500" : "bg-red-500"
)} /> )} />
{report.recommendation?.toUpperCase()} SIGNAL {report.recommendation?.toUpperCase()} SIGNAL
</div> </div>
@ -366,7 +366,7 @@ export function FinanceFddDetailPage({ applicationId, onBack }: FinanceFddDetail
<div className="space-y-5"> <div className="space-y-5">
<Label className="text-[10px] text-slate-400 uppercase tracking-widest font-black mb-2 block">Available Documents</Label> <Label className="text-[10px] text-slate-400 uppercase tracking-widest font-black mb-2 block">Available Documents</Label>
{report.reportDocument ? ( {report.reportDocument ? (
<div className="bg-white border-2 border-slate-100 rounded-2xl p-4 flex items-center justify-between hover:border-amber-400 transition-all hover:shadow-lg cursor-default" data-testid={`onboarding-finance-fdd-report-doc-${idx}-${reportIdx}`}> <div className="bg-white border-2 border-slate-100 rounded-2xl p-4 flex items-center justify-between hover:border-red-300 transition-all hover:shadow-lg cursor-default" data-testid={`onboarding-finance-fdd-report-doc-${idx}-${reportIdx}`}>
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
<div className="w-12 h-12 rounded-xl bg-red-50 flex items-center justify-center shadow-sm"> <div className="w-12 h-12 rounded-xl bg-red-50 flex items-center justify-center shadow-sm">
<FileText className="w-6 h-6 text-red-500" /> <FileText className="w-6 h-6 text-red-500" />
@ -380,7 +380,7 @@ export function FinanceFddDetailPage({ applicationId, onBack }: FinanceFddDetail
<Button <Button
variant="ghost" variant="ghost"
size="icon" size="icon"
className="h-10 w-10 text-slate-400 hover:text-amber-600 hover:bg-amber-50 rounded-xl" className="h-10 w-10 text-slate-400 hover:text-re-red hover:bg-red-50 rounded-xl"
onClick={() => window.open(`http://localhost:5000/${report.reportDocument.filePath}`, '_blank')} onClick={() => window.open(`http://localhost:5000/${report.reportDocument.filePath}`, '_blank')}
data-testid={`onboarding-finance-fdd-report-download-${idx}-${reportIdx}`} data-testid={`onboarding-finance-fdd-report-download-${idx}-${reportIdx}`}
> >
@ -389,7 +389,7 @@ export function FinanceFddDetailPage({ applicationId, onBack }: FinanceFddDetail
<Button <Button
variant="ghost" variant="ghost"
size="icon" size="icon"
className="h-10 w-10 text-slate-400 hover:text-amber-600 hover:bg-amber-50 rounded-xl" className="h-10 w-10 text-slate-400 hover:text-re-red hover:bg-red-50 rounded-xl"
onClick={() => { onClick={() => {
setPreviewDoc(report.reportDocument); setPreviewDoc(report.reportDocument);
setShowPreviewModal(true); setShowPreviewModal(true);
@ -413,9 +413,9 @@ export function FinanceFddDetailPage({ applicationId, onBack }: FinanceFddDetail
<span className="text-[10px] font-black text-green-700 uppercase tracking-widest">Audited & Verified</span> <span className="text-[10px] font-black text-green-700 uppercase tracking-widest">Audited & Verified</span>
</div> </div>
) : ( ) : (
<div className="flex items-center gap-2 bg-amber-50 border border-amber-100 px-4 py-2 rounded-full shadow-sm" data-testid={`onboarding-finance-fdd-report-pending-${idx}-${reportIdx}`}> <div className="flex items-center gap-2 bg-red-50 border border-red-100 px-4 py-2 rounded-full shadow-sm" data-testid={`onboarding-finance-fdd-report-pending-${idx}-${reportIdx}`}>
<Clock className="w-4 h-4 text-amber-600" /> <Clock className="w-4 h-4 text-re-red" />
<span className="text-[10px] font-black text-amber-700 uppercase tracking-widest">Pending Verification</span> <span className="text-[10px] font-black text-re-red-hover uppercase tracking-widest">Pending Verification</span>
</div> </div>
)} )}
</div> </div>
@ -437,7 +437,7 @@ export function FinanceFddDetailPage({ applicationId, onBack }: FinanceFddDetail
<Card className="border-slate-200 shadow-sm overflow-hidden rounded-2xl sticky top-6" data-testid="onboarding-finance-fdd-action-sidebar"> <Card className="border-slate-200 shadow-sm overflow-hidden rounded-2xl sticky top-6" data-testid="onboarding-finance-fdd-action-sidebar">
<CardHeader className="bg-slate-900 border-b border-slate-800 py-5"> <CardHeader className="bg-slate-900 border-b border-slate-800 py-5">
<CardTitle className="text-white text-sm font-black uppercase tracking-widest flex items-center gap-2"> <CardTitle className="text-white text-sm font-black uppercase tracking-widest flex items-center gap-2">
<CheckCircle className="w-4 h-4 text-amber-400" /> Finance Action <CheckCircle className="w-4 h-4 text-re-red" /> Finance Action
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
<CardContent className="p-6 space-y-6"> <CardContent className="p-6 space-y-6">
@ -446,7 +446,7 @@ export function FinanceFddDetailPage({ applicationId, onBack }: FinanceFddDetail
<Textarea <Textarea
id="remarks" id="remarks"
placeholder="Enter your assessment or audit sign-off remarks here..." placeholder="Enter your assessment or audit sign-off remarks here..."
className="min-h-[150px] bg-slate-50 border-slate-200 rounded-xl focus:ring-amber-500 focus:border-amber-500 text-sm font-medium" className="min-h-[150px] bg-slate-50 border-slate-200 rounded-xl focus:ring-re-red focus:border-re-red text-sm font-medium"
value={approvalRemark} value={approvalRemark}
onChange={(e) => setApprovalRemark(e.target.value)} onChange={(e) => setApprovalRemark(e.target.value)}
disabled={hasMadeDecision || isSubmitting} disabled={hasMadeDecision || isSubmitting}
@ -458,7 +458,7 @@ export function FinanceFddDetailPage({ applicationId, onBack }: FinanceFddDetail
!isReadOnly ? ( !isReadOnly ? (
<div className="space-y-3 pt-2"> <div className="space-y-3 pt-2">
<Button <Button
className="w-full h-14 bg-amber-600 hover:bg-amber-700 text-white font-black uppercase tracking-widest rounded-xl shadow-lg shadow-amber-200/50 transition-all active:scale-95" className="w-full h-14 bg-re-red hover:bg-re-red-hover text-white font-black uppercase tracking-widest rounded-xl shadow-lg shadow-red-200/50 transition-all active:scale-95"
onClick={() => handleDecision('Approved')} onClick={() => handleDecision('Approved')}
disabled={isSubmitting} disabled={isSubmitting}
data-testid="onboarding-finance-fdd-approve-btn" data-testid="onboarding-finance-fdd-approve-btn"
@ -473,7 +473,7 @@ export function FinanceFddDetailPage({ applicationId, onBack }: FinanceFddDetail
</Button> </Button>
<Button <Button
variant="outline" variant="outline"
className="w-full h-12 text-amber-600 border-amber-200 hover:bg-amber-50 font-black uppercase tracking-widest rounded-xl" className="w-full h-12 text-re-red border-red-200 hover:bg-red-50 font-black uppercase tracking-widest rounded-xl"
onClick={() => toast.info('Clarification request functionality coming soon')} onClick={() => toast.info('Clarification request functionality coming soon')}
disabled={isSubmitting} disabled={isSubmitting}
data-testid="onboarding-finance-fdd-revision-btn" data-testid="onboarding-finance-fdd-revision-btn"
@ -509,15 +509,15 @@ export function FinanceFddDetailPage({ applicationId, onBack }: FinanceFddDetail
<h5 className="text-[10px] text-slate-400 font-black uppercase tracking-widest mb-4">Verification Policy</h5> <h5 className="text-[10px] text-slate-400 font-black uppercase tracking-widest mb-4">Verification Policy</h5>
<ul className="space-y-3"> <ul className="space-y-3">
<li className="flex items-start gap-3" data-testid="onboarding-finance-fdd-policy-1"> <li className="flex items-start gap-3" data-testid="onboarding-finance-fdd-policy-1">
<div className="w-1.5 h-1.5 rounded-full bg-amber-500 mt-1.5 shrink-0" /> <div className="w-1.5 h-1.5 rounded-full bg-red-500 mt-1.5 shrink-0" />
<p className="text-[10px] text-slate-500 font-bold leading-tight">Must review PDF audit report for financial discrepancies before approval.</p> <p className="text-[10px] text-slate-500 font-bold leading-tight">Must review PDF audit report for financial discrepancies before approval.</p>
</li> </li>
<li className="flex items-start gap-3" data-testid="onboarding-finance-fdd-policy-2"> <li className="flex items-start gap-3" data-testid="onboarding-finance-fdd-policy-2">
<div className="w-1.5 h-1.5 rounded-full bg-amber-500 mt-1.5 shrink-0" /> <div className="w-1.5 h-1.5 rounded-full bg-red-500 mt-1.5 shrink-0" />
<p className="text-[10px] text-slate-500 font-bold leading-tight">Approval triggers the progression to Security Deposit payment state.</p> <p className="text-[10px] text-slate-500 font-bold leading-tight">Approval triggers the progression to Security Deposit payment state.</p>
</li> </li>
<li className="flex items-start gap-3" data-testid="onboarding-finance-fdd-policy-3"> <li className="flex items-start gap-3" data-testid="onboarding-finance-fdd-policy-3">
<div className="w-1.5 h-1.5 rounded-full bg-amber-500 mt-1.5 shrink-0" /> <div className="w-1.5 h-1.5 rounded-full bg-red-500 mt-1.5 shrink-0" />
<p className="text-[10px] text-slate-500 font-bold leading-tight">Remarks are mandatory for audit trail and compliance tracking.</p> <p className="text-[10px] text-slate-500 font-bold leading-tight">Remarks are mandatory for audit trail and compliance tracking.</p>
</li> </li>
</ul> </ul>
@ -527,8 +527,8 @@ export function FinanceFddDetailPage({ applicationId, onBack }: FinanceFddDetail
<Card className="bg-slate-50 border-slate-200 rounded-2xl" data-testid="onboarding-finance-fdd-progress-card"> <Card className="bg-slate-50 border-slate-200 rounded-2xl" data-testid="onboarding-finance-fdd-progress-card">
<CardContent className="p-6 flex items-center gap-4"> <CardContent className="p-6 flex items-center gap-4">
<div className="w-10 h-10 rounded-full bg-blue-100 flex items-center justify-center shrink-0"> <div className="w-10 h-10 rounded-full bg-red-50 flex items-center justify-center shrink-0">
<Clock className="w-5 h-5 text-blue-600" /> <Clock className="w-5 h-5 text-re-red" />
</div> </div>
<div> <div>
<h5 className="text-[10px] font-black uppercase tracking-widest text-slate-400">Current Progress</h5> <h5 className="text-[10px] font-black uppercase tracking-widest text-slate-400">Current Progress</h5>
@ -588,8 +588,8 @@ export function FinanceFddDetailPage({ applicationId, onBack }: FinanceFddDetail
disabled={isNoteSubmitting} disabled={isNoteSubmitting}
data-testid="onboarding-finance-fdd-new-note-input" data-testid="onboarding-finance-fdd-new-note-input"
/> />
<Button <Button
className="shrink-0 bg-blue-600 hover:bg-blue-700 h-auto self-stretch rounded-xl px-6" className="shrink-0 bg-re-red hover:bg-re-red-hover h-auto self-stretch rounded-xl px-6"
onClick={handlePostNote} onClick={handlePostNote}
disabled={isNoteSubmitting || !newNote.trim()} disabled={isNoteSubmitting || !newNote.trim()}
data-testid="onboarding-finance-fdd-send-note-btn" data-testid="onboarding-finance-fdd-send-note-btn"

View File

@ -55,7 +55,7 @@ export function FinanceOnboardingPage({ onViewPaymentDetails }: FinanceOnboardin
const paymentRows = applications.flatMap((app: any) => { const paymentRows = applications.flatMap((app: any) => {
const s = app.overallStatus || app.status; const s = app.overallStatus || app.status;
const isPaymentStage = [ const isPaymentStage = [
'Payment Pending', 'Security Details', 'LOI In Progress', 'LOI Issued', 'Payment Pending', 'Security Deposit', 'Security Details', 'LOI In Progress', 'LOI Issued',
'LOA Pending', 'Dealer Code Generation', 'LOA_APPROVAL' 'LOA Pending', 'Dealer Code Generation', 'LOA_APPROVAL'
].includes(s); ].includes(s);
const deposits = app.securityDeposits || []; const deposits = app.securityDeposits || [];
@ -77,7 +77,7 @@ export function FinanceOnboardingPage({ onViewPaymentDetails }: FinanceOnboardin
// Keep virtual pending rows for in-flight cases with no deposit record yet // Keep virtual pending rows for in-flight cases with no deposit record yet
if (isPaymentStage) { if (isPaymentStage) {
if (['Payment Pending', 'Security Details', 'LOI In Progress'].includes(s)) { if (['Payment Pending', 'Security Deposit', 'Security Details', 'LOI In Progress'].includes(s)) {
return [{ return [{
id: `virtual-${app.id}-sd`, id: `virtual-${app.id}-sd`,
applicationId: app.applicationId || app.id, applicationId: app.applicationId || app.id,
@ -121,7 +121,7 @@ export function FinanceOnboardingPage({ onViewPaymentDetails }: FinanceOnboardin
if (loading) { if (loading) {
return ( return (
<div className="flex items-center justify-center p-20 text-blue-600"> <div className="flex items-center justify-center p-20 text-re-red">
<Clock className="w-8 h-8 animate-spin mr-3" /> <Clock className="w-8 h-8 animate-spin mr-3" />
<span>Loading Finance Queue...</span> <span>Loading Finance Queue...</span>
</div> </div>
@ -134,7 +134,7 @@ export function FinanceOnboardingPage({ onViewPaymentDetails }: FinanceOnboardin
<div className="flex items-center justify-between bg-white p-6 rounded-2xl border border-slate-100 shadow-sm"> <div className="flex items-center justify-between bg-white p-6 rounded-2xl border border-slate-100 shadow-sm">
<div> <div>
<h1 className="text-3xl font-bold text-slate-900 tracking-tight mb-1">Payment Verification</h1> <h1 className="text-3xl font-bold text-slate-900 tracking-tight mb-1">Payment Verification</h1>
<p className="text-slate-500">Review and verify dealer security deposits and first fill payments</p> <p className="text-slate-500">Review and verify Security Deposit and First Fill payments for dealers</p>
</div> </div>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<Button onClick={fetchApplications} variant="outline" size="sm" className="bg-white hover:bg-slate-50" data-testid="onboarding-finance-queue-sync-btn"> <Button onClick={fetchApplications} variant="outline" size="sm" className="bg-white hover:bg-slate-50" data-testid="onboarding-finance-queue-sync-btn">
@ -148,7 +148,7 @@ export function FinanceOnboardingPage({ onViewPaymentDetails }: FinanceOnboardin
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4"> <div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4">
<div className="inline-flex p-1 bg-slate-100 rounded-xl"> <div className="inline-flex p-1 bg-slate-100 rounded-xl">
<div className="flex items-center px-4 py-2 bg-white rounded-lg text-slate-900 shadow-sm font-medium text-sm" data-testid="onboarding-finance-queue-pending-count"> <div className="flex items-center px-4 py-2 bg-white rounded-lg text-slate-900 shadow-sm font-medium text-sm" data-testid="onboarding-finance-queue-pending-count">
<IndianRupee className="w-4 h-4 mr-2 text-blue-600" /> <IndianRupee className="w-4 h-4 mr-2 text-re-red" />
Pending Payments ({paymentRows.filter((row: any) => !isVerifiedLikeStatus(row.paymentStatus)).length}) Pending Payments ({paymentRows.filter((row: any) => !isVerifiedLikeStatus(row.paymentStatus)).length})
</div> </div>
</div> </div>
@ -203,10 +203,10 @@ export function FinanceOnboardingPage({ onViewPaymentDetails }: FinanceOnboardin
const statusLabel = row.paymentStatus || 'Awaiting Payment'; const statusLabel = row.paymentStatus || 'Awaiting Payment';
const app = row.application || {}; const app = row.application || {};
return ( return (
<TableRow key={row.id} className="hover:bg-blue-50/20 group transition-all" data-testid={`onboarding-finance-queue-row-${idx}`}> <TableRow key={row.id} className="hover:bg-red-50/20 group transition-all" data-testid={`onboarding-finance-queue-row-${idx}`}>
<TableCell className="py-4 pl-6"> <TableCell className="py-4 pl-6">
<div className="flex flex-col"> <div className="flex flex-col">
<span className="font-mono text-xs font-bold text-blue-600 mb-1" data-testid={`onboarding-finance-queue-app-id-${idx}`}>{app.applicationId || app.id}</span> <span className="font-mono text-xs font-bold text-re-red mb-1" data-testid={`onboarding-finance-queue-app-id-${idx}`}>{app.applicationId || app.id}</span>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="font-semibold text-slate-900" data-testid={`onboarding-finance-queue-name-${idx}`}>{app.applicantName}</span> <span className="font-semibold text-slate-900" data-testid={`onboarding-finance-queue-name-${idx}`}>{app.applicantName}</span>
</div> </div>
@ -231,7 +231,7 @@ export function FinanceOnboardingPage({ onViewPaymentDetails }: FinanceOnboardin
className={ className={
statusLabel === 'Verified' ? 'bg-emerald-50 text-emerald-700 border-emerald-100 px-3 py-1 rounded-full' : statusLabel === 'Verified' ? 'bg-emerald-50 text-emerald-700 border-emerald-100 px-3 py-1 rounded-full' :
statusLabel === 'Rejected' ? 'bg-rose-50 text-rose-700 border-rose-100 px-3 py-1 rounded-full' : statusLabel === 'Rejected' ? 'bg-rose-50 text-rose-700 border-rose-100 px-3 py-1 rounded-full' :
'bg-amber-50 text-amber-700 border-amber-100 px-3 py-1 rounded-full' 'bg-red-50 text-re-red-hover border-red-100 px-3 py-1 rounded-full'
} }
variant="outline" variant="outline"
data-testid={`onboarding-finance-queue-status-${idx}`} data-testid={`onboarding-finance-queue-status-${idx}`}
@ -244,7 +244,7 @@ export function FinanceOnboardingPage({ onViewPaymentDetails }: FinanceOnboardin
size="sm" size="sm"
variant={isVerifiedLikeStatus(statusLabel) ? 'outline' : 'default'} variant={isVerifiedLikeStatus(statusLabel) ? 'outline' : 'default'}
className={!isVerifiedLikeStatus(statusLabel) className={!isVerifiedLikeStatus(statusLabel)
? 'bg-blue-600 hover:bg-blue-700 shadow-md' ? 'bg-re-red hover:bg-re-red-hover shadow-md'
: 'bg-white text-slate-600 border-slate-200'} : 'bg-white text-slate-600 border-slate-200'}
onClick={() => handleAction(row.applicationId || app.id)} onClick={() => handleAction(row.applicationId || app.id)}
data-testid={`onboarding-finance-queue-action-btn-${idx}`} data-testid={`onboarding-finance-queue-action-btn-${idx}`}

View File

@ -11,6 +11,14 @@ import {
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from '@/components/ui/select'; } from '@/components/ui/select';
import {
Popover,
PopoverContent,
PopoverTrigger,
} from '@/components/ui/popover';
import { Calendar as CalendarPicker } from '@/components/ui/calendar';
import { format } from 'date-fns';
import { cn } from '@/components/ui/utils';
import { import {
Search, Search,
Download, Download,
@ -182,9 +190,12 @@ export function NonOpportunitiesPage({ onViewDetails }: NonOpportunitiesPageProp
setApplicationsData(mappedApps); setApplicationsData(mappedApps);
// Extract unique locations // Extract unique locations and states from the returned data
const uniqueLocations = Array.from(new Set(mappedApps.map(app => app.preferredLocation))).filter(Boolean); const newLocations = Array.from(new Set(mappedApps.map(app => app.preferredLocation))).filter(Boolean) as string[];
setLocations(uniqueLocations); setLocations(prev => Array.from(new Set([...prev, ...newLocations])));
const newStates = Array.from(new Set(mappedApps.map(app => app.state))).filter(Boolean) as string[];
setStates(prev => Array.from(new Set([...prev, ...newStates])));
} catch (error) { } catch (error) {
console.error('Failed to fetch applications:', error); console.error('Failed to fetch applications:', error);
toast.error('Failed to load non-opportunity requests'); toast.error('Failed to load non-opportunity requests');
@ -226,7 +237,7 @@ export function NonOpportunitiesPage({ onViewDetails }: NonOpportunitiesPageProp
</div> </div>
<div className="bg-white rounded-lg border border-slate-200 p-4" data-testid="onboarding-non-opps-stat-exp"> <div className="bg-white rounded-lg border border-slate-200 p-4" data-testid="onboarding-non-opps-stat-exp">
<p className="text-slate-600">With Experience</p> <p className="text-slate-600">With Experience</p>
<p className="text-2xl text-amber-600 mt-1"> <p className="text-2xl text-re-red mt-1">
{paginationMeta?.stats?.withExperience || 0} {paginationMeta?.stats?.withExperience || 0}
</p> </p>
</div> </div>
@ -247,29 +258,55 @@ export function NonOpportunitiesPage({ onViewDetails }: NonOpportunitiesPageProp
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="relative w-full md:w-36"> <Popover>
<Calendar className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400 pointer-events-none" /> <PopoverTrigger asChild>
<Input <Button
type="date" variant="outline"
value={fromDate} className={cn(
onChange={(e) => setFromDate(e.target.value)} "w-full md:w-36 justify-start text-left font-normal h-10 px-3",
className="pl-10 text-xs" !fromDate && "text-muted-foreground"
placeholder="From" )}
data-testid="onboarding-non-opps-from-date" data-testid="onboarding-non-opps-from-date-trigger"
/> >
</div> <Calendar className="mr-2 h-4 w-4 text-slate-400" />
{fromDate ? format(new Date(fromDate), "PP") : <span className="text-xs text-slate-500">From Date</span>}
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0" align="start">
<CalendarPicker
mode="single"
selected={fromDate ? new Date(fromDate) : undefined}
onSelect={(date) => setFromDate(date ? date.toISOString().split('T')[0] : '')}
initialFocus
/>
</PopoverContent>
</Popover>
<span className="text-slate-400">to</span> <span className="text-slate-400">to</span>
<div className="relative w-full md:w-36">
<Calendar className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400 pointer-events-none" /> <Popover>
<Input <PopoverTrigger asChild>
type="date" <Button
value={toDate} variant="outline"
onChange={(e) => setToDate(e.target.value)} className={cn(
className="pl-10 text-xs" "w-full md:w-36 justify-start text-left font-normal h-10 px-3",
placeholder="To" !toDate && "text-muted-foreground"
data-testid="onboarding-non-opps-to-date" )}
/> data-testid="onboarding-non-opps-to-date-trigger"
</div> >
<Calendar className="mr-2 h-4 w-4 text-slate-400" />
{toDate ? format(new Date(toDate), "PP") : <span className="text-xs text-slate-500">To Date</span>}
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0" align="start">
<CalendarPicker
mode="single"
selected={toDate ? new Date(toDate) : undefined}
onSelect={(date) => setToDate(date ? date.toISOString().split('T')[0] : '')}
initialFocus
/>
</PopoverContent>
</Popover>
</div> </div>
<Select value={locationFilter} onValueChange={setLocationFilter}> <Select value={locationFilter} onValueChange={setLocationFilter}>
@ -284,6 +321,22 @@ export function NonOpportunitiesPage({ onViewDetails }: NonOpportunitiesPageProp
</SelectContent> </SelectContent>
</Select> </Select>
<Button
variant="ghost"
size="sm"
className="text-slate-500 hover:text-slate-700 h-10 px-3"
onClick={() => {
setFromDate('');
setToDate('');
setLocationFilter('all');
setStateFilter('all');
setSearchQuery('');
}}
data-testid="onboarding-non-opps-clear-filters"
>
Clear Filters
</Button>
<Select value={stateFilter} onValueChange={setStateFilter}> <Select value={stateFilter} onValueChange={setStateFilter}>
<SelectTrigger className="w-full lg:w-48" data-testid="onboarding-non-opps-state-select"> <SelectTrigger className="w-full lg:w-48" data-testid="onboarding-non-opps-state-select">
<SelectValue placeholder="All States" /> <SelectValue placeholder="All States" />
@ -302,7 +355,7 @@ export function NonOpportunitiesPage({ onViewDetails }: NonOpportunitiesPageProp
{selectedIds.length > 0 && ( {selectedIds.length > 0 && (
<Button <Button
className="bg-amber-600 hover:bg-amber-700 font-bold" className="bg-re-red hover:bg-re-red-hover font-bold"
onClick={handleBulkConvert} onClick={handleBulkConvert}
disabled={isBulkConverting} disabled={isBulkConverting}
> >
@ -343,7 +396,7 @@ export function NonOpportunitiesPage({ onViewDetails }: NonOpportunitiesPageProp
{isGlobalLoading ? ( {isGlobalLoading ? (
<TableRow> <TableRow>
<TableCell colSpan={11} className="text-center py-20"> <TableCell colSpan={11} className="text-center py-20">
<Loader2 className="w-8 h-8 mx-auto animate-spin text-amber-600 mb-2" /> <Loader2 className="w-8 h-8 mx-auto animate-spin text-re-red mb-2" />
<p className="text-slate-500 text-sm">Loading applications...</p> <p className="text-slate-500 text-sm">Loading applications...</p>
</TableCell> </TableCell>
</TableRow> </TableRow>
@ -360,7 +413,7 @@ export function NonOpportunitiesPage({ onViewDetails }: NonOpportunitiesPageProp
<TableRow <TableRow
key={lead.id} key={lead.id}
data-testid={`onboarding-non-opps-row-${idx}`} data-testid={`onboarding-non-opps-row-${idx}`}
className={selectedIds.includes(lead.id) ? 'bg-amber-50/50' : ''} className={selectedIds.includes(lead.id) ? 'bg-red-50/50' : ''}
> >
<TableCell> <TableCell>
<Checkbox <Checkbox

View File

@ -11,6 +11,14 @@ import {
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from '@/components/ui/select'; } from '@/components/ui/select';
import {
Popover,
PopoverContent,
PopoverTrigger,
} from '@/components/ui/popover';
import { Calendar as CalendarPicker } from '@/components/ui/calendar';
import { format } from 'date-fns';
import { cn } from '@/components/ui/utils';
import { import {
Pagination, Pagination,
PaginationContent, PaginationContent,
@ -27,7 +35,6 @@ import {
Mail, Mail,
Grid3x3, Grid3x3,
List, List,
AlertCircle,
Loader2, Loader2,
Calendar, Calendar,
ArrowUpDown ArrowUpDown
@ -41,7 +48,7 @@ import {
TableRow, TableRow,
TableCell TableCell
} from '@/components/ui/table'; } from '@/components/ui/table';
import { Progress } from '@/components/ui/progress'; import { ApplicationProgressBar } from '@/features/onboarding/components/ApplicationProgressBar';
import { Checkbox } from '@/components/ui/checkbox'; import { Checkbox } from '@/components/ui/checkbox';
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog'; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { Label } from '@/components/ui/label'; import { Label } from '@/components/ui/label';
@ -67,6 +74,7 @@ export function OpportunityRequestsPage({ onViewDetails }: OpportunityRequestsPa
const [currentPage, setCurrentPage] = useState(1); const [currentPage, setCurrentPage] = useState(1);
const [paginationMeta, setPaginationMeta] = useState<any>(null); const [paginationMeta, setPaginationMeta] = useState<any>(null);
const [selectedIds, setSelectedIds] = useState<string[]>([]); const [selectedIds, setSelectedIds] = useState<string[]>([]);
const [isSendingReminders, setIsSendingReminders] = useState(false);
const [showShortlistModal, setShowShortlistModal] = useState(false); const [showShortlistModal, setShowShortlistModal] = useState(false);
const [shortlistRemark, setShortlistRemark] = useState(''); const [shortlistRemark, setShortlistRemark] = useState('');
@ -160,9 +168,13 @@ export function OpportunityRequestsPage({ onViewDetails }: OpportunityRequestsPa
setApplicationsData(mappedApps); setApplicationsData(mappedApps);
// Extract unique locations for filtering // Extract unique locations and states from the returned data
const uniqueLocations = Array.from(new Set(mappedApps.map(app => app.preferredLocation))).filter(Boolean); // Note: This appends new ones to the existing list to ensure all found locations are selectable
setLocations(uniqueLocations); const newLocations = Array.from(new Set(mappedApps.map(app => app.preferredLocation))).filter(Boolean) as string[];
setLocations(prev => Array.from(new Set([...prev, ...newLocations])));
const newStates = Array.from(new Set(mappedApps.map(app => app.state))).filter(Boolean) as string[];
setStates(prev => Array.from(new Set([...prev, ...newStates])));
} catch (error) { } catch (error) {
console.error('Failed to fetch applications:', error); console.error('Failed to fetch applications:', error);
toast.error('Failed to load opportunity requests'); toast.error('Failed to load opportunity requests');
@ -216,7 +228,7 @@ export function OpportunityRequestsPage({ onViewDetails }: OpportunityRequestsPa
if (response && response.success) { if (response && response.success) {
// Refresh data from server to ensure correct filtering and pagination // Refresh data from server to ensure correct filtering and pagination
await fetchApplications(); await fetchApplications();
setSelectedIds([]); setSelectedIds([]);
setShowShortlistModal(false); setShowShortlistModal(false);
setShortlistRemark(''); setShortlistRemark('');
@ -231,21 +243,34 @@ export function OpportunityRequestsPage({ onViewDetails }: OpportunityRequestsPa
} }
}; };
const handleBulkReminders = () => { const handleBulkReminders = async () => {
if (selectedIds.length === 0) { if (selectedIds.length === 0) {
toast.error('Please select at least one application'); toast.error('Please select at least one application');
return; return;
} }
toast.success(`Reminder emails sent to ${selectedIds.length} applicant(s)`);
try {
setIsSendingReminders(true);
const res = await onboardingService.sendBulkReminders(selectedIds);
if (res.success) {
toast.success(res.message || `Reminder emails sent to ${selectedIds.length} applicant(s)`);
setSelectedIds([]);
}
} catch (error: any) {
console.error('Failed to send reminders:', error);
toast.error(error.message || 'Failed to send reminders');
} finally {
setIsSendingReminders(false);
}
}; };
const handleExport = async () => { const handleExport = async () => {
// Exclude 'Questionnaire Pending' from export as they have no responses yet // Exclude 'Questionnaire Pending' from export as they have no responses yet
const validApplications = filteredApplications.filter(app => app.status !== 'Questionnaire Pending'); const validApplications = filteredApplications.filter(app => app.status !== 'Questionnaire Pending');
const selectedValidApps = validApplications.filter(app => selectedIds.includes(app.id)); const selectedValidApps = validApplications.filter(app => selectedIds.includes(app.id));
let idsToExport: string[] = []; let idsToExport: string[] = [];
if (selectedIds.length > 0) { if (selectedIds.length > 0) {
if (selectedValidApps.length === 0) { if (selectedValidApps.length === 0) {
toast.error('Selected applications are in "Questionnaire Pending" status and cannot be exported.'); toast.error('Selected applications are in "Questionnaire Pending" status and cannot be exported.');
@ -258,7 +283,7 @@ export function OpportunityRequestsPage({ onViewDetails }: OpportunityRequestsPa
} else { } else {
idsToExport = validApplications.map(a => a.id); idsToExport = validApplications.map(a => a.id);
} }
if (idsToExport.length === 0) { if (idsToExport.length === 0) {
toast.error('No applications with completed questionnaires available for export'); toast.error('No applications with completed questionnaires available for export');
return; return;
@ -268,7 +293,7 @@ export function OpportunityRequestsPage({ onViewDetails }: OpportunityRequestsPa
const loadingToast = toast.loading('Preparing Excel export...'); const loadingToast = toast.loading('Preparing Excel export...');
const data = await onboardingService.exportResponses(idsToExport); const data = await onboardingService.exportResponses(idsToExport);
toast.dismiss(loadingToast); toast.dismiss(loadingToast);
if (!data || data.length === 0) { if (!data || data.length === 0) {
toast.error('No response data found'); toast.error('No response data found');
return; return;
@ -278,7 +303,7 @@ export function OpportunityRequestsPage({ onViewDetails }: OpportunityRequestsPa
const headers = Object.keys(data[0]); const headers = Object.keys(data[0]);
const csvRows = [ const csvRows = [
headers.join(','), // Header row headers.join(','), // Header row
...data.map((row: any) => ...data.map((row: any) =>
headers.map(header => { headers.map(header => {
const val = row[header] ?? ''; const val = row[header] ?? '';
// Escape quotes and wrap in quotes for CSV safety // Escape quotes and wrap in quotes for CSV safety
@ -297,7 +322,7 @@ export function OpportunityRequestsPage({ onViewDetails }: OpportunityRequestsPa
document.body.appendChild(link); document.body.appendChild(link);
link.click(); link.click();
document.body.removeChild(link); document.body.removeChild(link);
toast.success(`Exported ${idsToExport.length} records to Excel successfully`); toast.success(`Exported ${idsToExport.length} records to Excel successfully`);
} catch (error: any) { } catch (error: any) {
console.error('Export failed:', error); console.error('Export failed:', error);
@ -326,7 +351,7 @@ export function OpportunityRequestsPage({ onViewDetails }: OpportunityRequestsPa
'Level 2 Recommended': 'bg-teal-100 text-teal-800', 'Level 2 Recommended': 'bg-teal-100 text-teal-800',
'Level 3 Interview Pending': 'bg-orange-100 text-orange-800', 'Level 3 Interview Pending': 'bg-orange-100 text-orange-800',
'FDD Verification': 'bg-indigo-100 text-indigo-800', 'FDD Verification': 'bg-indigo-100 text-indigo-800',
'Payment Pending': 'bg-amber-100 text-amber-800', 'Payment Pending': 'bg-red-50 text-red-800',
'LOI Issued': 'bg-sky-100 text-sky-800', 'LOI Issued': 'bg-sky-100 text-sky-800',
'Dealer Code Generation': 'bg-purple-100 text-purple-800', 'Dealer Code Generation': 'bg-purple-100 text-purple-800',
'Architecture Team Assigned': 'bg-blue-100 text-blue-800', 'Architecture Team Assigned': 'bg-blue-100 text-blue-800',
@ -353,6 +378,9 @@ export function OpportunityRequestsPage({ onViewDetails }: OpportunityRequestsPa
'Level 3 Approved': 'bg-green-100 text-green-800', 'Level 3 Approved': 'bg-green-100 text-green-800',
'LOI In Progress': 'bg-sky-50 text-sky-700', 'LOI In Progress': 'bg-sky-50 text-sky-700',
'LOI Approved': 'bg-green-100 text-green-800', 'LOI Approved': 'bg-green-100 text-green-800',
'Security Deposit In Progress': 'bg-blue-50 text-blue-700',
'Security Deposit Approved': 'bg-green-100 text-green-800',
'Security Deposit': 'bg-blue-100 text-blue-800',
'Security Details In Progress': 'bg-blue-50 text-blue-700', 'Security Details In Progress': 'bg-blue-50 text-blue-700',
'Security Details Approved': 'bg-green-100 text-green-800', 'Security Details Approved': 'bg-green-100 text-green-800',
'Security Details': 'bg-blue-100 text-blue-800', 'Security Details': 'bg-blue-100 text-blue-800',
@ -373,27 +401,13 @@ export function OpportunityRequestsPage({ onViewDetails }: OpportunityRequestsPa
if (loading) { if (loading) {
return ( return (
<div className="flex justify-center items-center h-96"> <div className="flex justify-center items-center h-96">
<Loader2 className="w-8 h-8 animate-spin text-amber-600" /> <Loader2 className="w-8 h-8 animate-spin text-re-red" />
</div> </div>
); );
} }
return ( return (
<div className="space-y-6"> <div className="space-y-6">
{/* Info Banner */}
<div className="bg-amber-50 border border-amber-200 rounded-lg p-4" data-testid="onboarding-opp-requests-banner">
<div className="flex items-start gap-3">
<AlertCircle className="w-5 h-5 text-amber-600 flex-shrink-0 mt-0.5" />
<div>
<h3 className="text-amber-900 mb-1">DD Lead Workflow - Opportunity Requests</h3>
<p className="text-amber-800">
This page shows <strong>applications where dealerships are being offered</strong> at the applicant's preferred location.
These have been shortlisted by DD and are waiting for your review. Select and <strong>Shortlist</strong> promising candidates
to move them to the <strong>Dealership Requests</strong> page for further processing.
</p>
</div>
</div>
</div>
{/* Header with Filters */} {/* Header with Filters */}
<div className="bg-white rounded-lg border border-slate-200 p-6"> <div className="bg-white rounded-lg border border-slate-200 p-6">
@ -424,6 +438,23 @@ export function OpportunityRequestsPage({ onViewDetails }: OpportunityRequestsPa
</SelectContent> </SelectContent>
</Select> </Select>
<Button
variant="ghost"
size="sm"
className="text-slate-500 hover:text-slate-700 h-9"
onClick={() => {
setFromDate('');
setToDate('');
setStatusFilter('all');
setLocationFilter('all');
setStateFilter('all');
setSearchQuery('');
}}
data-testid="onboarding-opp-requests-clear-filters"
>
Clear Filters
</Button>
<Select value={stateFilter} onValueChange={setStateFilter}> <Select value={stateFilter} onValueChange={setStateFilter}>
<SelectTrigger className="w-full md:w-48" data-testid="onboarding-opp-requests-state-select"> <SelectTrigger className="w-full md:w-48" data-testid="onboarding-opp-requests-state-select">
<SelectValue placeholder="Filter by state" /> <SelectValue placeholder="Filter by state" />
@ -449,29 +480,55 @@ export function OpportunityRequestsPage({ onViewDetails }: OpportunityRequestsPa
</Select> </Select>
<div className="flex items-center gap-2 flex-1 md:flex-none"> <div className="flex items-center gap-2 flex-1 md:flex-none">
<div className="relative w-full md:w-40"> <Popover>
<Calendar className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400 pointer-events-none" /> <PopoverTrigger asChild>
<Input <Button
type="date" variant="outline"
value={fromDate} className={cn(
onChange={(e) => setFromDate(e.target.value)} "w-full md:w-40 justify-start text-left font-normal h-9 px-3",
className="pl-10 text-xs" !fromDate && "text-muted-foreground"
placeholder="From" )}
data-testid="onboarding-opp-requests-from-date" data-testid="onboarding-opp-requests-from-date-trigger"
/> >
</div> <Calendar className="mr-2 h-4 w-4 text-slate-400" />
{fromDate ? format(new Date(fromDate), "PPP") : <span className="text-xs">From Date</span>}
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0" align="start">
<CalendarPicker
mode="single"
selected={fromDate ? new Date(fromDate) : undefined}
onSelect={(date) => setFromDate(date ? date.toISOString().split('T')[0] : '')}
initialFocus
/>
</PopoverContent>
</Popover>
<span className="text-slate-400">to</span> <span className="text-slate-400">to</span>
<div className="relative w-full md:w-40">
<Calendar className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400 pointer-events-none" /> <Popover>
<Input <PopoverTrigger asChild>
type="date" <Button
value={toDate} variant="outline"
onChange={(e) => setToDate(e.target.value)} className={cn(
className="pl-10 text-xs" "w-full md:w-40 justify-start text-left font-normal h-9 px-3",
placeholder="To" !toDate && "text-muted-foreground"
data-testid="onboarding-opp-requests-to-date" )}
/> data-testid="onboarding-opp-requests-to-date-trigger"
</div> >
<Calendar className="mr-2 h-4 w-4 text-slate-400" />
{toDate ? format(new Date(toDate), "PPP") : <span className="text-xs">To Date</span>}
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0" align="start">
<CalendarPicker
mode="single"
selected={toDate ? new Date(toDate) : undefined}
onSelect={(date) => setToDate(date ? date.toISOString().split('T')[0] : '')}
initialFocus
/>
</PopoverContent>
</Popover>
</div> </div>
<Select value={sortBy} onValueChange={setSortBy}> <Select value={sortBy} onValueChange={setSortBy}>
@ -497,7 +554,7 @@ export function OpportunityRequestsPage({ onViewDetails }: OpportunityRequestsPa
variant={viewMode === 'grid' ? 'default' : 'outline'} variant={viewMode === 'grid' ? 'default' : 'outline'}
size="sm" size="sm"
onClick={() => setViewMode('grid')} onClick={() => setViewMode('grid')}
className={viewMode === 'grid' ? 'bg-amber-600 hover:bg-amber-700' : ''} className={viewMode === 'grid' ? 'bg-re-red hover:bg-re-red-hover' : ''}
data-testid="onboarding-opp-requests-view-grid-btn" data-testid="onboarding-opp-requests-view-grid-btn"
> >
<Grid3x3 className="w-4 h-4 mr-2" /> <Grid3x3 className="w-4 h-4 mr-2" />
@ -507,7 +564,7 @@ export function OpportunityRequestsPage({ onViewDetails }: OpportunityRequestsPa
variant={viewMode === 'table' ? 'default' : 'outline'} variant={viewMode === 'table' ? 'default' : 'outline'}
size="sm" size="sm"
onClick={() => setViewMode('table')} onClick={() => setViewMode('table')}
className={viewMode === 'table' ? 'bg-amber-600 hover:bg-amber-700' : ''} className={viewMode === 'table' ? 'bg-re-red hover:bg-re-red-hover' : ''}
data-testid="onboarding-opp-requests-view-table-btn" data-testid="onboarding-opp-requests-view-table-btn"
> >
<List className="w-4 h-4 mr-2" /> <List className="w-4 h-4 mr-2" />
@ -516,9 +573,9 @@ export function OpportunityRequestsPage({ onViewDetails }: OpportunityRequestsPa
</div> </div>
<Button variant="outline" size="sm" onClick={handleExport} data-testid="onboarding-opp-requests-export-btn"> <Button variant="outline" size="sm" onClick={handleExport} data-testid="onboarding-opp-requests-export-btn">
<Download className="w-4 h-4 mr-2" /> <Download className="w-4 h-4 mr-2" />
Export Export
</Button> </Button>
{selectedIds.length > 0 && ( {selectedIds.length > 0 && (
<> <>
@ -526,9 +583,14 @@ export function OpportunityRequestsPage({ onViewDetails }: OpportunityRequestsPa
variant="outline" variant="outline"
size="sm" size="sm"
onClick={handleBulkReminders} onClick={handleBulkReminders}
disabled={isSendingReminders}
data-testid="onboarding-opp-requests-bulk-reminder-btn" data-testid="onboarding-opp-requests-bulk-reminder-btn"
> >
<Mail className="w-4 h-4 mr-2" /> {isSendingReminders ? (
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
) : (
<Mail className="w-4 h-4 mr-2" />
)}
Send Reminders ({selectedIds.length}) Send Reminders ({selectedIds.length})
</Button> </Button>
@ -644,10 +706,12 @@ export function OpportunityRequestsPage({ onViewDetails }: OpportunityRequestsPa
<Badge variant="outline" data-testid={`onboarding-opp-requests-shortlisted-badge-${idx}`}>No</Badge> <Badge variant="outline" data-testid={`onboarding-opp-requests-shortlisted-badge-${idx}`}>No</Badge>
</TableCell> </TableCell>
<TableCell> <TableCell>
<div className="flex items-center gap-2" data-testid={`onboarding-opp-requests-progress-container-${idx}`}> <ApplicationProgressBar
<Progress value={app.progress} className="w-20" /> value={app.progress}
<span className="text-slate-600">{app.progress}%</span> status={app.status}
</div> showPercent
data-testid={`onboarding-opp-requests-progress-${idx}`}
/>
</TableCell> </TableCell>
<TableCell> <TableCell>
<span className="text-slate-600" data-testid={`onboarding-opp-requests-date-${idx}`}>{formatDateTime(app.submissionDate)}</span> <span className="text-slate-600" data-testid={`onboarding-opp-requests-date-${idx}`}>{formatDateTime(app.submissionDate)}</span>
@ -672,23 +736,23 @@ export function OpportunityRequestsPage({ onViewDetails }: OpportunityRequestsPa
<Pagination> <Pagination>
<PaginationContent> <PaginationContent>
<PaginationItem> <PaginationItem>
<PaginationPrevious <PaginationPrevious
onClick={() => setCurrentPage(p => Math.max(1, p - 1))} onClick={() => setCurrentPage(p => Math.max(1, p - 1))}
className={currentPage === 1 ? "pointer-events-none opacity-50" : "cursor-pointer"} className={currentPage === 1 ? "pointer-events-none opacity-50" : "cursor-pointer"}
/> />
</PaginationItem> </PaginationItem>
{[...Array(paginationMeta.totalPages)].map((_, i) => { {[...Array(paginationMeta.totalPages)].map((_, i) => {
const pageNum = i + 1; const pageNum = i + 1;
// Simple pagination: show first, last, and current +/- 1 // Simple pagination: show first, last, and current +/- 1
if ( if (
pageNum === 1 || pageNum === 1 ||
pageNum === paginationMeta.totalPages || pageNum === paginationMeta.totalPages ||
(pageNum >= currentPage - 1 && pageNum <= currentPage + 1) (pageNum >= currentPage - 1 && pageNum <= currentPage + 1)
) { ) {
return ( return (
<PaginationItem key={pageNum}> <PaginationItem key={pageNum}>
<PaginationLink <PaginationLink
isActive={currentPage === pageNum} isActive={currentPage === pageNum}
onClick={() => setCurrentPage(pageNum)} onClick={() => setCurrentPage(pageNum)}
className="cursor-pointer" className="cursor-pointer"
@ -698,7 +762,7 @@ export function OpportunityRequestsPage({ onViewDetails }: OpportunityRequestsPa
</PaginationItem> </PaginationItem>
); );
} else if ( } else if (
pageNum === currentPage - 2 || pageNum === currentPage - 2 ||
pageNum === currentPage + 2 pageNum === currentPage + 2
) { ) {
return ( return (
@ -711,7 +775,7 @@ export function OpportunityRequestsPage({ onViewDetails }: OpportunityRequestsPa
})} })}
<PaginationItem> <PaginationItem>
<PaginationNext <PaginationNext
onClick={() => setCurrentPage(p => Math.min(paginationMeta.totalPages, p + 1))} onClick={() => setCurrentPage(p => Math.min(paginationMeta.totalPages, p + 1))}
className={currentPage === paginationMeta.totalPages ? "pointer-events-none opacity-50" : "cursor-pointer"} className={currentPage === paginationMeta.totalPages ? "pointer-events-none opacity-50" : "cursor-pointer"}
/> />

View File

@ -9,13 +9,16 @@ import {
Building, Building,
Landmark, Landmark,
CheckCircle2, CheckCircle2,
Check,
Info, Info,
User, User,
MapPin MapPin
} from 'lucide-react'; } from 'lucide-react';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { API } from '@/api/API'; import { API } from '@/api/API';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import * as SelectPrimitive from '@radix-ui/react-select';
import { cn } from '@/components/ui/utils';
import { Select, SelectContent, SelectTrigger, SelectValue } from '@/components/ui/select';
interface Props { interface Props {
id: string; id: string;
@ -180,7 +183,7 @@ export function ProspectiveApplicationDetails({ id, onBack }: Props) {
if (loading) { if (loading) {
return ( return (
<div className="flex items-center justify-center py-20"> <div className="flex items-center justify-center py-20">
<RefreshCw className="w-8 h-8 animate-spin text-amber-600" /> <RefreshCw className="w-8 h-8 animate-spin text-re-red" />
</div> </div>
); );
} }
@ -189,7 +192,7 @@ export function ProspectiveApplicationDetails({ id, onBack }: Props) {
return ( return (
<div className="bg-white rounded-xl border border-slate-200 p-8 text-center"> <div className="bg-white rounded-xl border border-slate-200 p-8 text-center">
<p className="text-slate-600 mb-4">Application details not found.</p> <p className="text-slate-600 mb-4">Application details not found.</p>
<button onClick={onBack} className="bg-amber-600 text-white px-4 py-2 rounded-md hover:bg-amber-700">Go Back</button> <button onClick={onBack} className="bg-re-red text-white px-4 py-2 rounded-md hover:bg-re-red-hover">Go Back</button>
</div> </div>
); );
} }
@ -224,19 +227,13 @@ export function ProspectiveApplicationDetails({ id, onBack }: Props) {
<div className="animate-in fade-in duration-500 space-y-6"> <div className="animate-in fade-in duration-500 space-y-6">
{/* Status & Tracking Summary Card */} {/* Status & Tracking Summary Card */}
<div className="bg-white rounded-xl border border-slate-200 shadow-sm p-6" data-testid="onboarding-prospective-details-summary-card"> <div className="bg-white rounded-xl border border-slate-200 shadow-sm p-6" data-testid="onboarding-prospective-details-summary-card">
<div className="flex items-center justify-between mb-4 border-b pb-2"> <div className="mb-4 border-b pb-2">
<h4 className="text-lg font-semibold text-slate-900 flex items-center gap-2"> <h4 className="text-lg font-semibold text-slate-900 flex items-center gap-2">
<Info className="w-5 h-5 text-amber-600" /> Application Summary <Info className="w-5 h-5 text-re-red" /> Application Summary
</h4> </h4>
<div className="text-right">
<p className="text-[10px] text-slate-500 uppercase font-bold tracking-widest">Current Stage</p>
<span className="bg-amber-100 text-amber-700 px-3 py-1 rounded-full text-xs font-bold uppercase tracking-wide" data-testid="onboarding-prospective-details-current-stage">
{details.currentStage || details.overallStatus}
</span>
</div>
</div> </div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6"> <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="space-y-4"> <div className="space-y-4">
<div className="flex items-start gap-3" data-testid="onboarding-prospective-details-applicant-info"> <div className="flex items-start gap-3" data-testid="onboarding-prospective-details-applicant-info">
<div className="p-2 bg-blue-50 rounded-lg"> <div className="p-2 bg-blue-50 rounded-lg">
@ -249,8 +246,8 @@ export function ProspectiveApplicationDetails({ id, onBack }: Props) {
</div> </div>
</div> </div>
<div className="flex items-start gap-3" data-testid="onboarding-prospective-details-location-info"> <div className="flex items-start gap-3" data-testid="onboarding-prospective-details-location-info">
<div className="p-2 bg-amber-50 rounded-lg"> <div className="p-2 bg-red-50 rounded-lg">
<MapPin className="w-4 h-4 text-amber-600" /> <MapPin className="w-4 h-4 text-re-red" />
</div> </div>
<div> <div>
<p className="text-[10px] text-slate-500 uppercase font-bold">Proposed Location</p> <p className="text-[10px] text-slate-500 uppercase font-bold">Proposed Location</p>
@ -283,21 +280,6 @@ export function ProspectiveApplicationDetails({ id, onBack }: Props) {
</div> </div>
</div> </div>
</div> </div>
<div className="bg-slate-50 rounded-xl p-4 flex flex-col justify-center border border-slate-100" data-testid="onboarding-prospective-details-progress-card">
<div className="flex justify-between items-center mb-2">
<p className="text-xs font-bold text-slate-700 uppercase tracking-tight">Onboarding Progress</p>
<p className="text-xs font-black text-amber-600">{details.progressPercentage || 0}%</p>
</div>
<div className="w-full bg-slate-200 rounded-full h-2 shadow-inner">
<div className="bg-amber-500 h-2 rounded-full transition-all duration-1000 ease-out shadow-sm" style={{ width: `${details.progressPercentage || 0}%` }}></div>
</div>
{details.statusHistory?.[0]?.changeReason && (
<p className="mt-3 text-[11px] text-slate-600 italic leading-relaxed border-l-2 border-amber-300 pl-2">
"{details.statusHistory[0].changeReason}"
</p>
)}
</div>
</div> </div>
</div> </div>
@ -307,12 +289,12 @@ export function ProspectiveApplicationDetails({ id, onBack }: Props) {
<div className="bg-white rounded-xl border border-slate-200 shadow-sm overflow-hidden" data-testid="onboarding-prospective-details-statutory-card"> <div className="bg-white rounded-xl border border-slate-200 shadow-sm overflow-hidden" data-testid="onboarding-prospective-details-statutory-card">
<div className="p-4 bg-slate-900 text-white flex justify-between items-center"> <div className="p-4 bg-slate-900 text-white flex justify-between items-center">
<h4 className="flex items-center gap-2 text-sm font-bold uppercase tracking-widest"> <h4 className="flex items-center gap-2 text-sm font-bold uppercase tracking-widest">
<CreditCard className="w-4 h-4 text-amber-400" /> Statutory & Bank Details <CreditCard className="w-4 h-4 text-re-red" /> Statutory & Bank Details
</h4> </h4>
<button <button
onClick={handleSaveDetails} onClick={handleSaveDetails}
disabled={isSaving} disabled={isSaving}
className={`text-xs text-white px-3 py-1 rounded font-bold transition-all flex items-center gap-1 disabled:opacity-50 ${isFormDirty ? 'bg-emerald-600 hover:bg-emerald-700 ring-2 ring-emerald-300 animate-pulse' : 'bg-amber-600 hover:bg-amber-700'}`} className={`text-xs text-white px-3 py-1 rounded font-bold transition-all flex items-center gap-1 disabled:opacity-50 ${isFormDirty ? 'bg-emerald-600 hover:bg-emerald-700 ring-2 ring-emerald-300 animate-pulse' : 'bg-re-red hover:bg-re-red-hover'}`}
data-testid="onboarding-prospective-details-save-statutory-btn" data-testid="onboarding-prospective-details-save-statutory-btn"
> >
{isSaving ? <RefreshCw className="w-3 h-3 animate-spin" /> : <CheckCircle2 className="w-3 h-3" />} {isSaving ? <RefreshCw className="w-3 h-3 animate-spin" /> : <CheckCircle2 className="w-3 h-3" />}
@ -325,7 +307,7 @@ export function ProspectiveApplicationDetails({ id, onBack }: Props) {
<label className="text-[10px] font-bold text-slate-500 uppercase">Registered Business Name</label> <label className="text-[10px] font-bold text-slate-500 uppercase">Registered Business Name</label>
<input <input
type="text" type="text"
className="w-full rounded-md border border-slate-200 bg-white px-3 py-2 text-sm focus:ring-2 focus:ring-amber-500 outline-none transition-all" className="w-full rounded-md border border-slate-200 bg-white px-3 py-2 text-sm focus:ring-2 focus:ring-re-red outline-none transition-all"
value={form.accountHolderName} value={form.accountHolderName}
onChange={(e) => setForm({ ...form, accountHolderName: e.target.value })} onChange={(e) => setForm({ ...form, accountHolderName: e.target.value })}
placeholder="As per legal documents" placeholder="As per legal documents"
@ -336,7 +318,7 @@ export function ProspectiveApplicationDetails({ id, onBack }: Props) {
<label className="text-[10px] font-bold text-slate-500 uppercase">Permanent Account Number (PAN)</label> <label className="text-[10px] font-bold text-slate-500 uppercase">Permanent Account Number (PAN)</label>
<input <input
type="text" type="text"
className="w-full rounded-md border border-slate-200 bg-white px-3 py-2 text-sm focus:ring-2 focus:ring-amber-500 outline-none transition-all uppercase" className="w-full rounded-md border border-slate-200 bg-white px-3 py-2 text-sm focus:ring-2 focus:ring-re-red outline-none transition-all uppercase"
value={form.panNumber} value={form.panNumber}
onChange={(e) => setForm({ ...form, panNumber: e.target.value.toUpperCase() })} onChange={(e) => setForm({ ...form, panNumber: e.target.value.toUpperCase() })}
placeholder="ABCDE1234F" placeholder="ABCDE1234F"
@ -348,7 +330,7 @@ export function ProspectiveApplicationDetails({ id, onBack }: Props) {
<label className="text-[10px] font-bold text-slate-500 uppercase">GST Identification Number (GSTIN)</label> <label className="text-[10px] font-bold text-slate-500 uppercase">GST Identification Number (GSTIN)</label>
<input <input
type="text" type="text"
className="w-full rounded-md border border-slate-200 bg-white px-3 py-2 text-sm focus:ring-2 focus:ring-amber-500 outline-none transition-all uppercase" className="w-full rounded-md border border-slate-200 bg-white px-3 py-2 text-sm focus:ring-2 focus:ring-re-red outline-none transition-all uppercase"
value={form.gstNumber} value={form.gstNumber}
onChange={(e) => setForm({ ...form, gstNumber: e.target.value.toUpperCase() })} onChange={(e) => setForm({ ...form, gstNumber: e.target.value.toUpperCase() })}
placeholder="27ABCDE1234F1Z5" placeholder="27ABCDE1234F1Z5"
@ -360,7 +342,7 @@ export function ProspectiveApplicationDetails({ id, onBack }: Props) {
<label className="text-[10px] font-bold text-slate-500 uppercase">Registered Office Address</label> <label className="text-[10px] font-bold text-slate-500 uppercase">Registered Office Address</label>
<textarea <textarea
rows={1} rows={1}
className="w-full rounded-md border border-slate-200 bg-white px-3 py-2 text-sm focus:ring-2 focus:ring-amber-500 outline-none transition-all" className="w-full rounded-md border border-slate-200 bg-white px-3 py-2 text-sm focus:ring-2 focus:ring-re-red outline-none transition-all"
value={form.registeredAddress} value={form.registeredAddress}
onChange={(e) => setForm({ ...form, registeredAddress: e.target.value })} onChange={(e) => setForm({ ...form, registeredAddress: e.target.value })}
placeholder="Full legal address" placeholder="Full legal address"
@ -378,7 +360,7 @@ export function ProspectiveApplicationDetails({ id, onBack }: Props) {
<label className="text-[10px] font-bold text-slate-500 uppercase">Bank Name</label> <label className="text-[10px] font-bold text-slate-500 uppercase">Bank Name</label>
<input <input
type="text" type="text"
className="w-full rounded-md border border-slate-200 bg-white px-3 py-2 text-sm focus:ring-2 focus:ring-amber-500 outline-none transition-all" className="w-full rounded-md border border-slate-200 bg-white px-3 py-2 text-sm focus:ring-2 focus:ring-re-red outline-none transition-all"
value={form.bankName} value={form.bankName}
onChange={(e) => setForm({ ...form, bankName: e.target.value })} onChange={(e) => setForm({ ...form, bankName: e.target.value })}
placeholder="e.g. HDFC Bank" placeholder="e.g. HDFC Bank"
@ -389,7 +371,7 @@ export function ProspectiveApplicationDetails({ id, onBack }: Props) {
<label className="text-[10px] font-bold text-slate-500 uppercase">Account Number</label> <label className="text-[10px] font-bold text-slate-500 uppercase">Account Number</label>
<input <input
type="text" type="text"
className="w-full rounded-md border border-slate-200 bg-white px-3 py-2 text-sm focus:ring-2 focus:ring-amber-500 outline-none transition-all" className="w-full rounded-md border border-slate-200 bg-white px-3 py-2 text-sm focus:ring-2 focus:ring-re-red outline-none transition-all"
value={form.accountNumber} value={form.accountNumber}
onChange={(e) => setForm({ ...form, accountNumber: e.target.value })} onChange={(e) => setForm({ ...form, accountNumber: e.target.value })}
placeholder="Bank account number" placeholder="Bank account number"
@ -400,7 +382,7 @@ export function ProspectiveApplicationDetails({ id, onBack }: Props) {
<label className="text-[10px] font-bold text-slate-500 uppercase">IFSC Code</label> <label className="text-[10px] font-bold text-slate-500 uppercase">IFSC Code</label>
<input <input
type="text" type="text"
className="w-full rounded-md border border-slate-200 bg-white px-3 py-2 text-sm focus:ring-2 focus:ring-amber-500 outline-none transition-all uppercase" className="w-full rounded-md border border-slate-200 bg-white px-3 py-2 text-sm focus:ring-2 focus:ring-re-red outline-none transition-all uppercase"
value={form.ifscCode} value={form.ifscCode}
onChange={(e) => setForm({ ...form, ifscCode: e.target.value.toUpperCase() })} onChange={(e) => setForm({ ...form, ifscCode: e.target.value.toUpperCase() })}
placeholder="HDFC0001234" placeholder="HDFC0001234"
@ -412,7 +394,7 @@ export function ProspectiveApplicationDetails({ id, onBack }: Props) {
<label className="text-[10px] font-bold text-slate-500 uppercase">Branch Name</label> <label className="text-[10px] font-bold text-slate-500 uppercase">Branch Name</label>
<input <input
type="text" type="text"
className="w-full rounded-md border border-slate-200 bg-white px-3 py-2 text-sm focus:ring-2 focus:ring-amber-500 outline-none transition-all" className="w-full rounded-md border border-slate-200 bg-white px-3 py-2 text-sm focus:ring-2 focus:ring-re-red outline-none transition-all"
value={form.branchName} value={form.branchName}
onChange={(e) => setForm({ ...form, branchName: e.target.value })} onChange={(e) => setForm({ ...form, branchName: e.target.value })}
placeholder="e.g. South Mumbai" placeholder="e.g. South Mumbai"
@ -427,7 +409,7 @@ export function ProspectiveApplicationDetails({ id, onBack }: Props) {
<div className="bg-white rounded-xl border border-slate-200 shadow-sm overflow-hidden" data-testid="onboarding-prospective-details-upload-card"> <div className="bg-white rounded-xl border border-slate-200 shadow-sm overflow-hidden" data-testid="onboarding-prospective-details-upload-card">
<div className="p-4 border-b border-slate-200 bg-slate-50 flex justify-between items-center"> <div className="p-4 border-b border-slate-200 bg-slate-50 flex justify-between items-center">
<h4 className="flex items-center gap-2 text-sm font-bold uppercase tracking-widest text-slate-900"> <h4 className="flex items-center gap-2 text-sm font-bold uppercase tracking-widest text-slate-900">
<Upload className="w-4 h-4 text-blue-600" /> Required Documents <Upload className="w-4 h-4 text-re-red" /> Required Documents
</h4> </h4>
</div> </div>
<div className="p-6 space-y-6"> <div className="p-6 space-y-6">
@ -436,7 +418,7 @@ export function ProspectiveApplicationDetails({ id, onBack }: Props) {
<div className="flex items-center justify-between gap-2"> <div className="flex items-center justify-between gap-2">
<label className="text-[10px] font-bold text-slate-500 uppercase">Document Category</label> <label className="text-[10px] font-bold text-slate-500 uppercase">Document Category</label>
{selectedDocType && ( {selectedDocType && (
<span className={`rounded-full px-2 py-0.5 text-[10px] font-bold ${selectedDocAlreadyUploaded ? 'bg-blue-100 text-blue-700' : 'bg-amber-100 text-amber-700'}`}> <span className="rounded-full bg-red-50 px-2 py-0.5 text-[10px] font-bold text-re-red">
{selectedDocAlreadyUploaded ? 'Already uploaded' : 'Pending upload'} {selectedDocAlreadyUploaded ? 'Already uploaded' : 'Pending upload'}
</span> </span>
)} )}
@ -444,24 +426,39 @@ export function ProspectiveApplicationDetails({ id, onBack }: Props) {
<div className="relative"> <div className="relative">
<CheckCircle2 className={`pointer-events-none absolute left-3 top-1/2 z-10 h-4 w-4 -translate-y-1/2 ${selectedDocAlreadyUploaded ? 'text-green-600' : 'text-slate-300'}`} /> <CheckCircle2 className={`pointer-events-none absolute left-3 top-1/2 z-10 h-4 w-4 -translate-y-1/2 ${selectedDocAlreadyUploaded ? 'text-green-600' : 'text-slate-300'}`} />
<Select value={selectedDocType} onValueChange={setSelectedDocType} disabled={isUploading}> <Select value={selectedDocType} onValueChange={setSelectedDocType} disabled={isUploading}>
<SelectTrigger className="h-12 rounded-xl border-slate-200 bg-gradient-to-r from-white to-slate-50 pl-10 pr-3 text-sm font-medium text-slate-700 shadow-sm focus:border-amber-300 focus:ring-2 focus:ring-amber-500" data-testid="onboarding-prospective-details-doc-type-select"> <SelectTrigger className="h-12 rounded-xl border-slate-200 bg-gradient-to-r from-white to-slate-50 pl-10 pr-3 text-sm font-medium text-slate-700 shadow-sm focus:border-re-red/40 focus:ring-2 focus:ring-re-red" data-testid="onboarding-prospective-details-doc-type-select">
<SelectValue placeholder="Choose document type" /> <SelectValue placeholder="Choose document type" />
</SelectTrigger> </SelectTrigger>
<SelectContent className="rounded-xl border-slate-200 shadow-lg" data-testid="onboarding-prospective-details-doc-type-content"> <SelectContent className="rounded-xl border-slate-200 shadow-lg" data-testid="onboarding-prospective-details-doc-type-content">
{requiredDocumentTypes.map((docType) => { {requiredDocumentTypes.map((docType) => {
const isUploaded = uploadedDocumentTypes.has(docType.toLowerCase()); const isUploaded = uploadedDocumentTypes.has(docType.toLowerCase());
return ( return (
<SelectItem <SelectPrimitive.Item
key={docType} key={docType}
value={docType} value={docType}
className="rounded-lg px-3 py-2 text-sm text-slate-700 focus:bg-amber-50 focus:text-slate-900" textValue={docType}
className={cn(
'relative flex w-full cursor-default select-none items-center gap-2 rounded-lg py-2 pl-2 pr-8 text-sm text-slate-700 outline-none',
'focus:bg-red-50 focus:text-slate-900 data-[disabled]:pointer-events-none data-[disabled]:opacity-50',
)}
data-testid={`onboarding-prospective-details-doc-type-item-${docType.replace(/\s+/g, '-').toLowerCase()}`} data-testid={`onboarding-prospective-details-doc-type-item-${docType.replace(/\s+/g, '-').toLowerCase()}`}
> >
<span className="flex items-center gap-2"> <span className="absolute right-2 flex size-3.5 items-center justify-center">
<CheckCircle2 className={`h-4 w-4 ${isUploaded ? 'text-green-600' : 'text-slate-300'}`} /> <SelectPrimitive.ItemIndicator>
{docType} <Check className="size-4" />
</SelectPrimitive.ItemIndicator>
</span> </span>
</SelectItem> <div className="flex min-w-0 flex-1 items-center gap-2">
<CheckCircle2
className={cn(
'h-4 w-4 shrink-0',
isUploaded ? 'text-green-600' : 'text-slate-300',
)}
aria-hidden
/>
<SelectPrimitive.ItemText>{docType}</SelectPrimitive.ItemText>
</div>
</SelectPrimitive.Item>
); );
})} })}
</SelectContent> </SelectContent>
@ -473,7 +470,7 @@ export function ProspectiveApplicationDetails({ id, onBack }: Props) {
<input <input
type="file" type="file"
id="file-upload" id="file-upload"
className="w-full text-xs text-slate-600 file:mr-4 file:py-1 file:px-4 file:rounded-full file:border-0 file:text-xs file:font-semibold file:bg-amber-50 file:text-amber-700 hover:file:bg-amber-100" className="w-full text-xs text-slate-600 file:mr-4 file:py-1 file:px-4 file:rounded-full file:border-0 file:text-xs file:font-semibold file:bg-red-50 file:text-re-red hover:file:bg-red-100"
onChange={handleFileChange} onChange={handleFileChange}
disabled={isUploading} disabled={isUploading}
data-testid="onboarding-prospective-details-file-input" data-testid="onboarding-prospective-details-file-input"
@ -483,7 +480,7 @@ export function ProspectiveApplicationDetails({ id, onBack }: Props) {
<button <button
onClick={handleUpload} onClick={handleUpload}
disabled={!file || !selectedDocType || isUploading} disabled={!file || !selectedDocType || isUploading}
className="bg-blue-600 text-white px-5 py-2 rounded-md hover:bg-blue-700 disabled:opacity-50 text-xs font-bold transition-all shadow-sm flex items-center gap-2" className="bg-re-red text-white px-5 py-2 rounded-md hover:bg-re-red-hover disabled:opacity-50 text-xs font-bold transition-all shadow-sm flex items-center gap-2"
data-testid="onboarding-prospective-details-upload-btn" data-testid="onboarding-prospective-details-upload-btn"
> >
{isUploading ? <RefreshCw className="w-3.5 h-3.5 animate-spin" /> : <Upload className="w-3.5 h-3.5" />} {isUploading ? <RefreshCw className="w-3.5 h-3.5 animate-spin" /> : <Upload className="w-3.5 h-3.5" />}
@ -499,7 +496,7 @@ export function ProspectiveApplicationDetails({ id, onBack }: Props) {
</div> </div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-3" data-testid="onboarding-prospective-details-doc-library"> <div className="grid grid-cols-1 md:grid-cols-2 gap-3" data-testid="onboarding-prospective-details-doc-library">
{documents.length > 0 ? documents.map((doc, idx) => ( {documents.length > 0 ? documents.map((doc, idx) => (
<div key={doc.id} className="flex justify-between items-center p-3 border border-slate-100 rounded-xl bg-slate-50 group hover:border-amber-200 transition-all" data-testid={`onboarding-prospective-details-doc-item-${idx}`}> <div key={doc.id} className="flex justify-between items-center p-3 border border-slate-100 rounded-xl bg-slate-50 group hover:border-red-200 transition-all" data-testid={`onboarding-prospective-details-doc-item-${idx}`}>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<div className="w-8 h-8 rounded bg-white flex items-center justify-center border border-slate-200 group-hover:bg-blue-50"> <div className="w-8 h-8 rounded bg-white flex items-center justify-center border border-slate-200 group-hover:bg-blue-50">
<File className="w-4 h-4 text-slate-400 group-hover:text-blue-600" /> <File className="w-4 h-4 text-slate-400 group-hover:text-blue-600" />
@ -509,7 +506,7 @@ export function ProspectiveApplicationDetails({ id, onBack }: Props) {
<p className="text-[10px] text-slate-400 truncate w-32">{doc.fileName}</p> <p className="text-[10px] text-slate-400 truncate w-32">{doc.fileName}</p>
</div> </div>
</div> </div>
<span className={`text-[9px] px-2 py-0.5 rounded-full font-black uppercase tracking-tighter ${doc.status === 'Approved' ? 'bg-green-100 text-green-700' : 'bg-amber-100 text-amber-700'}`} data-testid={`onboarding-prospective-details-doc-status-${idx}`}> <span className={`text-[9px] px-2 py-0.5 rounded-full font-black uppercase tracking-tighter ${doc.status === 'Approved' ? 'bg-green-100 text-green-700' : 'bg-red-50 text-re-red'}`} data-testid={`onboarding-prospective-details-doc-status-${idx}`}>
{doc.status || 'Pending'} {doc.status || 'Pending'}
</span> </span>
</div> </div>

View File

@ -211,7 +211,7 @@ export function WorkNotesPage(props: Partial<WorkNotesPageProps>) {
'bg-green-600', 'bg-green-600',
'bg-blue-600', 'bg-blue-600',
'bg-purple-600', 'bg-purple-600',
'bg-amber-600', 'bg-re-red',
'bg-pink-600', 'bg-pink-600',
'bg-indigo-600', 'bg-indigo-600',
'bg-teal-600', 'bg-teal-600',

View File

@ -14,18 +14,15 @@ import { toast } from 'sonner';
import { dealerService } from '@/services/dealer.service'; import { dealerService } from '@/services/dealer.service';
import { masterService } from '@/services/master.service'; import { masterService } from '@/services/master.service';
import { formatDateTime } from '@/components/ui/utils'; import { formatDateTime } from '@/components/ui/utils';
import { getRequestStatusBadgeClass, getStatusProgressBarClass } from '@/lib/statusProgressTheme';
interface DealerRelocationPageProps { interface DealerRelocationPageProps {
currentUser: UserType | null; currentUser: UserType | null;
onViewDetails?: (id: string) => void; onViewDetails?: (id: string) => void;
} }
const getStatusColor = (status: string) => { const getStatusColor = (status: string, currentStage?: string) =>
if (status === 'Completed') return 'bg-green-100 text-green-700 border-green-300'; getRequestStatusBadgeClass(status, currentStage);
if (status.includes('Review') || status.includes('Pending')) return 'bg-yellow-100 text-yellow-700 border-yellow-300';
if (status.includes('Rejected') || status.includes('Revoked')) return 'bg-red-100 text-red-700 border-red-300';
return 'bg-slate-100 text-slate-700 border-slate-300';
};
const getApiErrorMessage = (error: any, fallback: string) => const getApiErrorMessage = (error: any, fallback: string) =>
error?.response?.data?.message || error?.data?.message || error?.message || fallback; error?.response?.data?.message || error?.data?.message || error?.message || fallback;
@ -203,7 +200,7 @@ export function DealerRelocationPage({ onViewDetails }: DealerRelocationPageProp
title: 'Pending', title: 'Pending',
value: requests.filter(r => r.status !== 'Completed' && r.status !== 'Rejected').length, value: requests.filter(r => r.status !== 'Completed' && r.status !== 'Rejected').length,
icon: Calendar, icon: Calendar,
color: 'bg-yellow-500', color: 'bg-re-red',
}, },
{ {
title: 'Approved', title: 'Approved',
@ -218,7 +215,7 @@ export function DealerRelocationPage({ onViewDetails }: DealerRelocationPageProp
{/* Loading Overlay */} {/* Loading Overlay */}
{loading && ( {loading && (
<div className="min-h-[400px] flex items-center justify-center"> <div className="min-h-[400px] flex items-center justify-center">
<Loader2 className="w-8 h-8 text-amber-600 animate-spin" /> <Loader2 className="w-8 h-8 text-re-red animate-spin" />
</div> </div>
)} )}
@ -235,7 +232,7 @@ export function DealerRelocationPage({ onViewDetails }: DealerRelocationPageProp
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}> <Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
<DialogTrigger asChild> <DialogTrigger asChild>
<Button className="bg-amber-600 hover:bg-amber-700 text-white"> <Button className="bg-re-red hover:bg-re-red-hover text-white">
<Plus className="w-4 h-4 mr-2" /> <Plus className="w-4 h-4 mr-2" />
New Relocation Request New Relocation Request
</Button> </Button>
@ -404,7 +401,7 @@ export function DealerRelocationPage({ onViewDetails }: DealerRelocationPageProp
</Button> </Button>
<Button <Button
type="submit" type="submit"
className="bg-amber-600 hover:bg-amber-700 text-white" className="bg-re-red hover:bg-re-red-hover text-white"
disabled={submitting} disabled={submitting}
> >
{submitting ? ( {submitting ? (
@ -498,7 +495,7 @@ export function DealerRelocationPage({ onViewDetails }: DealerRelocationPageProp
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="flex-1 bg-slate-200 rounded-full h-2 min-w-[60px]"> <div className="flex-1 bg-slate-200 rounded-full h-2 min-w-[60px]">
<div <div
className="bg-amber-500 h-2 rounded-full" className={`h-2 rounded-full transition-all duration-300 ${getStatusProgressBarClass(request.status, request.currentStage)}`}
style={{ width: `${request.progressPercentage || 0}%` }} style={{ width: `${request.progressPercentage || 0}%` }}
/> />
</div> </div>

View File

@ -15,6 +15,16 @@ import { useNavigate } from 'react-router-dom';
import { User as UserType } from '@/lib/mock-data'; import { User as UserType } from '@/lib/mock-data';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { API } from '@/api/API'; import { API } from '@/api/API';
import { SlaBadge } from '@/components/sla/SlaBadge';
import { useSlaBatchStatus } from '@/hooks/useSlaBatchStatus';
import {
getCurrentStageBadgeClass,
getOffboardingRequestStatusBadgeClass,
getStatusLabelBadgeClass,
getStatusProgressBarClass,
isOffboardingTerminalNegative,
WORKFLOW_IN_PROGRESS_ACCENT,
} from '@/lib/offboardingDisplay';
interface RelocationRequestDetailsProps { interface RelocationRequestDetailsProps {
requestId: string; requestId: string;
@ -29,11 +39,9 @@ const workflowStages = [
{ id: 3, name: 'DD ZM Review', key: 'DD_ZM_REVIEW', role: 'DD-ZM' }, { id: 3, name: 'DD ZM Review', key: 'DD_ZM_REVIEW', role: 'DD-ZM' },
{ id: 4, name: 'ZBH Review', key: 'ZBH_REVIEW', role: 'ZBH' }, { id: 4, name: 'ZBH Review', key: 'ZBH_REVIEW', role: 'ZBH' },
{ id: 5, name: 'DD Lead Review', key: 'DD_LEAD_REVIEW', role: 'DD Lead' }, { id: 5, name: 'DD Lead Review', key: 'DD_LEAD_REVIEW', role: 'DD Lead' },
{ id: 6, name: 'DD Head Approval', key: 'DD_HEAD_APPROVAL', role: 'DD Head' }, { id: 6, name: 'NBH Approval', key: 'NBH_APPROVAL', role: 'NBH' },
{ id: 7, name: 'NBH Approval', key: 'NBH_APPROVAL', role: 'NBH' }, { id: 7, name: 'Legal Clearance', key: 'LEGAL_CLEARANCE', role: 'Legal Admin' },
{ id: 8, name: 'Legal Clearance', key: 'LEGAL_CLEARANCE', role: 'Legal Admin' }, { id: 8, name: 'Relocation Complete', key: 'COMPLETED', role: 'System' }
{ id: 9, name: 'NBH Clearance with EOR', key: 'NBH_CLEARANCE_EOR', role: 'NBH' },
{ id: 10, name: 'Relocation Complete', key: 'COMPLETED', role: 'System' }
]; ];
/** Map API stage / status label to 1-based workflow row index; 0 = unknown; length+1 = finished */ /** Map API stage / status label to 1-based workflow row index; 0 = unknown; length+1 = finished */
@ -174,13 +182,12 @@ const requiredDocuments = [
'Water supply documents' 'Water supply documents'
]; ];
const getStatusColor = (status: string) => { const getStatusColor = (status: string) => getStatusLabelBadgeClass(status);
if (status === 'Completed' || status === 'Verified' || status === 'Closed') return 'bg-green-100 text-green-700 border-green-300';
if (status.includes('Review') || status.includes('Pending') || status === 'In Progress') return 'bg-yellow-100 text-yellow-700 border-yellow-300'; const getDocChecklistUploadButtonClass = (isRejected: boolean) =>
if (status.includes('Rejected') || status.includes('Revoked')) return 'bg-red-100 text-red-700 border-red-300'; isRejected
if (status.includes('Collection') || status.includes('Completion') || status.includes('Infra')) return 'bg-blue-100 text-blue-700 border-blue-300'; ? 'h-7 px-2 text-red-700 hover:bg-red-50 hover:text-red-800 flex-shrink-0'
return 'bg-slate-100 text-slate-700 border-slate-300'; : 'h-7 px-2 text-slate-700 hover:bg-slate-50 flex-shrink-0';
};
const getApiErrorMessage = (error: any, fallback: string) => { const getApiErrorMessage = (error: any, fallback: string) => {
const responseData = error?.response?.data || error?.data; const responseData = error?.response?.data || error?.data;
@ -201,6 +208,13 @@ const getApiErrorMessage = (error: any, fallback: string) => {
export function RelocationRequestDetails({ requestId, onBack, currentUser }: RelocationRequestDetailsProps) { export function RelocationRequestDetails({ requestId, onBack, currentUser }: RelocationRequestDetailsProps) {
const navigate = useNavigate(); const navigate = useNavigate();
const [request, setRequest] = useState<any>(null); const [request, setRequest] = useState<any>(null);
// URL slug may be the human-readable code (e.g. `REL-...`); SLA expects UUID.
// Feed the SLA hook the resolved UUID once the request has loaded.
const slaEntityId: string = request?.id || '';
const { get: getSla } = useSlaBatchStatus(
slaEntityId ? [{ entityType: 'relocation', entityId: slaEntityId }] : [],
Boolean(slaEntityId)
);
const [auditLogs, setAuditLogs] = useState<any[]>([]); const [auditLogs, setAuditLogs] = useState<any[]>([]);
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const [isSubmitting, setIsSubmitting] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false);
@ -213,10 +227,17 @@ export function RelocationRequestDetails({ requestId, onBack, currentUser }: Rel
const [isSubmittingEor, setIsSubmittingEor] = useState(false); const [isSubmittingEor, setIsSubmittingEor] = useState(false);
const [selectedFile, setSelectedFile] = useState<File | null>(null); const [selectedFile, setSelectedFile] = useState<File | null>(null);
const [selectedDocType, setSelectedDocType] = useState<string>(requiredDocuments[0]); const [selectedDocType, setSelectedDocType] = useState<string>(requiredDocuments[0]);
// True when the dialog was opened from a checklist row -> doc type is implicit,
// so we hide the dropdown and show the doc name as a read-only badge instead.
const [docTypeLocked, setDocTypeLocked] = useState(false);
const [isUploading, setIsUploading] = useState(false); const [isUploading, setIsUploading] = useState(false);
const [activeTab, setActiveTab] = useState('workflow'); const [activeTab, setActiveTab] = useState('workflow');
const [isPreviewOpen, setIsPreviewOpen] = useState(false); const [isPreviewOpen, setIsPreviewOpen] = useState(false);
const [selectedDoc, setSelectedDoc] = useState<any>(null); const [selectedDoc, setSelectedDoc] = useState<any>(null);
const [rejectDocDialogOpen, setRejectDocDialogOpen] = useState(false);
const [rejectDocId, setRejectDocId] = useState<string | null>(null);
const [rejectDocReason, setRejectDocReason] = useState('');
const [isRejectingDoc, setIsRejectingDoc] = useState(false);
useEffect(() => { useEffect(() => {
fetchRequestDetails(); fetchRequestDetails();
@ -296,15 +317,6 @@ export function RelocationRequestDetails({ requestId, onBack, currentUser }: Rel
if (response.data.success) { if (response.data.success) {
const req = response.data.request; const req = response.data.request;
setRequest(req); setRequest(req);
const currentStage = req.currentStage;
if (
currentStage === 'NBH_CLEARANCE_EOR' ||
currentStage === 'NBH Clearance with EOR' ||
req.status === 'Completed'
) {
fetchEorChecklist(req.id);
}
} }
} catch (error) { } catch (error) {
console.error('Fetch relocation request details error:', error); console.error('Fetch relocation request details error:', error);
@ -351,7 +363,7 @@ export function RelocationRequestDetails({ requestId, onBack, currentUser }: Rel
const timelineResolvedOrdinal = relocationTimelineResolvedOrdinal(timelineEntries); const timelineResolvedOrdinal = relocationTimelineResolvedOrdinal(timelineEntries);
const auditResolvedOrdinal = relocationAuditResolvedOrdinal(auditLogs); const auditResolvedOrdinal = relocationAuditResolvedOrdinal(auditLogs);
const dbOrdinal = request ? getDbStageOrdinal() : 1; const dbOrdinal = request ? getDbStageOrdinal() : 1;
/** Audit/timeline can reference later steps (e.g. NBH EOR) while the request still sits at NBH Approval — do not use that to drive the active step. */ /** Audit/timeline can reference later steps while the request still sits in a prior stage — do not use that to drive the active step. */
const workflowProgressMismatch = const workflowProgressMismatch =
Boolean(request) && Boolean(request) &&
Math.max(timelineResolvedOrdinal, auditResolvedOrdinal) > dbOrdinal && Math.max(timelineResolvedOrdinal, auditResolvedOrdinal) > dbOrdinal &&
@ -365,11 +377,20 @@ export function RelocationRequestDetails({ requestId, onBack, currentUser }: Rel
request?.status === 'Completed' || request?.status === 'Completed' ||
request?.currentStage === 'Completed' || request?.currentStage === 'Completed' ||
dbOrdinal >= workflowStages.length + 1; dbOrdinal >= workflowStages.length + 1;
/** Match backend: N/10 while on pipeline (NBH EOR = 9 → 90%); 100% only when completed — avoids stale API 100% at NBH EOR. */ /** Match backend: N/(pipeline+1) while in flight; 100% only when completed. */
const timelineProgressPct = allWorkflowComplete const timelineProgressPct = allWorkflowComplete
? 100 ? 100
: Math.min(100, Math.round((dbOrdinal / workflowStages.length) * 100)); : Math.min(100, Math.round((dbOrdinal / workflowStages.length) * 100));
const displayProgressPct = allWorkflowComplete ? 100 : timelineProgressPct; const displayProgressPct = allWorkflowComplete ? 100 : timelineProgressPct;
const workflowTerminalNegative = request
? isOffboardingTerminalNegative(request.status, request.currentStage)
: false;
const statusProgressBarClass = request
? getStatusProgressBarClass(request.status, request.currentStage)
: 'bg-status-progress';
const requestStatusBadgeClass = request
? getOffboardingRequestStatusBadgeClass(request.status, request.currentStage)
: 'bg-re-red hover:bg-re-red-hover text-white border-transparent';
const missingRequiredDocs = request const missingRequiredDocs = request
? requiredDocuments.filter((doc) => !request.documents?.some((d: any) => ? requiredDocuments.filter((doc) => !request.documents?.some((d: any) =>
@ -422,7 +443,7 @@ export function RelocationRequestDetails({ requestId, onBack, currentUser }: Rel
request.currentStage && request.currentStage &&
request.currentStage !== 'ASM Review' && request.currentStage !== 'ASM Review' &&
request.currentStage !== 'Rejected'; request.currentStage !== 'Rejected';
const canRevoke = showActions && ['ZBH', 'DD Lead', 'DD Head', 'NBH', 'Legal Admin', 'Super Admin'].includes(currentUser?.role || ''); const canRevoke = showActions && ['ZBH', 'DD Lead', 'NBH', 'Legal Admin', 'Super Admin'].includes(currentUser?.role || '');
const requiresDocGate = request?.currentStage === 'NBH Approval' || request?.currentStage === 'Legal Clearance'; const requiresDocGate = request?.currentStage === 'NBH Approval' || request?.currentStage === 'Legal Clearance';
const canApprove = showActions && (!requiresDocGate || (missingRequiredDocs.length === 0 && pendingVerificationDocs.length === 0)); const canApprove = showActions && (!requiresDocGate || (missingRequiredDocs.length === 0 && pendingVerificationDocs.length === 0));
@ -526,17 +547,31 @@ export function RelocationRequestDetails({ requestId, onBack, currentUser }: Rel
} }
}; };
const handleRejectDocument = async (documentId: string) => { const submitRejectDocument = async () => {
if (!rejectDocId || !String(rejectDocReason).trim()) {
toast.error('Please enter a rejection reason.');
return;
}
try { try {
const response = await API.rejectRelocationDocument(requestId, documentId, { remarks: 'Rejected by reviewer' }) as any; setIsRejectingDoc(true);
if (response.data.success) { const response = await API.rejectRelocationDocument(requestId, rejectDocId, {
remarks: rejectDocReason.trim()
}) as any;
if (response.data?.success) {
toast.success('Document rejected successfully'); toast.success('Document rejected successfully');
setRejectDocDialogOpen(false);
setRejectDocId(null);
setRejectDocReason('');
fetchRequestDetails(true); fetchRequestDetails(true);
fetchAuditLogs(); fetchAuditLogs();
} else {
toast.error(response.data?.message || 'Failed to reject document');
} }
} catch (error) { } catch (error) {
console.error('Reject document error:', error); console.error('Reject document error:', error);
toast.error(getApiErrorMessage(error, 'Failed to reject document')); toast.error(getApiErrorMessage(error, 'Failed to reject document'));
} finally {
setIsRejectingDoc(false);
} }
}; };
@ -554,7 +589,7 @@ export function RelocationRequestDetails({ requestId, onBack, currentUser }: Rel
if (isLoading) { if (isLoading) {
return ( return (
<div className="flex flex-col items-center justify-center min-h-[400px] space-y-4"> <div className="flex flex-col items-center justify-center min-h-[400px] space-y-4">
<Loader2 className="w-10 h-10 text-amber-600 animate-spin" /> <Loader2 className="w-10 h-10 text-re-red animate-spin" />
<p className="text-slate-500 font-medium">Loading request details...</p> <p className="text-slate-500 font-medium">Loading request details...</p>
</div> </div>
); );
@ -588,12 +623,15 @@ export function RelocationRequestDetails({ requestId, onBack, currentUser }: Rel
<p className="text-slate-600"> <p className="text-slate-600">
{request.outlet?.name} ({request.outlet?.code}) {request.outlet?.name} ({request.outlet?.code})
</p> </p>
<div className="mt-1">
<SlaBadge status={getSla('relocation', slaEntityId)} />
</div>
</div> </div>
</div> </div>
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<Button <Button
variant="outline" variant="outline"
className="relative hover:bg-amber-50 hover:border-amber-300 hover:text-amber-700 transition-all shadow-sm" className="relative hover:bg-red-50 hover:border-red-300 hover:text-re-red-hover transition-all shadow-sm"
onClick={() => navigate(`/worknotes/relocation/${requestId}`, { onClick={() => navigate(`/worknotes/relocation/${requestId}`, {
state: { state: {
applicationName: request?.outlet?.name || 'Relocation', applicationName: request?.outlet?.name || 'Relocation',
@ -605,13 +643,13 @@ export function RelocationRequestDetails({ requestId, onBack, currentUser }: Rel
<MessageSquare className="w-4 h-4 mr-2" /> <MessageSquare className="w-4 h-4 mr-2" />
View Work Notes View Work Notes
{request?.worknotes?.length > 0 && ( {request?.worknotes?.length > 0 && (
<Badge className="ml-2 bg-amber-600 hover:bg-amber-700 text-white h-5 px-2"> <Badge className="ml-2 bg-re-red hover:bg-re-red-hover text-white h-5 px-2">
{request.worknotes.length} {request.worknotes.length}
</Badge> </Badge>
)} )}
</Button> </Button>
<Badge className={getStatusColor(request.status)}> <Badge className={requestStatusBadgeClass}>
{request.status} {request.status}
</Badge> </Badge>
</div> </div>
@ -641,7 +679,7 @@ export function RelocationRequestDetails({ requestId, onBack, currentUser }: Rel
</div> </div>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Navigation className="w-4 h-4 text-amber-600" /> <Navigation className="w-4 h-4 text-re-red" />
<div> <div>
<p className="text-slate-600 text-xs">To (Proposed)</p> <p className="text-slate-600 text-xs">To (Proposed)</p>
<p className="text-slate-900 text-sm">{request.proposedLocation || `${request.newAddress}, ${request.newCity}`}</p> <p className="text-slate-900 text-sm">{request.proposedLocation || `${request.newAddress}, ${request.newCity}`}</p>
@ -652,7 +690,7 @@ export function RelocationRequestDetails({ requestId, onBack, currentUser }: Rel
Type: {request.relocationType} Type: {request.relocationType}
</Badge> </Badge>
{request.distance && ( {request.distance && (
<Badge variant="outline" className="border-amber-200 bg-amber-50 text-amber-700"> <Badge variant="outline" className="border-red-200 bg-red-50 text-re-red-hover">
Distance: {request.distance} Distance: {request.distance}
</Badge> </Badge>
)} )}
@ -673,9 +711,12 @@ export function RelocationRequestDetails({ requestId, onBack, currentUser }: Rel
<p className="text-slate-600 text-sm mb-1">Request Information</p> <p className="text-slate-600 text-sm mb-1">Request Information</p>
<p className="text-slate-900 text-sm">Submitted: {formatDateTime(request.createdAt)}</p> <p className="text-slate-900 text-sm">Submitted: {formatDateTime(request.createdAt)}</p>
<p className="text-slate-600 text-sm">By: {request.dealer?.fullName}</p> <p className="text-slate-600 text-sm">By: {request.dealer?.fullName}</p>
<p className="text-slate-900 text-sm mt-2"> <div className="mt-2 flex flex-wrap items-center gap-2">
Current Stage: {String(request.currentStage || '').replace(/_/g, ' ')} <span className="text-slate-600 text-sm">Current Stage:</span>
</p> <Badge className={getCurrentStageBadgeClass(request.currentStage, request.status)}>
{String(request.currentStage || '').replace(/_/g, ' ')}
</Badge>
</div>
</div> </div>
</div> </div>
@ -692,13 +733,10 @@ export function RelocationRequestDetails({ requestId, onBack, currentUser }: Rel
<Card> <Card>
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full"> <Tabs value={activeTab} onValueChange={setActiveTab} className="w-full">
<CardHeader className="pb-4"> <CardHeader className="pb-4">
<div className="overflow-x-auto -mx-6 px-6"> <div className="overflow-x-auto custom-scrollbar-x-slim -mx-6 px-6">
<TabsList className="w-max min-w-full justify-start"> <TabsList className="w-max min-w-full justify-start">
<TabsTrigger value="workflow">Workflow Progress</TabsTrigger> <TabsTrigger value="workflow">Workflow Progress</TabsTrigger>
<TabsTrigger value="documents">Documents</TabsTrigger> <TabsTrigger value="documents">Documents</TabsTrigger>
{(request.currentStage === 'NBH Clearance with EOR' || request.status === 'Completed' || request.currentStage === 'NBH_CLEARANCE_EOR') && (
<TabsTrigger value="eor">EOR Checklist</TabsTrigger>
)}
<TabsTrigger value="history">History & Audit Trail</TabsTrigger> <TabsTrigger value="history">History & Audit Trail</TabsTrigger>
</TabsList> </TabsList>
</div> </div>
@ -706,23 +744,32 @@ export function RelocationRequestDetails({ requestId, onBack, currentUser }: Rel
<CardContent> <CardContent>
{/* Workflow Progress Tab */} {/* Workflow Progress Tab */}
<TabsContent value="workflow" className="mt-0"> <TabsContent value="workflow" className="mt-0 status-progress-ui">
{/* Progress Bar */} {/* Progress Bar */}
<div className="mb-8"> <div className="mb-8">
<div className="flex items-center justify-between mb-2"> <div className="flex items-center justify-between mb-2">
<span className="text-slate-900">Overall Progress</span> <span className="text-slate-900">Overall Progress</span>
<span className="text-slate-600">{displayProgressPct}%</span> <Badge className={`${statusProgressBarClass} text-white border-transparent hover:opacity-90`}>
{displayProgressPct}% Complete
</Badge>
</div> </div>
<div className="h-3 bg-slate-200 rounded-full overflow-hidden"> <div className="h-3 bg-slate-200 rounded-full overflow-hidden">
<div <div
className="h-full bg-amber-600 transition-all duration-500" className={`h-full transition-all duration-500 ${statusProgressBarClass}`}
style={{ width: `${displayProgressPct}%` }} style={{ width: `${displayProgressPct}%` }}
/> />
</div> </div>
</div> </div>
{workflowProgressMismatch && ( {workflowTerminalNegative && (
<div className="mb-6 rounded-lg border border-amber-300 bg-amber-50 px-4 py-3 text-sm text-amber-900"> <div className="mb-6 rounded-lg border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-800">
This request is closed as <strong>{String(request.status)}</strong>. The approval path below is
for reference only.
</div>
)}
{workflowProgressMismatch && !workflowTerminalNegative && (
<div className="mb-6 rounded-lg border border-red-300 bg-red-50 px-4 py-3 text-sm text-red-900">
<span className="font-medium">Activity ahead of current stage:</span> Some timeline or audit <span className="font-medium">Activity ahead of current stage:</span> Some timeline or audit
entries reference steps after the official current stage ({String(request.currentStage)}). entries reference steps after the official current stage ({String(request.currentStage)}).
Per-step history below may include future steps; the highlighted step and approvals follow the Per-step history below may include future steps; the highlighted step and approvals follow the
@ -739,7 +786,16 @@ export function RelocationRequestDetails({ requestId, onBack, currentUser }: Rel
{/* Workflow stages + per-stage timeline (same pattern as ResignationDetails progress tab) */} {/* Workflow stages + per-stage timeline (same pattern as ResignationDetails progress tab) */}
<div className="space-y-4"> <div className="space-y-4">
{workflowStages.map((stage: any, index: number) => { {workflowTerminalNegative ? (
<ul className="list-disc space-y-1 pl-5 text-sm text-slate-600">
{workflowStages.map((stage: any) => (
<li key={stage.id}>
<span className="text-slate-900">{stage.name}</span> {stage.role}
</li>
))}
</ul>
) : (
workflowStages.map((stage: any, index: number) => {
const isCompleted = allWorkflowComplete || index < dbOrdinal - 1; const isCompleted = allWorkflowComplete || index < dbOrdinal - 1;
const isCurrent = !allWorkflowComplete && index === dbOrdinal - 1; const isCurrent = !allWorkflowComplete && index === dbOrdinal - 1;
const stageTimelineEntries = getRelocationTimelineEntriesForStage( const stageTimelineEntries = getRelocationTimelineEntriesForStage(
@ -760,7 +816,7 @@ export function RelocationRequestDetails({ requestId, onBack, currentUser }: Rel
isCompleted isCompleted
? 'bg-green-100 text-green-600' ? 'bg-green-100 text-green-600'
: isCurrent : isCurrent
? 'bg-amber-100 text-amber-600' ? WORKFLOW_IN_PROGRESS_ACCENT.icon
: 'bg-slate-100 text-slate-400' : 'bg-slate-100 text-slate-400'
}`} }`}
> >
@ -781,9 +837,7 @@ export function RelocationRequestDetails({ requestId, onBack, currentUser }: Rel
<div <div
className={`flex-1 pb-8 ${ className={`flex-1 pb-8 ${
isCurrent isCurrent ? WORKFLOW_IN_PROGRESS_ACCENT.panel : ''
? 'bg-amber-50 -ml-4 pl-4 pr-4 py-3 rounded-lg border border-amber-200'
: ''
}`} }`}
> >
<div className="flex items-center justify-between mb-1 gap-2"> <div className="flex items-center justify-between mb-1 gap-2">
@ -793,7 +847,7 @@ export function RelocationRequestDetails({ requestId, onBack, currentUser }: Rel
isCompleted isCompleted
? 'text-green-700' ? 'text-green-700'
: isCurrent : isCurrent
? 'text-amber-900' ? WORKFLOW_IN_PROGRESS_ACCENT.title
: 'text-slate-900' : 'text-slate-900'
} }
> >
@ -801,7 +855,7 @@ export function RelocationRequestDetails({ requestId, onBack, currentUser }: Rel
</h4> </h4>
<p <p
className={`text-sm ${ className={`text-sm ${
isCurrent ? 'text-amber-700' : 'text-slate-600' isCurrent ? WORKFLOW_IN_PROGRESS_ACCENT.subtitle : 'text-slate-600'
}`} }`}
> >
Responsible: {stage.role} Responsible: {stage.role}
@ -818,7 +872,7 @@ export function RelocationRequestDetails({ requestId, onBack, currentUser }: Rel
isCompleted isCompleted
? 'bg-green-100 text-green-700 border-green-300' ? 'bg-green-100 text-green-700 border-green-300'
: isCurrent : isCurrent
? 'bg-amber-100 text-amber-700 border-amber-300' ? WORKFLOW_IN_PROGRESS_ACCENT.stageBadge
: 'bg-slate-100 text-slate-500 border-slate-300' : 'bg-slate-100 text-slate-500 border-slate-300'
} }
> >
@ -864,7 +918,8 @@ export function RelocationRequestDetails({ requestId, onBack, currentUser }: Rel
</div> </div>
</div> </div>
); );
})} })
)}
</div> </div>
</TabsContent> </TabsContent>
@ -882,9 +937,26 @@ export function RelocationRequestDetails({ requestId, onBack, currentUser }: Rel
{/* Upload Button */} {/* Upload Button */}
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<h4 className="text-slate-900">Required Documents</h4> <h4 className="text-slate-900">Required Documents</h4>
<Dialog open={isUploadDialogOpen} onOpenChange={setIsUploadDialogOpen}> <Dialog
open={isUploadDialogOpen}
onOpenChange={(open) => {
setIsUploadDialogOpen(open);
if (!open) {
setDocTypeLocked(false);
setSelectedFile(null);
}
}}
>
<DialogTrigger asChild> <DialogTrigger asChild>
<Button size="sm" className="bg-amber-600 hover:bg-amber-700"> <Button
size="sm"
className="bg-re-red hover:bg-re-red-hover"
onClick={() => {
setDocTypeLocked(false);
setSelectedDocType(requiredDocuments[0]);
setSelectedFile(null);
}}
>
<Upload className="w-4 h-4 mr-2" /> <Upload className="w-4 h-4 mr-2" />
Upload Document Upload Document
</Button> </Button>
@ -893,29 +965,42 @@ export function RelocationRequestDetails({ requestId, onBack, currentUser }: Rel
<DialogHeader> <DialogHeader>
<DialogTitle>Upload Document</DialogTitle> <DialogTitle>Upload Document</DialogTitle>
<DialogDescription> <DialogDescription>
Select the document type and upload the file {docTypeLocked
? 'Pick a file for the selected document.'
: 'Select the document type and upload the file.'}
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
<div className="space-y-4"> <div className="space-y-4">
<div> {docTypeLocked ? (
<Label>Document Type</Label> <div>
<select <Label>Document</Label>
className="w-full mt-1 px-3 py-2 border border-slate-300 rounded-md" <div className="mt-1 flex items-center gap-2 bg-red-50 border border-red-200 rounded-md px-3 h-10">
value={selectedDocType} <Badge className="bg-re-red text-white border-transparent">
onChange={(e) => setSelectedDocType(e.target.value)} {selectedDocType}
> </Badge>
{requiredDocuments.map((doc, index) => { </div>
const isAlreadyUploaded = request.documents?.some((d: any) => </div>
d.type === doc || d.name.toLowerCase().includes(doc.toLowerCase().split(' ')[0]) ) : (
); <div>
return ( <Label>Document Type</Label>
<option key={index} value={doc}> <select
{isAlreadyUploaded ? `${doc}` : doc} className="w-full mt-1 px-3 py-2 border border-slate-300 rounded-md"
</option> value={selectedDocType}
); onChange={(e) => setSelectedDocType(e.target.value)}
})} >
</select> {requiredDocuments.map((doc, index) => {
</div> const isAlreadyUploaded = request.documents?.some((d: any) =>
d.type === doc || d.name.toLowerCase().includes(doc.toLowerCase().split(' ')[0])
);
return (
<option key={index} value={doc}>
{isAlreadyUploaded ? `${doc}` : doc}
</option>
);
})}
</select>
</div>
)}
<div> <div>
<Label>Upload File</Label> <Label>Upload File</Label>
<Input <Input
@ -930,7 +1015,7 @@ export function RelocationRequestDetails({ requestId, onBack, currentUser }: Rel
Cancel Cancel
</Button> </Button>
<Button <Button
className="bg-amber-600 hover:bg-amber-700" className="bg-re-red hover:bg-re-red-hover"
onClick={handleUploadDocument} onClick={handleUploadDocument}
disabled={isUploading} disabled={isUploading}
> >
@ -954,20 +1039,51 @@ export function RelocationRequestDetails({ requestId, onBack, currentUser }: Rel
const uploaded = request.documents?.find((d: any) => const uploaded = request.documents?.find((d: any) =>
d.type === doc || d.name.toLowerCase().includes(doc.toLowerCase().split(' ')[0]) d.type === doc || d.name.toLowerCase().includes(doc.toLowerCase().split(' ')[0])
); );
const isRejected = uploaded && String(uploaded.status) === 'Rejected';
const ok = uploaded && !isRejected;
return ( return (
<div <div
key={index} key={index}
className={`flex items-center gap-2 p-2 rounded border text-sm ${uploaded ? 'bg-green-50 border-green-200' : 'bg-slate-50 border-slate-200' className={`flex items-center justify-between gap-2 p-2 rounded border text-sm ${
}`} isRejected
? 'bg-red-50 border-red-200'
: ok
? 'bg-green-50 border-green-200'
: 'bg-slate-50 border-slate-200'
}`}
> >
{uploaded ? ( <div className="flex items-center gap-2 min-w-0">
<CheckCircle2 className="w-4 h-4 text-green-600 flex-shrink-0" /> {isRejected ? (
) : ( <AlertCircle className="w-4 h-4 text-red-600 flex-shrink-0" />
<AlertCircle className="w-4 h-4 text-slate-400 flex-shrink-0" /> ) : ok ? (
<CheckCircle2 className="w-4 h-4 text-green-600 flex-shrink-0" />
) : (
<AlertCircle className="w-4 h-4 text-slate-400 flex-shrink-0" />
)}
<span
className={`truncate ${
isRejected ? 'text-red-900' : ok ? 'text-green-900' : 'text-slate-700'
}`}
>
{doc}
</span>
</div>
{!ok && (
<Button
size="sm"
variant="ghost"
className={getDocChecklistUploadButtonClass(!!isRejected)}
onClick={() => {
setSelectedDocType(doc);
setSelectedFile(null);
setDocTypeLocked(true);
setIsUploadDialogOpen(true);
}}
>
<Upload className="w-3.5 h-3.5 mr-1" />
{isRejected ? 'Re-upload' : 'Upload'}
</Button>
)} )}
<span className={uploaded ? 'text-green-900' : 'text-slate-700'}>
{doc}
</span>
</div> </div>
); );
})} })}
@ -994,7 +1110,10 @@ export function RelocationRequestDetails({ requestId, onBack, currentUser }: Rel
</TableHeader> </TableHeader>
<TableBody> <TableBody>
{request.documents.map((doc: any) => ( {request.documents.map((doc: any) => (
<TableRow key={doc.id}> <TableRow
key={doc.id}
className={String(doc.status) === 'Rejected' ? 'bg-red-50/80' : undefined}
>
<TableCell className="text-slate-900"> <TableCell className="text-slate-900">
{doc.name} {doc.name}
</TableCell> </TableCell>
@ -1039,7 +1158,7 @@ export function RelocationRequestDetails({ requestId, onBack, currentUser }: Rel
{doc.status === 'Pending Verification' && (() => { {doc.status === 'Pending Verification' && (() => {
const role = currentUser?.role || currentUser?.roleCode || ''; const role = currentUser?.role || currentUser?.roleCode || '';
// SRS — only authorized review roles can verify relocation documents // SRS — only authorized review roles can verify relocation documents
return ['DD Lead', 'DD Head', 'NBH', 'Legal Admin', 'DD Admin', 'Super Admin', 'SUPER_ADMIN', 'DD_ADMIN'].includes(role); return ['DD Lead', 'NBH', 'Legal Admin', 'DD Admin', 'Super Admin', 'SUPER_ADMIN', 'DD_ADMIN'].includes(role);
})() && ( })() && (
<> <>
<Button <Button
@ -1055,7 +1174,11 @@ export function RelocationRequestDetails({ requestId, onBack, currentUser }: Rel
size="sm" size="sm"
variant="destructive" variant="destructive"
className="h-8 gap-1" className="h-8 gap-1"
onClick={() => handleRejectDocument(doc.id)} onClick={() => {
setRejectDocId(doc.id);
setRejectDocReason('');
setRejectDocDialogOpen(true);
}}
title="Reject Document" title="Reject Document"
> >
<AlertCircle className="w-4 h-4" /> <AlertCircle className="w-4 h-4" />
@ -1102,7 +1225,7 @@ export function RelocationRequestDetails({ requestId, onBack, currentUser }: Rel
<div className="bg-slate-50 border border-dashed border-slate-300 rounded-lg p-12 text-center"> <div className="bg-slate-50 border border-dashed border-slate-300 rounded-lg p-12 text-center">
{isEorLoading ? ( {isEorLoading ? (
<div className="flex flex-col items-center gap-2"> <div className="flex flex-col items-center gap-2">
<Loader2 className="w-8 h-8 text-amber-600 animate-spin" /> <Loader2 className="w-8 h-8 text-re-red animate-spin" />
<p className="text-slate-500">Fetching checklist...</p> <p className="text-slate-500">Fetching checklist...</p>
</div> </div>
) : ( ) : (
@ -1134,7 +1257,7 @@ export function RelocationRequestDetails({ requestId, onBack, currentUser }: Rel
{(!eorChecklist.items || eorChecklist.items.length === 0) ? ( {(!eorChecklist.items || eorChecklist.items.length === 0) ? (
<TableRow> <TableRow>
<TableCell colSpan={4} className="text-center text-slate-500 py-8 text-sm"> <TableCell colSpan={4} className="text-center text-slate-500 py-8 text-sm">
No checklist rows returned. Use &quot;Try Refreshing&quot; above or reload the page; rows are created when the request enters NBH Clearance with EOR. No checklist rows returned. Use &quot;Try Refreshing&quot; above or reload the page.
</TableCell> </TableCell>
</TableRow> </TableRow>
) : ( ) : (
@ -1146,7 +1269,7 @@ export function RelocationRequestDetails({ requestId, onBack, currentUser }: Rel
checked={item.isCompliant} checked={item.isCompliant}
onChange={(e) => handleUpdateEorItem(item.description, e.target.checked, item.itemType)} onChange={(e) => handleUpdateEorItem(item.description, e.target.checked, item.itemType)}
disabled={eorChecklist.status === 'Completed' || (currentUser?.role !== 'NBH' && currentUser?.role !== 'Super Admin')} disabled={eorChecklist.status === 'Completed' || (currentUser?.role !== 'NBH' && currentUser?.role !== 'Super Admin')}
className="w-4 h-4 rounded border-slate-300 text-amber-600" className="w-4 h-4 rounded border-slate-300 text-re-red"
/> />
</TableCell> </TableCell>
<TableCell> <TableCell>
@ -1184,7 +1307,7 @@ export function RelocationRequestDetails({ requestId, onBack, currentUser }: Rel
</Button> </Button>
</> </>
) : item.proofDocumentId ? ( ) : item.proofDocumentId ? (
<span className="text-xs text-amber-700">Proof linked (refresh if file details are missing)</span> <span className="text-xs text-re-red-hover">Proof linked (refresh if file details are missing)</span>
) : ( ) : (
<Button <Button
type="button" type="button"
@ -1229,7 +1352,7 @@ export function RelocationRequestDetails({ requestId, onBack, currentUser }: Rel
)} )}
{!eorChecklist.items?.every((i: any) => i.isCompliant) && ( {!eorChecklist.items?.every((i: any) => i.isCompliant) && (
<p className="text-right text-xs text-amber-600 italic"> <p className="text-right text-xs text-re-red italic">
All items must be marked as compliant before final submission. All items must be marked as compliant before final submission.
</p> </p>
)} )}
@ -1252,7 +1375,7 @@ export function RelocationRequestDetails({ requestId, onBack, currentUser }: Rel
${(entry.description || entry.action || entry.details?.action || '').toLowerCase().includes('reject') || (entry.description || entry.action || entry.details?.action || '').toLowerCase().includes('revok') ${(entry.description || entry.action || entry.details?.action || '').toLowerCase().includes('reject') || (entry.description || entry.action || entry.details?.action || '').toLowerCase().includes('revok')
? 'bg-red-100 text-red-700 border-red-200' ? 'bg-red-100 text-red-700 border-red-200'
: (entry.description || entry.action || entry.details?.action || '').toLowerCase().includes('sent back') || (entry.description || entry.action || entry.details?.action || '').toLowerCase().includes('send back') : (entry.description || entry.action || entry.details?.action || '').toLowerCase().includes('sent back') || (entry.description || entry.action || entry.details?.action || '').toLowerCase().includes('send back')
? 'bg-amber-100 text-amber-700 border-amber-200' ? 'bg-red-50 text-re-red-hover border-red-200'
: (entry.description || entry.action || entry.details?.action || '').toLowerCase().includes('approv') || (entry.description || entry.action || entry.details?.action || '').toLowerCase().includes('initi') || (entry.action || '').toLowerCase().includes('complete') : (entry.description || entry.action || entry.details?.action || '').toLowerCase().includes('approv') || (entry.description || entry.action || entry.details?.action || '').toLowerCase().includes('initi') || (entry.action || '').toLowerCase().includes('complete')
? 'bg-emerald-100 text-emerald-700 border-emerald-200' ? 'bg-emerald-100 text-emerald-700 border-emerald-200'
: 'bg-slate-100 text-slate-700 border-slate-200'} : 'bg-slate-100 text-slate-700 border-slate-200'}
@ -1300,14 +1423,16 @@ export function RelocationRequestDetails({ requestId, onBack, currentUser }: Rel
<CardContent className="space-y-4"> <CardContent className="space-y-4">
<div> <div>
<p className="text-slate-600 text-sm">Current Stage</p> <p className="text-slate-600 text-sm">Current Stage</p>
<p className="text-slate-900">{request.currentStage}</p> <Badge className={getCurrentStageBadgeClass(request.currentStage, request.status)}>
{request.currentStage}
</Badge>
</div> </div>
<div> <div>
<p className="text-slate-600 text-sm">Progress</p> <p className="text-slate-600 text-sm">Progress</p>
<div className="flex items-center gap-2 mt-2"> <div className="flex items-center gap-2 mt-2">
<div className="flex-1 h-2 bg-slate-200 rounded-full overflow-hidden"> <div className="flex-1 h-2 bg-slate-200 rounded-full overflow-hidden">
<div <div
className="h-full bg-amber-600 transition-all duration-300" className={`h-full transition-all duration-300 ${statusProgressBarClass}`}
style={{ width: `${displayProgressPct}%` }} style={{ width: `${displayProgressPct}%` }}
/> />
</div> </div>
@ -1342,9 +1467,9 @@ export function RelocationRequestDetails({ requestId, onBack, currentUser }: Rel
Approve Request Approve Request
</Button> </Button>
{!canApprove && ( {!canApprove && (
<p className="text-xs text-amber-700"> <div className="rounded-md border border-red-200 bg-red-50 px-3 py-2 text-xs text-red-800">
Approval is blocked until mandatory documents are uploaded and verified for this stage. Approval is blocked until mandatory documents are uploaded and verified for this stage.
</p> </div>
)} )}
<Button <Button
@ -1364,7 +1489,7 @@ export function RelocationRequestDetails({ requestId, onBack, currentUser }: Rel
{canSendBack && ( {canSendBack && (
<Button <Button
variant="outline" variant="outline"
className="w-full border-amber-400 text-amber-900 hover:bg-amber-50" className="w-full border-red-300 text-red-900 hover:bg-red-50"
onClick={() => handleAction('sendBack')} onClick={() => handleAction('sendBack')}
disabled={isSubmitting} disabled={isSubmitting}
> >
@ -1414,34 +1539,6 @@ export function RelocationRequestDetails({ requestId, onBack, currentUser }: Rel
</CardContent> </CardContent>
</Card> </Card>
{/* Assigned Evaluators Card */}
{request.participants && request.participants.length > 0 && (
<Card>
<CardHeader>
<CardTitle>Assigned Evaluators</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
{request.participants.map((participant: any) => (
<div key={participant.id} className="flex items-center justify-between p-2 bg-slate-50 rounded-lg">
<div>
<p className="text-slate-900 text-sm font-medium">{participant.user?.fullName || 'Unknown'}</p>
<p className="text-slate-600 text-xs">{participant.user?.roleCode || 'User'}</p>
{participant.metadata?.stage && (
<Badge variant="outline" className="mt-1 text-xs">
{participant.metadata.stage.replace(/_/g, ' ')}
</Badge>
)}
</div>
{participant.metadata?.autoAssigned && (
<Badge className="bg-blue-100 text-blue-700 border-blue-300 text-xs">
Auto-assigned
</Badge>
)}
</div>
))}
</CardContent>
</Card>
)}
</div> </div>
</div> </div>
@ -1505,12 +1602,12 @@ export function RelocationRequestDetails({ requestId, onBack, currentUser }: Rel
actionType === 'approve' actionType === 'approve'
? 'bg-green-600 hover:bg-green-700' ? 'bg-green-600 hover:bg-green-700'
: actionType === 'reject' : actionType === 'reject'
? 'bg-red-600 hover:bg-red-700' ? 'bg-re-red hover:bg-re-red-hover'
: actionType === 'sendBack' : actionType === 'sendBack'
? 'bg-amber-600 hover:bg-amber-700' ? 'bg-re-red hover:bg-re-red-hover'
: actionType === 'revoke' : actionType === 'revoke'
? 'bg-red-700 hover:bg-red-800' ? 'bg-re-red hover:bg-re-red-hover'
: 'bg-amber-600 hover:bg-amber-700' : 'bg-re-red hover:bg-re-red-hover'
} }
disabled={isSubmitting} disabled={isSubmitting}
> >
@ -1536,6 +1633,40 @@ export function RelocationRequestDetails({ requestId, onBack, currentUser }: Rel
{/* Worknotes Dialog */} {/* Worknotes Dialog */}
{/* Worknotes Dialog - handled in Header */} {/* Worknotes Dialog - handled in Header */}
<Dialog open={rejectDocDialogOpen} onOpenChange={setRejectDocDialogOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>Reject document</DialogTitle>
<DialogDescription>
Mark this upload as rejected and provide a reason. The action is recorded in the audit trail.
</DialogDescription>
</DialogHeader>
<div className="space-y-3">
<Label htmlFor="rejectDocReason">Rejection reason *</Label>
<Textarea
id="rejectDocReason"
rows={4}
value={rejectDocReason}
onChange={(e) => setRejectDocReason(e.target.value)}
placeholder="Explain what must be corrected or re-uploaded…"
/>
</div>
<DialogFooter>
<Button type="button" variant="outline" onClick={() => setRejectDocDialogOpen(false)}>
Cancel
</Button>
<Button
type="button"
variant="destructive"
disabled={isRejectingDoc}
onClick={() => void submitRejectDocument()}
>
{isRejectingDoc ? <Loader2 className="w-4 h-4 animate-spin" /> : 'Confirm reject'}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
<DocumentPreviewModal <DocumentPreviewModal
isOpen={isPreviewOpen} isOpen={isPreviewOpen}
onClose={() => setIsPreviewOpen(false)} onClose={() => setIsPreviewOpen(false)}

View File

@ -14,7 +14,13 @@ import { useState, useEffect } from 'react';
import { User } from '@/lib/mock-data'; import { User } from '@/lib/mock-data';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { API } from '@/api/API'; import { API } from '@/api/API';
import { SlaBadge } from '@/components/sla/SlaBadge';
import { useSlaBatchStatus } from '@/hooks/useSlaBatchStatus';
import { formatDateTime } from '@/components/ui/utils'; import { formatDateTime } from '@/components/ui/utils';
import {
getCurrentStageBadgeClass,
getStatusProgressBarClass,
} from '@/lib/statusProgressTheme';
import { import {
Pagination, Pagination,
PaginationContent, PaginationContent,
@ -33,13 +39,8 @@ interface RelocationRequestPageProps {
const getApiErrorMessage = (error: any, fallback: string) => const getApiErrorMessage = (error: any, fallback: string) =>
error?.response?.data?.message || error?.data?.message || error?.message || fallback; error?.response?.data?.message || error?.data?.message || error?.message || fallback;
const getStatusColor = (status: string) => { const getStageBadgeClass = (stage: string, requestStatus?: string) =>
if (status === 'Completed' || status === 'Closed') return 'bg-green-100 text-green-700 border-green-300'; getCurrentStageBadgeClass(stage, requestStatus);
if (status.includes('Review') || status.includes('Pending') || status === 'In Progress') return 'bg-yellow-100 text-yellow-700 border-yellow-300';
if (status.includes('Rejected') || status.includes('Revoked')) return 'bg-red-100 text-red-700 border-red-300';
if (status.includes('Collection') || status.includes('Completion') || status.includes('Infra')) return 'bg-blue-100 text-blue-700 border-blue-300';
return 'bg-slate-100 text-slate-700 border-slate-300';
};
export function RelocationRequestPage({ currentUser, onViewDetails }: RelocationRequestPageProps) { export function RelocationRequestPage({ currentUser, onViewDetails }: RelocationRequestPageProps) {
const [requests, setRequests] = useState<any[]>([]); const [requests, setRequests] = useState<any[]>([]);
@ -74,6 +75,12 @@ export function RelocationRequestPage({ currentUser, onViewDetails }: Relocation
// Constants // Constants
const isSuperAdmin = currentUser?.role === 'Super Admin' || currentUser?.roleCode === 'Super Admin'; const isSuperAdmin = currentUser?.role === 'Super Admin' || currentUser?.roleCode === 'Super Admin';
const slaItems = requests.map((r: any) => ({
entityType: 'relocation',
entityId: r.id || r.requestId
}));
const { get: getSla } = useSlaBatchStatus(slaItems, requests.length > 0);
const isCompletedRequest = (request: any) => const isCompletedRequest = (request: any) =>
request.status === 'Completed' || request.status === 'Closed' || request.currentStage === 'Completed'; request.status === 'Completed' || request.status === 'Closed' || request.currentStage === 'Completed';
@ -243,7 +250,7 @@ export function RelocationRequestPage({ currentUser, onViewDetails }: Relocation
title: 'Pending Review', title: 'Pending Review',
value: requests.filter((r: any) => isPendingReviewRequest(r)).length, value: requests.filter((r: any) => isPendingReviewRequest(r)).length,
icon: Calendar, icon: Calendar,
color: 'bg-yellow-500', color: 'bg-re-red',
}, },
{ {
title: 'Completed', title: 'Completed',
@ -276,7 +283,7 @@ export function RelocationRequestPage({ currentUser, onViewDetails }: Relocation
{isSuperAdmin && ( {isSuperAdmin && (
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}> <Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
<DialogTrigger asChild> <DialogTrigger asChild>
<Button className="bg-amber-600 hover:bg-amber-700 text-white"> <Button className="bg-re-red hover:bg-re-red-hover text-white">
<Plus className="w-4 h-4 mr-2" /> <Plus className="w-4 h-4 mr-2" />
New Relocation Request New Relocation Request
</Button> </Button>
@ -435,7 +442,7 @@ export function RelocationRequestPage({ currentUser, onViewDetails }: Relocation
</Button> </Button>
<Button <Button
type="submit" type="submit"
className="bg-amber-600 hover:bg-amber-700 text-white" className="bg-re-red hover:bg-re-red-hover text-white"
disabled={submitting} disabled={submitting}
> >
{submitting ? ( {submitting ? (
@ -514,7 +521,7 @@ export function RelocationRequestPage({ currentUser, onViewDetails }: Relocation
<TableRow> <TableRow>
<TableCell colSpan={8} className="h-32 text-center"> <TableCell colSpan={8} className="h-32 text-center">
<div className="flex flex-col items-center justify-center space-y-2"> <div className="flex flex-col items-center justify-center space-y-2">
<Loader2 className="w-6 h-6 text-amber-600 animate-spin" /> <Loader2 className="w-6 h-6 text-re-red animate-spin" />
<p className="text-slate-500 text-sm">Loading requests...</p> <p className="text-slate-500 text-sm">Loading requests...</p>
</div> </div>
</TableCell> </TableCell>
@ -542,7 +549,7 @@ export function RelocationRequestPage({ currentUser, onViewDetails }: Relocation
<span>{request.currentLocation}</span> <span>{request.currentLocation}</span>
</div> </div>
<div className="flex items-center gap-1 text-slate-900 text-sm"> <div className="flex items-center gap-1 text-slate-900 text-sm">
<Navigation className="w-3 h-3 text-amber-600" /> <Navigation className="w-3 h-3 text-re-red" />
<span className="text-slate-500">To:</span> <span className="text-slate-500">To:</span>
<span>{request.proposedLocation}</span> <span>{request.proposedLocation}</span>
</div> </div>
@ -554,7 +561,8 @@ export function RelocationRequestPage({ currentUser, onViewDetails }: Relocation
</Badge> </Badge>
</TableCell> </TableCell>
<TableCell> <TableCell>
<Badge className={`border ${getStatusColor(request.currentStage)}`}> <SlaBadge status={getSla('relocation', request.id || request.requestId)} compact />
<Badge className={getStageBadgeClass(request.currentStage, request.status)}>
{request.currentStage} {request.currentStage}
</Badge> </Badge>
</TableCell> </TableCell>
@ -562,7 +570,7 @@ export function RelocationRequestPage({ currentUser, onViewDetails }: Relocation
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="flex-1 h-2 bg-slate-200 rounded-full overflow-hidden"> <div className="flex-1 h-2 bg-slate-200 rounded-full overflow-hidden">
<div <div
className="h-full bg-amber-600 transition-all duration-300" className={`h-full transition-all duration-300 ${getStatusProgressBarClass(request.status, request.currentStage)}`}
style={{ width: `${request.progressPercentage || 0}%` }} style={{ width: `${request.progressPercentage || 0}%` }}
/> />
</div> </div>
@ -607,7 +615,7 @@ export function RelocationRequestPage({ currentUser, onViewDetails }: Relocation
{isLoading ? ( {isLoading ? (
<TableRow> <TableRow>
<TableCell colSpan={5} className="h-32 text-center"> <TableCell colSpan={5} className="h-32 text-center">
<Loader2 className="w-6 h-6 text-amber-600 animate-spin mx-auto" /> <Loader2 className="w-6 h-6 text-re-red animate-spin mx-auto" />
</TableCell> </TableCell>
</TableRow> </TableRow>
) : ( ) : (
@ -629,7 +637,8 @@ export function RelocationRequestPage({ currentUser, onViewDetails }: Relocation
</div> </div>
</TableCell> </TableCell>
<TableCell> <TableCell>
<Badge className={`border ${getStatusColor(request.currentStage)}`}> <SlaBadge status={getSla('relocation', request.id || request.requestId)} compact />
<Badge className={getStageBadgeClass(request.currentStage, request.status)}>
{request.currentStage} {request.currentStage}
</Badge> </Badge>
</TableCell> </TableCell>
@ -668,7 +677,7 @@ export function RelocationRequestPage({ currentUser, onViewDetails }: Relocation
{isLoading ? ( {isLoading ? (
<TableRow> <TableRow>
<TableCell colSpan={6} className="h-32 text-center"> <TableCell colSpan={6} className="h-32 text-center">
<Loader2 className="w-6 h-6 text-amber-600 animate-spin mx-auto" /> <Loader2 className="w-6 h-6 text-re-red animate-spin mx-auto" />
</TableCell> </TableCell>
</TableRow> </TableRow>
) : ( ) : (
@ -693,7 +702,7 @@ export function RelocationRequestPage({ currentUser, onViewDetails }: Relocation
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<div className="flex-1 h-2 bg-slate-200 rounded-full overflow-hidden"> <div className="flex-1 h-2 bg-slate-200 rounded-full overflow-hidden">
<div <div
className="h-full bg-red-500 transition-all duration-300" className={`h-full transition-all duration-300 ${getStatusProgressBarClass(request.status, request.currentStage)}`}
style={{ width: `${request.progressPercentage || 0}%` }} style={{ width: `${request.progressPercentage || 0}%` }}
/> />
</div> </div>
@ -701,7 +710,8 @@ export function RelocationRequestPage({ currentUser, onViewDetails }: Relocation
</div> </div>
</TableCell> </TableCell>
<TableCell> <TableCell>
<Badge className={`border ${getStatusColor(request.currentStage)}`}> <SlaBadge status={getSla('relocation', request.id || request.requestId)} compact />
<Badge className={getStageBadgeClass(request.currentStage, request.status)}>
{request.currentStage} {request.currentStage}
</Badge> </Badge>
</TableCell> </TableCell>
@ -739,7 +749,7 @@ export function RelocationRequestPage({ currentUser, onViewDetails }: Relocation
{isLoading ? ( {isLoading ? (
<TableRow> <TableRow>
<TableCell colSpan={5} className="h-32 text-center"> <TableCell colSpan={5} className="h-32 text-center">
<Loader2 className="w-6 h-6 text-amber-600 animate-spin mx-auto" /> <Loader2 className="w-6 h-6 text-re-red animate-spin mx-auto" />
</TableCell> </TableCell>
</TableRow> </TableRow>
) : ( ) : (

View File

@ -1,17 +1,12 @@
import { ArrowLeft, Calendar, CheckCircle2, Clock, FileText, MapPin, MessageSquare, Upload, User, XCircle } from 'lucide-react'; import { ArrowLeft, CheckCircle2, Clock, MapPin, User, XCircle } from 'lucide-react';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label'; import { Label } from '@/components/ui/label';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { resignationService } from '@/services/resignation.service'; import { resignationService } from '@/services/resignation.service';
import { formatDateTime } from '@/components/ui/utils'; import { formatDateTime } from '@/components/ui/utils';
import { RESIGNATION_STAGE_OPTIONS, RESIGNATION_DOCUMENT_TYPES } from '@/lib/offboardingDocumentOptions';
import { import {
AlertDialog, AlertDialog,
AlertDialogAction, AlertDialogAction,
@ -28,22 +23,9 @@ interface DealerResignationDetailsPageProps {
onBack: () => void; onBack: () => void;
} }
const getStatusColor = (status: string) => {
if (status === 'Completed') return 'bg-green-100 text-green-700 border-green-300';
if (status === 'Rejected' || status === 'Withdrawn') return 'bg-red-100 text-red-700 border-red-300';
if (status.includes('Review') || status.includes('Pending')) return 'bg-yellow-100 text-yellow-700 border-yellow-300';
return 'bg-slate-100 text-slate-700 border-slate-300';
};
export function DealerResignationDetailsPage({ resignationId, onBack }: DealerResignationDetailsPageProps) { export function DealerResignationDetailsPage({ resignationId, onBack }: DealerResignationDetailsPageProps) {
const navigate = useNavigate();
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [details, setDetails] = useState<any>(null); const [details, setDetails] = useState<any>(null);
const [uploading, setUploading] = useState(false);
const [uploadFile, setUploadFile] = useState<File | null>(null);
const [uploadDocType, setUploadDocType] = useState<string>(RESIGNATION_DOCUMENT_TYPES[0]);
const [uploadStage, setUploadStage] = useState<string>('');
const [auditLogs, setAuditLogs] = useState<any[]>([]);
const [withdrawing, setWithdrawing] = useState(false); const [withdrawing, setWithdrawing] = useState(false);
const [isWithdrawDialogOpen, setIsWithdrawDialogOpen] = useState(false); const [isWithdrawDialogOpen, setIsWithdrawDialogOpen] = useState(false);
const [withdrawalReason, setWithdrawalReason] = useState('User requested withdrawal'); const [withdrawalReason, setWithdrawalReason] = useState('User requested withdrawal');
@ -52,12 +34,8 @@ export function DealerResignationDetailsPage({ resignationId, onBack }: DealerRe
const fetchDetails = async () => { const fetchDetails = async () => {
try { try {
setLoading(true); setLoading(true);
const [data, audits] = await Promise.all([ const data = await resignationService.getResignationById(resignationId);
resignationService.getResignationById(resignationId),
fetchAuditLogs(resignationId)
]);
setDetails(data); setDetails(data);
setAuditLogs(audits);
} catch (error) { } catch (error) {
console.error('Failed to fetch resignation details:', error); console.error('Failed to fetch resignation details:', error);
toast.error('Unable to load resignation details'); toast.error('Unable to load resignation details');
@ -71,53 +49,15 @@ export function DealerResignationDetailsPage({ resignationId, onBack }: DealerRe
} }
}, [resignationId]); }, [resignationId]);
const fetchAuditLogs = async (id: string) => {
try {
// Lazy import through existing API helper shape used in other modules.
const { API } = await import('@/api/API');
const response = await API.getAuditLogs('resignation', id) as any;
if (response?.data?.success) return response.data.data || [];
return [];
} catch (error) {
return [];
}
};
const refreshDetails = async () => { const refreshDetails = async () => {
try { try {
const [data, audits] = await Promise.all([ const data = await resignationService.getResignationById(resignationId);
resignationService.getResignationById(resignationId),
fetchAuditLogs(resignationId)
]);
setDetails(data); setDetails(data);
setAuditLogs(audits);
} catch (error) { } catch (error) {
toast.error('Unable to refresh resignation details'); toast.error('Unable to refresh resignation details');
} }
}; };
const handleUpload = async () => {
if (!uploadFile) {
toast.error('Please choose a file');
return;
}
try {
setUploading(true);
const formData = new FormData();
formData.append('file', uploadFile);
formData.append('documentType', uploadDocType);
if (uploadStage) formData.append('stage', uploadStage);
await resignationService.uploadDocument(resignationId, formData);
toast.success('Document uploaded successfully');
setUploadFile(null);
await refreshDetails();
} catch (error: any) {
toast.error(error?.response?.data?.message || 'Document upload failed');
} finally {
setUploading(false);
}
};
const handleWithdraw = async () => { const handleWithdraw = async () => {
try { try {
setWithdrawing(true); setWithdrawing(true);
@ -135,7 +75,7 @@ export function DealerResignationDetailsPage({ resignationId, onBack }: DealerRe
if (loading) { if (loading) {
return ( return (
<div className="min-h-[320px] flex items-center justify-center"> <div className="min-h-[320px] flex items-center justify-center">
<Clock className="w-8 h-8 animate-spin text-amber-600" /> <Clock className="w-8 h-8 animate-spin text-re-red" />
</div> </div>
); );
} }
@ -156,9 +96,6 @@ export function DealerResignationDetailsPage({ resignationId, onBack }: DealerRe
); );
} }
const docs = details.uploadedDocuments || [];
const timeline = Array.isArray(details.timeline) ? details.timeline : [];
return ( return (
<div className="space-y-6"> <div className="space-y-6">
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
@ -169,13 +106,13 @@ export function DealerResignationDetailsPage({ resignationId, onBack }: DealerRe
<div className="flex-1"> <div className="flex-1">
<h1 className="text-slate-900">Resignation Request Details</h1> <h1 className="text-slate-900">Resignation Request Details</h1>
<p className="text-slate-600 text-sm"> <p className="text-slate-600 text-sm">
Track your request progress and uploaded documents Review your resignation request details
</p> </p>
</div> </div>
{details.status !== 'Withdrawn' && {details.status !== 'Withdrawn' &&
details.status !== 'Completed' && details.status !== 'Completed' &&
details.status !== 'Rejected' && details.status !== 'Rejected' &&
!['NBH', 'DD Admin', 'Legal', 'F&F Initiated'].includes(details.currentStage) && ( !['NBH', 'DD Admin', 'Legal', 'Awaiting F&F', 'F&F Initiated'].includes(details.currentStage) && (
<Button <Button
variant="destructive" variant="destructive"
className="bg-red-600 hover:bg-red-700" className="bg-red-600 hover:bg-red-700"
@ -224,26 +161,16 @@ export function DealerResignationDetailsPage({ resignationId, onBack }: DealerRe
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle className="flex items-center gap-2"> <CardTitle className="flex items-center gap-2">
<CheckCircle2 className="w-5 h-5 text-amber-600" /> <CheckCircle2 className="w-5 h-5 text-re-red" />
Request Summary Request Summary
</CardTitle> </CardTitle>
<CardDescription>Current request status and key metadata</CardDescription> <CardDescription>Key details about your resignation request</CardDescription>
</CardHeader> </CardHeader>
<CardContent className="grid grid-cols-1 md:grid-cols-3 gap-4"> <CardContent className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div> <div>
<p className="text-xs text-slate-500">Request ID</p> <p className="text-xs text-slate-500">Request ID</p>
<p className="text-slate-900">{details.resignationId || details.id}</p> <p className="text-slate-900">{details.resignationId || details.id}</p>
</div> </div>
<div>
<p className="text-xs text-slate-500">Status</p>
<Badge className={`border ${getStatusColor(details.status || 'Pending')}`}>
{details.status || 'Pending'}
</Badge>
</div>
<div>
<p className="text-xs text-slate-500">Current Stage</p>
<p className="text-slate-900">{details.currentStage || 'Submitted'}</p>
</div>
<div> <div>
<p className="text-xs text-slate-500">Submitted On</p> <p className="text-xs text-slate-500">Submitted On</p>
<p className="text-slate-900">{formatDateTime(details.submittedOn || details.createdAt)}</p> <p className="text-slate-900">{formatDateTime(details.submittedOn || details.createdAt)}</p>
@ -252,10 +179,6 @@ export function DealerResignationDetailsPage({ resignationId, onBack }: DealerRe
<p className="text-xs text-slate-500">Resignation Type</p> <p className="text-xs text-slate-500">Resignation Type</p>
<p className="text-slate-900">{details.resignationType || 'N/A'}</p> <p className="text-slate-900">{details.resignationType || 'N/A'}</p>
</div> </div>
<div>
<p className="text-xs text-slate-500">Progress</p>
<p className="text-slate-900">{details.progressPercentage || 0}%</p>
</div>
</CardContent> </CardContent>
</Card> </Card>
@ -296,164 +219,6 @@ export function DealerResignationDetailsPage({ resignationId, onBack }: DealerRe
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<FileText className="w-5 h-5 text-purple-600" />
Uploaded Documents
</CardTitle>
<CardDescription>Dealer can upload resignation-related documents for review</CardDescription>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-4 gap-3 mb-4 p-3 border rounded-lg bg-slate-50">
<div>
<Label className="text-xs">Document Type</Label>
<Select value={uploadDocType} onValueChange={setUploadDocType}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
{RESIGNATION_DOCUMENT_TYPES.map((docType) => (
<SelectItem key={docType} value={docType}>{docType}</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div>
<Label className="text-xs">Stage (Optional)</Label>
<Select value={uploadStage || 'none'} onValueChange={(v) => setUploadStage(v === 'none' ? '' : v)}>
<SelectTrigger>
<SelectValue placeholder="Select stage" />
</SelectTrigger>
<SelectContent>
<SelectItem value="none">No Stage</SelectItem>
{RESIGNATION_STAGE_OPTIONS.map((stage) => (
<SelectItem key={stage} value={stage}>{stage}</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div>
<Label className="text-xs">File</Label>
<input
type="file"
className="h-10 w-full rounded-md border border-slate-200 bg-white px-3 py-2 text-sm"
onChange={(e) => setUploadFile(e.target.files?.[0] || null)}
/>
</div>
<div className="flex items-end">
<Button className="w-full" onClick={handleUpload} disabled={uploading}>
<Upload className="w-4 h-4 mr-2" />
{uploading ? 'Uploading...' : 'Upload'}
</Button>
</div>
</div>
<Table>
<TableHeader>
<TableRow>
<TableHead>Document Type</TableHead>
<TableHead>File</TableHead>
<TableHead>Uploaded By</TableHead>
<TableHead>Uploaded On</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{docs.length === 0 ? (
<TableRow>
<TableCell colSpan={4} className="text-center text-slate-500 py-6">
No documents uploaded yet.
</TableCell>
</TableRow>
) : (
docs.map((doc: any) => (
<TableRow key={doc.id}>
<TableCell>{doc.documentType || '-'}</TableCell>
<TableCell>{doc.fileName || '-'}</TableCell>
<TableCell>{doc.uploader?.fullName || '-'}</TableCell>
<TableCell>{formatDateTime(doc.createdAt)}</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<MessageSquare className="w-5 h-5 text-blue-600" />
Work Notes Communication
</CardTitle>
<CardDescription>Official channel for internal-dealer clarifications</CardDescription>
</CardHeader>
<CardContent>
<Button
variant="outline"
onClick={() =>
navigate(`/worknotes/resignation/${resignationId}`, {
state: {
applicationName: details?.dealer?.fullName || 'Resignation Request',
registrationNumber: details?.resignationId || resignationId
}
})
}
>
<MessageSquare className="w-4 h-4 mr-2" />
Open Work Notes
</Button>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Calendar className="w-5 h-5 text-amber-600" />
Progress Timeline
</CardTitle>
</CardHeader>
<CardContent>
{timeline.length === 0 ? (
<p className="text-sm text-slate-500">No timeline events available yet.</p>
) : (
<div className="space-y-2">
{timeline.slice().reverse().map((entry: any, idx: number) => (
<div key={`${entry.timestamp || entry.createdAt}-${idx}`} className="p-3 border rounded-lg bg-slate-50">
<p className="text-sm text-slate-900">{entry.action || entry.stage || 'Stage Update'}</p>
<p className="text-xs text-slate-500">{formatDateTime(entry.timestamp || entry.createdAt)}</p>
<p className="text-xs text-slate-600">{entry.comments || entry.remarks || 'No remarks'}</p>
</div>
))}
</div>
)}
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle>Audit Trail</CardTitle>
<CardDescription>Traceability of status/actions on this request</CardDescription>
</CardHeader>
<CardContent>
{auditLogs.length === 0 ? (
<p className="text-sm text-slate-500">No audit records found.</p>
) : (
<div className="space-y-2">
{auditLogs.map((log: any) => (
<div key={log.id} className="p-3 border rounded-lg">
<div className="flex items-center justify-between">
<p className="text-sm text-slate-900">{log.action || 'Action'}</p>
<p className="text-xs text-slate-500">{formatDateTime(log.createdAt || log.timestamp)}</p>
</div>
<p className="text-xs text-slate-600 mt-1">{log.remarks || log.description || 'No remarks'}</p>
</div>
))}
</div>
)}
</CardContent>
</Card>
</div> </div>
); );
} }

View File

@ -20,13 +20,6 @@ interface DealerResignationPageProps {
onViewDetails?: (id: string) => void; onViewDetails?: (id: string) => void;
} }
const getStatusColor = (status: string) => {
if (status === 'Completed') return 'bg-green-100 text-green-700 border-green-300';
if (status.includes('Review') || status.includes('Pending')) return 'bg-yellow-100 text-yellow-700 border-yellow-300';
if (status.includes('Rejected')) return 'bg-red-100 text-red-700 border-red-300';
return 'bg-slate-100 text-slate-700 border-slate-300';
};
export function DealerResignationPage({ onViewDetails }: DealerResignationPageProps) { export function DealerResignationPage({ onViewDetails }: DealerResignationPageProps) {
const [isDialogOpen, setIsDialogOpen] = useState(false); const [isDialogOpen, setIsDialogOpen] = useState(false);
const [selectedOutlet, setSelectedOutlet] = useState<any | null>(null); const [selectedOutlet, setSelectedOutlet] = useState<any | null>(null);
@ -134,7 +127,7 @@ export function DealerResignationPage({ onViewDetails }: DealerResignationPagePr
title: 'Pending Resignations', title: 'Pending Resignations',
value: outlets.filter(o => o.status === 'Pending Resignation').length, value: outlets.filter(o => o.status === 'Pending Resignation').length,
icon: Clock, icon: Clock,
color: 'bg-amber-500', color: 'bg-re-red',
}, },
]; ];
@ -143,7 +136,7 @@ export function DealerResignationPage({ onViewDetails }: DealerResignationPagePr
{/* Loading Overlay */} {/* Loading Overlay */}
{loading && ( {loading && (
<div className="min-h-[400px] flex items-center justify-center"> <div className="min-h-[400px] flex items-center justify-center">
<Loader2 className="w-8 h-8 text-amber-600 animate-spin" /> <Loader2 className="w-8 h-8 text-re-red animate-spin" />
</div> </div>
)} )}
@ -212,7 +205,7 @@ export function DealerResignationPage({ onViewDetails }: DealerResignationPagePr
outlet.status === 'Active' outlet.status === 'Active'
? 'bg-green-100 text-green-700 border-green-300' ? 'bg-green-100 text-green-700 border-green-300'
: outlet.status === 'Pending Resignation' : outlet.status === 'Pending Resignation'
? 'bg-amber-100 text-amber-700 border-amber-300' ? 'bg-red-50 text-re-red border-red-200'
: 'bg-slate-100 text-slate-700 border-slate-300' : 'bg-slate-100 text-slate-700 border-slate-300'
}`} }`}
> >
@ -236,8 +229,8 @@ export function DealerResignationPage({ onViewDetails }: DealerResignationPagePr
</div> </div>
{hasActiveResignation ? ( {hasActiveResignation ? (
<div className="bg-amber-50 border border-amber-200 rounded p-3 text-sm"> <div className="bg-red-50 border border-red-200 rounded p-3 text-sm">
<p className="text-amber-800"> <p className="text-slate-800">
Resignation in progress - <span className="underline cursor-pointer" onClick={() => onViewDetails && resignation?.resignationId && onViewDetails(resignation.resignationId)}>View Request</span> Resignation in progress - <span className="underline cursor-pointer" onClick={() => onViewDetails && resignation?.resignationId && onViewDetails(resignation.resignationId)}>View Request</span>
</p> </p>
</div> </div>
@ -362,9 +355,9 @@ export function DealerResignationPage({ onViewDetails }: DealerResignationPagePr
</div> </div>
{/* Important Info */} {/* Important Info */}
<div className="bg-amber-50 border border-amber-200 rounded-lg p-4"> <div className="bg-red-50 border border-red-200 rounded-lg p-4">
<h4 className="text-amber-900 mb-2">Important Information</h4> <h4 className="text-re-red mb-2 font-semibold">Important Information</h4>
<ul className="text-amber-800 text-sm space-y-1"> <ul className="text-slate-700 text-sm space-y-1">
<li> F&F settlement process will be initiated after submission</li> <li> F&F settlement process will be initiated after submission</li>
<li> All department clearances must be obtained</li> <li> All department clearances must be obtained</li>
<li> Final settlement will be processed after closure</li> <li> Final settlement will be processed after closure</li>
@ -407,7 +400,7 @@ export function DealerResignationPage({ onViewDetails }: DealerResignationPagePr
<CardHeader> <CardHeader>
<CardTitle>My Resignation Requests</CardTitle> <CardTitle>My Resignation Requests</CardTitle>
<CardDescription> <CardDescription>
Track the progress of your resignation requests View your submitted resignation requests
</CardDescription> </CardDescription>
</CardHeader> </CardHeader>
<CardContent> <CardContent>
@ -418,15 +411,13 @@ export function DealerResignationPage({ onViewDetails }: DealerResignationPagePr
<TableHead>Outlet</TableHead> <TableHead>Outlet</TableHead>
<TableHead>Type</TableHead> <TableHead>Type</TableHead>
<TableHead>Submitted On</TableHead> <TableHead>Submitted On</TableHead>
<TableHead>Status</TableHead>
<TableHead>Progress</TableHead>
<TableHead>Actions</TableHead> <TableHead>Actions</TableHead>
</TableRow> </TableRow>
</TableHeader> </TableHeader>
<TableBody> <TableBody>
{resignations.length === 0 ? ( {resignations.length === 0 ? (
<TableRow> <TableRow>
<TableCell colSpan={7} className="text-center py-4 text-slate-500"> <TableCell colSpan={5} className="text-center py-4 text-slate-500">
No resignation requests found No resignation requests found
</TableCell> </TableCell>
</TableRow> </TableRow>
@ -437,22 +428,6 @@ export function DealerResignationPage({ onViewDetails }: DealerResignationPagePr
<TableCell>{request.outlet?.name}</TableCell> <TableCell>{request.outlet?.name}</TableCell>
<TableCell>{request.resignationType}</TableCell> <TableCell>{request.resignationType}</TableCell>
<TableCell>{formatDateTime(request.submittedOn)}</TableCell> <TableCell>{formatDateTime(request.submittedOn)}</TableCell>
<TableCell>
<Badge className={`border ${getStatusColor(request.status)}`}>
{request.status}
</Badge>
</TableCell>
<TableCell>
<div className="flex items-center gap-2">
<div className="flex-1 bg-slate-200 rounded-full h-2 min-w-[60px]">
<div
className="bg-red-600 h-2 rounded-full"
style={{ width: `${request.progressPercentage}%` }}
/>
</div>
<span className="text-xs text-slate-600">{request.progressPercentage}%</span>
</div>
</TableCell>
<TableCell> <TableCell>
<Button <Button
variant="ghost" variant="ghost"

View File

@ -1,4 +1,4 @@
import { ArrowLeft, Check, X, RotateCcw, UserPlus, MessageSquare, FileText, Calendar, Send, AlertCircle, Loader2, Upload, Ban } from 'lucide-react'; import { ArrowLeft, Check, X, RotateCcw, UserPlus, MessageSquare, FileText, Calendar, Send, AlertCircle, Loader2, Upload, Ban, Mail } from 'lucide-react';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
@ -9,15 +9,22 @@ import { Textarea } from '@/components/ui/textarea';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Input } from '@/components/ui/input'; import { Input } from '@/components/ui/input';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
import { useState, useEffect } from 'react'; import { Separator } from '@/components/ui/separator';
import { useState, useEffect, useMemo } from 'react';
import { useNavigate } from 'react-router-dom'; import { useNavigate } from 'react-router-dom';
import { User as UserType } from '@/lib/mock-data'; import { User as UserType } from '@/lib/mock-data';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { resignationService } from '@/services/resignation.service'; import { resignationService } from '@/services/resignation.service';
import { SlaBadge } from '@/components/sla/SlaBadge';
import { useSlaBatchStatus } from '@/hooks/useSlaBatchStatus';
import { API } from '@/api/API'; import { API } from '@/api/API';
import { DocumentPreviewModal } from '@/components/ui/DocumentPreviewModal'; import { DocumentPreviewModal } from '@/components/ui/DocumentPreviewModal';
import { formatDateTime } from '@/components/ui/utils'; import { formatDateTime } from '@/components/ui/utils';
import {
formatOffboardingStatusLabel,
LAST_WORKING_DAY_LABEL
} from '@/lib/offboardingDisplay';
import { RESIGNATION_DOCUMENT_TYPES, RESIGNATION_STAGE_OPTIONS } from '@/lib/offboardingDocumentOptions'; import { RESIGNATION_DOCUMENT_TYPES, RESIGNATION_STAGE_OPTIONS } from '@/lib/offboardingDocumentOptions';
import { WIDE_DIALOG_CLASS } from '@/lib/dialogStyles'; import { WIDE_DIALOG_CLASS } from '@/lib/dialogStyles';
@ -35,6 +42,7 @@ const STAGE_TO_ROLE_MAP: Record<string, string> = {
'RBM': 'RBM', 'RBM': 'RBM',
'ZBH': 'ZBH', 'ZBH': 'ZBH',
'DD Lead': 'DD Lead', 'DD Lead': 'DD Lead',
'DD Head': 'DD Head',
'NBH': 'NBH', 'NBH': 'NBH',
'DD Admin': 'DD Admin', 'DD Admin': 'DD Admin',
'Legal': 'Legal Admin' 'Legal': 'Legal Admin'
@ -44,12 +52,15 @@ const STAGE_TO_ROLE_MAP: Record<string, string> = {
const TERMINAL_STAGE_LABELS = ['REJECTED', 'Rejected', 'REVOKED', 'Revoked', 'WITHDRAWN', 'Withdrawn']; const TERMINAL_STAGE_LABELS = ['REJECTED', 'Rejected', 'REVOKED', 'Revoked', 'WITHDRAWN', 'Withdrawn'];
const RESIGNATION_STAGE_ALIASES: Record<string, string[]> = { const RESIGNATION_STAGE_ALIASES: Record<string, string[]> = {
'ASM': ['ASM', 'ASM Review', 'Submission', 'Submitted'], 'Request Submitted': ['Submission', 'Submitted', 'Initiation'],
'RBM': ['RBM', 'RBM Review', 'Regional Review'], 'ASM': ['ASM', 'ASM Review'],
'RBM': ['RBM', 'RBM Review', 'Regional Review', 'RBM + DD-ZM Review'],
'ZBH': ['ZBH', 'ZBH Review', 'ZM Review'], 'ZBH': ['ZBH', 'ZBH Review', 'ZM Review'],
'DD Lead': ['DD Lead', 'DD Lead Review', 'DDL Review'], 'DD Lead': ['DD Lead', 'DD Lead Review', 'DDL Review'],
'DD Head': ['DD Head', 'DD Head Review', 'DDH Review'],
'NBH': ['NBH', 'NBH Approval', 'NBH Review'], 'NBH': ['NBH', 'NBH Approval', 'NBH Review'],
'DD Admin': ['DD Admin', 'DD Admin Review'], 'DD Admin': ['DD Admin', 'DD Admin Review'],
'Awaiting F&F': ['Awaiting F&F', 'Awaiting F&F — manual initiation'],
'Legal': ['Legal', 'Legal - Resignation Letter'], 'Legal': ['Legal', 'Legal - Resignation Letter'],
'F&F Initiated': ['F&F Initiated', 'FNF_INITIATED', 'FNF Initiated'], 'F&F Initiated': ['F&F Initiated', 'FNF_INITIATED', 'FNF Initiated'],
'Completed': ['Completed'] 'Completed': ['Completed']
@ -79,7 +90,7 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
}; };
const navigate = useNavigate(); const navigate = useNavigate();
const [actionDialog, setActionDialog] = useState<{ open: boolean, type: 'approve' | 'reject' | 'withdrawal' | 'sendBack' | 'assign' | 'pushfnf' | 'revoke' | null }>({ open: false, type: null }); const [actionDialog, setActionDialog] = useState<{ open: boolean, type: 'approve' | 'reject' | 'withdrawal' | 'sendBack' | 'assign' | 'pushfnf' | 'revoke' | 'dispatch' | null }>({ open: false, type: null });
const [remarks, setRemarks] = useState(''); const [remarks, setRemarks] = useState('');
const [assignToUser, setAssignToUser] = useState<string>(''); const [assignToUser, setAssignToUser] = useState<string>('');
const [userSearchQuery, setUserSearchQuery] = useState(''); const [userSearchQuery, setUserSearchQuery] = useState('');
@ -89,6 +100,13 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
const [forceTriggerFnF, setForceTriggerFnF] = useState(false); const [forceTriggerFnF, setForceTriggerFnF] = useState(false);
const [stageDocumentsDialog, setStageDocumentsDialog] = useState<{ open: boolean; stageName: string; documents: any[] }>({ open: false, stageName: '', documents: [] }); const [stageDocumentsDialog, setStageDocumentsDialog] = useState<{ open: boolean; stageName: string; documents: any[] }>({ open: false, stageName: '', documents: [] });
const [resignationData, setResignationData] = useState<any>(null); // Real data from API const [resignationData, setResignationData] = useState<any>(null); // Real data from API
// URL slug may be the human-readable code (e.g. `RES-...`); SLA expects UUID.
// Feed the SLA hook the resolved UUID once the request has loaded.
const slaEntityId: string = resignationData?.id || '';
const { get: getSla } = useSlaBatchStatus(
slaEntityId ? [{ entityType: 'resignation', entityId: slaEntityId }] : [],
Boolean(slaEntityId)
);
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [auditLogs, setAuditLogs] = useState<any[]>([]); const [auditLogs, setAuditLogs] = useState<any[]>([]);
const [isSubmitting, setIsSubmitting] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false);
@ -97,6 +115,16 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
const [uploadFile, setUploadFile] = useState<File | null>(null); const [uploadFile, setUploadFile] = useState<File | null>(null);
const [uploadDocType, setUploadDocType] = useState<string>(RESIGNATION_DOCUMENT_TYPES[0]); const [uploadDocType, setUploadDocType] = useState<string>(RESIGNATION_DOCUMENT_TYPES[0]);
const [uploadStage, setUploadStage] = useState(''); const [uploadStage, setUploadStage] = useState('');
const hasUploadedPPT = useMemo(() => {
const allDocs = [
...(resignationData?.documents || []),
...(resignationData?.uploadedDocuments || [])
];
return allDocs.some(doc =>
(doc.documentType || doc.type) === 'PPT Presentation'
);
}, [resignationData]);
const fetchResignation = async () => { const fetchResignation = async () => {
try { try {
setIsLoading(true); setIsLoading(true);
@ -127,18 +155,21 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
// Progress stages logic based on live data // Progress stages logic based on live data
const progressStages = [ const progressStages = [
{ id: 1, name: 'ASM Review', key: 'ASM', description: 'Area Sales Manager review' }, { id: 1, name: 'Request Submitted', key: 'Request Submitted', description: 'Dealer submitted the resignation request' },
{ id: 2, name: 'RBM Review', key: 'RBM', description: 'Regional Business Manager evaluation' }, { id: 2, name: 'ASM Review', key: 'ASM', description: 'Area Sales Manager review' },
{ id: 3, name: 'ZBH Review', key: 'ZBH', description: 'Zonal Business Head approval' }, { id: 3, name: 'RBM + DD-ZM Review', key: 'RBM', description: 'Joint approval by Regional Business Manager and DD-ZM' },
{ id: 4, name: 'DD Lead Review', key: 'DD Lead', description: 'DD Lead final review' }, { id: 4, name: 'ZBH Review', key: 'ZBH', description: 'Zonal Business Head approval' },
{ id: 5, name: 'NBH Approval', key: 'NBH', description: 'National Business Head approval' }, { id: 5, name: 'DD Lead Review', key: 'DD Lead', description: 'DD Lead consolidated review' },
{ id: 6, name: 'DD Admin Review', key: 'DD Admin', description: 'DD Admin verification' }, { id: 6, name: 'DD Head Review', key: 'DD Head', description: 'DD Head final dealer development approval' },
{ id: 7, name: 'Legal - Resignation Letter', key: 'Legal', description: 'Legal team issues resignation approval letter' }, { id: 7, name: 'NBH Approval', key: 'NBH', description: 'National Business Head approval' },
{ id: 8, name: 'F&F Settlement', key: 'F&F Initiated', description: 'Full & Final settlement process' }, { id: 8, name: 'Legal - Resignation Letter', key: 'Legal', description: 'Legal team issues resignation approval letter' },
{ id: 9, name: 'Completed', key: 'Completed', description: 'Resignation process finalized' } { id: 9, name: 'DD Admin Review', key: 'DD Admin', description: 'DD Admin verification and final closure' },
{ id: 10, name: 'Awaiting F&F', key: 'Awaiting F&F', description: 'Internal review complete — start Full & Final using Push to F&F when ready' },
{ id: 11, name: 'F&F Settlement', key: 'F&F Initiated', description: 'Full & Final settlement process' },
{ id: 12, name: 'Completed', key: 'Completed', description: 'Resignation process finalized' }
]; ];
const stagesOrdered = ['ASM', 'RBM', 'ZBH', 'DD Lead', 'NBH', 'DD Admin', 'Legal', 'F&F Initiated', 'Completed']; const stagesOrdered = ['Request Submitted', 'ASM', 'RBM', 'ZBH', 'DD Lead', 'DD Head', 'NBH', 'Legal', 'DD Admin', 'Awaiting F&F', 'F&F Initiated', 'Completed'];
const legalStageApproved = (() => { const legalStageApproved = (() => {
if (!resignationData) return false; if (!resignationData) return false;
@ -157,6 +188,8 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
const atLegal = stage === 'legal' || stage === 'legal - resignation letter'; const atLegal = stage === 'legal' || stage === 'legal - resignation letter';
const legalApprovedTransition = const legalApprovedTransition =
targetStage === 'legal' || targetStage === 'legal' ||
targetStage === 'dd admin' ||
targetStage === 'awaiting f&f' ||
targetStage === 'f&f initiated' || targetStage === 'f&f initiated' ||
targetStage === 'fnf_initiated' || targetStage === 'fnf_initiated' ||
action.includes('approved'); action.includes('approved');
@ -167,14 +200,23 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
const getResignationPermissions = () => { const getResignationPermissions = () => {
if (!resignationData || !currentUser) { if (!resignationData || !currentUser) {
return { canApprove: false, canWithdraw: false, canSendBack: false, canPushToFnF: false, canAssign: false }; return { canApprove: false, canDispatch: false, dispatchMissed: false, canWithdraw: false, canSendBack: false, canPushToFnF: false, canAssign: false };
} }
const currentStage = resignationData.currentStage; const currentStage = resignationData.currentStage;
const status = resignationData.status; const status = resignationData.status;
const userRole = currentUser.role; const userRole = currentUser.role;
const isZmRbmStage = currentStage === 'RBM' || currentStage === 'RBM Review' || currentStage === 'RBM + DD-ZM Review';
const userRoleCode = String(currentUser.roleCode || currentUser.role || '').trim().toUpperCase();
// Final states where no more actions are possible // Check if current user already partially approved this request at this stage
const hasAlreadyPartiallyApproved = isZmRbmStage && auditLogs.some(log =>
log.action === 'PARTIAL_APPROVE' &&
(log.actor?.id === currentUser.id || log.actorId === currentUser.id || log.actor?.email === currentUser.email || log.userEmail === currentUser.email) &&
(log.details?.roleCode === userRoleCode || (log.details?.roleCode === 'DD-ZM' && userRoleCode === 'DD ZM'))
);
const isFinalState = ['Completed', 'Rejected', 'Withdrawn', 'Revoked'].includes(status); const isFinalState = ['Completed', 'Rejected', 'Withdrawn', 'Revoked'].includes(status);
// Check if it's already in the settlement phase // Check if it's already in the settlement phase
@ -184,33 +226,148 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
const nbhIndex = stagesOrdered.indexOf('NBH'); const nbhIndex = stagesOrdered.indexOf('NBH');
const isPastNBH = stageIndex !== -1 && nbhIndex !== -1 && stageIndex >= nbhIndex; const isPastNBH = stageIndex !== -1 && nbhIndex !== -1 && stageIndex >= nbhIndex;
const isCurrentlyAssigned = userRole === 'Super Admin' || userRole === STAGE_TO_ROLE_MAP[currentStage]; const isCurrentlyAssigned = userRoleCode === 'SUPER_ADMIN' ||
(isZmRbmStage && (userRoleCode === 'RBM' || userRoleCode === 'DD-ZM' || userRoleCode === 'DD ZM')) ||
userRole === STAGE_TO_ROLE_MAP[currentStage];
const isDDLeadStage = currentStage === 'DD Lead' || currentStage === 'DD Lead Review';
const isDDLead = userRoleCode === 'DD_LEAD' || userRoleCode === 'DD LEAD';
const isLwdReached = (() => {
const today = new Date();
today.setHours(0, 0, 0, 0);
const lwdString = resignationData.lastOperationalDateServices || resignationData.lastOperationalDateSales;
if (!lwdString) return true;
const lwd = new Date(lwdString);
lwd.setHours(0, 0, 0, 0);
return today >= lwd;
})();
const isAwaitingFnfGate = currentStage === 'Awaiting F&F';
const resolvedStageKey = (() => {
const normalized = String(currentStage || '').trim();
const matched = stagesOrdered.find(
(key) => key === normalized || (RESIGNATION_STAGE_ALIASES[key] || []).includes(normalized)
);
return matched || normalized;
})();
const isNbHApprovalStep = resolvedStageKey === 'NBH';
const isAwaitingFnfStep = resolvedStageKey === 'Awaiting F&F';
/** Legacy rows only: acceptance letter at Legal before DD Admin completed Awaiting F&F gate */
const isLegalLegacyFnfStep = resolvedStageKey === 'Legal';
const fnfPushRoles = ['DD Lead', 'DD Head', 'DD Admin', 'Super Admin'];
const fnfPushLegacyRoles = ['DD Lead', 'DD Head', 'DD Admin', 'Super Admin'];
// Dispatch action — Legal uploaded the acceptance letter and DD Admin (or
// Super Admin) must dispatch a formal copy to the dealer. The button stays
// visible from the DD Admin step through Awaiting F&F / F&F Initiated so
// an admin who skipped the step earlier can still send the letter
// retroactively. Once dispatched it disappears (audit log is the source of
// truth) so we don't re-send duplicates.
const isDDAdminStage = currentStage === 'DD Admin' || currentStage === 'DD Admin Review';
const isDDAdmin = userRoleCode === 'DD_ADMIN' || userRole === 'DD Admin';
const isSuperAdmin = userRoleCode === 'SUPER_ADMIN' || userRole === 'Super Admin';
const allResignationDocs: any[] = [
...(resignationData.documents || []),
...(resignationData.uploadedDocuments || [])
];
const hasAcceptanceLetter = allResignationDocs.some((doc: any) => {
const docType = String(doc?.documentType || doc?.type || '').toLowerCase();
const docStage = String(doc?.stage || '').toLowerCase();
return docType.includes('acceptance letter') || docStage === 'legal';
});
const hasBeenDispatched =
auditLogs.some((log: any) => {
const action = String(log?.action || '').toUpperCase();
const desc = String(log?.description || log?.details?.action || '').toLowerCase();
return (
action === 'RESIGNATION_LETTER_DISPATCHED' ||
desc.includes('resignation letter dispatched')
);
}) ||
(resignationData.timeline || []).some((entry: any) =>
String(entry?.action || '').toLowerCase().includes('resignation letter dispatched')
);
const ddAdminIdx = stagesOrdered.indexOf('DD Admin');
const completedIdx = stagesOrdered.indexOf('Completed');
const currentStageIdx = stagesOrdered.indexOf(
stagesOrdered.find(
(key) => key === currentStage || (RESIGNATION_STAGE_ALIASES[key] || []).includes(currentStage)
) || currentStage
);
const isAtOrAfterDDAdmin =
ddAdminIdx !== -1 &&
currentStageIdx !== -1 &&
currentStageIdx >= ddAdminIdx &&
(completedIdx === -1 || currentStageIdx < completedIdx);
const canDispatch =
isAtOrAfterDDAdmin &&
(isDDAdmin || isSuperAdmin) &&
hasAcceptanceLetter &&
!hasBeenDispatched &&
!isFinalState;
const dispatchMissed = canDispatch && !isDDAdminStage;
const canApprove = isCurrentlyAssigned && const canApprove = isCurrentlyAssigned &&
!isFinalState && !isFinalState &&
!isSettlementPhase && !isSettlementPhase &&
!(currentStage === 'Legal' && legalStageApproved); !hasAlreadyPartiallyApproved &&
!(currentStage === 'Legal' && legalStageApproved) &&
!(isDDLead && isDDLeadStage && !hasUploadedPPT) &&
!(currentStage === 'DD Admin' && !isLwdReached) &&
// Dispatch replaces Approve at the DD Admin stage so the admin must
// explicitly send the acceptance letter to the dealer.
!isDDAdminStage &&
!isAwaitingFnfGate;
return { return {
canApprove, canApprove,
canSendBack: isCurrentlyAssigned && !isFinalState && !isSettlementPhase && stageIndex > 0, canDispatch,
dispatchMissed,
// SRS §7.3.2: Send Back returns to DD Admin for correction. Legal Admin only drafts/uploads
// the Resignation Acceptance Letter and cannot send the case back to earlier reviewers.
canSendBack:
isCurrentlyAssigned &&
!isFinalState &&
!isSettlementPhase &&
stageIndex > 0 &&
userRole !== 'Legal Admin' &&
userRoleCode !== 'LEGAL_ADMIN',
canWithdraw: userRole === 'Dealer' && !isPastNBH && !isFinalState, canWithdraw: userRole === 'Dealer' && !isPastNBH && !isFinalState,
canRevoke: (userRole === 'Super Admin' || userRole === 'DD Admin') && !isFinalState && !isSettlementPhase, canRevoke: (userRoleCode === 'SUPER_ADMIN' || userRole === 'DD Admin') && !isFinalState && !isSettlementPhase,
canPushToFnF: ['DD Lead', 'DD Head', 'NBH', 'DD Admin', 'Super Admin'].includes(userRole) && // Push to F&F: after DD Admin gate only — not during NBH (or any earlier) approval.
!isSettlementPhase && !isFinalState, // Roles: DD Lead / DD Head / DD Admin / Super Admin (not NBH).
canPushToFnF:
fnfPushRoles.includes(userRole) &&
!isSettlementPhase &&
!isFinalState &&
!isNbHApprovalStep &&
isLwdReached &&
(isAwaitingFnfStep || (isLegalLegacyFnfStep && fnfPushLegacyRoles.includes(userRole))),
canAssign: userRole !== 'Dealer' && !isFinalState canAssign: userRole !== 'Dealer' && !isFinalState
}; };
}; };
const permissions = getResignationPermissions(); const permissions = getResignationPermissions();
const isNationalLevel = ['Super Admin', 'DD Lead', 'DD Head', 'NBH', 'DD Admin', 'Legal Admin', 'DD-ZM'].includes(currentUser?.role || '');
const stageAliases: Record<string, string[]> = { const stageAliases: Record<string, string[]> = {
'ASM': ['ASM', 'ASM Review', 'Request Initiated'], 'ASM': ['ASM', 'ASM Review', 'Request Initiated'],
'RBM': ['RBM', 'RBM Review'], 'RBM': ['RBM', 'RBM Review', 'RBM + DD-ZM Review'],
'ZBH': ['ZBH', 'ZBH Review'], 'ZBH': ['ZBH', 'ZBH Review'],
'DD Lead': ['DD Lead', 'DD Lead Review', 'Lead Review'], 'DD Lead': ['DD Lead', 'DD Lead Review', 'Lead Review'],
'DD Head': ['DD Head', 'DD Head Review', 'Head Review'],
'NBH': ['NBH', 'NBH Approval', 'NBH Review'], 'NBH': ['NBH', 'NBH Approval', 'NBH Review'],
'DD Admin': ['DD Admin', 'DD Admin Review'], 'DD Admin': ['DD Admin', 'DD Admin Review'],
'Awaiting F&F': ['Awaiting F&F', 'Awaiting F&F — manual initiation'],
'Legal': ['Legal', 'Legal - Resignation Letter', 'Legal Review'], 'Legal': ['Legal', 'Legal - Resignation Letter', 'Legal Review'],
'F&F Initiated': ['F&F Initiated', 'FNF_INITIATED', 'F&F Settlement', 'Settled'], 'F&F Initiated': ['F&F Initiated', 'FNF_INITIATED', 'F&F Settlement', 'Settled'],
'Completed': ['Completed', 'Finalized'] 'Completed': ['Completed', 'Finalized']
@ -260,7 +417,7 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
return 'pending'; return 'pending';
}; };
const handleAction = (type: 'approve' | 'withdrawal' | 'sendBack' | 'assign' | 'pushfnf' | 'revoke') => { const handleAction = (type: 'approve' | 'withdrawal' | 'sendBack' | 'assign' | 'pushfnf' | 'revoke' | 'dispatch') => {
setActionDialog({ open: true, type }); setActionDialog({ open: true, type });
}; };
@ -279,7 +436,7 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
}; };
const handleSubmitAction = async () => { const handleSubmitAction = async () => {
if (!remarks && !['assign', 'pushfnf'].includes(actionDialog.type || '')) { if (!remarks && !['assign', 'pushfnf', 'dispatch'].includes(actionDialog.type || '')) {
toast.error('Please provide remarks (min 5 characters)'); toast.error('Please provide remarks (min 5 characters)');
return; return;
} }
@ -322,7 +479,9 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
// When Legal approval bumps into LWD gate for F&F initiation, guide user explicitly. // When Legal approval bumps into LWD gate for F&F initiation, guide user explicitly.
if (response.data?.canForce) { if (response.data?.canForce) {
toast.info('LWD restriction hit. Use "Push to F&F" and enable "Force Initiate F&F Settlement Immediately" if urgent.'); toast.info(
`${LAST_WORKING_DAY_LABEL} restriction: use "Push to F&F" and enable "Force Initiate F&F Settlement Immediately" if urgent.`
);
} }
} }
} catch (error: any) { } catch (error: any) {
@ -330,7 +489,9 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
toast.error(error.response?.data?.message || 'Failed to submit action'); toast.error(error.response?.data?.message || 'Failed to submit action');
if (error?.response?.data?.canForce) { if (error?.response?.data?.canForce) {
toast.info('LWD restriction hit. Use "Push to F&F" with force option if business-approved.'); toast.info(
`${LAST_WORKING_DAY_LABEL} restriction: use "Push to F&F" with the force option if business-approved.`
);
} }
} finally { } finally {
setIsSubmitting(false); setIsSubmitting(false);
@ -409,7 +570,7 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
if (isLoading && !resignationData) { if (isLoading && !resignationData) {
return ( return (
<div className="flex items-center justify-center min-h-[400px]"> <div className="flex items-center justify-center min-h-[400px]">
<Loader2 className="w-8 h-8 animate-spin text-amber-600" /> <Loader2 className="w-8 h-8 animate-spin text-re-red" />
</div> </div>
); );
} }
@ -433,139 +594,74 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
? 'bg-red-100 text-red-700 border-red-300' ? 'bg-red-100 text-red-700 border-red-300'
: 'bg-yellow-100 text-yellow-700 border-yellow-300' : 'bg-yellow-100 text-yellow-700 border-yellow-300'
}> }>
{resignationData?.status === 'Settled' ? 'Completed' : (resignationData?.status || 'Pending')} {resignationData?.status === 'Settled'
? 'Completed'
: formatOffboardingStatusLabel(resignationData?.status || 'Pending')}
</Badge> </Badge>
<SlaBadge status={getSla('resignation', slaEntityId)} />
</div> </div>
</div> </div>
{/* Action Bar - Professional Layout */}
<Card className="border-slate-200 shadow-sm">
<CardContent className="pt-6">
<div className="flex flex-col gap-4">
{/* Primary Actions Row */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<span className="text-sm text-slate-600 mr-2">Workflow Actions:</span>
{permissions.canApprove && (
<Button
size="sm"
disabled={isSubmitting}
className="bg-green-600 hover:bg-green-700 transition-all hover:shadow-md"
onClick={() => handleAction('approve')}
>
{isSubmitting && actionDialog.type === 'approve' ? <Loader2 className="w-4 h-4 mr-2 animate-spin" /> : <Check className="w-4 h-4 mr-2" />}
Approve
</Button>
)}
{permissions.canSendBack && (
<Button
size="sm"
variant="outline"
disabled={isSubmitting}
className="hover:bg-slate-50 transition-all font-bold"
onClick={() => handleAction('sendBack')}
>
{isSubmitting && actionDialog.type === 'sendBack' ? <Loader2 className="w-4 h-4 mr-2 animate-spin" /> : <RotateCcw className="w-4 h-4 mr-2" />}
Send Back
</Button>
)}
{permissions.canWithdraw && (
<Button
size="sm"
variant="outline"
disabled={isSubmitting}
className="text-red-600 border-red-300 hover:bg-red-50 transition-all font-bold"
onClick={() => handleAction('withdrawal')}
>
{isSubmitting && actionDialog.type === 'withdrawal' ? <Loader2 className="w-4 h-4 mr-2 animate-spin" /> : <X className="w-4 h-4 mr-2" />}
Withdrawal
</Button>
)}
{permissions.canRevoke && (
<Button
size="sm"
variant="outline"
disabled={isSubmitting}
className="text-orange-600 border-orange-300 hover:bg-orange-50 transition-all font-bold"
onClick={() => handleAction('revoke')}
>
{isSubmitting && actionDialog.type === 'revoke' ? <Loader2 className="w-4 h-4 mr-2 animate-spin" /> : <Ban className="w-4 h-4 mr-2" />}
Revoke
</Button>
)}
</div>
{/* Secondary Actions */}
<div className="flex items-center gap-2">
{permissions.canPushToFnF && (
<Button
size="sm"
variant="outline"
disabled={isSubmitting}
className="text-amber-600 border-blue-300 hover:bg-blue-50 transition-all"
onClick={() => handleAction('pushfnf')}
>
{isSubmitting && actionDialog.type === 'pushfnf' ? <Loader2 className="w-4 h-4 mr-2 animate-spin" /> : <Send className="w-4 h-4 mr-2" />}
Push to F&F
</Button>
)}
{permissions.canAssign && (
<Button
size="sm"
variant="outline"
disabled={isSubmitting}
className="hover:bg-slate-50 transition-all"
onClick={() => handleAction('assign')}
>
{isSubmitting && actionDialog.type === 'assign' ? <Loader2 className="w-4 h-4 mr-2 animate-spin" /> : <UserPlus className="w-4 h-4 mr-2" />}
Assign User
</Button>
)}
</div>
</div>
{/* Work Notes Button - Independent Section */}
<div className="flex items-center justify-between pt-4 border-t border-slate-200">
<div className="flex items-center gap-2">
<MessageSquare className="w-4 h-4 text-slate-500" />
<span className="text-sm text-slate-600">Communication & Notes</span>
</div>
<Button
size="sm"
variant="outline"
className="relative hover:bg-amber-50 hover:border-amber-300 hover:text-amber-700 transition-all shadow-sm"
onClick={() => navigate(`/worknotes/resignation/${resignationId}`, {
state: {
applicationName: resignationData?.outlet?.name || 'Resignation',
registrationNumber: resignationData?.resignationId || '',
participants: resignationData?.participants || []
}
})}
>
<MessageSquare className="w-4 h-4 mr-2" />
View Work Notes
{resignationData?.worknotes?.length > 0 && (
<Badge className="ml-2 bg-amber-600 hover:bg-amber-700 text-white h-5 px-2">
{resignationData.worknotes.length}
</Badge>
)}
</Button>
</div>
</div>
</CardContent>
</Card>
{/* Tabs */}
<Tabs defaultValue="details" className="w-full"> <Tabs defaultValue="details" className="w-full">
<TabsList className="bg-slate-100 p-1"> <TabsList className="bg-slate-100 p-1">
<TabsTrigger value="details" className="data-[state=active]:bg-white">Details</TabsTrigger> <TabsTrigger value="details" className="data-[state=active]:bg-white">Details</TabsTrigger>
<TabsTrigger value="progress" className="data-[state=active]:bg-white">Progress</TabsTrigger> <TabsTrigger value="progress" className="data-[state=active]:bg-white">Progress</TabsTrigger>
<TabsTrigger value="documents" className="data-[state=active]:bg-white">Documents</TabsTrigger> <TabsTrigger value="documents" className="data-[state=active]:bg-white">Documents</TabsTrigger>
<TabsTrigger value="audit" className="data-[state=active]:bg-white">Audit Trail</TabsTrigger> <TabsTrigger value="audit" className="data-[state=active]:bg-white">Audit Trail</TabsTrigger>
{isNationalLevel && (
<TabsTrigger value="approvals" className="data-[state=active]:bg-white">Approval Summary</TabsTrigger>
)}
</TabsList> </TabsList>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 mt-6">
<div className="lg:col-span-2 space-y-6">
{/* Details Tab */} {/* Details Tab */}
<TabsContent value="details" className="space-y-6"> <TabsContent value="details" className="space-y-6">
<Card>
<CardHeader>
<CardTitle>Resignation Details</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div>
<Label className="text-slate-600">Resignation Type</Label>
<p>{resignationData?.resignationType}</p>
</div>
<div>
<Label className="text-slate-600">Reason</Label>
<p>{resignationData?.reason}</p>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<Label className="text-slate-600">Last Operational Date (Sales)</Label>
<p>{resignationData?.lastOperationalDateSales ? formatDateTime(resignationData.lastOperationalDateSales, 'date') : 'N/A'}</p>
</div>
<div>
<Label className="text-slate-600">Last Operational Date (Services)</Label>
<p>{resignationData?.lastOperationalDateServices ? formatDateTime(resignationData.lastOperationalDateServices, 'date') : 'N/A'}</p>
</div>
</div>
<div>
<Label className="text-slate-600">Additional Info / Dealer Voice</Label>
<p>{resignationData?.additionalInfo || 'No additional info provided'}</p>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<Label className="text-slate-600">Submitted On</Label>
<p>{resignationData?.submittedOn ? formatDateTime(resignationData.submittedOn) : 'N/A'}</p>
</div>
<div>
<Label className="text-slate-600">Current Stage</Label>
<p>{resignationData?.currentStage}</p>
</div>
</div>
</div>
</CardContent>
</Card>
<Card> <Card>
<CardHeader> <CardHeader>
<CardTitle>Request Information</CardTitle> <CardTitle>Request Information</CardTitle>
@ -637,51 +733,6 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
</div> </div>
</CardContent> </CardContent>
</Card> </Card>
<Card>
<CardHeader>
<CardTitle>Resignation Details</CardTitle>
</CardHeader>
<CardContent>
<div className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<div>
<Label className="text-slate-600">Resignation Type</Label>
<p>{resignationData?.resignationType}</p>
</div>
<div>
<Label className="text-slate-600">Reason</Label>
<p>{resignationData?.reason}</p>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<Label className="text-slate-600">Last Operational Date (Sales)</Label>
<p>{resignationData?.lastOperationalDateSales ? formatDateTime(resignationData.lastOperationalDateSales, 'date') : 'N/A'}</p>
</div>
<div>
<Label className="text-slate-600">Last Operational Date (Services)</Label>
<p>{resignationData?.lastOperationalDateServices ? formatDateTime(resignationData.lastOperationalDateServices, 'date') : 'N/A'}</p>
</div>
</div>
<div>
<Label className="text-slate-600">Additional Info / Dealer Voice</Label>
<p>{resignationData?.additionalInfo || 'No additional info provided'}</p>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<Label className="text-slate-600">Submitted On</Label>
<p>{resignationData?.submittedOn ? formatDateTime(resignationData.submittedOn) : 'N/A'}</p>
</div>
<div>
<Label className="text-slate-600">Current Stage</Label>
<p>{resignationData?.currentStage}</p>
</div>
</div>
</div>
</CardContent>
</Card>
</TabsContent> </TabsContent>
{/* Progress Tab */} {/* Progress Tab */}
@ -708,7 +759,7 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
<div className="flex flex-col items-center"> <div className="flex flex-col items-center">
<div className={`w-10 h-10 rounded-full flex items-center justify-center ${ <div className={`w-10 h-10 rounded-full flex items-center justify-center ${
status === 'completed' ? 'bg-green-100 text-green-600' : status === 'completed' ? 'bg-green-100 text-green-600' :
status === 'active' ? 'bg-blue-100 text-amber-600' : status === 'active' ? 'bg-blue-100 text-re-red' :
'bg-slate-100 text-slate-400' 'bg-slate-100 text-slate-400'
}`}> }`}>
{status === 'completed' ? ( {status === 'completed' ? (
@ -728,13 +779,13 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<h3 className={ <h3 className={
status === 'completed' ? 'text-green-600' : status === 'completed' ? 'text-green-600' :
status === 'active' ? 'text-amber-600' : status === 'active' ? 'text-re-red' :
'text-slate-400' 'text-slate-400'
}>{stage.name}</h3> }>{stage.name}</h3>
{stageDocumentCount > 0 && ( {stageDocumentCount > 0 && (
<button <button
onClick={() => handleViewStageDocuments(stage.name, stage.key)} onClick={() => handleViewStageDocuments(stage.name, stage.key)}
className="flex items-center gap-1 px-2 py-1 rounded-full bg-amber-100 hover:bg-amber-200 text-amber-700 text-xs transition-colors cursor-pointer" className="flex items-center gap-1 px-2 py-1 rounded-full bg-red-50 hover:bg-red-100 text-re-red-hover text-xs transition-colors cursor-pointer"
> >
<FileText className="w-3 h-3" /> <FileText className="w-3 h-3" />
<span>{stageDocumentCount} {stageDocumentCount === 1 ? 'doc' : 'docs'}</span> <span>{stageDocumentCount} {stageDocumentCount === 1 ? 'doc' : 'docs'}</span>
@ -750,19 +801,27 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
</div> </div>
<p className="text-slate-600 text-sm mb-1">{stage.description}</p> <p className="text-slate-600 text-sm mb-1">{stage.description}</p>
{timelineEntry && (
<div className="space-y-2"> {stageTimelineEntries.length > 0 && (
<div className="flex items-center gap-2"> <div className="space-y-4 mt-3">
<Badge variant="secondary" className="bg-slate-100 text-[10px] font-bold uppercase"> {stageTimelineEntries.map((entry: any, i: number) => (
{timelineEntry.user || 'System'} <div key={i} className="space-y-2">
</Badge> <div className="flex items-center gap-2">
<span className="text-[10px] text-slate-500 italic"> <Badge variant="secondary" className="bg-slate-100 text-[10px] font-bold uppercase">
{timelineEntry.action} {entry.user || 'System'}
</span> </Badge>
</div> <span className="text-[10px] text-slate-500 italic">
<div className="bg-slate-50 p-3 rounded-lg border border-slate-100 text-sm text-slate-700 shadow-sm"> {entry.action}
{timelineEntry.comments || timelineEntry.remarks || 'No remarks provided.'} </span>
</div> <span className="text-[10px] text-slate-400 ml-auto">
{formatDateTime(entry.timestamp || entry.createdAt)}
</span>
</div>
<div className="bg-slate-50 p-3 rounded-lg border border-slate-100 text-sm text-slate-700 shadow-sm">
{entry.comments || entry.remarks || 'No remarks provided.'}
</div>
</div>
))}
</div> </div>
)} )}
</div> </div>
@ -782,7 +841,7 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
<CardTitle>Documents</CardTitle> <CardTitle>Documents</CardTitle>
<CardDescription>View and manage resignation documents</CardDescription> <CardDescription>View and manage resignation documents</CardDescription>
</div> </div>
<Button size="sm" onClick={() => setShowUploadDialog(true)} className="bg-amber-600 hover:bg-amber-700"> <Button size="sm" onClick={() => setShowUploadDialog(true)} className="bg-re-red hover:bg-re-red-hover">
<Upload className="w-4 h-4 mr-2" /> <Upload className="w-4 h-4 mr-2" />
Upload Document Upload Document
</Button> </Button>
@ -907,7 +966,7 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
${(log.description || log.action || log.details?.action || '').toLowerCase().includes('reject') || (log.description || log.action || log.details?.action || '').toLowerCase().includes('revok') ${(log.description || log.action || log.details?.action || '').toLowerCase().includes('reject') || (log.description || log.action || log.details?.action || '').toLowerCase().includes('revok')
? 'bg-red-100 text-red-700 border-red-200' ? 'bg-red-100 text-red-700 border-red-200'
: (log.description || log.action || log.details?.action || '').toLowerCase().includes('sent back') || (log.description || log.action || log.details?.action || '').toLowerCase().includes('send back') : (log.description || log.action || log.details?.action || '').toLowerCase().includes('sent back') || (log.description || log.action || log.details?.action || '').toLowerCase().includes('send back')
? 'bg-amber-100 text-amber-700 border-amber-200' ? 'bg-red-50 text-re-red-hover border-red-200'
: (log.description || log.action || log.details?.action || '').toLowerCase().includes('approv') || (log.description || log.action || log.details?.action || '').toLowerCase().includes('initi') : (log.description || log.action || log.details?.action || '').toLowerCase().includes('approv') || (log.description || log.action || log.details?.action || '').toLowerCase().includes('initi')
? 'bg-emerald-100 text-emerald-700 border-emerald-200' ? 'bg-emerald-100 text-emerald-700 border-emerald-200'
: 'bg-slate-100 text-slate-700 border-slate-200'} : 'bg-slate-100 text-slate-700 border-slate-200'}
@ -938,6 +997,223 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
</CardContent> </CardContent>
</Card> </Card>
</TabsContent> </TabsContent>
{/* Approval Summary Tab */}
{isNationalLevel && (
<TabsContent value="approvals">
<Card>
<CardHeader className="flex flex-row items-center justify-between">
<div>
<CardTitle>Approval Summary</CardTitle>
<CardDescription>Comprehensive view of all approvals and remarks</CardDescription>
</div>
{permissions.canApprove && (
<Button onClick={() => handleAction('approve')} className="bg-green-600 hover:bg-green-700">
<Check className="w-4 h-4 mr-2" />
Approve Request
</Button>
)}
</CardHeader>
<CardContent>
<Table className="w-full border-collapse">
<TableHeader>
<TableRow className="bg-slate-50/50">
<TableHead className="min-w-[120px]">Stage</TableHead>
<TableHead className="min-w-[120px]">Approver</TableHead>
<TableHead className="min-w-[200px]">Action</TableHead>
<TableHead className="w-full min-w-[300px]">Remarks</TableHead>
<TableHead className="min-w-[180px] text-right">Date</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{(resignationData?.timeline || []).length > 0 ? (
resignationData.timeline.map((entry: any, index: number) => (
<TableRow key={index}>
<TableCell className="font-medium">{entry.stage}</TableCell>
<TableCell>
<Badge variant="outline">{entry.user || 'System'}</Badge>
</TableCell>
<TableCell className="whitespace-normal break-words">{entry.action}</TableCell>
<TableCell className="whitespace-normal break-words">
{entry.remarks || entry.comments || '-'}
</TableCell>
<TableCell className="text-slate-500 whitespace-nowrap text-right">
{formatDateTime(entry.timestamp || entry.createdAt)}
</TableCell>
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={5} className="text-center py-6 text-slate-500">
No approval records found
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</CardContent>
</Card>
</TabsContent>
)}
</div>
{/* Sidebar */}
<div className="space-y-6">
<Card>
<CardHeader>
<CardTitle>Actions</CardTitle>
</CardHeader>
<CardContent className="space-y-3">
{(() => {
const roleNormalized = String(currentUser?.roleCode || currentUser?.role || '').trim().toUpperCase();
const isDDLeadUser = roleNormalized === 'DD LEAD' || roleNormalized === 'DD_LEAD';
const isDDLeadStageCurrent = ['DD Lead', 'DD Lead Review', 'DDL Review'].includes(resignationData?.currentStage);
if (isDDLeadUser && isDDLeadStageCurrent) {
return (
<Button
variant="outline"
className="w-full text-re-red-hover border-red-300 hover:bg-red-50"
onClick={() => {
setUploadDocType('PPT Presentation');
setUploadStage('DD Lead');
setShowUploadDialog(true);
}}
>
<Upload className="w-4 h-4 mr-2" />
Upload PPT
</Button>
);
}
return null;
})()}
{permissions.canApprove && (
<Button
disabled={isSubmitting}
className="w-full bg-green-600 hover:bg-green-700 font-bold"
onClick={() => handleAction('approve')}
>
{isSubmitting && actionDialog.type === 'approve' ? <Loader2 className="w-4 h-4 mr-2 animate-spin" /> : <Check className="w-4 h-4 mr-2" />}
Approve
</Button>
)}
{permissions.canDispatch && (
<>
{permissions.dispatchMissed && (
<div className="rounded-md border border-amber-300 bg-amber-50 text-amber-800 px-3 py-2 text-xs flex items-start gap-2">
<AlertCircle className="w-4 h-4 mt-0.5 flex-shrink-0" />
<span>
Resignation acceptance letter was not dispatched at the DD Admin step.
Please send it to the dealer now.
</span>
</div>
)}
<Button
disabled={isSubmitting}
className={`w-full font-bold ${
permissions.dispatchMissed
? 'bg-amber-600 hover:bg-amber-700'
: 'bg-re-red hover:bg-re-red-hover'
}`}
onClick={() => handleAction('dispatch')}
>
{isSubmitting && actionDialog.type === 'dispatch' ? (
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
) : (
<Mail className="w-4 h-4 mr-2" />
)}
{permissions.dispatchMissed ? 'Dispatch Resignation Letter (Pending)' : 'Dispatch Resignation Letter'}
</Button>
</>
)}
{permissions.canSendBack && (
<Button
variant="outline"
disabled={isSubmitting}
className="w-full font-bold"
onClick={() => handleAction('sendBack')}
>
{isSubmitting && actionDialog.type === 'sendBack' ? <Loader2 className="w-4 h-4 mr-2 animate-spin" /> : <RotateCcw className="w-4 h-4 mr-2" />}
Send Back
</Button>
)}
{permissions.canWithdraw && (
<Button
variant="outline"
disabled={isSubmitting}
className="w-full text-red-600 border-red-300 hover:bg-red-50 font-bold"
onClick={() => handleAction('withdrawal')}
>
{isSubmitting && actionDialog.type === 'withdrawal' ? <Loader2 className="w-4 h-4 mr-2 animate-spin" /> : <X className="w-4 h-4 mr-2" />}
Withdrawal
</Button>
)}
{permissions.canRevoke && (
<Button
variant="outline"
disabled={isSubmitting}
className="w-full text-orange-600 border-orange-300 hover:bg-orange-50 font-bold"
onClick={() => handleAction('revoke')}
>
{isSubmitting && actionDialog.type === 'revoke' ? <Loader2 className="w-4 h-4 mr-2 animate-spin" /> : <Ban className="w-4 h-4 mr-2" />}
Revoke
</Button>
)}
{permissions.canPushToFnF && (
<Button
variant="outline"
disabled={isSubmitting}
className="w-full text-re-red-hover border-red-300 hover:bg-red-50"
onClick={() => handleAction('pushfnf')}
>
{isSubmitting && actionDialog.type === 'pushfnf' ? <Loader2 className="w-4 h-4 mr-2 animate-spin" /> : <Send className="w-4 h-4 mr-2" />}
Push to F&F
</Button>
)}
{permissions.canAssign && (
<Button
variant="outline"
disabled={isSubmitting}
className="w-full"
onClick={() => handleAction('assign')}
>
{isSubmitting && actionDialog.type === 'assign' ? <Loader2 className="w-4 h-4 mr-2 animate-spin" /> : <UserPlus className="w-4 h-4 mr-2" />}
Assign User
</Button>
)}
<Separator />
<Button
variant="outline"
className="w-full"
onClick={() => navigate(`/worknotes/resignation/${resignationId}`, {
state: {
applicationName: resignationData?.outlet?.name || 'Resignation',
registrationNumber: resignationData?.resignationId || '',
participants: resignationData?.participants || []
}
})}
>
<MessageSquare className="w-4 h-4 mr-2" />
View Work Notes
{resignationData?.worknotes?.length > 0 && (
<Badge className="ml-auto bg-re-red hover:bg-re-red-hover text-white h-5 px-2">
{resignationData.worknotes.length}
</Badge>
)}
</Button>
</CardContent>
</Card>
</div>
</div>
</Tabs> </Tabs>
{/* Action Dialogs */} {/* Action Dialogs */}
@ -951,13 +1227,16 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
{actionDialog.type === 'revoke' && 'Revoke Resignation Request'} {actionDialog.type === 'revoke' && 'Revoke Resignation Request'}
{actionDialog.type === 'assign' && 'Assign to User'} {actionDialog.type === 'assign' && 'Assign to User'}
{actionDialog.type === 'pushfnf' && 'Push to Full & Final Settlement'} {actionDialog.type === 'pushfnf' && 'Push to Full & Final Settlement'}
{actionDialog.type === 'dispatch' && 'Dispatch Resignation Letter'}
</DialogTitle> </DialogTitle>
<DialogDescription> <DialogDescription>
{actionDialog.type === 'assign' {actionDialog.type === 'assign'
? 'Select a user to assign this request to' ? 'Select a user to assign this request to'
: actionDialog.type === 'pushfnf' : actionDialog.type === 'pushfnf'
? 'This will move the resignation request to F&F for dues clearance' ? 'This will move the resignation request to F&F for dues clearance'
: 'Please provide remarks for this action' : actionDialog.type === 'dispatch'
? 'The Legal-issued acceptance letter will be emailed to the dealer and the request will advance to Awaiting F&F. Remarks are optional.'
: 'Please provide remarks for this action'
} }
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
@ -1030,11 +1309,14 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
</div> </div>
) : actionDialog.type === 'pushfnf' ? ( ) : actionDialog.type === 'pushfnf' ? (
<div className="space-y-4"> <div className="space-y-4">
<div className="p-3 bg-amber-50 border border-amber-200 rounded-lg flex items-start gap-3"> <div className="p-3 bg-red-50 border border-red-200 rounded-lg flex items-start gap-3">
<AlertCircle className="w-5 h-5 text-amber-600 mt-0.5" /> <AlertCircle className="w-5 h-5 text-re-red mt-0.5" />
<div className="text-sm text-amber-800"> <div className="text-sm text-red-800">
<p className="font-bold">Manual Trigger Notice</p> <p className="font-bold">Manual Trigger Notice</p>
<p>Normally F&F is triggered after LWD. Use manual trigger only if urgent clearance is required.</p> <p>
Normally F&F is triggered after the {LAST_WORKING_DAY_LABEL.toLowerCase()}. Use manual
trigger only if urgent clearance is required.
</p>
</div> </div>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
@ -1082,13 +1364,13 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
className={ className={
actionDialog.type === 'approve' ? 'bg-green-600 hover:bg-green-700' : actionDialog.type === 'approve' ? 'bg-green-600 hover:bg-green-700' :
actionDialog.type === 'withdrawal' ? 'bg-red-600 hover:bg-red-700' : actionDialog.type === 'withdrawal' ? 'bg-red-600 hover:bg-red-700' :
'bg-amber-600 hover:bg-amber-700' 'bg-re-red hover:bg-re-red-hover'
} }
> >
{isSubmitting ? ( {isSubmitting ? (
<> <>
<Loader2 className="w-4 h-4 mr-2 animate-spin" /> <Loader2 className="w-4 h-4 mr-2 animate-spin" />
Processing... {actionDialog.type === 'dispatch' ? 'Dispatching...' : 'Processing...'}
</> </>
) : ( ) : (
<> <>
@ -1098,6 +1380,7 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
{actionDialog.type === 'revoke' && 'Revoke'} {actionDialog.type === 'revoke' && 'Revoke'}
{actionDialog.type === 'assign' && 'Assign'} {actionDialog.type === 'assign' && 'Assign'}
{actionDialog.type === 'pushfnf' && 'Push to F&F'} {actionDialog.type === 'pushfnf' && 'Push to F&F'}
{actionDialog.type === 'dispatch' && 'Send to Dealer'}
</> </>
)} )}
</Button> </Button>
@ -1110,7 +1393,7 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
<DialogContent className={WIDE_DIALOG_CLASS}> <DialogContent className={WIDE_DIALOG_CLASS}>
<DialogHeader> <DialogHeader>
<DialogTitle className="flex items-center gap-2"> <DialogTitle className="flex items-center gap-2">
<FileText className="w-5 h-5 text-amber-600" /> <FileText className="w-5 h-5 text-re-red" />
Documents - {stageDocumentsDialog.stageName} Documents - {stageDocumentsDialog.stageName}
</DialogTitle> </DialogTitle>
<DialogDescription> <DialogDescription>
@ -1143,7 +1426,7 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
<Button <Button
size="sm" size="sm"
variant="outline" variant="outline"
className="text-amber-600 hover:text-blue-700" className="text-re-red hover:text-blue-700"
onClick={() => { onClick={() => {
if (!doc.filePath) return; if (!doc.filePath) return;
const fullPath = doc.filePath.startsWith('/uploads/') && !doc.filePath.startsWith('/uploads/documents/') const fullPath = doc.filePath.startsWith('/uploads/') && !doc.filePath.startsWith('/uploads/documents/')

View File

@ -5,6 +5,8 @@ import { Button } from '@/components/ui/button';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { API } from '@/api/API'; import { API } from '@/api/API';
import { SlaBadge } from '@/components/sla/SlaBadge';
import { useSlaBatchStatus } from '@/hooks/useSlaBatchStatus';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { User as UserType } from '@/lib/mock-data'; import { User as UserType } from '@/lib/mock-data';
import { formatDateTime } from '@/components/ui/utils'; import { formatDateTime } from '@/components/ui/utils';
@ -71,6 +73,9 @@ export function ResignationPage({ onViewDetails }: ResignationPageProps) {
const openRequests = statusTab === 'open' ? resignations : []; const openRequests = statusTab === 'open' ? resignations : [];
const completedRequests = statusTab === 'completed' ? resignations : []; const completedRequests = statusTab === 'completed' ? resignations : [];
const slaItems = resignations.map((r: any) => ({ entityType: 'resignation', entityId: r.id }));
const { get: getSla } = useSlaBatchStatus(slaItems, resignations.length > 0);
return ( return (
<div className="space-y-6"> <div className="space-y-6">
{/* Header Stats */} {/* Header Stats */}
@ -141,8 +146,8 @@ export function ResignationPage({ onViewDetails }: ResignationPageProps) {
<CardContent className="pt-6"> <CardContent className="pt-6">
<div className="flex items-start justify-between"> <div className="flex items-start justify-between">
<div className="flex items-start gap-4 flex-1"> <div className="flex items-start gap-4 flex-1">
<div className="p-3 bg-amber-100 rounded-lg"> <div className="p-3 bg-red-50 rounded-lg">
<FileText className="w-6 h-6 text-amber-600" /> <FileText className="w-6 h-6 text-re-red" />
</div> </div>
<div className="flex-1 text-left"> <div className="flex-1 text-left">
<div className="flex items-center gap-3 mb-2"> <div className="flex items-center gap-3 mb-2">
@ -150,6 +155,7 @@ export function ResignationPage({ onViewDetails }: ResignationPageProps) {
<Badge className={getStatusColor(request.status)}> <Badge className={getStatusColor(request.status)}>
{request.status} {request.status}
</Badge> </Badge>
<SlaBadge status={getSla('resignation', request.id)} compact />
</div> </div>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm"> <div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm">
<div> <div>
@ -276,6 +282,7 @@ export function ResignationPage({ onViewDetails }: ResignationPageProps) {
<Badge className={getStatusColor(request.status)}> <Badge className={getStatusColor(request.status)}>
{request.status} {request.status}
</Badge> </Badge>
<SlaBadge status={getSla('resignation', request.id)} compact />
</div> </div>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm"> <div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm">
<div> <div>
@ -337,6 +344,7 @@ export function ResignationPage({ onViewDetails }: ResignationPageProps) {
<Badge className={getStatusColor(request.status)}> <Badge className={getStatusColor(request.status)}>
{request.status} {request.status}
</Badge> </Badge>
<SlaBadge status={getSla('resignation', request.id)} compact />
</div> </div>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm"> <div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm">
<div> <div>

File diff suppressed because it is too large Load Diff

View File

@ -8,9 +8,10 @@ import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label'; import { Label } from '@/components/ui/label';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Textarea } from '@/components/ui/textarea'; import { Textarea } from '@/components/ui/textarea';
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import { API } from '@/api/API'; import { API } from '@/api/API';
import { slaService, SlaStatusSnapshot } from '@/services/sla.service';
import { SlaBadge } from '@/components/sla/SlaBadge';
import { formatDateTime } from '@/components/ui/utils'; import { formatDateTime } from '@/components/ui/utils';
import { import {
Pagination, Pagination,
@ -23,6 +24,11 @@ import {
} from "@/components/ui/pagination"; } from "@/components/ui/pagination";
import { User } from '@/lib/mock-data'; import { User } from '@/lib/mock-data';
import { toast } from 'sonner'; import { toast } from 'sonner';
import {
formatTerminationStatusLabel,
LAST_WORKING_DAY_LABEL,
PROPOSED_LAST_WORKING_DAY_LABEL
} from '@/lib/terminationDisplay';
interface TerminationPageProps { interface TerminationPageProps {
currentUser: User | null; currentUser: User | null;
@ -51,6 +57,8 @@ const getStatusColor = (status: string) => {
return 'bg-blue-100 text-blue-700 border-blue-300'; return 'bg-blue-100 text-blue-700 border-blue-300';
}; };
const formatStatus = formatTerminationStatusLabel;
export function TerminationPage({ currentUser, onViewDetails }: TerminationPageProps) { export function TerminationPage({ currentUser, onViewDetails }: TerminationPageProps) {
const [isDialogOpen, setIsDialogOpen] = useState(false); const [isDialogOpen, setIsDialogOpen] = useState(false);
const [dealers, setDealers] = useState<any[]>([]); const [dealers, setDealers] = useState<any[]>([]);
@ -59,6 +67,7 @@ export function TerminationPage({ currentUser, onViewDetails }: TerminationPageP
const [dealerCode, setDealerCode] = useState(''); const [dealerCode, setDealerCode] = useState('');
const [autoFilledData, setAutoFilledData] = useState<any>(null); const [autoFilledData, setAutoFilledData] = useState<any>(null);
const [terminations, setTerminations] = useState<any[]>([]); const [terminations, setTerminations] = useState<any[]>([]);
const [slaById, setSlaById] = useState<Record<string, SlaStatusSnapshot | null>>({});
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [currentPage, setCurrentPage] = useState(1); const [currentPage, setCurrentPage] = useState(1);
const [paginationMeta, setPaginationMeta] = useState<any>(null); const [paginationMeta, setPaginationMeta] = useState<any>(null);
@ -69,7 +78,7 @@ export function TerminationPage({ currentUser, onViewDetails }: TerminationPageP
reason: '', reason: '',
proposedLwd: '', proposedLwd: '',
comments: '', comments: '',
document: null as File | null documents: [] as File[]
}); });
const fetchTerminations = async () => { const fetchTerminations = async () => {
@ -84,6 +93,23 @@ export function TerminationPage({ currentUser, onViewDetails }: TerminationPageP
if (data?.success) { if (data?.success) {
setTerminations(data.terminations); setTerminations(data.terminations);
setPaginationMeta(data.meta); setPaginationMeta(data.meta);
const rows = data.terminations || [];
if (rows.length) {
slaService
.getBatchStatus(rows.map((t: any) => ({ entityType: 'termination', entityId: t.id })))
.then((slaRes) => {
if (slaRes?.success) {
const map: Record<string, SlaStatusSnapshot | null> = {};
rows.forEach((t: any) => {
map[t.id] = slaRes.data[`termination:${t.id}`] ?? null;
});
setSlaById(map);
}
})
.catch(() => setSlaById({}));
} else {
setSlaById({});
}
} }
} catch (error) { } catch (error) {
console.error('Error fetching terminations:', error); console.error('Error fetching terminations:', error);
@ -103,7 +129,7 @@ export function TerminationPage({ currentUser, onViewDetails }: TerminationPageP
}; };
useEffect(() => { useEffect(() => {
if (!isDialogOpen || !isDDLead) return; if (!isDialogOpen || !canCreateTermination) return;
let cancelled = false; let cancelled = false;
(async () => { (async () => {
@ -188,6 +214,41 @@ export function TerminationPage({ currentUser, onViewDetails }: TerminationPageP
setAutoFilledData(mapDealerToFormData(matchedDealer)); setAutoFilledData(mapDealerToFormData(matchedDealer));
}; };
const isSuperAdmin = currentUser?.role === 'Super Admin';
const isPresentationMandatory = !isSuperAdmin;
const isPptFile = (file: File) => {
const name = file.name.toLowerCase();
return name.endsWith('.ppt') || name.endsWith('.pptx');
};
const handleFilesPicked = (files: FileList | null, inputEl?: HTMLInputElement | null) => {
if (!files || files.length === 0) return;
setFormData((prev) => {
const existing = prev.documents;
const seen = new Set(existing.map((f) => `${f.name}::${f.size}`));
const additions: File[] = [];
Array.from(files).forEach((file) => {
const key = `${file.name}::${file.size}`;
if (!seen.has(key)) {
seen.add(key);
additions.push(file);
}
});
return { ...prev, documents: [...existing, ...additions] };
});
if (inputEl) inputEl.value = '';
};
const removeDocumentAt = (index: number) => {
setFormData({
...formData,
documents: formData.documents.filter((_, i) => i !== index)
});
};
const handleSubmit = async (e: React.FormEvent) => { const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
if (!autoFilledData) { if (!autoFilledData) {
@ -195,43 +256,50 @@ export function TerminationPage({ currentUser, onViewDetails }: TerminationPageP
return; return;
} }
try { if (isPresentationMandatory) {
const payload = { if (formData.documents.length === 0) {
dealerId: autoFilledData.dealerId || autoFilledData.id, toast.error('Please upload at least one Presentation (.ppt or .pptx)');
category: formData.terminationCategory, return;
reason: formData.reason, }
proposedLwd: formData.proposedLwd, if (!formData.documents.some(isPptFile)) {
comments: formData.comments toast.error('At least one PowerPoint file (.ppt or .pptx) is required');
}; return;
}
}
if (!payload.dealerId) { try {
const dealerId = autoFilledData.dealerId || autoFilledData.id;
if (!dealerId) {
toast.error('Dealer record not found for the selected dealer'); toast.error('Dealer record not found for the selected dealer');
return; return;
} }
const response = await API.createTermination(payload); let requestBody: any;
if (formData.documents.length > 0) {
const fd = new FormData();
fd.append('dealerId', String(dealerId));
fd.append('category', formData.terminationCategory);
fd.append('reason', formData.reason);
fd.append('proposedLwd', formData.proposedLwd);
fd.append('comments', formData.comments);
formData.documents.forEach((file) => fd.append('files', file));
requestBody = fd;
} else {
requestBody = {
dealerId,
category: formData.terminationCategory,
reason: formData.reason,
proposedLwd: formData.proposedLwd,
comments: formData.comments
};
}
const response = await API.createTermination(requestBody);
const data = response.data as any; const data = response.data as any;
if (data?.success) { if (data?.success) {
// Use termination.id which is the UUID toast.success(formData.documents.length > 0
const newId = data.termination?.id; ? 'Termination request and documents submitted'
: 'Termination request submitted successfully');
// Upload document if selected
if (newId && formData.document) {
const docFormData = new FormData();
docFormData.append('file', formData.document);
docFormData.append('documentType', 'Termination Recommendation');
docFormData.append('stage', 'Submitted');
try {
await API.uploadTerminationDocument(newId, docFormData);
toast.success('Termination request and supporting document submitted');
} catch (docErr) {
console.error('Error uploading supporting document:', docErr);
toast.warning('Termination created, but document upload failed. You can upload it from the details page.');
}
} else {
toast.success('Termination request submitted successfully');
}
setIsDialogOpen(false); setIsDialogOpen(false);
fetchTerminations(); fetchTerminations();
@ -245,7 +313,7 @@ export function TerminationPage({ currentUser, onViewDetails }: TerminationPageP
reason: '', reason: '',
proposedLwd: '', proposedLwd: '',
comments: '', comments: '',
document: null documents: []
}); });
} }
} catch (error: any) { } catch (error: any) {
@ -254,7 +322,8 @@ export function TerminationPage({ currentUser, onViewDetails }: TerminationPageP
} }
}; };
const isDDLead = currentUser?.role === 'DD Lead'; const allowedRoles = ['DD Lead', 'ASM', 'DD Admin', 'DD AM', 'Super Admin'];
const canCreateTermination = currentUser?.role && allowedRoles.includes(currentUser.role);
// Map terminations to tab-specific views (already filtered by backend, but need variables for render) // Map terminations to tab-specific views (already filtered by backend, but need variables for render)
const openRequests = activeTab === 'open' || activeTab === 'all' ? terminations : []; const openRequests = activeTab === 'open' || activeTab === 'all' ? terminations : [];
@ -263,13 +332,7 @@ export function TerminationPage({ currentUser, onViewDetails }: TerminationPageP
return ( return (
<div className="space-y-6"> <div className="space-y-6">
{/* Warning Alert */} {/* Warning Alert */}
<Alert className="border-red-200 bg-red-50">
<AlertTriangle className="h-4 w-4 text-red-600" />
<AlertTitle className="text-red-900">Restricted Access</AlertTitle>
<AlertDescription className="text-red-700">
This section contains sensitive information. All termination actions are logged and require proper authorization.
</AlertDescription>
</Alert>
{/* Header Stats */} {/* Header Stats */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4"> <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
@ -312,14 +375,9 @@ export function TerminationPage({ currentUser, onViewDetails }: TerminationPageP
<CardTitle>Termination Requests</CardTitle> <CardTitle>Termination Requests</CardTitle>
<CardDescription> <CardDescription>
Manage dealer termination proceedings and legal compliance Manage dealer termination proceedings and legal compliance
{!isDDLead && (
<span className="block mt-1 text-red-600">
Note: Only DD Lead can create termination requests. Current role: {currentUser?.role || 'Not logged in'}
</span>
)}
</CardDescription> </CardDescription>
</div> </div>
{isDDLead && ( {canCreateTermination && (
<Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}> <Dialog open={isDialogOpen} onOpenChange={setIsDialogOpen}>
<DialogTrigger asChild> <DialogTrigger asChild>
<Button className="bg-red-600 hover:bg-red-700"> <Button className="bg-red-600 hover:bg-red-700">
@ -408,14 +466,14 @@ export function TerminationPage({ currentUser, onViewDetails }: TerminationPageP
<SelectContent> <SelectContent>
<SelectItem value="Working Capital">Working Capital</SelectItem> <SelectItem value="Working Capital">Working Capital</SelectItem>
<SelectItem value="Performance Issues">Performance Issues</SelectItem> <SelectItem value="Performance Issues">Performance Issues</SelectItem>
<SelectItem value="Unethical Practical">Unethical Practical</SelectItem> <SelectItem value="Unethical Practice">Unethical Practice</SelectItem>
<SelectItem value="Unforeseen Circumstances">Unforeseen Circumstances</SelectItem> <SelectItem value="Unforeseen Circumstances">Unforeseen Circumstances</SelectItem>
<SelectItem value="Others">Others</SelectItem> <SelectItem value="Others">Others</SelectItem>
</SelectContent> </SelectContent>
</Select> </Select>
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label>Proposed LWD *</Label> <Label>{PROPOSED_LAST_WORKING_DAY_LABEL} *</Label>
<Input <Input
type="date" type="date"
value={formData.proposedLwd} value={formData.proposedLwd}
@ -449,12 +507,45 @@ export function TerminationPage({ currentUser, onViewDetails }: TerminationPageP
</div> </div>
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="document">Upload Supporting Document</Label> <Label htmlFor="documents">
{isPresentationMandatory ? 'Upload Documents *' : 'Upload Supporting Documents'}
</Label>
<Input <Input
id="document" id="documents"
type="file" type="file"
onChange={(e) => setFormData({...formData, document: e.target.files?.[0] || null})} multiple
accept=".ppt,.pptx,.pdf,.doc,.docx,.xls,.xlsx,.jpg,.jpeg,.png"
onChange={(e) => handleFilesPicked(e.target.files, e.currentTarget)}
required={isPresentationMandatory && formData.documents.length === 0}
/> />
{isPresentationMandatory && (
<p className="text-xs text-slate-500">
At least one PowerPoint (.ppt / .pptx) is mandatory. You can also attach MOM, dealer commitments, and other supporting files (PDF / DOC / XLS / image).
</p>
)}
{formData.documents.length > 0 && (
<div className="border rounded-md divide-y bg-slate-50">
{formData.documents.map((file, idx) => (
<div key={`${file.name}-${idx}`} className="flex items-center justify-between px-3 py-2 text-sm">
<div className="flex items-center gap-2 min-w-0">
<span className="truncate">{file.name}</span>
{isPptFile(file) && (
<Badge className="bg-blue-100 text-blue-700 border-blue-300">Presentation</Badge>
)}
</div>
<Button
type="button"
variant="ghost"
size="sm"
onClick={() => removeDocumentAt(idx)}
className="text-red-600 hover:text-red-700"
>
Remove
</Button>
</div>
))}
</div>
)}
</div> </div>
<DialogFooter> <DialogFooter>
@ -495,13 +586,14 @@ export function TerminationPage({ currentUser, onViewDetails }: TerminationPageP
<div className="flex-1"> <div className="flex-1">
<div className="flex items-center gap-3 mb-2"> <div className="flex items-center gap-3 mb-2">
<h3 className="text-lg font-bold">{request.requestId || (request.dealer?.dealerCode?.code || 'N/A')}</h3> <h3 className="text-lg font-bold">{request.requestId || (request.dealer?.dealerCode?.code || 'N/A')}</h3>
<span className="text-slate-400 text-xs">#{request.id.substring(0, 8)}</span>
<Badge className={getSeverityColor(request.severity || 'Medium')}> <Badge className={getSeverityColor(request.severity || 'Medium')}>
{request.severity || 'Normal'} {request.severity || 'Normal'}
</Badge> </Badge>
<Badge className={getStatusColor(request.status)}> <Badge className={getStatusColor(request.status)}>
{request.status} {formatStatus(request.status)}
</Badge> </Badge>
<SlaBadge status={slaById[request.id]} compact />
</div> </div>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm"> <div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm">
<div> <div>
@ -518,10 +610,10 @@ export function TerminationPage({ currentUser, onViewDetails }: TerminationPageP
</div> </div>
<div> <div>
<p className="text-slate-600">Current Stage</p> <p className="text-slate-600">Current Stage</p>
<p>{request.currentStage}</p> <p>{formatStatus(request.currentStage)}</p>
</div> </div>
<div> <div>
<p className="text-slate-600">Proposed LWD</p> <p className="text-slate-600">{PROPOSED_LAST_WORKING_DAY_LABEL}</p>
<div className="flex items-center gap-1"> <div className="flex items-center gap-1">
<Calendar className="w-4 h-4 text-slate-500" /> <Calendar className="w-4 h-4 text-slate-500" />
<p>{request.proposedLwd}</p> <p>{request.proposedLwd}</p>
@ -570,10 +662,11 @@ export function TerminationPage({ currentUser, onViewDetails }: TerminationPageP
<div className="flex-1"> <div className="flex-1">
<div className="flex items-center gap-3 mb-2"> <div className="flex items-center gap-3 mb-2">
<h3 className="text-lg font-bold">{request.requestId || (request.dealer?.dealerCode?.code || 'N/A')}</h3> <h3 className="text-lg font-bold">{request.requestId || (request.dealer?.dealerCode?.code || 'N/A')}</h3>
<span className="text-slate-400 text-xs">#{request.id.substring(0, 8)}</span>
<Badge className={getStatusColor(request.status)}> <Badge className={getStatusColor(request.status)}>
{request.status} {formatStatus(request.status)}
</Badge> </Badge>
<SlaBadge status={slaById[request.id]} compact />
</div> </div>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm"> <div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm">
<div> <div>
@ -586,7 +679,7 @@ export function TerminationPage({ currentUser, onViewDetails }: TerminationPageP
</div> </div>
<div> <div>
<p className="text-slate-600">Current Stage</p> <p className="text-slate-600">Current Stage</p>
<p>{request.currentStage}</p> <p>{formatStatus(request.currentStage)}</p>
</div> </div>
<div> <div>
<p className="text-slate-600">Submitted On</p> <p className="text-slate-600">Submitted On</p>
@ -632,10 +725,11 @@ export function TerminationPage({ currentUser, onViewDetails }: TerminationPageP
<div className="flex-1"> <div className="flex-1">
<div className="flex items-center gap-3 mb-2"> <div className="flex items-center gap-3 mb-2">
<h3 className="text-lg font-bold">{request.requestId || (request.dealer?.dealerCode?.code || 'N/A')}</h3> <h3 className="text-lg font-bold">{request.requestId || (request.dealer?.dealerCode?.code || 'N/A')}</h3>
<span className="text-slate-400 text-xs">#{request.id.substring(0, 8)}</span>
<Badge className={getStatusColor(request.status)}> <Badge className={getStatusColor(request.status)}>
{request.status} {formatStatus(request.status)}
</Badge> </Badge>
<SlaBadge status={slaById[request.id]} compact />
</div> </div>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm"> <div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm">
<div> <div>
@ -651,7 +745,7 @@ export function TerminationPage({ currentUser, onViewDetails }: TerminationPageP
<p>{request.category}</p> <p>{request.category}</p>
</div> </div>
<div> <div>
<p className="text-slate-600">LWD</p> <p className="text-slate-600">{LAST_WORKING_DAY_LABEL}</p>
<p>{request.proposedLwd}</p> <p>{request.proposedLwd}</p>
</div> </div>
</div> </div>

View File

@ -0,0 +1,27 @@
import { useEffect, useState } from 'react';
import { slaService, SlaStatusSnapshot } from '@/services/sla.service';
export function useSlaBatchStatus(
items: Array<{ entityType: string; entityId: string }>,
enabled = true
) {
const [byKey, setByKey] = useState<Record<string, SlaStatusSnapshot | null>>({});
useEffect(() => {
if (!enabled || items.length === 0) {
setByKey({});
return;
}
slaService
.getBatchStatus(items)
.then((res) => {
if (res?.success) setByKey(res.data);
})
.catch(() => setByKey({}));
}, [enabled, JSON.stringify(items.map((i) => `${i.entityType}:${i.entityId}`).sort())]);
const get = (entityType: string, entityId: string) =>
byKey[`${entityType}:${entityId}`] ?? null;
return { byKey, get };
}

Some files were not shown because too many files have changed in this diff Show More