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
This commit is contained in:
laxman h 2026-04-30 18:52:17 +05:30
parent 95032cf2a7
commit 2f82699572
12 changed files with 460 additions and 260 deletions

View File

@ -78,10 +78,15 @@ export default function App() {
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 normalizedRole = String(currentRole).trim().toLowerCase();
const hasRole = (roles: string[]) => roles.map((r) => r.toLowerCase()).includes(normalizedRole); const hasRole = (roles: string[]) => {
const resignationRoles = ['DD Admin', 'ASM', 'RBM', 'DD Lead', 'ZBH', 'NBH', 'Legal', 'Legal Admin', 'Super Admin']; const normalizedTargetRoles = roles.map((r) => r.toLowerCase());
const terminationRoles = ['ASM', 'RBM', 'ZBH', 'DD Lead', 'DD Head', 'NBH', 'Legal Admin', 'Legal', 'DD Admin', 'CCO', 'CEO', 'Super Admin']; const userRole = String(currentUser?.role || '').toLowerCase();
const fnfRoles = ['DD Admin', 'DD Lead', 'NBH', 'Finance', 'Finance Admin', 'Super Admin']; 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']; const financeRoles = ['Finance', 'Finance Admin'];
useEffect(() => { useEffect(() => {

View File

@ -48,11 +48,16 @@ export function Sidebar({ onLogout }: SidebarProps) {
const currentRole = currentUser?.role || currentUser?.roleCode || ''; const currentRole = currentUser?.role || currentUser?.roleCode || '';
const normalizedRole = String(currentRole).trim().toLowerCase(); 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 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);

View File

@ -1,7 +1,7 @@
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 { Badge } from '@/components/ui/badge';
@ -38,13 +38,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 +67,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 +161,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 +175,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 +209,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 +224,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 +234,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 +340,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 +385,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,7 +438,6 @@ 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 ? (
@ -477,28 +476,28 @@ export const MasterPage: React.FC = () => {
<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 +528,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 +556,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 +581,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

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

@ -51,6 +51,7 @@ interface ApplicationDetailsTabsProps {
setShowUploadForm: (value: boolean) => void; setShowUploadForm: (value: boolean) => void;
handleRetriggerEvaluators: () => void; handleRetriggerEvaluators: () => void;
handleCancelInterview: (interviewId: any) => 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;
@ -86,6 +87,7 @@ export function ApplicationDetailsTabs(props: ApplicationDetailsTabsProps) {
setShowUploadForm, setShowUploadForm,
handleRetriggerEvaluators, handleRetriggerEvaluators,
handleCancelInterview, handleCancelInterview,
handleRescheduleInterview,
setSelectedEvaluationForView, setSelectedEvaluationForView,
setShowFeedbackDetailsModal, setShowFeedbackDetailsModal,
renderFddAuditContent, renderFddAuditContent,
@ -627,11 +629,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>

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 {
@ -606,6 +650,7 @@ export function useApplicationDetailsAdminActions(params: UseApplicationDetailsA
fetchUsers, fetchUsers,
maybeFetchUsersForModal, maybeFetchUsersForModal,
handleScheduleInterview, handleScheduleInterview,
handleRescheduleInterview,
handleCancelInterview, handleCancelInterview,
handleConfirmCancelInterview, handleConfirmCancelInterview,
handleUpload, handleUpload,

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);
@ -98,6 +99,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,

View File

@ -62,6 +62,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,
@ -266,6 +267,7 @@ export const ApplicationDetails = () => {
handleRemoveInterviewer, handleRemoveInterviewer,
maybeFetchUsersForModal, maybeFetchUsersForModal,
handleScheduleInterview, handleScheduleInterview,
handleRescheduleInterview,
handleCancelInterview, handleCancelInterview,
handleConfirmCancelInterview, handleConfirmCancelInterview,
handleUpload, handleUpload,
@ -291,10 +293,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 +326,8 @@ export const ApplicationDetails = () => {
setShowCancelInterviewModal, setShowCancelInterviewModal,
interviewIdToCancel, interviewIdToCancel,
setInterviewIdToCancel, setInterviewIdToCancel,
interviewToReschedule,
setInterviewToReschedule,
setIsCancellingInterview, setIsCancellingInterview,
setIsUploading, setIsUploading,
setShowUploadForm, setShowUploadForm,
@ -445,6 +454,7 @@ export const ApplicationDetails = () => {
setShowUploadForm={setShowUploadForm} setShowUploadForm={setShowUploadForm}
handleRetriggerEvaluators={handleRetriggerEvaluators} handleRetriggerEvaluators={handleRetriggerEvaluators}
handleCancelInterview={handleCancelInterview} handleCancelInterview={handleCancelInterview}
handleRescheduleInterview={handleRescheduleInterview}
setSelectedEvaluationForView={setSelectedEvaluationForView} setSelectedEvaluationForView={setSelectedEvaluationForView}
setShowFeedbackDetailsModal={setShowFeedbackDetailsModal} setShowFeedbackDetailsModal={setShowFeedbackDetailsModal}
renderFddAuditContent={renderFddAuditContent} renderFddAuditContent={renderFddAuditContent}
@ -533,6 +543,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

@ -9,7 +9,7 @@ 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 { 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';
@ -97,6 +97,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);
@ -132,13 +142,13 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
{ id: 3, name: 'ZBH Review', key: 'ZBH', description: 'Zonal Business Head approval' }, { 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: 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: 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: 6, name: 'Legal - Resignation Letter', key: 'Legal', description: 'Legal team issues resignation approval letter' },
{ id: 7, 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: 8, name: 'F&F Settlement', key: 'F&F Initiated', description: 'Full & Final settlement process' },
{ id: 9, name: 'Completed', key: 'Completed', description: 'Resignation process finalized' } { 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 = (() => { const legalStageApproved = (() => {
if (!resignationData) return false; if (!resignationData) return false;
@ -157,6 +167,7 @@ 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 === 'f&f initiated' || targetStage === 'f&f initiated' ||
targetStage === 'fnf_initiated' || targetStage === 'fnf_initiated' ||
action.includes('approved'); action.includes('approved');
@ -173,8 +184,17 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
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,18 +204,25 @@ 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 canApprove = isCurrentlyAssigned && const canApprove = isCurrentlyAssigned &&
!isFinalState && !isFinalState &&
!isSettlementPhase && !isSettlementPhase &&
!(currentStage === 'Legal' && legalStageApproved); !hasAlreadyPartiallyApproved &&
!(currentStage === 'Legal' && legalStageApproved) &&
!(isDDLead && isDDLeadStage && !hasUploadedPPT);
return { return {
canApprove, canApprove,
canSendBack: isCurrentlyAssigned && !isFinalState && !isSettlementPhase && stageIndex > 0, canSendBack: isCurrentlyAssigned && !isFinalState && !isSettlementPhase && stageIndex > 0,
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) && canPushToFnF: ['DD Lead', 'DD Head', 'NBH', 'DD Admin', 'Super Admin'].includes(userRole) &&
!isSettlementPhase && !isFinalState, !isSettlementPhase && !isFinalState,
canAssign: userRole !== 'Dealer' && !isFinalState canAssign: userRole !== 'Dealer' && !isFinalState
@ -203,7 +230,7 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
}; };
const permissions = getResignationPermissions(); 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<string, string[]> = { const stageAliases: Record<string, string[]> = {
'ASM': ['ASM', 'ASM Review', 'Request Initiated'], 'ASM': ['ASM', 'ASM Review', 'Request Initiated'],
@ -447,6 +474,31 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
<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">
<span className="text-sm text-slate-600 mr-2">Workflow Actions:</span> <span className="text-sm text-slate-600 mr-2">Workflow Actions:</span>
{/* 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 (
<Button
size="sm"
variant="outline"
className="text-amber-700 border-amber-300 hover:bg-amber-50 shadow-sm"
onClick={() => {
setUploadDocType('PPT Presentation');
setUploadStage('DD Lead');
setShowUploadDialog(true);
}}
>
<Upload className="w-4 h-4 mr-2" />
Upload PPT
</Button>
);
}
return null;
})()}
{permissions.canApprove && ( {permissions.canApprove && (
<Button <Button
size="sm" size="sm"

View File

@ -153,7 +153,7 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
}; };
// Check if user can push to F&F (DD Lead and above) // Check if user can push to F&F (DD Lead and above)
const canPushToFnF = currentUser && ['DD Lead', 'DD Head', 'NBH', 'DD Admin', 'Super Admin'].includes(currentUser.role || currentUser.roleCode); const canPushToFnF = currentUser && ['DD Lead', 'DD Head', 'DD_HEAD', 'NBH', 'DD Admin', 'Super Admin'].includes(currentUser.role || currentUser.roleCode);
// Centralized Permissions Utility for Termination logic (Robust Validation) // Centralized Permissions Utility for Termination logic (Robust Validation)
const getTerminationPermissions = () => { const getTerminationPermissions = () => {
@ -169,12 +169,38 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
const isFinalState = ['Completed', 'Rejected', 'Withdrawn', 'Terminated'].includes(status) || currentStage === 'Terminated'; const isFinalState = ['Completed', 'Rejected', 'Withdrawn', 'Terminated'].includes(status) || currentStage === 'Terminated';
const isSettlementPhase = status === 'F&F Initiated' || currentStage === 'F&F Initiated' || status === 'Settled' || status === 'FNF_INITIATED'; const isSettlementPhase = status === 'F&F Initiated' || currentStage === 'F&F Initiated' || status === 'Settled' || status === 'FNF_INITIATED';
const userHasApprovedJointly = auditLogs.some(log => {
const logUserId = log.userId || log.user?.id || log.actor?.id || log.actorId;
const isThisUser = String(logUserId) === String(currentUser.id);
const actionText = (log.action || log.description || '').toUpperCase();
const isPartialApprove = actionText.includes('PARTIAL_APPROVE') || actionText.includes('PARTIAL APPROVED');
const stageMatches =
log.details?.stage === 'RBM + DD-ZM Review' ||
log.stage === 'RBM + DD-ZM Review' ||
(log.remarks || '').includes('Partial approval by');
const result = isThisUser && isPartialApprove && stageMatches;
if (result) console.log('[TerminationDebug] Found matching partial approval log:', log);
return result;
});
if (currentStage === 'RBM + DD-ZM Review' && (userRole === 'RBM' || userRole === 'DD-ZM')) {
console.log('[TerminationDebug] Joint Stage Detection:', {
currentStage,
userRole,
userId: currentUser.id,
userHasApprovedJointly,
auditLogsCount: auditLogs.length
});
}
const isCurrentlyAssigned = userRole === 'Super Admin' || userRole === 'DD Admin' || ( const isCurrentlyAssigned = userRole === 'Super Admin' || userRole === 'DD Admin' || (
(currentStage === 'RBM Review' && userRole === 'RBM') || (currentStage === 'RBM + DD-ZM Review' && (userRole === 'RBM' || userRole === 'DD-ZM') && !userHasApprovedJointly) ||
(currentStage === 'ZBH Review' && userRole === 'ZBH') || (currentStage === 'ZBH Review' && userRole === 'ZBH') ||
(currentStage === 'DD Lead Review' && userRole === 'DD Lead') || (currentStage === 'DD Lead Review' && userRole === 'DD Lead') ||
(currentStage === 'Legal Verification' && userRole === 'Legal Admin') || (currentStage === 'Legal Verification' && userRole === 'Legal Admin') ||
(currentStage === 'DD Head Review' && userRole === 'DD Head') || (currentStage === 'DD Head Review' && (userRole === 'DD Head' || userRole === 'DD_HEAD')) ||
(currentStage === 'NBH Evaluation' && userRole === 'NBH') || (currentStage === 'NBH Evaluation' && userRole === 'NBH') ||
(currentStage === 'NBH Final Approval' && userRole === 'NBH') || (currentStage === 'NBH Final Approval' && userRole === 'NBH') ||
(currentStage === 'CCO Approval' && userRole === 'CCO') || (currentStage === 'CCO Approval' && userRole === 'CCO') ||
@ -206,8 +232,8 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
const isScnStage = ['Show Cause Notice', 'Show Cause Notice (SCN)', 'SCN'].includes(request.currentStage); const isScnStage = ['Show Cause Notice', 'Show Cause Notice (SCN)', 'SCN'].includes(request.currentStage);
const stageAliases: Record<string, string[]> = { const stageAliases: Record<string, string[]> = {
'Submitted': ['Submitted', 'Request Initiated'], 'Submitted': ['Submitted'],
'RBM Review': ['RBM Review'], 'RBM + DD-ZM Review': ['RBM + DD-ZM Review'],
'ZBH Review': ['ZBH Review'], 'ZBH Review': ['ZBH Review'],
'DD Lead Review': ['DD Lead Review'], 'DD Lead Review': ['DD Lead Review'],
'Legal Verification': ['Legal Verification'], 'Legal Verification': ['Legal Verification'],
@ -224,7 +250,7 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
const stageSequence = [ const stageSequence = [
'Submitted', 'Submitted',
'RBM Review', 'RBM + DD-ZM Review',
'ZBH Review', 'ZBH Review',
'DD Lead Review', 'DD Lead Review',
'Legal Verification', 'Legal Verification',
@ -301,21 +327,17 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
return acc; return acc;
}, {}); }, {});
const getLatestStageTimelineEntry = (stageName: string) => { const getStageTimelineEntries = (stageName: string) => {
const aliases = stageAliases[stageName] || [stageName]; const aliases = stageAliases[stageName] || [stageName];
const entries = (request.timeline || []).filter((entry: any) => aliases.includes(entry.stage)); const entries = (request.timeline || []).filter((entry: any) =>
aliases.includes(entry.stage) ||
(stageName === 'Submitted' && (entry.stage === 'Submitted' || entry.stage === 'Request Initiated'))
);
if (entries.length === 0) return null; // Sort by timestamp
return entries.sort((a: any, b: any) =>
// Keep submitted row anchored to initiation details, not later stage-transition remarks. new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime()
if (stageName === 'Submitted') { );
const initiatedEntry = entries.find((entry: any) =>
String(entry?.action || '').toLowerCase().includes('initiated')
);
return initiatedEntry || entries[0];
}
return entries[entries.length - 1];
}; };
const progressStages = [ const progressStages = [
@ -332,9 +354,9 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
}, },
{ {
id: 2, id: 2,
name: 'RBM Review', name: 'RBM + DD-ZM Review',
status: getProgressStatus('RBM Review'), status: getProgressStatus('RBM + DD-ZM Review'),
description: 'Regional Business Manager review' description: 'Joint review and approval by RBM and DD-ZM'
}, },
{ {
id: 3, id: 3,
@ -448,27 +470,34 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
setIsProcessing(true); setIsProcessing(true);
try { try {
let response: any;
if (actionType === 'approve' || actionType === 'sendBack' || actionType === 'withdrawal' || actionType === 'revoke') { if (actionType === 'approve' || actionType === 'sendBack' || actionType === 'withdrawal' || actionType === 'revoke') {
await terminationService.updateTerminationStatus(terminationId, effectiveAction, remarks); response = await terminationService.updateTerminationStatus(terminationId, effectiveAction, remarks);
} else if (actionType === 'pushfnf') { } else if (actionType === 'pushfnf') {
// Logic for push to fnf (using existing service if available) response = await terminationService.updateTerminationStatus(terminationId, 'pushfnf', remarks);
await terminationService.updateTerminationStatus(terminationId, 'pushfnf', remarks);
} else { } else {
toast.error('Action logic not fully implemented for this type'); toast.error('Action logic not fully implemented for this type');
setIsProcessing(false); setIsProcessing(false);
return; return;
} }
if (response && (response.success === false || response.ok === false)) {
console.error('[TerminationDetails] Action failed:', response);
toast.error(response.message || response.data?.message || 'Failed to perform action');
setIsProcessing(false);
return;
}
const actionMessages: Record<string, string> = { const actionMessages: Record<string, string> = {
approve: 'Request approved and forwarded', approve: 'Request approved and forwarded',
withdrawal: 'Request withdrawn successfully', withdrawal: 'Request withdrawn successfully',
sendBack: 'Request sent back for clarification', sendBack: 'Request sent back for clarification',
assign: `Request assigned to ${assignToUser}`, assign: `Request assigned successfully`,
pushfnf: 'Request pushed to F&F successfully', pushfnf: 'Request pushed to F&F successfully',
revoke: 'Request revoked and withdrawn' revoke: 'Request revoked and withdrawn'
}; };
toast.success(actionMessages[actionType!] || 'Action completed'); toast.success(actionMessages[actionType!] || response?.message || 'Action completed');
setActionDialog({ open: false, type: null }); setActionDialog({ open: false, type: null });
setRemarks(''); setRemarks('');
setAssignToUser(''); setAssignToUser('');
@ -844,7 +873,7 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
<div className="space-y-4"> <div className="space-y-4">
{progressStages.map((stage, index) => { {progressStages.map((stage, index) => {
const documentCount = stageDocuments[stage.name]?.length || 0; const documentCount = stageDocuments[stage.name]?.length || 0;
const timelineEntry = getLatestStageTimelineEntry(stage.name); const stageEntries = getStageTimelineEntries(stage.name);
return ( return (
<div key={stage.id} className="flex gap-4"> <div key={stage.id} className="flex gap-4">
<div className="flex flex-col items-center"> <div className="flex flex-col items-center">
@ -884,29 +913,59 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
</button> </button>
)} )}
</div> </div>
{(timelineEntry?.timestamp || stage.date) && ( {(stageEntries[0]?.timestamp || stage.date) && (
<div className="flex items-center gap-1 text-sm text-slate-600"> <div className="flex items-center gap-1 text-xs text-slate-500 bg-slate-50 px-2 py-0.5 rounded-full border border-slate-100">
<Calendar className="w-4 h-4" /> <Calendar className="w-3 h-3" />
<span>{formatDateTime(timelineEntry?.timestamp || stage.date)}</span> <span>{formatDateTime(stageEntries[0]?.timestamp || stage.date)}</span>
</div> </div>
)} )}
</div> </div>
<p className="text-slate-600 text-sm">{stage.description}</p> <p className="text-slate-600 text-sm">{stage.description}</p>
{timelineEntry && ( {stageEntries.length > 0 && (
<div className="mt-3 space-y-2"> <div className="mt-3 space-y-3">
<div className="flex items-center gap-2"> {stageEntries.map((entry: any, entryIdx: number) => {
<Badge className="bg-blue-100 text-blue-700 border-blue-300">{timelineEntry.action || 'Updated'}</Badge> const rawRemarks = entry.remarks || entry.comments || '';
<span className="text-xs text-slate-500">by {timelineEntry.user || 'System'}</span> const isAttachment = rawRemarks?.startsWith('Attachment:');
</div> const remarksContent = isAttachment
<div className="bg-slate-50 border border-slate-200 rounded-lg p-3"> ? rawRemarks.replace('Attachment:', '').trim()
<div className="space-y-2"> : rawRemarks;
<div>
<Label className="text-xs text-slate-600">Remarks:</Label> return (
<p className="text-sm text-slate-700 mt-1">{timelineEntry.remarks || 'No remarks provided.'}</p> <div key={entryIdx} className="group">
<div className="flex items-center gap-2 mb-1">
<Badge className={`
text-[10px] h-4 px-1.5
${entry.action?.toLowerCase().includes('rejected') || entry.action?.toLowerCase().includes('revoked')
? 'bg-red-100 text-red-700'
: entry.action?.toLowerCase().includes('approved')
? 'bg-emerald-100 text-emerald-700'
: 'bg-blue-100 text-blue-700'}
`}>
{entry.action || 'Action'}
</Badge>
<span className="text-[10px] text-slate-500 font-medium">
by {entry.user || 'System'} {formatDateTime(entry.timestamp)}
</span>
</div>
<div className={`
p-2.5 rounded-lg border text-sm
${isAttachment
? 'bg-amber-50/50 border-amber-100 text-amber-900'
: 'bg-slate-50 border-slate-100 text-slate-700'}
`}>
{isAttachment ? (
<div className="flex items-center gap-2">
<FileText className="w-3.5 h-3.5 text-amber-600" />
<span className="font-medium truncate">{remarksContent}</span>
</div>
) : (
<p className="leading-relaxed">{remarksContent || 'No remarks provided.'}</p>
)}
</div>
</div> </div>
</div> );
</div> })}
</div> </div>
)} )}
</div> </div>

View File

@ -5,6 +5,7 @@ export const RESIGNATION_DOCUMENT_TYPES = [
"Legal Communication", "Legal Communication",
"Handover Document", "Handover Document",
"Settlement Supporting Document", "Settlement Supporting Document",
"PPT Presentation",
"Other", "Other",
] as const; ] as const;
@ -32,7 +33,7 @@ export const TERMINATION_DOCUMENT_TYPES = [
export const TERMINATION_STAGE_OPTIONS = [ export const TERMINATION_STAGE_OPTIONS = [
"Submitted", "Submitted",
"RBM Review", "RBM + DD-ZM Review",
"ZBH Review", "ZBH Review",
"DD Lead Review", "DD Lead Review",
"Legal Verification", "Legal Verification",

View File

@ -11,6 +11,9 @@
--popover: oklch(1 0 0); --popover: oklch(1 0 0);
--popover-foreground: oklch(0.145 0 0); --popover-foreground: oklch(0.145 0 0);
--primary: #da291c; --primary: #da291c;
--primary-600: #da291c;
--primary-700: #b82216;
--primary-50: #fef2f2;
--primary-foreground: oklch(1 0 0); --primary-foreground: oklch(1 0 0);
--secondary: oklch(0.95 0.0058 264.53); --secondary: oklch(0.95 0.0058 264.53);
--secondary-foreground: #030213; --secondary-foreground: #030213;
@ -99,6 +102,9 @@
--color-popover: var(--popover); --color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground); --color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary); --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-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary); --color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground); --color-secondary-foreground: var(--secondary-foreground);