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
This commit is contained in:
parent
95032cf2a7
commit
2f82699572
13
src/App.tsx
13
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(() => {
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -176,8 +176,8 @@ export const MasterPage: React.FC = () => {
|
||||
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);
|
||||
const msg = error?.response?.data?.message || error?.message || 'Failed to save ASM';
|
||||
toast.error(msg);
|
||||
}
|
||||
};
|
||||
|
||||
@ -225,8 +225,8 @@ export const MasterPage: React.FC = () => {
|
||||
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);
|
||||
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');
|
||||
}
|
||||
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 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');
|
||||
}
|
||||
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 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<string, unknown>;
|
||||
try {
|
||||
data = JSON.parse(testDataInput) as Record<string, unknown>;
|
||||
} 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<string, unknown>;
|
||||
try {
|
||||
data = JSON.parse(testDataInput) as Record<string, unknown>;
|
||||
} 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); }
|
||||
};
|
||||
|
||||
@ -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 = () => {
|
||||
<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>
|
||||
</div>
|
||||
<Badge className="bg-gradient-to-r from-purple-600 to-indigo-600 px-4 py-1">Admin Control Panel</Badge>
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
@ -557,17 +556,17 @@ 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
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -582,27 +581,27 @@ export const MasterPage: React.FC = () => {
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="approvals" className="animate-in fade-in duration-300">
|
||||
<ApprovalPoliciesPage />
|
||||
<ApprovalPoliciesPage />
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="documents" className="animate-in fade-in duration-300">
|
||||
<DocumentConfigManagement />
|
||||
<DocumentConfigManagement />
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="governance" className="animate-in fade-in duration-300">
|
||||
<AutoAssignmentSettings />
|
||||
<AutoAssignmentSettings />
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="settings" className="animate-in fade-in duration-300">
|
||||
<SecurityDepositMaster />
|
||||
<SecurityDepositMaster />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
)}
|
||||
|
||||
{/* 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} />
|
||||
<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)} />
|
||||
<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} />
|
||||
<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
|
||||
isOpen={showZMDialog}
|
||||
onOpenChange={setShowZMDialog}
|
||||
@ -616,7 +615,7 @@ export const MasterPage: React.FC = () => {
|
||||
selectedRegions={selectedZMRegions}
|
||||
setSelectedRegions={setSelectedZMRegions}
|
||||
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} />
|
||||
<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} />
|
||||
|
||||
@ -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
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<Dialog open={showScheduleModal} onOpenChange={setShowScheduleModal}>
|
||||
<Dialog open={showScheduleModal} onOpenChange={(open) => {
|
||||
setShowScheduleModal(open);
|
||||
if (!open) setInterviewToReschedule(null);
|
||||
}}>
|
||||
<DialogContent data-testid="onboarding-schedule-modal">
|
||||
<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>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4">
|
||||
@ -304,10 +311,15 @@ export function ApplicationDetailsActionModals(props: ApplicationDetailsActionMo
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<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 className="flex-1 bg-amber-600 hover:bg-amber-700" onClick={handleScheduleInterview} disabled={isScheduling} data-testid="onboarding-schedule-submit-button">{isScheduling ? 'Scheduling...' : 'Schedule'}</Button>
|
||||
</div>
|
||||
<div className="flex gap-3">
|
||||
<Button variant="outline" className="flex-1" onClick={() => {
|
||||
setShowScheduleModal(false);
|
||||
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>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
@ -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) {
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="text-red-500 hover:text-red-700 hover:bg-red-50 h-8 px-2"
|
||||
data-testid={`onboarding-interview-cancel-${idx}`}
|
||||
onClick={() => handleCancelInterview(interview.id)}
|
||||
className="text-primary-600 hover:text-primary-700 hover:bg-primary-50 h-8 px-2"
|
||||
data-testid={`onboarding-interview-reschedule-${idx}`}
|
||||
onClick={() => handleRescheduleInterview(interview)}
|
||||
>
|
||||
Cancel
|
||||
Reschedule
|
||||
</Button>
|
||||
)}
|
||||
</TableCell>
|
||||
|
||||
@ -17,10 +17,15 @@ interface UseApplicationDetailsAdminActionsParams {
|
||||
participantType: string;
|
||||
users: any[];
|
||||
interviewDate: string;
|
||||
setInterviewDate: Dispatch<SetStateAction<string>>;
|
||||
interviewType: string;
|
||||
setInterviewType: Dispatch<SetStateAction<string>>;
|
||||
interviewMode: string;
|
||||
setInterviewMode: Dispatch<SetStateAction<string>>;
|
||||
meetingLink: string;
|
||||
setMeetingLink: Dispatch<SetStateAction<string>>;
|
||||
location: string;
|
||||
setLocation: Dispatch<SetStateAction<string>>;
|
||||
scheduledInterviewParticipants: any[];
|
||||
uploadFile: File | null;
|
||||
uploadDocType: string;
|
||||
@ -45,6 +50,8 @@ interface UseApplicationDetailsAdminActionsParams {
|
||||
setShowCancelInterviewModal: Dispatch<SetStateAction<boolean>>;
|
||||
interviewIdToCancel: string;
|
||||
setInterviewIdToCancel: Dispatch<SetStateAction<string>>;
|
||||
interviewToReschedule: any;
|
||||
setInterviewToReschedule: Dispatch<SetStateAction<any>>;
|
||||
setIsCancellingInterview: Dispatch<SetStateAction<boolean>>;
|
||||
setIsUploading: Dispatch<SetStateAction<boolean>>;
|
||||
setShowUploadForm: Dispatch<SetStateAction<boolean>>;
|
||||
@ -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<number, string[]> = {
|
||||
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,
|
||||
|
||||
@ -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<any>(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,
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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<File | null>(null);
|
||||
const [uploadDocType, setUploadDocType] = useState<string>(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');
|
||||
@ -174,7 +185,16 @@ export function ResignationDetails({ resignationId, onBack, currentUser }: Resig
|
||||
const status = resignationData.status;
|
||||
const userRole = currentUser.role;
|
||||
|
||||
// Final states where no more actions are possible
|
||||
const isZmRbmStage = currentStage === 'RBM' || currentStage === 'RBM Review' || currentStage === 'RBM + DD-ZM Review';
|
||||
const userRoleCode = String(currentUser.roleCode || currentUser.role || '').trim().toUpperCase();
|
||||
|
||||
// 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<string, string[]> = {
|
||||
'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 gap-2">
|
||||
<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 && (
|
||||
<Button
|
||||
size="sm"
|
||||
|
||||
@ -153,7 +153,7 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
|
||||
};
|
||||
|
||||
// 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)
|
||||
const getTerminationPermissions = () => {
|
||||
@ -169,12 +169,38 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
|
||||
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 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' || (
|
||||
(currentStage === 'RBM Review' && userRole === 'RBM') ||
|
||||
(currentStage === 'RBM + DD-ZM Review' && (userRole === 'RBM' || userRole === 'DD-ZM') && !userHasApprovedJointly) ||
|
||||
(currentStage === 'ZBH Review' && userRole === 'ZBH') ||
|
||||
(currentStage === 'DD Lead Review' && userRole === 'DD Lead') ||
|
||||
(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 Final Approval' && userRole === 'NBH') ||
|
||||
(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 stageAliases: Record<string, string[]> = {
|
||||
'Submitted': ['Submitted', 'Request Initiated'],
|
||||
'RBM Review': ['RBM Review'],
|
||||
'Submitted': ['Submitted'],
|
||||
'RBM + DD-ZM Review': ['RBM + DD-ZM Review'],
|
||||
'ZBH Review': ['ZBH Review'],
|
||||
'DD Lead Review': ['DD Lead Review'],
|
||||
'Legal Verification': ['Legal Verification'],
|
||||
@ -224,7 +250,7 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
|
||||
|
||||
const stageSequence = [
|
||||
'Submitted',
|
||||
'RBM Review',
|
||||
'RBM + DD-ZM Review',
|
||||
'ZBH Review',
|
||||
'DD Lead Review',
|
||||
'Legal Verification',
|
||||
@ -301,21 +327,17 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
const getLatestStageTimelineEntry = (stageName: string) => {
|
||||
const getStageTimelineEntries = (stageName: string) => {
|
||||
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;
|
||||
|
||||
// Keep submitted row anchored to initiation details, not later stage-transition remarks.
|
||||
if (stageName === 'Submitted') {
|
||||
const initiatedEntry = entries.find((entry: any) =>
|
||||
String(entry?.action || '').toLowerCase().includes('initiated')
|
||||
);
|
||||
return initiatedEntry || entries[0];
|
||||
}
|
||||
|
||||
return entries[entries.length - 1];
|
||||
// Sort by timestamp
|
||||
return entries.sort((a: any, b: any) =>
|
||||
new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime()
|
||||
);
|
||||
};
|
||||
|
||||
const progressStages = [
|
||||
@ -332,9 +354,9 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: 'RBM Review',
|
||||
status: getProgressStatus('RBM Review'),
|
||||
description: 'Regional Business Manager review'
|
||||
name: 'RBM + DD-ZM Review',
|
||||
status: getProgressStatus('RBM + DD-ZM Review'),
|
||||
description: 'Joint review and approval by RBM and DD-ZM'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
@ -448,27 +470,34 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
|
||||
|
||||
setIsProcessing(true);
|
||||
try {
|
||||
let response: any;
|
||||
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') {
|
||||
// Logic for push to fnf (using existing service if available)
|
||||
await terminationService.updateTerminationStatus(terminationId, 'pushfnf', remarks);
|
||||
response = await terminationService.updateTerminationStatus(terminationId, 'pushfnf', remarks);
|
||||
} else {
|
||||
toast.error('Action logic not fully implemented for this type');
|
||||
setIsProcessing(false);
|
||||
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> = {
|
||||
approve: 'Request approved and forwarded',
|
||||
withdrawal: 'Request withdrawn successfully',
|
||||
sendBack: 'Request sent back for clarification',
|
||||
assign: `Request assigned to ${assignToUser}`,
|
||||
assign: `Request assigned successfully`,
|
||||
pushfnf: 'Request pushed to F&F successfully',
|
||||
revoke: 'Request revoked and withdrawn'
|
||||
};
|
||||
|
||||
toast.success(actionMessages[actionType!] || 'Action completed');
|
||||
toast.success(actionMessages[actionType!] || response?.message || 'Action completed');
|
||||
setActionDialog({ open: false, type: null });
|
||||
setRemarks('');
|
||||
setAssignToUser('');
|
||||
@ -844,7 +873,7 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
|
||||
<div className="space-y-4">
|
||||
{progressStages.map((stage, index) => {
|
||||
const documentCount = stageDocuments[stage.name]?.length || 0;
|
||||
const timelineEntry = getLatestStageTimelineEntry(stage.name);
|
||||
const stageEntries = getStageTimelineEntries(stage.name);
|
||||
return (
|
||||
<div key={stage.id} className="flex gap-4">
|
||||
<div className="flex flex-col items-center">
|
||||
@ -884,29 +913,59 @@ export function TerminationDetails({ terminationId, onBack, currentUser }: Termi
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
{(timelineEntry?.timestamp || stage.date) && (
|
||||
<div className="flex items-center gap-1 text-sm text-slate-600">
|
||||
<Calendar className="w-4 h-4" />
|
||||
<span>{formatDateTime(timelineEntry?.timestamp || stage.date)}</span>
|
||||
{(stageEntries[0]?.timestamp || stage.date) && (
|
||||
<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-3 h-3" />
|
||||
<span>{formatDateTime(stageEntries[0]?.timestamp || stage.date)}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-slate-600 text-sm">{stage.description}</p>
|
||||
|
||||
{timelineEntry && (
|
||||
<div className="mt-3 space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge className="bg-blue-100 text-blue-700 border-blue-300">{timelineEntry.action || 'Updated'}</Badge>
|
||||
<span className="text-xs text-slate-500">by {timelineEntry.user || 'System'}</span>
|
||||
</div>
|
||||
<div className="bg-slate-50 border border-slate-200 rounded-lg p-3">
|
||||
<div className="space-y-2">
|
||||
<div>
|
||||
<Label className="text-xs text-slate-600">Remarks:</Label>
|
||||
<p className="text-sm text-slate-700 mt-1">{timelineEntry.remarks || 'No remarks provided.'}</p>
|
||||
{stageEntries.length > 0 && (
|
||||
<div className="mt-3 space-y-3">
|
||||
{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 (
|
||||
<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>
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user