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 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(() => {
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
@ -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} />
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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,
|
||||||
|
|||||||
@ -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}
|
||||||
|
|||||||
@ -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"
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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",
|
||||||
|
|||||||
@ -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);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user