From 2f826995726786ca9377bf2d644dd19d76b64861 Mon Sep 17 00:00:00 2001 From: laxman h Date: Thu, 30 Apr 2026 18:52:17 +0530 Subject: [PATCH] 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 --- src/App.tsx | 13 +- src/components/layout/Sidebar.tsx | 13 +- src/features/master/pages/MasterPage.tsx | 361 +++++++++--------- .../ApplicationDetailsActionModals.tsx | 24 +- .../ApplicationDetailsTabs.tsx | 10 +- .../useApplicationDetailsAdminActions.ts | 57 ++- .../hooks/useApplicationDetailsUIState.ts | 2 + .../onboarding/pages/ApplicationDetails.tsx | 12 + .../resignation/pages/ResignationDetails.tsx | 70 +++- .../termination/pages/TerminationDetails.tsx | 149 +++++--- src/lib/offboardingDocumentOptions.ts | 3 +- src/styles/globals.css | 6 + 12 files changed, 460 insertions(+), 260 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index 6df7194..d7ac911 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -78,10 +78,15 @@ export default function App() { const location = useLocation(); const currentRole = currentUser?.role || currentUser?.roleCode || ''; const normalizedRole = String(currentRole).trim().toLowerCase(); - const hasRole = (roles: string[]) => roles.map((r) => r.toLowerCase()).includes(normalizedRole); - const resignationRoles = ['DD Admin', 'ASM', 'RBM', 'DD Lead', 'ZBH', 'NBH', 'Legal', 'Legal Admin', 'Super Admin']; - const terminationRoles = ['ASM', 'RBM', 'ZBH', 'DD Lead', 'DD Head', 'NBH', 'Legal Admin', 'Legal', 'DD Admin', 'CCO', 'CEO', 'Super Admin']; - const fnfRoles = ['DD Admin', 'DD Lead', 'NBH', 'Finance', 'Finance Admin', 'Super Admin']; + const hasRole = (roles: string[]) => { + const normalizedTargetRoles = roles.map((r) => r.toLowerCase()); + const userRole = String(currentUser?.role || '').toLowerCase(); + const userRoleCode = String(currentUser?.roleCode || '').toLowerCase(); + 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']; useEffect(() => { diff --git a/src/components/layout/Sidebar.tsx b/src/components/layout/Sidebar.tsx index c3001f9..f31329e 100644 --- a/src/components/layout/Sidebar.tsx +++ b/src/components/layout/Sidebar.tsx @@ -48,11 +48,16 @@ export function Sidebar({ onLogout }: SidebarProps) { const currentRole = currentUser?.role || currentUser?.roleCode || ''; const normalizedRole = String(currentRole).trim().toLowerCase(); - const hasRole = (roles: string[]) => roles.map((r) => r.toLowerCase()).includes(normalizedRole); + const hasRole = (roles: string[]) => { + const normalizedTargetRoles = roles.map((r) => r.toLowerCase()); + 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 terminationRoles = ['ASM', 'RBM', 'ZBH', 'DD Lead', 'DD Head', 'NBH', 'Legal Admin', 'Legal', 'DD Admin', 'CCO', 'CEO', 'Super Admin']; - const fnfRoles = ['DD Admin', 'DD Lead', 'NBH', 'Finance', 'Finance 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', '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 canSeeResignation = hasRole(resignationRoles); const canSeeTermination = hasRole(terminationRoles); const canSeeFnF = hasRole(fnfRoles); diff --git a/src/features/master/pages/MasterPage.tsx b/src/features/master/pages/MasterPage.tsx index d6c539f..c7415b9 100644 --- a/src/features/master/pages/MasterPage.tsx +++ b/src/features/master/pages/MasterPage.tsx @@ -1,7 +1,7 @@ import React, { useState, useEffect, useCallback, useMemo } from 'react'; import { useSelector } from 'react-redux'; -import { - Tabs, TabsContent, TabsList, TabsTrigger +import { + Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { Globe, Shield, Mail, MapPin, SlidersHorizontal, Settings, FileText, Settings2 } from 'lucide-react'; import { Badge } from '@/components/ui/badge'; @@ -38,13 +38,13 @@ import { RootState } from '@/store'; export const MasterPage: React.FC = () => { const { fetchInitialData, fetchAreas } = useMasterData(); - const { + const { asms, zonalManagerMappings, allStates, - allDistricts, + allDistricts, users, roles, - loading + loading } = useSelector((state: RootState) => state.master); // Tab & Selection State @@ -67,7 +67,7 @@ export const MasterPage: React.FC = () => { const [selectedASMStates, setSelectedASMStates] = useState([]); const [selectedASMDistricts, setSelectedASMDistricts] = useState([]); const [asmRoleCode, setAsmRoleCode] = useState<'ASM' | 'DD-AM'>('DD-AM'); - + // ZM Management State const [showZMDialog, setShowZMDialog] = useState(false); const [editingZMId, setEditingZMId] = useState(null); @@ -161,11 +161,11 @@ export const MasterPage: React.FC = () => { return; } try { - const payload = { - userId: asmManagerId, + const payload = { + userId: asmManagerId, roleCode: asmRoleCode, - districts: selectedASMDistricts, - status: asmStatus + districts: selectedASMDistricts, + status: asmStatus }; const res = await masterService.saveASM(payload) as any; if (res.success) { @@ -175,9 +175,9 @@ export const MasterPage: React.FC = () => { } else { toast.error(res.message || 'Failed to save ASM'); } - } catch (error: any) { - const msg = error?.response?.data?.message || error?.message || 'Failed to save ASM'; - toast.error(msg); + } catch (error: any) { + const msg = error?.response?.data?.message || error?.message || 'Failed to save ASM'; + toast.error(msg); } }; @@ -209,13 +209,13 @@ export const MasterPage: React.FC = () => { return; } try { - const payload = { - userId: zmManagerId, + const payload = { + userId: zmManagerId, zoneId: selectedZMZone, - regionIds: selectedZMRegions, + regionIds: selectedZMRegions, status: zmStatus }; - + const res = await (masterService as any).saveZonalManager(payload) as any; if (res.success) { toast.success(`Zonal Manager ${editingZMId ? 'updated' : 'assigned'} successfully`); @@ -224,9 +224,9 @@ export const MasterPage: React.FC = () => { } else { toast.error(res.message || 'Failed to save Zonal Manager'); } - } catch (error: any) { - const msg = error?.response?.data?.message || error?.message || 'Failed to save Zonal Manager'; - toast.error(msg); + } catch (error: any) { + const msg = error?.response?.data?.message || error?.message || 'Failed to save Zonal Manager'; + toast.error(msg); } }; @@ -234,99 +234,99 @@ export const MasterPage: React.FC = () => { const handleSaveZone = async () => { try { - const payload = { - id: editingZoneId, - name: zoneName, - code: zoneCode, - description: zoneDescription, - managerId: zonalBusinessHeadId === 'none' ? null : zonalBusinessHeadId - }; - const res = await masterService.saveZone(payload) as any; - if (res.success) { - toast.success('Zone saved successfully'); - setShowZoneDialog(false); - fetchInitialData(); - } else { - toast.error(res.message || 'Error saving zone'); - } - } catch (error: any) { - const msg = error?.response?.data?.message || error?.message || 'Error saving zone'; - toast.error(msg); + const payload = { + id: editingZoneId, + name: zoneName, + code: zoneCode, + description: zoneDescription, + managerId: zonalBusinessHeadId === 'none' ? null : zonalBusinessHeadId + }; + const res = await masterService.saveZone(payload) as any; + if (res.success) { + toast.success('Zone saved successfully'); + setShowZoneDialog(false); + fetchInitialData(); + } else { + toast.error(res.message || 'Error saving zone'); + } + } catch (error: any) { + const msg = error?.response?.data?.message || error?.message || 'Error saving zone'; + toast.error(msg); } }; const handleSaveRegion = async () => { try { - const payload = { - ...(editingRegionId ? { id: editingRegionId } : {}), - name: regionName, - description: regionDescription, - parentId: selectedRegionZone, - managerId: regionalManagerId, - districts: selectedRegionDistricts, - status: 'Active' - }; - const res = await masterService.saveRegion(payload) as any; - if (res.success) { - toast.success('Region saved successfully'); - setShowRegionDialog(false); - fetchInitialData(); - } else { - toast.error(res.message || 'Error saving region'); - } - } catch (error: any) { - const msg = error?.response?.data?.message || error?.message || 'Error saving region'; - toast.error(msg); + const payload = { + ...(editingRegionId ? { id: editingRegionId } : {}), + name: regionName, + description: regionDescription, + parentId: selectedRegionZone, + managerId: regionalManagerId, + districts: selectedRegionDistricts, + status: 'Active' + }; + const res = await masterService.saveRegion(payload) as any; + if (res.success) { + toast.success('Region saved successfully'); + setShowRegionDialog(false); + fetchInitialData(); + } else { + toast.error(res.message || 'Error saving region'); + } + } catch (error: any) { + const msg = error?.response?.data?.message || error?.message || 'Error saving region'; + toast.error(msg); } }; const handleSaveTemplate = async (body: string) => { try { - if (!editingTemplate?.id) { - toast.error('Open a template from the list to edit.'); - return; - } - const res = await masterService.updateEmailTemplate(editingTemplate.id, { - ...editingTemplate, - body - }) as any; - if (res.success) { - toast.success('Template saved'); - setShowTemplateDialog(false); - fetchInitialData(); - } else { - toast.error(res.message || 'Error saving template'); - } + if (!editingTemplate?.id) { + toast.error('Open a template from the list to edit.'); + return; + } + const res = await masterService.updateEmailTemplate(editingTemplate.id, { + ...editingTemplate, + body + }) as any; + if (res.success) { + toast.success('Template saved'); + setShowTemplateDialog(false); + fetchInitialData(); + } else { + toast.error(res.message || 'Error saving template'); + } } catch (error: any) { - const msg = error?.response?.data?.message || error?.message || 'Error saving template'; - toast.error(msg); + const msg = error?.response?.data?.message || error?.message || 'Error saving template'; + toast.error(msg); } }; const handlePreviewTemplate = async (body: string) => { setPreviewLoading(true); try { - let data: Record; - try { - data = JSON.parse(testDataInput) as Record; - } catch { - toast.error('Mock test data must be valid JSON'); - return; - } - const res = await masterService.previewEmailTemplate({ - subject: editingTemplate?.subject, - body, - data - }) as any; - if (res.success) { - setPreviewContent(res.data); - } else { - toast.error(res.message || 'Preview failed'); - } + let data: Record; + try { + data = JSON.parse(testDataInput) as Record; + } catch { + toast.error('Mock test data must be valid JSON'); + return; + } + const res = await masterService.previewEmailTemplate({ + subject: editingTemplate?.subject, + body, + data + }) as any; + if (res.success) { + setPreviewContent(res.data); + } else { + toast.error(res.message || 'Preview failed'); + } } catch (error: any) { - const d = error?.response?.data; - const detail = d?.error || d?.message; - toast.error(detail || error?.message || 'Preview failed'); + const d = error?.response?.data; + const detail = d?.error || d?.message; + toast.error(detail || error?.message || 'Preview failed'); } finally { setPreviewLoading(false); } }; @@ -340,9 +340,9 @@ export const MasterPage: React.FC = () => { } else { 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'; - toast.error(msg); + toast.error(msg); } }; @@ -385,48 +385,48 @@ export const MasterPage: React.FC = () => { const handleSaveLocation = async () => { try { - if (!locationState) { - toast.error('Please select a state'); - return; - } - if (!locationDistrict) { - toast.error('Please select a district'); - return; - } + if (!locationState) { + toast.error('Please select a state'); + return; + } + if (!locationDistrict) { + toast.error('Please select a district'); + return; + } - const selectedState = allStates.find((s: any) => s.id === locationState); - const selectedDistrict = allDistricts.find((d: any) => d.id === locationDistrict); - const payload = { - id: editingLocationId, - stateId: locationState, - stateName: (selectedState as any)?.name || (selectedState as any)?.stateName || '', - districtId: locationDistrict, - name: locationCity || selectedDistrict?.name || 'New Location', - city: locationCity, - status: locationStatus, - openFrom: locationActiveFrom, - openTo: locationActiveTo, - isOpportunity: locationStatus === 'active' - }; - const res = await (editingLocationId - ? masterService.updateArea(editingLocationId, payload) - : masterService.createArea(payload)) as any; - if (res.success) { - toast.success('Location saved'); - setShowLocationDialog(false); - fetchAreas({ search: districtsSearch, page: districtsPage }); - } + const selectedState = allStates.find((s: any) => s.id === locationState); + const selectedDistrict = allDistricts.find((d: any) => d.id === locationDistrict); + const payload = { + id: editingLocationId, + stateId: locationState, + stateName: (selectedState as any)?.name || (selectedState as any)?.stateName || '', + districtId: locationDistrict, + name: locationCity || selectedDistrict?.name || 'New Location', + city: locationCity, + status: locationStatus, + openFrom: locationActiveFrom, + openTo: locationActiveTo, + isOpportunity: locationStatus === 'active' + }; + const res = await (editingLocationId + ? masterService.updateArea(editingLocationId, payload) + : masterService.createArea(payload)) as any; + if (res.success) { + toast.success('Location saved'); + setShowLocationDialog(false); + fetchAreas({ search: districtsSearch, page: districtsPage }); + } } catch (error) { toast.error('Error saving location'); } }; useEffect(() => { const handler = setTimeout(() => { - fetchAreas({ - search: districtsSearch, - page: districtsPage, - stateId: locationStateFilter === 'all' ? undefined : locationStateFilter, - isOpportunity: locationStatusFilter === 'all' ? undefined : (locationStatusFilter === 'active' ? 'true' : 'false') - }); + fetchAreas({ + search: districtsSearch, + page: districtsPage, + stateId: locationStateFilter === 'all' ? undefined : locationStateFilter, + isOpportunity: locationStatusFilter === 'all' ? undefined : (locationStatusFilter === 'active' ? 'true' : 'false') + }); }, 500); return () => clearTimeout(handler); }, [districtsSearch, districtsPage, locationStateFilter, locationStatusFilter, fetchAreas]); @@ -438,7 +438,6 @@ export const MasterPage: React.FC = () => {

Master Configuration

Centralized governance for locations, roles, and operational policies

- Admin Control Panel {loading ? ( @@ -477,28 +476,28 @@ export const MasterPage: React.FC = () => { setSelectedZone(selectedZone === id ? 'all' : id)} /> - { setEditingZoneId(null); setZoneName(''); setZoneCode(''); setZoneDescription(''); setZonalBusinessHeadId('none'); setShowZoneDialog(true); }} + { 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); }} /> - + { setEditingRegionId(null); setRegionName(''); setSelectedRegionZone(selectedZone === 'all' ? '' : selectedZone); setRegionalManagerId(''); setSelectedRegionDistricts([]); setShowRegionDialog(true); }} - onEditRegion={(r) => { - setEditingRegionId(r.id); - setRegionName(r.name); - setSelectedRegionZone(r.zoneId); - setRegionalManagerId(r.regionalManager?.id || ''); - setSelectedRegionDistricts(r.districts?.map((d: any) => d.id) || []); - setShowRegionDialog(true); + onEditRegion={(r) => { + setEditingRegionId(r.id); + setRegionName(r.name); + setSelectedRegionZone(r.zoneId); + setRegionalManagerId(r.regionalManager?.id || ''); + setSelectedRegionDistricts(r.districts?.map((d: any) => d.id) || []); + setShowRegionDialog(true); }} onDeleteRegion={() => toast.error('Regional office deletion is restricted via portal')} /> - { + { setEditingZMId(null); setZmManagerId(''); setZmStatus('active'); setSelectedZMZone(selectedZone === 'all' ? '' : selectedZone); setSelectedZMRegions([]); - setShowZMDialog(true); - }} - onEditZM={handleEditZM} + setShowZMDialog(true); + }} + onEditZM={handleEditZM} onDeleteZM={() => toast.error('ZM deletion restricted')} /> { setEditingASMId(null); setAsmManagerId(''); setSelectedASMZone(selectedZone === 'all' ? '' : selectedZone); setSelectedASMRegion(''); setSelectedASMStates([]); setSelectedASMDistricts([]); setAsmRoleCode('DD-AM'); setShowASMDialog(true); }} @@ -529,13 +528,13 @@ export const MasterPage: React.FC = () => { setTestDataInput('{}'); } setShowTemplateDialog(true); - }} - onDeleteTemplate={() => toast.error('Delete Template restricted')} + }} + onDeleteTemplate={() => toast.error('Delete Template restricted')} /> - { @@ -557,21 +556,21 @@ export const MasterPage: React.FC = () => { setLocationStatus('active'); setShowLocationDialog(true); }} - onEditLocation={handleEditLocation} + onEditLocation={handleEditLocation} onDeleteLocation={(id) => { if (window.confirm('Are you sure you want to delete this location?')) { (masterService as any).deleteArea(id).then((res: any) => { if (res.success) { - toast.success('Location deleted'); - fetchAreas({ - search: districtsSearch, - page: districtsPage, - stateId: locationStateFilter === 'all' ? undefined : locationStateFilter - }); + toast.success('Location deleted'); + fetchAreas({ + search: districtsSearch, + page: districtsPage, + stateId: locationStateFilter === 'all' ? undefined : locationStateFilter + }); } }); } - }} + }} onSearch={(term) => { setDistrictsSearch(term); setDistrictsPage(1); // Reset to first page on search @@ -582,41 +581,41 @@ export const MasterPage: React.FC = () => { - + - + - + - + - + )} {/* Main Dialogs */} - 0 ? users.map(u => ({...u, name: u.fullName || u.name, role: u.role?.roleName, roleCode: u.role?.roleCode, allRoles: u.allRoles})) : asms} /> - 0 ? users.map(u => ({...u, name: u.fullName || u.name, role: u.role?.roleName, roleCode: u.role?.roleCode, allRoles: u.allRoles})) : asms} /> - 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)} /> - 0 ? users.map(u => ({...u, name: u.fullName || u.name, role: u.role?.roleName, roleCode: u.role?.roleCode, allRoles: u.allRoles})) : asms} + 0 ? users.map(u => ({ ...u, name: u.fullName || u.name, role: u.role?.roleName, roleCode: u.role?.roleCode, allRoles: u.allRoles })) : asms} /> + 0 ? users.map(u => ({ ...u, name: u.fullName || u.name, role: u.role?.roleName, roleCode: u.role?.roleCode, allRoles: u.allRoles })) : asms} /> + 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)} /> + 0 ? users.map(u => ({ ...u, name: u.fullName || u.name, role: u.role?.roleName, roleCode: u.role?.roleCode, allRoles: u.allRoles })) : asms} /> diff --git a/src/features/onboarding/components/application-details/ApplicationDetailsActionModals.tsx b/src/features/onboarding/components/application-details/ApplicationDetailsActionModals.tsx index 48695e4..d491da8 100644 --- a/src/features/onboarding/components/application-details/ApplicationDetailsActionModals.tsx +++ b/src/features/onboarding/components/application-details/ApplicationDetailsActionModals.tsx @@ -35,6 +35,8 @@ interface ApplicationDetailsActionModalsProps { setInterviewIdToCancel: (value: string) => void; isCancellingInterview: boolean; handleConfirmCancelInterview: () => void; + interviewToReschedule: any; + setInterviewToReschedule: (value: any) => void; interviewType: string; setInterviewType: (value: string) => void; interviewMode: string; @@ -99,6 +101,8 @@ export function ApplicationDetailsActionModals(props: ApplicationDetailsActionMo setInterviewIdToCancel, isCancellingInterview, handleConfirmCancelInterview, + interviewToReschedule, + setInterviewToReschedule, interviewType, setInterviewType, interviewMode, @@ -252,10 +256,13 @@ export function ApplicationDetailsActionModals(props: ApplicationDetailsActionMo - + { + setShowScheduleModal(open); + if (!open) setInterviewToReschedule(null); + }}> - Schedule Interview + {interviewToReschedule ? 'Reschedule Interview' : 'Schedule Interview'} Set up an interview session with the applicant and relevant team members.
@@ -304,10 +311,15 @@ export function ApplicationDetailsActionModals(props: ApplicationDetailsActionMo
)} -
- - -
+
+ + +
diff --git a/src/features/onboarding/components/application-details/ApplicationDetailsTabs.tsx b/src/features/onboarding/components/application-details/ApplicationDetailsTabs.tsx index 81449a3..041e5c5 100644 --- a/src/features/onboarding/components/application-details/ApplicationDetailsTabs.tsx +++ b/src/features/onboarding/components/application-details/ApplicationDetailsTabs.tsx @@ -51,6 +51,7 @@ interface ApplicationDetailsTabsProps { setShowUploadForm: (value: boolean) => void; handleRetriggerEvaluators: () => void; handleCancelInterview: (interviewId: any) => void; + handleRescheduleInterview: (interview: any) => void; setSelectedEvaluationForView: (value: any) => void; setShowFeedbackDetailsModal: (value: boolean) => void; renderFddAuditContent: () => React.ReactNode; @@ -86,6 +87,7 @@ export function ApplicationDetailsTabs(props: ApplicationDetailsTabsProps) { setShowUploadForm, handleRetriggerEvaluators, handleCancelInterview, + handleRescheduleInterview, setSelectedEvaluationForView, setShowFeedbackDetailsModal, renderFddAuditContent, @@ -627,11 +629,11 @@ export function ApplicationDetailsTabs(props: ApplicationDetailsTabsProps) { )} diff --git a/src/features/onboarding/hooks/useApplicationDetailsAdminActions.ts b/src/features/onboarding/hooks/useApplicationDetailsAdminActions.ts index 8aaf9f1..49f3f49 100644 --- a/src/features/onboarding/hooks/useApplicationDetailsAdminActions.ts +++ b/src/features/onboarding/hooks/useApplicationDetailsAdminActions.ts @@ -17,10 +17,15 @@ interface UseApplicationDetailsAdminActionsParams { participantType: string; users: any[]; interviewDate: string; + setInterviewDate: Dispatch>; interviewType: string; + setInterviewType: Dispatch>; interviewMode: string; + setInterviewMode: Dispatch>; meetingLink: string; + setMeetingLink: Dispatch>; location: string; + setLocation: Dispatch>; scheduledInterviewParticipants: any[]; uploadFile: File | null; uploadDocType: string; @@ -45,6 +50,8 @@ interface UseApplicationDetailsAdminActionsParams { setShowCancelInterviewModal: Dispatch>; interviewIdToCancel: string; setInterviewIdToCancel: Dispatch>; + interviewToReschedule: any; + setInterviewToReschedule: Dispatch>; setIsCancellingInterview: Dispatch>; setIsUploading: Dispatch>; setShowUploadForm: Dispatch>; @@ -79,10 +86,15 @@ export function useApplicationDetailsAdminActions(params: UseApplicationDetailsA participantType, users, interviewDate, + setInterviewDate, interviewType, + setInterviewType, interviewMode, + setInterviewMode, meetingLink, + setMeetingLink, location, + setLocation, scheduledInterviewParticipants, uploadFile, uploadDocType, @@ -107,6 +119,8 @@ export function useApplicationDetailsAdminActions(params: UseApplicationDetailsA setShowCancelInterviewModal, interviewIdToCancel, setInterviewIdToCancel, + interviewToReschedule, + setInterviewToReschedule, setIsCancellingInterview, setIsUploading, setShowUploadForm, @@ -176,7 +190,7 @@ export function useApplicationDetailsAdminActions(params: UseApplicationDetailsA }, [currentUser, application, setUsers]); const prefillInterviewParticipants = useCallback(() => { - if (!showScheduleModal || !application) return; + if (!showScheduleModal || !application || interviewToReschedule) return; const levelNum = parseInt(interviewType.replace('level', '')) || 1; const requiredRolesByLevel: Record = { 1: ['DD-ZM', 'RBM'], @@ -233,7 +247,7 @@ export function useApplicationDetailsAdminActions(params: UseApplicationDetailsA } }); setScheduledInterviewParticipants(unique); - }, [showScheduleModal, application, interviewType, setScheduledInterviewParticipants]); + }, [showScheduleModal, application, interviewType, interviewToReschedule, setScheduledInterviewParticipants]); const handleScheduleInterview = async () => { if (!interviewDate) { @@ -242,20 +256,32 @@ export function useApplicationDetailsAdminActions(params: UseApplicationDetailsA } try { setIsScheduling(true); - await onboardingService.scheduleInterview({ + const payload = { applicationId: application?.id, level: interviewType, scheduledAt: interviewDate, type: interviewMode === 'virtual' ? 'Virtual Interview' : 'Physical Interview', location: interviewMode === 'virtual' ? meetingLink : location, 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); + setInterviewToReschedule(null); await fetchInterviews(); await fetchApplication(); } catch { - toast.error('Failed to schedule interview'); + toast.error(interviewToReschedule ? 'Failed to reschedule interview' : 'Failed to schedule interview'); } finally { setIsScheduling(false); } @@ -266,6 +292,24 @@ export function useApplicationDetailsAdminActions(params: UseApplicationDetailsA 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 () => { if (!interviewIdToCancel) return; try { @@ -606,6 +650,7 @@ export function useApplicationDetailsAdminActions(params: UseApplicationDetailsA fetchUsers, maybeFetchUsersForModal, handleScheduleInterview, + handleRescheduleInterview, handleCancelInterview, handleConfirmCancelInterview, handleUpload, diff --git a/src/features/onboarding/hooks/useApplicationDetailsUIState.ts b/src/features/onboarding/hooks/useApplicationDetailsUIState.ts index 006381d..ed13a90 100644 --- a/src/features/onboarding/hooks/useApplicationDetailsUIState.ts +++ b/src/features/onboarding/hooks/useApplicationDetailsUIState.ts @@ -19,6 +19,7 @@ export function useApplicationDetailsUIState({ initialTab = 'questionnaire' }: U const [showScheduleModal, setShowScheduleModal] = useState(false); const [showCancelInterviewModal, setShowCancelInterviewModal] = useState(false); const [interviewIdToCancel, setInterviewIdToCancel] = useState(''); + const [interviewToReschedule, setInterviewToReschedule] = useState(null); const [showKTMatrixModal, setShowKTMatrixModal] = useState(false); const [showLevel2FeedbackModal, setShowLevel2FeedbackModal] = useState(false); const [showLevel3FeedbackModal, setShowLevel3FeedbackModal] = useState(false); @@ -98,6 +99,7 @@ export function useApplicationDetailsUIState({ initialTab = 'questionnaire' }: U showScheduleModal, setShowScheduleModal, showCancelInterviewModal, setShowCancelInterviewModal, interviewIdToCancel, setInterviewIdToCancel, + interviewToReschedule, setInterviewToReschedule, showKTMatrixModal, setShowKTMatrixModal, showLevel2FeedbackModal, setShowLevel2FeedbackModal, showLevel3FeedbackModal, setShowLevel3FeedbackModal, diff --git a/src/features/onboarding/pages/ApplicationDetails.tsx b/src/features/onboarding/pages/ApplicationDetails.tsx index 3264c3d..eb32d88 100644 --- a/src/features/onboarding/pages/ApplicationDetails.tsx +++ b/src/features/onboarding/pages/ApplicationDetails.tsx @@ -62,6 +62,7 @@ export const ApplicationDetails = () => { showScheduleModal, setShowScheduleModal, showCancelInterviewModal, setShowCancelInterviewModal, interviewIdToCancel, setInterviewIdToCancel, + interviewToReschedule, setInterviewToReschedule, showKTMatrixModal, setShowKTMatrixModal, showLevel2FeedbackModal, setShowLevel2FeedbackModal, showLevel3FeedbackModal, setShowLevel3FeedbackModal, @@ -266,6 +267,7 @@ export const ApplicationDetails = () => { handleRemoveInterviewer, maybeFetchUsersForModal, handleScheduleInterview, + handleRescheduleInterview, handleCancelInterview, handleConfirmCancelInterview, handleUpload, @@ -291,10 +293,15 @@ export const ApplicationDetails = () => { participantType, users, interviewDate, + setInterviewDate, interviewType, + setInterviewType, interviewMode, + setInterviewMode, meetingLink, + setMeetingLink, location, + setLocation, scheduledInterviewParticipants, uploadFile, uploadDocType, @@ -319,6 +326,8 @@ export const ApplicationDetails = () => { setShowCancelInterviewModal, interviewIdToCancel, setInterviewIdToCancel, + interviewToReschedule, + setInterviewToReschedule, setIsCancellingInterview, setIsUploading, setShowUploadForm, @@ -445,6 +454,7 @@ export const ApplicationDetails = () => { setShowUploadForm={setShowUploadForm} handleRetriggerEvaluators={handleRetriggerEvaluators} handleCancelInterview={handleCancelInterview} + handleRescheduleInterview={handleRescheduleInterview} setSelectedEvaluationForView={setSelectedEvaluationForView} setShowFeedbackDetailsModal={setShowFeedbackDetailsModal} renderFddAuditContent={renderFddAuditContent} @@ -533,6 +543,8 @@ export const ApplicationDetails = () => { setInterviewIdToCancel={setInterviewIdToCancel} isCancellingInterview={isCancellingInterview} handleConfirmCancelInterview={handleConfirmCancelInterview} + interviewToReschedule={interviewToReschedule} + setInterviewToReschedule={setInterviewToReschedule} interviewType={interviewType} setInterviewType={setInterviewType} interviewMode={interviewMode} diff --git a/src/features/resignation/pages/ResignationDetails.tsx b/src/features/resignation/pages/ResignationDetails.tsx index 4327883..d49aec2 100644 --- a/src/features/resignation/pages/ResignationDetails.tsx +++ b/src/features/resignation/pages/ResignationDetails.tsx @@ -9,7 +9,7 @@ import { Textarea } from '@/components/ui/textarea'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { Input } from '@/components/ui/input'; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'; -import { useState, useEffect } from 'react'; +import { useState, useEffect, useMemo } from 'react'; import { useNavigate } from 'react-router-dom'; import { User as UserType } from '@/lib/mock-data'; import { toast } from 'sonner'; @@ -97,6 +97,16 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig const [uploadFile, setUploadFile] = useState(null); const [uploadDocType, setUploadDocType] = useState(RESIGNATION_DOCUMENT_TYPES[0]); 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 () => { try { setIsLoading(true); @@ -132,13 +142,13 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig { id: 3, name: 'ZBH Review', key: 'ZBH', description: 'Zonal Business Head approval' }, { id: 4, name: 'DD Lead Review', key: 'DD Lead', description: 'DD Lead final review' }, { id: 5, name: 'NBH Approval', key: 'NBH', description: 'National Business Head approval' }, - { id: 6, name: 'DD Admin Review', key: 'DD Admin', description: 'DD Admin verification' }, - { id: 7, name: 'Legal - Resignation Letter', key: 'Legal', description: 'Legal team issues resignation approval letter' }, + { id: 6, name: 'Legal - Resignation Letter', key: 'Legal', description: 'Legal team issues resignation approval letter' }, + { id: 7, name: 'DD Admin Review', key: 'DD Admin', description: 'DD Admin verification and final closure' }, { id: 8, name: 'F&F Settlement', key: 'F&F Initiated', description: 'Full & Final settlement process' }, { id: 9, 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 = ['ASM', 'RBM', 'ZBH', 'DD Lead', 'NBH', 'Legal', 'DD Admin', 'F&F Initiated', 'Completed']; const legalStageApproved = (() => { if (!resignationData) return false; @@ -157,6 +167,7 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig const atLegal = stage === 'legal' || stage === 'legal - resignation letter'; const legalApprovedTransition = targetStage === 'legal' || + targetStage === 'dd admin' || targetStage === 'f&f initiated' || targetStage === 'fnf_initiated' || action.includes('approved'); @@ -173,8 +184,17 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig const currentStage = resignationData.currentStage; const status = resignationData.status; 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); // Check if it's already in the settlement phase @@ -184,18 +204,25 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig const nbhIndex = stagesOrdered.indexOf('NBH'); 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 canApprove = isCurrentlyAssigned && !isFinalState && !isSettlementPhase && - !(currentStage === 'Legal' && legalStageApproved); + !hasAlreadyPartiallyApproved && + !(currentStage === 'Legal' && legalStageApproved) && + !(isDDLead && isDDLeadStage && !hasUploadedPPT); return { canApprove, canSendBack: isCurrentlyAssigned && !isFinalState && !isSettlementPhase && stageIndex > 0, 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) && !isSettlementPhase && !isFinalState, canAssign: userRole !== 'Dealer' && !isFinalState @@ -203,7 +230,7 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig }; const permissions = getResignationPermissions(); - const isNationalLevel = ['Super Admin', 'DD Lead', 'DD Head', 'NBH', 'DD Admin', 'Legal Admin'].includes(currentUser?.role || ''); + const isNationalLevel = ['Super Admin', 'DD Lead', 'DD Head', 'NBH', 'DD Admin', 'Legal Admin', 'DD-ZM'].includes(currentUser?.role || ''); const stageAliases: Record = { 'ASM': ['ASM', 'ASM Review', 'Request Initiated'], @@ -447,6 +474,31 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
Workflow Actions: + {/* Debug for PPT button visibility */} + {(() => { + 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 ( + + ); + } + return null; + })()} {permissions.canApprove && ( )}
- {(timelineEntry?.timestamp || stage.date) && ( -
- - {formatDateTime(timelineEntry?.timestamp || stage.date)} + {(stageEntries[0]?.timestamp || stage.date) && ( +
+ + {formatDateTime(stageEntries[0]?.timestamp || stage.date)}
)}

{stage.description}

- {timelineEntry && ( -
-
- {timelineEntry.action || 'Updated'} - by {timelineEntry.user || 'System'} -
-
-
-
- -

{timelineEntry.remarks || 'No remarks provided.'}

+ {stageEntries.length > 0 && ( +
+ {stageEntries.map((entry: any, entryIdx: number) => { + const rawRemarks = entry.remarks || entry.comments || ''; + const isAttachment = rawRemarks?.startsWith('Attachment:'); + const remarksContent = isAttachment + ? rawRemarks.replace('Attachment:', '').trim() + : rawRemarks; + + return ( +
+
+ + {entry.action || 'Action'} + + + by {entry.user || 'System'} • {formatDateTime(entry.timestamp)} + +
+
+ {isAttachment ? ( +
+ + {remarksContent} +
+ ) : ( +

{remarksContent || 'No remarks provided.'}

+ )} +
-
-
+ ); + })}
)}
diff --git a/src/lib/offboardingDocumentOptions.ts b/src/lib/offboardingDocumentOptions.ts index 1c355ac..81152dc 100644 --- a/src/lib/offboardingDocumentOptions.ts +++ b/src/lib/offboardingDocumentOptions.ts @@ -5,6 +5,7 @@ export const RESIGNATION_DOCUMENT_TYPES = [ "Legal Communication", "Handover Document", "Settlement Supporting Document", + "PPT Presentation", "Other", ] as const; @@ -32,7 +33,7 @@ export const TERMINATION_DOCUMENT_TYPES = [ export const TERMINATION_STAGE_OPTIONS = [ "Submitted", - "RBM Review", + "RBM + DD-ZM Review", "ZBH Review", "DD Lead Review", "Legal Verification", diff --git a/src/styles/globals.css b/src/styles/globals.css index 1e6bd21..fbb4ee1 100644 --- a/src/styles/globals.css +++ b/src/styles/globals.css @@ -11,6 +11,9 @@ --popover: oklch(1 0 0); --popover-foreground: oklch(0.145 0 0); --primary: #da291c; + --primary-600: #da291c; + --primary-700: #b82216; + --primary-50: #fef2f2; --primary-foreground: oklch(1 0 0); --secondary: oklch(0.95 0.0058 264.53); --secondary-foreground: #030213; @@ -99,6 +102,9 @@ --color-popover: var(--popover); --color-popover-foreground: var(--popover-foreground); --color-primary: var(--primary); + --color-primary-600: var(--primary-600); + --color-primary-700: var(--primary-700); + --color-primary-50: var(--primary-50); --color-primary-foreground: var(--primary-foreground); --color-secondary: var(--secondary); --color-secondary-foreground: var(--secondary-foreground);