From 01e22e4aa7a7ec3ce0511b2802574abb97fea252 Mon Sep 17 00:00:00 2001 From: laxman h Date: Mon, 20 Apr 2026 19:56:33 +0530 Subject: [PATCH] in-app notiofication enhanced an SLA fe implementation done partially --- src/App.tsx | 8 +- src/api/API.ts | 8 +- src/components/layout/Sidebar.tsx | 8 +- .../pages/ConstitutionalChangeDetails.tsx | 501 +++++++++--------- src/features/dashboard/pages/Dashboard.tsx | 2 +- .../fnf/pages/FinanceFnFDetailsPage.tsx | 100 ++-- src/features/fnf/pages/FinanceFnFPage.tsx | 4 +- .../master/components/SLAConfiguration.tsx | 73 ++- src/features/master/components/SLADialog.tsx | 356 +++++++++++++ src/features/master/pages/MasterPage.tsx | 14 +- src/features/master/pages/SLAConfigPage.tsx | 180 +++++++ .../ApplicationDetailsTabs.tsx | 7 +- .../hooks/useApplicationDetailsStageData.ts | 2 +- .../pages/RelocationRequestDetails.tsx | 50 +- .../pages/DealerResignationDetailsPage.tsx | 81 ++- .../termination/pages/TerminationDetails.tsx | 10 +- src/lib/constants.ts | 65 +++ src/services/master.service.ts | 8 + src/services/resignation.service.ts | 9 + 19 files changed, 1135 insertions(+), 351 deletions(-) create mode 100644 src/features/master/components/SLADialog.tsx create mode 100644 src/features/master/pages/SLAConfigPage.tsx create mode 100644 src/lib/constants.ts diff --git a/src/App.tsx b/src/App.tsx index 77c5d47..f5a6990 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -35,6 +35,7 @@ import { FinanceFnFDetailsPage } from '@/features/fnf/pages/FinanceFnFDetailsPag import { MasterPage } from '@/features/master/pages/MasterPage'; import { UserManagementPage } from '@/components/admin/UserManagementPage'; import { ApprovalPoliciesPage } from '@/components/admin/ApprovalPoliciesPage'; +import { SLAConfigPage } from '@/features/master/pages/SLAConfigPage'; import { ConstitutionalChangePage } from '@/features/constitutional/pages/ConstitutionalChangePage'; import { ConstitutionalChangeDetails } from '@/features/constitutional/pages/ConstitutionalChangeDetails'; import { RelocationRequestPage } from '@/features/relocation/pages/RelocationRequestPage'; @@ -235,7 +236,7 @@ export default function App() { {/* All Applications */} navigate(`/applications/${id}`)} initialFilter="all" /> : + hasRole(['DD', 'DD Admin', 'Super Admin']) ? navigate(`/applications/${id}`)} initialFilter="all" /> : } /> {/* FDD Routes - Integrated into Layout */} @@ -254,6 +255,11 @@ export default function App() { ? : } /> + + : + } /> } /> } /> } /> diff --git a/src/api/API.ts b/src/api/API.ts index e3c99cc..217507d 100644 --- a/src/api/API.ts +++ b/src/api/API.ts @@ -153,6 +153,7 @@ export const API = { createResignation: (data: any) => client.post('/resignation', data), approveResignation: (id: string, data?: any) => client.post(`/resignation/${id}/approve`, data), rejectResignation: (id: string, data: any) => client.post(`/resignation/${id}/reject`, data), + withdrawResignation: (id: string, reason: string) => client.post(`/resignation/${id}/withdraw`, { reason }), getTerminations: () => client.get('/termination'), createTermination: (data: any) => client.post('/termination', data), @@ -164,6 +165,9 @@ export const API = { getFnFSettlementById: (id: string) => client.get(`/settlement/fnf/${id}`), calculateFnF: (id: string) => client.post(`/settlement/fnf/${id}/calculate`), updateFnF: (id: string, data: any) => client.put(`/settlement/fnf/${id}`, data), + uploadFnFDocument: (id: string, data: any) => client.post(`/settlement/fnf/${id}/documents`, data, { + headers: { 'Content-Type': 'multipart/form-data' } + }), getSettlementDepartments: () => client.get('/settlement/departments'), // Line items @@ -199,7 +203,9 @@ export const API = { uploadConstitutionalDocuments: (id: string, documents: any[]) => client.post(`/constitutional-change/${id}/documents`, { documents }), // SLA - getSlaConfigs: () => client.get('/sla/configs'), + getSlaConfigs: () => client.get('/master/sla-configs'), + saveSlaConfig: (data: any) => client.post('/master/sla-configs', data), + initializeDefaultSlas: () => client.post('/master/sla-configs/initialize'), // System Configs getSystemConfigs: (params?: any) => client.get('/master/system-configs', params), diff --git a/src/components/layout/Sidebar.tsx b/src/components/layout/Sidebar.tsx index 223aa44..6bb2d04 100644 --- a/src/components/layout/Sidebar.tsx +++ b/src/components/layout/Sidebar.tsx @@ -6,7 +6,6 @@ import { ChevronLeft, ChevronRight, Search, - Inbox, UserMinus, ChevronDown, ChevronUp, @@ -79,13 +78,15 @@ export function Sidebar({ onLogout }: SidebarProps) { { id: 'relocation-requests', label: 'Relocation Requests', icon: MapPin }, ]; + /* // Add All Applications for DD role (before Dealership Requests) - if (hasRole(['DD'])) { + if (hasRole(['DD', 'DD Admin', 'Super Admin'])) { menuItems.splice(1, 0, { id: 'all-applications', label: 'All Applications', icon: Inbox }); } + */ // Add All Requests for DD Lead role (before Dealership Requests) - if (hasRole(['DD Lead', 'Super Admin'])) { + if (hasRole(['DD Lead', 'DD Admin', 'Super Admin'])) { menuItems.splice(1, 0, { id: 'all-requests', label: 'All Requests', @@ -102,6 +103,7 @@ export function Sidebar({ onLogout }: SidebarProps) { // Add Master for Super Admin, DD Admin, and DD Lead if (hasRole(['Super Admin', 'DD Admin', 'DD Lead'])) { menuItems.push({ id: 'master', label: 'Master', icon: Settings }); + menuItems.push({ id: 'sla-configurations', label: 'SLA Matrix', icon: RefreshCcw }); } if (hasRole(['Super Admin'])) { diff --git a/src/features/constitutional/pages/ConstitutionalChangeDetails.tsx b/src/features/constitutional/pages/ConstitutionalChangeDetails.tsx index 0e7b9a4..a250897 100644 --- a/src/features/constitutional/pages/ConstitutionalChangeDetails.tsx +++ b/src/features/constitutional/pages/ConstitutionalChangeDetails.tsx @@ -76,7 +76,7 @@ const documentNames: Record = { // Helper functions moved above component to avoid lint errors const getTypeColor = (type: string) => { - switch(type) { + switch (type) { 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 'LLP': return 'bg-indigo-100 text-indigo-700 border-indigo-300'; @@ -116,7 +116,7 @@ const getConstitutionalHistoryPresentation = (entry: any) => { if (combined.includes('SENT BACK') || combined.includes('SEND BACK') || combined.includes('RECONSIDER')) { return { variant: 'pending' as const, badge: action.replace(/_/g, ' ') || 'SENT BACK' }; } - + if (combined.includes('APPROV') || combined.includes('INITI') || combined.includes('CREATE') || combined.includes('VERIF') || combined.includes('UPLOAD')) { return { variant: 'success' as const, badge: action.replace(/_/g, ' ') || 'APPROVED' }; } @@ -261,11 +261,11 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }: // Calculate current stage index mapping to backend stages const getCurrentStageIndex = () => { let currentStage = request.currentStage || ''; - + // For terminal states, resolve the last active stage from timeline if (['Rejected', 'Revoked', 'Withdrawn'].includes(request.status) && (currentStage === 'Rejected' || currentStage === 'Revoked' || !currentStage)) { - const lastEntry = [...(request.timeline || [])].reverse().find(e => e.stage && !['Rejected', 'Revoked', 'REJECTED', 'REVOKED'].includes(e.stage)); - if (lastEntry) currentStage = lastEntry.stage; + const lastEntry = [...(request.timeline || [])].reverse().find(e => e.stage && !['Rejected', 'Revoked', 'REJECTED', 'REVOKED'].includes(e.stage)); + if (lastEntry) currentStage = lastEntry.stage; } const stageMap: Record = { @@ -282,7 +282,7 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }: if (stageMap[currentStage]) return stageMap[currentStage]; - + // Case-insensitive fallback search const normalized = currentStage.trim().toLowerCase(); for (const [key, val] of Object.entries(stageMap)) { @@ -381,9 +381,9 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }: } const jointZmRbmMeta = (meta as any)?.jointApprovals?.zmRbm || {}; const isZmRbmStage = currentStage === 'ZM/RBM Review' || currentStage === 'ZM+RBM Review'; - const actorKey = (userRoleCode === 'RBM') ? 'RBM' : - (userRoleCode === 'DD-ZM' || userRoleCode === 'DD ZM' || userRoleCode === 'ZM') ? 'DD-ZM' : null; - + const actorKey = (userRoleCode === 'RBM') ? 'RBM' : + (userRoleCode === 'DD-ZM' || userRoleCode === 'DD ZM' || userRoleCode === 'ZM') ? 'DD-ZM' : null; + // GAP CLOSURE: Rely primarily on metadata for joint approval status. const hasCurrentUserApprovedZmRbm = isZmRbmStage && actorKey && Boolean(jointZmRbmMeta?.[actorKey]?.approvedByUserId); @@ -423,7 +423,7 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }: const remarksTrimmed = String(comments || '').trim(); const isHighRiskAction = actionType === 'sendBack' || actionType === 'revoke'; - + if (isHighRiskAction && remarksTrimmed.length < 5) { toast.error('Detailed remarks (minimum 5 characters) are required for Send Back and Revoke actions.'); return; @@ -451,9 +451,9 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }: if (response.data.success) { const actionText = actionType === 'approve' ? 'approved' : - actionType === 'reject' ? 'rejected' : - actionType === 'sendBack' ? 'sent back' : - 'revoked'; + actionType === 'reject' ? 'rejected' : + actionType === 'sendBack' ? 'sent back' : + 'revoked'; toast.success(`Request ${actionText} successfully`); setIsActionDialogOpen(false); setComments(''); @@ -612,12 +612,12 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }:

Constitutional Change

- - {request.outlet?.type || 'Proprietorship'} + + {request.oldValue || request.currentConstitution || request.outlet?.type || 'Proprietorship'} - - {request.changeType} + + {request.newValue || request.changeType}
@@ -628,7 +628,7 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }:

Current Stage: {request.currentStage}

- +

Reason for Change

{request.description}

@@ -700,7 +700,7 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }: {request.progressPercentage}%
-
@@ -723,131 +723,129 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }: ))} ) : ( - workflowStages.map((stage, index) => { - /** - * While DB stage is still `Submitted`, the filing step is already done; the queue is at ASM. - * Show “Submitted” as completed and “ASM Review” as in progress (no extra dealer action). - */ - // Adjusted logic for streamlined workflow (no separate 'Submitted' stage gate) - const isCompleted = - flowComplete || - index < currentStageIndex - 1; - const isCurrent = - !flowComplete && - index === currentStageIndex - 1; + workflowStages.map((stage, index) => { + /** + * While DB stage is still `Submitted`, the filing step is already done; the queue is at ASM. + * Show “Submitted” as completed and “ASM Review” as in progress (no extra dealer action). + */ + // Adjusted logic for streamlined workflow (no separate 'Submitted' stage gate) + const isCompleted = + flowComplete || + index < currentStageIndex - 1; + const isCurrent = + !flowComplete && + index === currentStageIndex - 1; - const timelineEntry = getLatestStageTimelineEntry(stage.name); - const explicitFeedback = timelineEntry?.comments || timelineEntry?.feedback || timelineEntry?.remarks; - const isJointZmRbmStage = stage.name === 'ZM/RBM Review'; - let metaObj = request.metadata || {}; - if (typeof metaObj === 'string') { - try { metaObj = JSON.parse(metaObj); } catch (e) { metaObj = {}; } - } - const jointZmRbmMetaObj = (metaObj as any)?.jointApprovals?.zmRbm || {}; - const rbmApproval = jointZmRbmMetaObj?.RBM; - const ddZmApproval = jointZmRbmMetaObj?.['DD-ZM']; - const currentRoleNormalized = String(currentUser?.roleCode || currentUser?.role || '').toUpperCase(); - const currentRoleApproval = - currentRoleNormalized === 'RBM' - ? rbmApproval - : (currentRoleNormalized === 'DD-ZM' || currentRoleNormalized === 'DD ZM' || currentRoleNormalized === 'ZM') - ? ddZmApproval - : null; + const timelineEntry = getLatestStageTimelineEntry(stage.name); + const explicitFeedback = timelineEntry?.comments || timelineEntry?.feedback || timelineEntry?.remarks; + const isJointZmRbmStage = stage.name === 'ZM/RBM Review'; + let metaObj = request.metadata || {}; + if (typeof metaObj === 'string') { + try { metaObj = JSON.parse(metaObj); } catch (e) { metaObj = {}; } + } + const jointZmRbmMetaObj = (metaObj as any)?.jointApprovals?.zmRbm || {}; + const rbmApproval = jointZmRbmMetaObj?.RBM; + const ddZmApproval = jointZmRbmMetaObj?.['DD-ZM']; + const currentRoleNormalized = String(currentUser?.roleCode || currentUser?.role || '').toUpperCase(); + const currentRoleApproval = + currentRoleNormalized === 'RBM' + ? rbmApproval + : (currentRoleNormalized === 'DD-ZM' || currentRoleNormalized === 'DD ZM' || currentRoleNormalized === 'ZM') + ? ddZmApproval + : null; - return ( -
- {/* Status Icon */} -
-
- {isCompleted ? ( - - ) : isCurrent ? ( - - ) : ( - + return ( +
+ {/* Status Icon */} +
+
+ {isCompleted ? ( + + ) : isCurrent ? ( + + ) : ( + + )} +
+ {index < workflowStages.length - 1 && ( +
)}
- {index < workflowStages.length - 1 && ( -
- )} -
- {/* Stage Info */} -
-
-
-

- {formatStageLabel(stage.name)} -

-

- {`Responsible: ${formatStageRole(stage.role)}`} -

+ {/* Stage Info */} +
+
+
+

+ {formatStageLabel(stage.name)} +

+

+ {`Responsible: ${formatStageRole(stage.role)}`} +

-
- - {isCompleted ? 'Completed' : isCurrent ? 'In Progress' : 'Pending'} - -
- {timelineEntry && ( -
-
- - Last updated by: {timelineEntry.user || timelineEntry.userName || 'System'} - - {formatDateTime(timelineEntry.timestamp || timelineEntry.createdAt)}
- {explicitFeedback && ( -
- {explicitFeedback} -
- )} + + {isCompleted ? 'Completed' : isCurrent ? 'In Progress' : 'Pending'} +
- )} - {isJointZmRbmStage && ( -
-

Joint approval status

-
- - RBM: {rbmApproval?.approvedByUserId ? 'Approved' : 'Pending'} - - - DD-ZM: {ddZmApproval?.approvedByUserId ? 'Approved' : 'Pending'} - - {currentRoleApproval?.approvedByUserId && ( - - Approved by you + {timelineEntry && ( +
+
+ + Last updated by: {timelineEntry.user || timelineEntry.userName || 'System'} + {formatDateTime(timelineEntry.timestamp || timelineEntry.createdAt)} +
+ {explicitFeedback && ( +
+ {explicitFeedback} +
)}
- {(rbmApproval?.remarks || ddZmApproval?.remarks) && ( -
-
-

RBM Comment

-

{rbmApproval?.remarks || '-'}

-
-
-

DD-ZM Comment

-

{ddZmApproval?.remarks || '-'}

-
+ )} + {isJointZmRbmStage && ( +
+

Joint approval status

+
+ + RBM: {rbmApproval?.approvedByUserId ? 'Approved' : 'Pending'} + + + DD-ZM: {ddZmApproval?.approvedByUserId ? 'Approved' : 'Pending'} + + {currentRoleApproval?.approvedByUserId && ( + + Approved by you + + )}
- )} -
- )} + {(rbmApproval?.remarks || ddZmApproval?.remarks) && ( +
+
+

RBM Comment

+

{rbmApproval?.remarks || '-'}

+
+
+

DD-ZM Comment

+

{ddZmApproval?.remarks || '-'}

+
+
+ )} +
+ )} +
-
- ); - }) + ); + }) )}
@@ -908,7 +906,7 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }: - - {doc.status !== 'Verified' && doc.status !== 'Rejected' && currentUser?.role !== 'Dealer' && ( - <> - - - - )} + {doc.status !== 'Verified' && doc.status !== 'Rejected' && (() => { + const role = currentUser?.role || currentUser?.roleCode || ''; + // SRS §12.2 — only authorized review roles can verify constitutional documents + const canVerifyDocs = ['DD Lead', 'DD Head', 'NBH', 'Legal Admin', 'DD Admin', 'Super Admin', 'SUPER_ADMIN', 'DD_ADMIN'].includes(role); + return canVerifyDocs; + })() && ( + <> + + + + )}
@@ -1053,56 +1055,73 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }: - {/* History Tab */} + {/* History Tab */}
{historyEntries.map((entry: any, index: number) => { const pres = getConstitutionalHistoryPresentation(entry); - return ( -
-
- {pres.variant === 'success' ? ( - - ) : pres.variant === 'danger' ? ( - - ) : pres.variant === 'pending' ? ( - - ) : ( - - )} -
-
-
-
-

{entry.stage || entry.action}

-

{entry.userName || 'System'}

-
- - {pres.badge} - -
-
- Comments: - {/* - Audit API puts user-entered text in `remarks` and a generated summary in `description` - (e.g. "Approval - Stage: ZM/RBM Review"). Prefer remarks so History matches the approve modal / Work Notes. - */} - {String(entry.remarks || '').trim() || - String(entry.description || '').trim() || - 'No remarks provided'} -
-

- {formatDateTime(entry.timestamp || entry.createdAt)} -

+ // Clean actor name — deduplicate "Legal Admin · Legal Admin" pattern + const rawActor = entry.actor?.name || entry.userName || 'System'; + const actorParts = rawActor.split('·').map((p: string) => p.trim()); + const actorName = [...new Set(actorParts)].join(', '); + + // Clean action label — strip [Master data] and UUID/technical prefixes + const rawStage = String(entry.stage || entry.action || '').trim(); + const cleanStage = rawStage + .replace(/\[Master data\]\s*/i, '') + .replace(/Constitutional change [A-Z0-9-]+ (completed|updated):/i, '$1:') + .replace(/dealer constitution updated from "(.*)" to "(.*)"\.?/i, '"$1" → "$2"') + .trim(); + + // Human-readable heading: prefer stage, fall back to cleaned description + const heading = cleanStage || pres.badge || 'Action'; + + // Remarks: prefer user-entered remarks, strip auto-generated system strings + const rawRemarks = String(entry.remarks || '').trim(); + const rawDescription = String(entry.description || '').trim(); + const systemPrefixes = [/^Approval\s*-\s*Stage:/i, /^Record (created|updated)/i, /^Document (uploaded|verified|rejected)/i]; + const isSystemDesc = systemPrefixes.some(re => re.test(rawDescription)); + const displayRemarks = rawRemarks || (!isSystemDesc ? rawDescription : '') || null; + + return ( +
+
+ {pres.variant === 'success' ? ( + + ) : pres.variant === 'danger' ? ( + + ) : pres.variant === 'pending' ? ( + + ) : ( + + )} +
+
+
+
+

{heading}

+

by {actorName}

+
+ + {pres.badge.replace(/_/g, ' ')} + +
+ {displayRemarks && ( +
+ {displayRemarks} +
+ )} +

+ {formatDateTime(entry.timestamp || entry.createdAt)} +

+
-
- ); + ); })} {historyEntries.length === 0 && (
@@ -1138,7 +1157,7 @@ export function ConstitutionalChangeDetails({ requestId, onBack, currentUser }: {permissions.canApprove && ( - )} - + {permissions.canReject && ( -
)} -
-