hirrachy enhanced nd creatded distric as centric and added dealer location stable ui made for the hirarchy
This commit is contained in:
parent
c210f640d8
commit
2fab9c5c2d
2
.env.example
Normal file
2
.env.example
Normal file
@ -0,0 +1,2 @@
|
||||
# API Server URL
|
||||
VITE_API_URL=http://localhost:5000/api
|
||||
@ -20,12 +20,15 @@ export const API = {
|
||||
getRegions: () => client.get('/master/regions'),
|
||||
getOutlets: () => client.get('/master/outlets'),
|
||||
getOutletByCode: (code: string) => client.get(`/master/outlets/code/${code}`),
|
||||
getStates: (zoneId?: string) => client.get('/master/states', { params: { zoneId } }),
|
||||
getDistricts: (stateId?: string) => client.get('/master/districts', { params: { stateId } }),
|
||||
getAreas: (districtId?: string) => client.get('/master/areas', { params: { districtId } }),
|
||||
getStates: (params?: any) => client.get('/master/states', typeof params === 'string' ? { zoneId: params } : params),
|
||||
getDistricts: (params?: any) => client.get('/master/districts', typeof params === 'string' ? { stateId: params } : params),
|
||||
getAreas: (params?: any) => client.get('/master/areas', params),
|
||||
updateArea: (id: string, data: any) => client.put(`/master/areas/${id}`, data),
|
||||
createArea: (data: any) => client.post('/master/areas', data),
|
||||
getAreaManagers: () => client.get('/master/area-managers'),
|
||||
getASMs: () => client.get('/master/asms'),
|
||||
getZonalManagers: () => client.get('/master/zonal-managers'),
|
||||
saveZonalManager: (data: any) => client.post('/master/zonal-managers', data),
|
||||
|
||||
// Onboarding
|
||||
submitApplication: (data: any) => client.post('/onboarding/apply', data),
|
||||
@ -67,13 +70,13 @@ export const API = {
|
||||
upsertApprovalPolicy: (stageCode: string, data: any) => client.put(`/assessment/approval-policies/${stageCode}`, data),
|
||||
|
||||
// Collaboration & Participants
|
||||
getWorknotes: (requestId: string, requestType: string) => client.get('/collaboration/worknotes', { params: { requestId, requestType } }),
|
||||
getWorknotes: (requestId: string, requestType: string) => client.get('/collaboration/worknotes', { requestId, requestType }),
|
||||
addWorknote: (data: any) => client.post('/collaboration/worknotes', data),
|
||||
addParticipant: (data: any) => client.post('/collaboration/participants', data),
|
||||
removeParticipant: (id: string) => client.delete(`/collaboration/participants/${id}`),
|
||||
|
||||
// User management routes
|
||||
getUsers: (params?: any) => client.get('/admin/users', { params }),
|
||||
getUsers: (params?: any) => client.get('/admin/users', params),
|
||||
createUser: (data: any) => client.post('/admin/users', data),
|
||||
updateUser: (id: string, data: any) => client.put(`/admin/users/${id}`, data),
|
||||
updateUserStatus: (id: string, data: any) => client.patch(`/admin/users/${id}/status`, data),
|
||||
@ -96,9 +99,9 @@ export const API = {
|
||||
|
||||
// Audit Trail
|
||||
getAuditLogs: (entityType: string, entityId: string, page: number = 1, limit: number = 50) =>
|
||||
client.get('/audit/logs', { params: { entityType, entityId, page, limit } }),
|
||||
client.get('/audit/logs', { entityType, entityId, page, limit }),
|
||||
getAuditSummary: (entityType: string, entityId: string) =>
|
||||
client.get('/audit/summary', { params: { entityType, entityId } }),
|
||||
client.get('/audit/summary', { entityType, entityId }),
|
||||
|
||||
// Prospective Login
|
||||
sendOtp: (phone: string) => client.post('/prospective-login/send-otp', { phone }),
|
||||
@ -119,7 +122,7 @@ export const API = {
|
||||
finalizeTermination: (id: string, data: any) => client.post(`/termination/${id}/finalize`, data),
|
||||
|
||||
// Lifecycle Modules (Self-Service)
|
||||
getResignations: (params?: any) => client.get('/resignation', { params }),
|
||||
getResignations: (params?: any) => client.get('/resignation', params),
|
||||
createResignation: (data: any) => client.post('/resignation', data),
|
||||
approveResignation: (id: string, data?: any) => client.post(`/resignation/${id}/approve`, data),
|
||||
rejectResignation: (id: string, data: any) => client.post(`/resignation/${id}/reject`, data),
|
||||
|
||||
@ -20,6 +20,7 @@ import { ZMManagement } from './MasterPage/ZMManagement';
|
||||
import { UserManagementTable } from './MasterPage/UserManagementTable';
|
||||
import { SLAConfiguration } from './MasterPage/SLAConfiguration';
|
||||
import { RolePermissions } from './MasterPage/RolePermissions';
|
||||
import { RoleDialog } from './MasterPage/RoleDialog';
|
||||
import { EmailTemplates } from './MasterPage/EmailTemplates';
|
||||
import { LocationManagement } from './MasterPage/LocationManagement';
|
||||
import { ASMDialog } from './MasterPage/ASMDialog';
|
||||
@ -32,15 +33,15 @@ import { ApprovalPoliciesPage } from '../admin/ApprovalPoliciesPage';
|
||||
import { RootState } from '../../store';
|
||||
|
||||
export const MasterPage: React.FC = () => {
|
||||
const { fetchInitialData } = useMasterData();
|
||||
const { fetchInitialData, fetchAreas } = useMasterData();
|
||||
const {
|
||||
asms, zonalManagerMappings,
|
||||
allDistricts,
|
||||
users,
|
||||
roles,
|
||||
loading
|
||||
} = useSelector((state: RootState) => state.master);
|
||||
|
||||
|
||||
// Tab & Selection State
|
||||
const [activeTab, setActiveTab] = useState('hierarchy');
|
||||
const [selectedZone, setSelectedZone] = useState('all');
|
||||
@ -63,6 +64,7 @@ export const MasterPage: React.FC = () => {
|
||||
const [selectedASMRegion, setSelectedASMRegion] = useState('');
|
||||
const [selectedASMStates, setSelectedASMStates] = useState<string[]>([]);
|
||||
const [selectedASMDistricts, setSelectedASMDistricts] = useState<string[]>([]);
|
||||
const [asmRoleCode, setAsmRoleCode] = useState<'ASM' | 'DD-AM'>('ASM');
|
||||
|
||||
// ZM Management State
|
||||
const [showZMDialog, setShowZMDialog] = useState(false);
|
||||
@ -76,6 +78,10 @@ export const MasterPage: React.FC = () => {
|
||||
const [selectedZMStates, setSelectedZMStates] = useState<string[]>([]);
|
||||
const [selectedZMDistricts, setSelectedZMDistricts] = useState<string[]>([]);
|
||||
|
||||
// Role Management State
|
||||
const [showRoleDialog, setShowRoleDialog] = useState(false);
|
||||
const [editingRole, setEditingRole] = useState<any>(null);
|
||||
|
||||
// Form State (Zone)
|
||||
const [editingZoneId, setEditingZoneId] = useState<string | null>(null);
|
||||
const [zoneName, setZoneName] = useState('');
|
||||
@ -99,20 +105,31 @@ export const MasterPage: React.FC = () => {
|
||||
const [previewContent, setPreviewContent] = useState<any>(null);
|
||||
|
||||
// Form State (Location)
|
||||
const [editingLocationId] = useState<string | null>(null);
|
||||
const [editingLocationId, setEditingLocationId] = useState<string | null>(null);
|
||||
const [locationState, setLocationState] = useState('');
|
||||
const [locationDistrict, setLocationDistrict] = useState('');
|
||||
const [locationCity, setLocationCity] = useState('');
|
||||
const [locationPincode, setLocationPincode] = useState('');
|
||||
const [locationActiveFrom, setLocationActiveFrom] = useState('');
|
||||
const [locationActiveTo, setLocationActiveTo] = useState('');
|
||||
const [locationStatus, setLocationStatus] = useState('active');
|
||||
|
||||
// Search & Pagination State (Locations)
|
||||
const [districtsSearch, setDistrictsSearch] = useState('');
|
||||
const [districtsPage, setDistrictsPage] = useState(1);
|
||||
|
||||
// Initial Load
|
||||
useEffect(() => {
|
||||
fetchInitialData();
|
||||
}, [fetchInitialData]);
|
||||
|
||||
// Sync editingRole with latest data from Redux
|
||||
useEffect(() => {
|
||||
if (editingRole && roles.length > 0) {
|
||||
const latest = roles.find((r: any) => r.id === editingRole.id);
|
||||
if (latest) setEditingRole(latest);
|
||||
}
|
||||
}, [roles, editingRole?.id]);
|
||||
|
||||
// Shared Data Helpers
|
||||
const districtsAssignedToOthers = useMemo(() => {
|
||||
const map: Record<string, string[]> = {};
|
||||
@ -127,9 +144,12 @@ export const MasterPage: React.FC = () => {
|
||||
return map;
|
||||
}, [asms, zonalManagerMappings]);
|
||||
|
||||
const getDistrictsForSelectedState = useCallback((stateName: string) => {
|
||||
const getDistrictsForSelectedState = useCallback((stateName: string, regionId?: string) => {
|
||||
return allDistricts
|
||||
.filter(d => d.stateName?.toUpperCase() === stateName?.toUpperCase())
|
||||
.filter(d =>
|
||||
d.stateName?.toUpperCase() === stateName?.toUpperCase() &&
|
||||
(!regionId || d.regionId === regionId)
|
||||
)
|
||||
.map(d => ({ id: d.id, name: d.name }));
|
||||
}, [allDistricts]);
|
||||
|
||||
@ -140,14 +160,25 @@ export const MasterPage: React.FC = () => {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const payload = { userId: asmManagerId, asmCode, districts: selectedASMDistricts, status: asmStatus };
|
||||
const payload = {
|
||||
userId: asmManagerId,
|
||||
roleCode: asmRoleCode,
|
||||
asmCode,
|
||||
districts: selectedASMDistricts,
|
||||
status: asmStatus
|
||||
};
|
||||
const res = await masterService.saveASM(payload) as any;
|
||||
if (res.success) {
|
||||
toast.success(`ASM ${editingASMId ? 'updated' : 'assigned'} successfully`);
|
||||
toast.success(`${asmRoleCode === 'ASM' ? 'ASM' : 'DD Area Manager'} ${editingASMId ? 'updated' : 'assigned'} successfully`);
|
||||
setShowASMDialog(false);
|
||||
fetchInitialData();
|
||||
} else {
|
||||
toast.error(res.message || 'Failed to save ASM');
|
||||
}
|
||||
} catch (error: any) {
|
||||
const msg = error?.response?.data?.message || error?.message || 'Failed to save ASM';
|
||||
toast.error(msg);
|
||||
}
|
||||
} catch (error) { toast.error('Failed to save ASM'); }
|
||||
};
|
||||
|
||||
const handleEditASM = (asm: any) => {
|
||||
@ -161,6 +192,7 @@ export const MasterPage: React.FC = () => {
|
||||
setSelectedASMRegion(asm.regionId);
|
||||
setSelectedASMStates(asm.stateNames || []);
|
||||
setSelectedASMDistricts(asm.areasManaged?.map((a: any) => a.id) || []);
|
||||
setAsmRoleCode(asm.roleCode === 'DD-AM' ? 'DD-AM' : 'ASM');
|
||||
setShowASMDialog(true);
|
||||
};
|
||||
|
||||
@ -168,9 +200,9 @@ export const MasterPage: React.FC = () => {
|
||||
setEditingZMId(zm.id);
|
||||
setZmManagerId(zm.id);
|
||||
setZmName(zm.name);
|
||||
setZmCode(zm.code === 'N/A' ? '' : zm.code);
|
||||
setZmEmployeeId(zm.code === 'N/A' ? '' : zm.code);
|
||||
setZmStatus(zm.status.toLowerCase() as 'active' | 'inactive');
|
||||
setZmCode(zm.zmCode || zm.code || '');
|
||||
setZmEmployeeId(zm.employeeId || '');
|
||||
setZmStatus(zm.status?.toLowerCase() === 'active' ? 'active' : 'inactive');
|
||||
setSelectedZMZone(zm.zoneId || '');
|
||||
setSelectedZMStates(zm.stateNames || []);
|
||||
setSelectedZMDistricts(zm.districts?.map((d: any) => d.id) || []);
|
||||
@ -185,21 +217,24 @@ export const MasterPage: React.FC = () => {
|
||||
try {
|
||||
const payload = {
|
||||
userId: zmManagerId,
|
||||
roleCode: 'DD-ZM',
|
||||
zmCode,
|
||||
zoneId: selectedZMZone,
|
||||
districts: selectedZMDistricts,
|
||||
status: zmStatus,
|
||||
locationId: selectedZMZone // ZM primary location is the zone
|
||||
status: zmStatus
|
||||
};
|
||||
|
||||
// Use the generic saveASM method which I generalized on the backend
|
||||
const res = await masterService.saveASM(payload) as any;
|
||||
const res = await (masterService as any).saveZonalManager(payload) as any;
|
||||
if (res.success) {
|
||||
toast.success(`Zonal Manager ${editingZMId ? 'updated' : 'assigned'} successfully`);
|
||||
setShowZMDialog(false);
|
||||
fetchInitialData();
|
||||
} else {
|
||||
toast.error(res.message || 'Failed to save Zonal Manager');
|
||||
}
|
||||
} catch (error: any) {
|
||||
const msg = error?.response?.data?.message || error?.message || 'Failed to save Zonal Manager';
|
||||
toast.error(msg);
|
||||
}
|
||||
} catch (error) { toast.error('Failed to save Zonal Manager'); }
|
||||
};
|
||||
|
||||
const handleSaveZone = async () => {
|
||||
@ -210,13 +245,19 @@ export const MasterPage: React.FC = () => {
|
||||
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);
|
||||
}
|
||||
} catch (error) { toast.error('Error saving zone'); }
|
||||
};
|
||||
|
||||
const handleSaveRegion = async () => {
|
||||
try {
|
||||
const payload = {
|
||||
...(editingRegionId ? { id: editingRegionId } : {}),
|
||||
name: regionName,
|
||||
code: regionCode,
|
||||
description: regionDescription,
|
||||
@ -230,8 +271,13 @@ export const MasterPage: React.FC = () => {
|
||||
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);
|
||||
}
|
||||
} catch (error) { toast.error('Error saving region'); }
|
||||
};
|
||||
|
||||
const handleSaveTemplate = async () => {
|
||||
@ -259,17 +305,49 @@ export const MasterPage: React.FC = () => {
|
||||
finally { setPreviewLoading(false); }
|
||||
};
|
||||
|
||||
const handleSaveRole = async (roleId: string, permissions: string[]) => {
|
||||
try {
|
||||
const res = await masterService.updateRole(roleId, { permissions }) as any;
|
||||
if (res.success) {
|
||||
toast.success('Role permissions updated successfully');
|
||||
setShowRoleDialog(false);
|
||||
fetchInitialData();
|
||||
} else {
|
||||
toast.error(res.message || 'Error saving role permissions');
|
||||
}
|
||||
} catch (error: any) {
|
||||
const msg = error?.response?.data?.message || error?.message || 'Error saving role permissions';
|
||||
toast.error(msg);
|
||||
}
|
||||
};
|
||||
|
||||
const handleEditRole = (role: any) => {
|
||||
setEditingRole(role);
|
||||
setShowRoleDialog(true);
|
||||
};
|
||||
|
||||
const handleEditLocation = (loc: any) => {
|
||||
setEditingLocationId(loc.id);
|
||||
setLocationState(loc.stateName || '');
|
||||
setLocationCity(loc.city || '');
|
||||
setLocationDistrict(loc.name || '');
|
||||
setLocationActiveFrom(loc.openFrom ? new Date(loc.openFrom).toISOString().split('T')[0] : '');
|
||||
setLocationActiveTo(loc.openTo ? new Date(loc.openTo).toISOString().split('T')[0] : '');
|
||||
setLocationStatus(loc.isActive ? 'active' : 'inactive');
|
||||
setShowLocationDialog(true);
|
||||
};
|
||||
|
||||
const handleSaveLocation = async () => {
|
||||
try {
|
||||
const payload = {
|
||||
id: editingLocationId,
|
||||
stateId: locationState,
|
||||
districtId: locationDistrict,
|
||||
stateName: locationState,
|
||||
name: locationDistrict,
|
||||
city: locationCity,
|
||||
pincode: locationPincode,
|
||||
status: locationStatus,
|
||||
activeFrom: locationActiveFrom,
|
||||
activeTo: locationActiveTo
|
||||
openFrom: locationActiveFrom,
|
||||
openTo: locationActiveTo,
|
||||
isActive: locationStatus === 'active'
|
||||
};
|
||||
const res = await (editingLocationId
|
||||
? masterService.updateArea(editingLocationId, payload)
|
||||
@ -277,11 +355,18 @@ export const MasterPage: React.FC = () => {
|
||||
if (res.success) {
|
||||
toast.success('Location saved');
|
||||
setShowLocationDialog(false);
|
||||
fetchInitialData();
|
||||
fetchAreas({ search: districtsSearch, page: districtsPage });
|
||||
}
|
||||
} catch (error) { toast.error('Error saving location'); }
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const handler = setTimeout(() => {
|
||||
fetchAreas({ search: districtsSearch, page: districtsPage });
|
||||
}, 500);
|
||||
return () => clearTimeout(handler);
|
||||
}, [districtsSearch, districtsPage, fetchAreas]);
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
@ -355,7 +440,7 @@ export const MasterPage: React.FC = () => {
|
||||
|
||||
<TabsContent value="roles" className="animate-in fade-in duration-300">
|
||||
<RolePermissions onAddRole={() => toast.info('Unified Role Management interface being updated')}
|
||||
onEditRole={() => toast.info('Unified Role Management interface being updated')} />
|
||||
onEditRole={handleEditRole} />
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="sla" className="animate-in fade-in duration-300">
|
||||
@ -368,8 +453,35 @@ export const MasterPage: React.FC = () => {
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="locations" className="animate-in fade-in duration-300">
|
||||
<LocationManagement onAddLocation={() => setShowLocationDialog(true)}
|
||||
onEditLocation={() => toast.info('Location Editor being updated')} onDeleteLocation={() => toast.error('Delete Location restricted')} />
|
||||
<LocationManagement
|
||||
onAddLocation={() => {
|
||||
setEditingLocationId(null);
|
||||
setLocationState('');
|
||||
setLocationCity('');
|
||||
setLocationDistrict('');
|
||||
setLocationActiveFrom('');
|
||||
setLocationActiveTo('');
|
||||
setLocationStatus('active');
|
||||
setShowLocationDialog(true);
|
||||
}}
|
||||
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 });
|
||||
}
|
||||
});
|
||||
}
|
||||
}}
|
||||
onSearch={(term) => {
|
||||
setDistrictsSearch(term);
|
||||
setDistrictsPage(1); // Reset to first page on search
|
||||
}}
|
||||
onPageChange={setDistrictsPage}
|
||||
searchTerm={districtsSearch}
|
||||
/>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="approvals" className="animate-in fade-in duration-300">
|
||||
@ -381,7 +493,7 @@ export const MasterPage: React.FC = () => {
|
||||
{/* 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} regionCode={regionCode} setRegionCode={setRegionCode} 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} asmName={asmName} setAsmName={setAsmName} asmCode={asmCode} setAsmCode={setAsmCode} asmEmployeeId={asmEmployeeId} setAsmEmployeeId={setAsmEmployeeId} asmStatus={asmStatus} setAsmStatus={setAsmStatus} selectedASMZone={selectedASMZone} setSelectedASMZone={setSelectedASMZone} selectedASMRegion={selectedASMRegion} setSelectedASMRegion={setSelectedASMRegion} selectedASMStates={selectedASMStates} setSelectedASMStates={setSelectedASMStates} selectedASMDistricts={selectedASMDistricts} setSelectedASMDistricts={setSelectedASMDistricts} onSave={handleSaveASM} 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={getDistrictsForSelectedState} />
|
||||
<ASMDialog isOpen={showASMDialog} onOpenChange={setShowASMDialog} editingASMId={editingASMId} asmManagerId={asmManagerId} setAsmManagerId={setAsmManagerId} asmName={asmName} setAsmName={setAsmName} asmCode={asmCode} setAsmCode={setAsmCode} asmEmployeeId={asmEmployeeId} setAsmEmployeeId={setAsmEmployeeId} 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}
|
||||
@ -408,7 +520,8 @@ export const MasterPage: React.FC = () => {
|
||||
getDistrictsForSelectedState={getDistrictsForSelectedState}
|
||||
/>
|
||||
<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} locationPincode={locationPincode} setLocationPincode={setLocationPincode} locationActiveFrom={locationActiveFrom} setLocationActiveFrom={setLocationActiveFrom} locationActiveTo={locationActiveTo} setLocationActiveTo={setLocationActiveTo} locationStatus={locationStatus} setLocationStatus={setLocationStatus} 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} onSave={handleSaveLocation} />
|
||||
<RoleDialog isOpen={showRoleDialog} onOpenChange={setShowRoleDialog} role={editingRole} onSave={handleSaveRole} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@ -32,6 +32,8 @@ interface ASMDialogProps {
|
||||
selectedASMDistricts: string[];
|
||||
setSelectedASMDistricts: (districts: string[]) => void;
|
||||
onSave: () => void;
|
||||
asmRoleCode: 'ASM' | 'DD-AM';
|
||||
setAsmRoleCode: (role: 'ASM' | 'DD-AM') => void;
|
||||
userAssignedData: any[];
|
||||
districtsAssignedToOthers: Record<string, string[]>;
|
||||
getDistrictsForSelectedState: (state: string) => { id: string; name: string }[];
|
||||
@ -43,6 +45,7 @@ export const ASMDialog: React.FC<ASMDialogProps> = ({
|
||||
asmStatus, setAsmStatus, selectedASMZone, setSelectedASMZone,
|
||||
selectedASMRegion, setSelectedASMRegion, selectedASMStates, setSelectedASMStates,
|
||||
selectedASMDistricts, setSelectedASMDistricts, onSave,
|
||||
asmRoleCode, setAsmRoleCode,
|
||||
userAssignedData, districtsAssignedToOthers, getDistrictsForSelectedState
|
||||
}) => {
|
||||
const { zones, regionalOffices } = useSelector((state: RootState) => state.master);
|
||||
@ -51,11 +54,27 @@ export const ASMDialog: React.FC<ASMDialogProps> = ({
|
||||
const roles = u.allRoles || [];
|
||||
return roles.some((r: string) => {
|
||||
const roleStr = (r || '').toUpperCase();
|
||||
return ['ASM', 'AREA SALES MANAGER', 'RM', 'RBM', 'REGIONAL MANAGER', 'ZBH', 'ZONE BUSINESS HEAD', 'ZONAL BUSINESS HEAD'].includes(roleStr) ||
|
||||
return ['ASM', 'AREA SALES MANAGER', 'DD-AM', 'AREA MANAGER', 'RM', 'RBM', 'REGIONAL MANAGER', 'ZBH', 'ZONE BUSINESS HEAD', 'ZONAL BUSINESS HEAD'].includes(roleStr) ||
|
||||
roleStr.includes('AREA SALES') || roleStr.includes('REGIONAL') || roleStr.includes('ZONAL');
|
||||
});
|
||||
});
|
||||
|
||||
// PRE-FILLING LOGIC: When manager or role changes, pre-select their districts
|
||||
React.useEffect(() => {
|
||||
if (asmManagerId && isOpen) {
|
||||
const manager = userAssignedData.find(u => u.id === asmManagerId);
|
||||
if (manager && manager.territoryProfile) {
|
||||
const assignedDistricts = manager.territoryProfile
|
||||
.filter((t: any) => t.roleCode === asmRoleCode && t.locationType === 'district')
|
||||
.map((t: any) => t.locationId);
|
||||
|
||||
if (assignedDistricts.length > 0) {
|
||||
setSelectedASMDistricts(assignedDistricts);
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [asmManagerId, asmRoleCode, isOpen, userAssignedData, setSelectedASMDistricts]);
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto">
|
||||
@ -84,6 +103,7 @@ export const ASMDialog: React.FC<ASMDialogProps> = ({
|
||||
</div>
|
||||
|
||||
{selectedASMZone && (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<Label>Regional Office</Label>
|
||||
<Select value={selectedASMRegion} onValueChange={(value) => {
|
||||
@ -103,6 +123,36 @@ export const ASMDialog: React.FC<ASMDialogProps> = ({
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label>Assignment Role</Label>
|
||||
<Select value={asmRoleCode} onValueChange={(val: any) => setAsmRoleCode(val)}>
|
||||
<SelectTrigger className="mt-2 text-slate-900">
|
||||
<SelectValue placeholder="Select role" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="ASM">Sales ASM</SelectItem>
|
||||
<SelectItem value="DD-AM">DD Area Manager</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label>Select {asmRoleCode === 'ASM' ? 'ASM' : 'DD Area Manager'} User</Label>
|
||||
<Select value={asmManagerId} onValueChange={setAsmManagerId}>
|
||||
<SelectTrigger className="mt-2 text-slate-900">
|
||||
<SelectValue placeholder={`Select ${asmRoleCode === 'ASM' ? 'ASM' : 'DD Area Manager'}`} />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="max-h-64">
|
||||
{filteredASMUsers.map(user => (
|
||||
<SelectItem key={user.id} value={user.id}>
|
||||
{user.name} ({user.employeeId || 'No ID'})
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedASMRegion && (
|
||||
@ -115,16 +165,16 @@ export const ASMDialog: React.FC<ASMDialogProps> = ({
|
||||
|
||||
return availableStates.length > 0 ? (
|
||||
<div className="space-y-2">
|
||||
{availableStates.map((state) => (
|
||||
{availableStates.map((state: string) => (
|
||||
<div key={state} className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
id={`asm-state-${state}`}
|
||||
checked={selectedASMStates.includes(state)}
|
||||
checked={selectedASMStates.some(s => s.toLowerCase() === state.toLowerCase())}
|
||||
onCheckedChange={(checked) => {
|
||||
if (checked) {
|
||||
setSelectedASMStates([...selectedASMStates, state]);
|
||||
} else {
|
||||
setSelectedASMStates(selectedASMStates.filter(s => s !== state));
|
||||
setSelectedASMStates(selectedASMStates.filter(s => s.toLowerCase() !== state.toLowerCase()));
|
||||
const stateDistricts = getDistrictsForSelectedState(state);
|
||||
setSelectedASMDistricts(selectedASMDistricts.filter(dId => !stateDistricts.some(sd => sd.id === dId)));
|
||||
}
|
||||
@ -208,9 +258,18 @@ export const ASMDialog: React.FC<ASMDialogProps> = ({
|
||||
const selectedUser = userAssignedData.find(u => u.id === value);
|
||||
if (selectedUser) {
|
||||
setAsmName(selectedUser.name);
|
||||
setAsmCode(selectedUser.asmCode || '');
|
||||
setAsmEmployeeId(selectedUser.employeeId || '');
|
||||
setSelectedASMDistricts(selectedUser.areasManaged?.map((a: any) => a.id) || []);
|
||||
|
||||
// Extraction logic: Look for managerCode in the territoryProfile matching the current role
|
||||
const roleProfile = (selectedUser.territoryProfile || []).find((t: any) => t.roleCode === asmRoleCode);
|
||||
const existingCode = roleProfile?.managerCode || selectedUser.asmCode || selectedUser.employeeId || '';
|
||||
setAsmCode(existingCode);
|
||||
|
||||
// Extract zone/region from assignments if present
|
||||
if (roleProfile?.zoneId) setSelectedASMZone(roleProfile.zoneId);
|
||||
if (roleProfile?.regionId) setSelectedASMRegion(roleProfile.regionId);
|
||||
|
||||
setSelectedASMDistricts(selectedUser.areasManaged?.filter((a: any) => a.roleCode === asmRoleCode).map((a: any) => a.id) || []);
|
||||
setSelectedASMStates(selectedUser.stateNames || []);
|
||||
}
|
||||
}}
|
||||
|
||||
@ -1,11 +1,9 @@
|
||||
import React from 'react';
|
||||
import { useSelector } from 'react-redux';
|
||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '../../ui/dialog';
|
||||
import { Button } from '../../ui/button';
|
||||
import { Label } from '../../ui/label';
|
||||
import { Input } from '../../ui/input';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../../ui/select';
|
||||
import { RootState } from '../../../store';
|
||||
|
||||
interface LocationDialogProps {
|
||||
isOpen: boolean;
|
||||
@ -17,8 +15,6 @@ interface LocationDialogProps {
|
||||
setLocationDistrict: (id: string) => void;
|
||||
locationCity: string;
|
||||
setLocationCity: (city: string) => void;
|
||||
locationPincode: string;
|
||||
setLocationPincode: (pincode: string) => void;
|
||||
locationActiveFrom: string;
|
||||
setLocationActiveFrom: (date: string) => void;
|
||||
locationActiveTo: string;
|
||||
@ -31,11 +27,9 @@ interface LocationDialogProps {
|
||||
export const LocationDialog: React.FC<LocationDialogProps> = ({
|
||||
isOpen, onOpenChange, editingLocationId, locationState, setLocationState,
|
||||
locationDistrict, setLocationDistrict, locationCity, setLocationCity,
|
||||
locationPincode, setLocationPincode, locationActiveFrom, setLocationActiveFrom,
|
||||
locationActiveFrom, setLocationActiveFrom,
|
||||
locationActiveTo, setLocationActiveTo, locationStatus, setLocationStatus, onSave
|
||||
}) => {
|
||||
const { allStates, allDistricts } = useSelector((state: RootState) => state.master);
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={onOpenChange}>
|
||||
<DialogContent>
|
||||
@ -45,80 +39,62 @@ export const LocationDialog: React.FC<LocationDialogProps> = ({
|
||||
</DialogHeader>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<Label>State</Label>
|
||||
<Select value={locationState} onValueChange={(val) => {
|
||||
setLocationState(val);
|
||||
setLocationDistrict('');
|
||||
}}>
|
||||
<SelectTrigger className="mt-2 text-slate-900">
|
||||
<SelectValue placeholder="Select State" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{allStates.map((state) => (
|
||||
<SelectItem key={state.id} value={state.id}>{state.stateName}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div>
|
||||
<Label>District</Label>
|
||||
<Select value={locationDistrict} onValueChange={setLocationDistrict} disabled={!locationState}>
|
||||
<SelectTrigger className="mt-2 text-slate-900">
|
||||
<SelectValue placeholder="Select District" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{allDistricts
|
||||
.filter(d => d.stateId === locationState || d.state?.id === locationState)
|
||||
.map((district) => (
|
||||
<SelectItem key={district.id} value={district.id}>{district.districtName}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div>
|
||||
<Label>Area Name / City</Label>
|
||||
<Label className="flex items-center gap-2 text-sm leading-none font-medium text-slate-700">State</Label>
|
||||
<Input
|
||||
placeholder="Enter area name (e.g., Connaught Place)"
|
||||
className="mt-2 text-slate-900"
|
||||
placeholder="Enter state name"
|
||||
className="mt-2 text-slate-900 border-slate-200 focus-visible:ring-amber-500/30 focus-visible:border-amber-500"
|
||||
value={locationState}
|
||||
onChange={(e) => setLocationState(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="flex items-center gap-2 text-sm leading-none font-medium text-slate-700">City</Label>
|
||||
<Input
|
||||
placeholder="Enter city name"
|
||||
className="mt-2 text-slate-900 border-slate-200 focus-visible:ring-amber-500/30 focus-visible:border-amber-500"
|
||||
value={locationCity}
|
||||
onChange={(e) => setLocationCity(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label>Pincode</Label>
|
||||
<Label className="flex items-center gap-2 text-sm leading-none font-medium text-slate-700">District</Label>
|
||||
<Input
|
||||
placeholder="Enter pincode"
|
||||
className="mt-2 text-slate-900"
|
||||
value={locationPincode}
|
||||
onChange={(e) => setLocationPincode(e.target.value)}
|
||||
placeholder="Enter district name"
|
||||
className="mt-2 text-slate-900 border-slate-200 focus-visible:ring-amber-500/30 focus-visible:border-amber-500"
|
||||
value={locationDistrict}
|
||||
onChange={(e) => setLocationDistrict(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="border rounded-lg p-4 bg-gradient-to-br from-blue-50 to-cyan-50 border-blue-200">
|
||||
<Label className="items-center gap-2 text-sm leading-none font-medium text-blue-900 mb-3 block">Active Period</Label>
|
||||
<p className="text-xs text-blue-700 mb-3">Define the time period when this location will be available for dealership applications</p>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label>Active From</Label>
|
||||
<Label className="flex items-center gap-2 text-sm leading-none font-medium text-slate-700">Start Date</Label>
|
||||
<Input
|
||||
type="date"
|
||||
className="mt-2 text-slate-900"
|
||||
className="mt-2 text-slate-900 border-slate-200 focus-visible:ring-blue-500/30 focus-visible:border-blue-500"
|
||||
value={locationActiveFrom}
|
||||
onChange={(e) => setLocationActiveFrom(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label>Active To</Label>
|
||||
<Label className="flex items-center gap-2 text-sm leading-none font-medium text-slate-700">End Date</Label>
|
||||
<Input
|
||||
type="date"
|
||||
className="mt-2 text-slate-900"
|
||||
className="mt-2 text-slate-900 border-slate-200 focus-visible:ring-blue-500/30 focus-visible:border-blue-500"
|
||||
value={locationActiveTo}
|
||||
onChange={(e) => setLocationActiveTo(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label>Status</Label>
|
||||
<Label className="flex items-center gap-2 text-sm leading-none font-medium text-slate-700">Status</Label>
|
||||
<Select value={locationStatus} onValueChange={setLocationStatus}>
|
||||
<SelectTrigger className="mt-2 text-slate-900">
|
||||
<SelectTrigger className="mt-2 text-slate-900 border-slate-200 focus:ring-amber-500/30 focus:border-amber-500">
|
||||
<SelectValue placeholder="Select Status" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@ -130,7 +106,7 @@ export const LocationDialog: React.FC<LocationDialogProps> = ({
|
||||
|
||||
<div className="flex gap-3 pt-4">
|
||||
<Button variant="outline" className="flex-1" onClick={() => onOpenChange(false)}>Cancel</Button>
|
||||
<Button className="flex-1 bg-amber-600 hover:bg-amber-700" onClick={onSave}>Save Location</Button>
|
||||
<Button className="flex-1 bg-amber-600 hover:bg-amber-700 text-white shadow-md hover:shadow-lg transition-all" onClick={onSave}>Save Location</Button>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
|
||||
@ -11,43 +11,65 @@ interface LocationManagementProps {
|
||||
onAddLocation: () => void;
|
||||
onEditLocation: (location: any) => void;
|
||||
onDeleteLocation: (id: string) => void;
|
||||
onSearch: (term: string) => void;
|
||||
onPageChange: (page: number) => void;
|
||||
searchTerm: string;
|
||||
}
|
||||
|
||||
export const LocationManagement: React.FC<LocationManagementProps> = ({
|
||||
onAddLocation, onEditLocation, onDeleteLocation
|
||||
onAddLocation, onEditLocation, onDeleteLocation, onSearch, onPageChange, searchTerm
|
||||
}) => {
|
||||
const { allDistricts } = useSelector((state: RootState) => state.master);
|
||||
const { allDistricts, areasPagination, isAreasLoading } = useSelector((state: RootState) => state.master);
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
|
||||
<div>
|
||||
<CardTitle>Dealership Locations</CardTitle>
|
||||
<CardDescription>Manage geographical locations and their operational status</CardDescription>
|
||||
<CardDescription>Manage {areasPagination.total} geographical locations and their operational status</CardDescription>
|
||||
</div>
|
||||
<Button onClick={onAddLocation} className="bg-amber-600 hover:bg-amber-700">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="relative">
|
||||
<Globe className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400" />
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search locations..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => onSearch(e.target.value)}
|
||||
className="pl-9 pr-4 py-2 bg-slate-50 border border-slate-200 rounded-md text-sm focus:outline-none focus:ring-2 focus:ring-amber-500 w-64 transition-all"
|
||||
/>
|
||||
</div>
|
||||
<Button onClick={onAddLocation} className="bg-amber-600 hover:bg-amber-700 whitespace-nowrap">
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
Add Location
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className={`relative ${isAreasLoading ? 'opacity-50 pointer-events-none' : ''}`}>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>State</TableHead>
|
||||
<TableHead>Area / City</TableHead>
|
||||
<TableHead>City</TableHead>
|
||||
<TableHead>District</TableHead>
|
||||
<TableHead>Pincode</TableHead>
|
||||
<TableHead>Manager</TableHead>
|
||||
<TableHead>Active Period</TableHead>
|
||||
<TableHead>Status</TableHead>
|
||||
<TableHead className="text-right">Actions</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{allDistricts.map((district) => (
|
||||
{allDistricts.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={6} className="h-32 text-center text-slate-500 italic">
|
||||
{searchTerm ? 'No locations found matching your search' : 'No locations available'}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
allDistricts.map((district) => (
|
||||
<TableRow key={district.id}>
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-2">
|
||||
@ -55,35 +77,110 @@ export const LocationManagement: React.FC<LocationManagementProps> = ({
|
||||
<span className="font-medium">{district.stateName || 'N/A'}</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="font-medium text-slate-900">{district.name}</TableCell>
|
||||
<TableCell className="text-slate-600 text-sm">{district.regionName || 'N/A'}</TableCell>
|
||||
<TableCell className="text-slate-600 text-sm">{district.code || 'N/A'}</TableCell>
|
||||
<TableCell className="font-medium text-slate-900">{district.city || 'N/A'}</TableCell>
|
||||
<TableCell className="text-slate-600 text-sm">{district.name}</TableCell>
|
||||
<TableCell>
|
||||
{district.asmName ? (
|
||||
<span className="text-slate-700 font-medium">{district.asmName}</span>
|
||||
{district.openFrom && district.openTo ? (
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center gap-2 text-xs">
|
||||
<span className="text-slate-600">From:</span>
|
||||
<Badge variant="outline" className="text-xs font-medium">
|
||||
{new Date(district.openFrom).toLocaleDateString('en-GB', { day: 'numeric', month: 'short', year: 'numeric' })}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-xs">
|
||||
<span className="text-slate-600">To:</span>
|
||||
<Badge variant="outline" className="text-xs font-medium">
|
||||
{new Date(district.openTo).toLocaleDateString('en-GB', { day: 'numeric', month: 'short', year: 'numeric' })}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<span className="text-slate-400 italic text-sm">Unassigned</span>
|
||||
<span className="text-slate-400 italic text-sm">Not Defined</span>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant={district.isActive ? 'default' : 'secondary'} className={district.isActive ? 'bg-emerald-100 text-emerald-700' : ''}>
|
||||
<Badge
|
||||
variant={district.isActive ? 'default' : 'secondary'}
|
||||
className={district.isActive ? 'bg-green-600 hover:bg-green-700 text-white border-transparent' : ''}
|
||||
>
|
||||
{district.isActive ? 'Active' : 'Inactive'}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<div className="flex gap-2 justify-end">
|
||||
<Button variant="ghost" size="sm" onClick={() => onEditLocation(district)}>
|
||||
<Button variant="outline" size="sm" onClick={() => onEditLocation(district)} className="h-8 w-8 p-0">
|
||||
<Edit2 className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button variant="ghost" size="sm" onClick={() => onDeleteLocation(district.id)} className="text-red-500 hover:text-red-600">
|
||||
<Button variant="outline" size="sm" onClick={() => onDeleteLocation(district.id)} className="h-8 w-8 p-0 text-red-600 hover:bg-red-50 hover:text-red-700">
|
||||
<Trash2 className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
{isAreasLoading && (
|
||||
<div className="absolute inset-0 flex items-center justify-center bg-white/10 backdrop-blur-[1px]">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-amber-600"></div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Pagination Controls */}
|
||||
{areasPagination.totalPages > 1 && (
|
||||
<div className="flex items-center justify-between mt-6 pt-4 border-t border-slate-100">
|
||||
<div className="text-sm text-slate-500">
|
||||
Showing <span className="font-medium text-slate-900">{(areasPagination.page - 1) * areasPagination.limit + 1}</span> to <span className="font-medium text-slate-900">{Math.min(areasPagination.page * areasPagination.limit, areasPagination.total)}</span> of <span className="font-medium text-slate-900">{areasPagination.total}</span> results
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => onPageChange(areasPagination.page - 1)}
|
||||
disabled={areasPagination.page <= 1 || isAreasLoading}
|
||||
className="h-8"
|
||||
>
|
||||
Previous
|
||||
</Button>
|
||||
<div className="flex items-center gap-1">
|
||||
{Array.from({ length: Math.min(5, areasPagination.totalPages) }, (_, i) => {
|
||||
// Simple pagination window logic
|
||||
let pageNum = areasPagination.page;
|
||||
if (areasPagination.page <= 3) pageNum = i + 1;
|
||||
else if (areasPagination.page >= areasPagination.totalPages - 2) pageNum = areasPagination.totalPages - 4 + i;
|
||||
else pageNum = areasPagination.page - 2 + i;
|
||||
|
||||
if (pageNum <= 0 || pageNum > areasPagination.totalPages) return null;
|
||||
|
||||
return (
|
||||
<Button
|
||||
key={pageNum}
|
||||
variant={areasPagination.page === pageNum ? 'default' : 'outline'}
|
||||
size="sm"
|
||||
onClick={() => onPageChange(pageNum)}
|
||||
disabled={isAreasLoading}
|
||||
className="h-8 w-8 p-0"
|
||||
>
|
||||
{pageNum}
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => onPageChange(areasPagination.page + 1)}
|
||||
disabled={areasPagination.page >= areasPagination.totalPages || isAreasLoading}
|
||||
className="h-8"
|
||||
>
|
||||
Next
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
|
||||
@ -24,10 +24,10 @@ interface RegionDialogProps {
|
||||
setSelectedRegionZone: (id: string) => void;
|
||||
regionalManagerId: string;
|
||||
setRegionalManagerId: (id: string) => void;
|
||||
selectedRegionStates: string[]; // This now contains District IDs
|
||||
selectedRegionStates: string[]; // District IDs selected
|
||||
setSelectedRegionStates: (districts: string[]) => void;
|
||||
onSave: () => void;
|
||||
userAssignedData: any[]; // Used for RM selection
|
||||
userAssignedData: any[];
|
||||
}
|
||||
|
||||
export const RegionDialog: React.FC<RegionDialogProps> = ({
|
||||
@ -36,42 +36,128 @@ export const RegionDialog: React.FC<RegionDialogProps> = ({
|
||||
selectedRegionZone, setSelectedRegionZone, regionalManagerId, setRegionalManagerId,
|
||||
selectedRegionStates, setSelectedRegionStates, onSave, userAssignedData
|
||||
}) => {
|
||||
const { zones, allDistricts, regionalOffices } = useSelector((state: RootState) => state.master);
|
||||
const { zones, allStates, allDistricts, regionalOffices } = useSelector((state: RootState) => state.master);
|
||||
|
||||
// Map of District ID -> Region Name for districts assigned to other regions
|
||||
const districtsAssignedToOthers = React.useMemo(() => {
|
||||
const mapping: Record<string, string> = {};
|
||||
// Internal: which states are checked (for filtering districts)
|
||||
const [selectedStateIds, setSelectedStateIds] = React.useState<string[]>([]);
|
||||
|
||||
// --- Build conflict map: districtId -> region name (for districts in OTHER regions) ---
|
||||
const assignedToOtherRegion = React.useMemo(() => {
|
||||
const map: Record<string, string> = {};
|
||||
(regionalOffices || []).forEach((r: any) => {
|
||||
if (r.id !== editingRegionId) {
|
||||
(r.districts || []).forEach((d: any) => {
|
||||
mapping[d.id] = r.name;
|
||||
map[d.id] = r.name;
|
||||
});
|
||||
}
|
||||
});
|
||||
return mapping;
|
||||
return map;
|
||||
}, [regionalOffices, editingRegionId]);
|
||||
|
||||
const filteredRMUsers = userAssignedData.filter(u => {
|
||||
const roles = u.allRoles || [];
|
||||
return roles.some((r: string) => {
|
||||
const roleStr = (r || '').toUpperCase();
|
||||
return ['RM', 'RBM', 'REGIONAL MANAGER', 'ZBH', 'ZONE BUSINESS HEAD', 'ZONAL BUSINESS HEAD', 'ASM', 'AREA SALES MANAGER'].includes(roleStr) ||
|
||||
roleStr.includes('REGIONAL') || roleStr.includes('ZONAL') || roleStr.includes('AREA SALES');
|
||||
});
|
||||
// --- States visible for the selected zone (only states that have at least 1 district in this zone OR unassigned) ---
|
||||
const statesForZone = React.useMemo(() => {
|
||||
if (!selectedRegionZone) return allStates;
|
||||
const stateIdsWithZoneDistricts = new Set(
|
||||
allDistricts
|
||||
.filter(d =>
|
||||
d.zoneId === selectedRegionZone ||
|
||||
!d.zoneId ||
|
||||
selectedRegionStates.includes(d.id)
|
||||
)
|
||||
.map(d => d.stateId)
|
||||
.filter(Boolean)
|
||||
);
|
||||
return allStates.filter((s: any) =>
|
||||
s.zoneId === selectedRegionZone ||
|
||||
stateIdsWithZoneDistricts.has(s.id) ||
|
||||
!s.zoneId
|
||||
);
|
||||
}, [allStates, allDistricts, selectedRegionZone, selectedRegionStates]);
|
||||
|
||||
// --- Districts shown = only those in selected states + (in this zone or unassigned) ---
|
||||
const availableDistricts = React.useMemo(() => {
|
||||
if (selectedStateIds.length === 0) return [];
|
||||
return allDistricts.filter(d =>
|
||||
selectedStateIds.includes(d.stateId as string) &&
|
||||
(!d.zoneId || d.zoneId === selectedRegionZone || d.regionId === editingRegionId)
|
||||
);
|
||||
}, [allDistricts, selectedStateIds, selectedRegionZone, editingRegionId]);
|
||||
|
||||
// --- Group districts by state for rendering ---
|
||||
const districtsByState = React.useMemo(() => {
|
||||
const map: Record<string, { stateName: string; districts: typeof allDistricts }> = {};
|
||||
availableDistricts.forEach(d => {
|
||||
const state = allStates.find((s: any) => s.id === d.stateId);
|
||||
const stateName = state?.name || (d.stateId as string);
|
||||
if (!map[d.stateId as string]) map[d.stateId as string] = { stateName, districts: [] };
|
||||
map[d.stateId as string].districts.push(d);
|
||||
});
|
||||
return Object.values(map);
|
||||
}, [availableDistricts, allStates]);
|
||||
|
||||
// --- Prefill selected states and manager when dialog opens in EDIT mode ---
|
||||
React.useEffect(() => {
|
||||
if (!isOpen) {
|
||||
setSelectedStateIds([]);
|
||||
return;
|
||||
}
|
||||
|
||||
// Derive states from already-selected districts
|
||||
if (selectedRegionStates.length > 0) {
|
||||
const derived = Array.from(new Set(
|
||||
allDistricts
|
||||
.filter(d => selectedRegionStates.includes(d.id))
|
||||
.map(d => d.stateId)
|
||||
.filter(Boolean)
|
||||
)) as string[];
|
||||
setSelectedStateIds(derived);
|
||||
}
|
||||
}, [isOpen]); // Reduced dependencies to avoid re-triggering during active selection
|
||||
|
||||
// --- When zone changes, reset state & district selections ---
|
||||
const handleZoneChange = (zoneId: string) => {
|
||||
setSelectedRegionZone(zoneId);
|
||||
setSelectedStateIds([]);
|
||||
setSelectedRegionStates([]);
|
||||
};
|
||||
|
||||
// --- Toggle state checkbox ---
|
||||
const handleStateToggle = (stateId: string, checked: boolean) => {
|
||||
if (checked) {
|
||||
setSelectedStateIds(prev => [...prev, stateId]);
|
||||
} else {
|
||||
setSelectedStateIds(prev => prev.filter(id => id !== stateId));
|
||||
// Remove districts of this state from selection
|
||||
const districtIdsInState = allDistricts.filter(d => d.stateId === stateId).map(d => d.id);
|
||||
setSelectedRegionStates(selectedRegionStates.filter(id => !districtIdsInState.includes(id)));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{editingRegionId ? 'Edit' : 'Add'} Regional Office</DialogTitle>
|
||||
<DialogDescription>Configure regional office and assign to a zone</DialogDescription>
|
||||
<DialogDescription>Configure regional office details and coverage area</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-4">
|
||||
{/* Region Code & Name */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label>Region Code</Label>
|
||||
<Input placeholder="e.g., NZ-R1" className="mt-2 text-slate-900" value={regionCode} onChange={(e) => setRegionCode(e.target.value)} />
|
||||
</div>
|
||||
<div>
|
||||
<Label>Region Name</Label>
|
||||
<Input placeholder="e.g., Delhi NCR Region" className="mt-2 text-slate-900" value={regionName} onChange={(e) => setRegionName(e.target.value)} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Zone */}
|
||||
<div>
|
||||
<Label>Zone</Label>
|
||||
<Select value={selectedRegionZone} onValueChange={setSelectedRegionZone}>
|
||||
<Select value={selectedRegionZone} onValueChange={handleZoneChange}>
|
||||
<SelectTrigger className="mt-2 text-slate-900">
|
||||
<SelectValue placeholder="Select zone" />
|
||||
</SelectTrigger>
|
||||
@ -82,54 +168,100 @@ export const RegionDialog: React.FC<RegionDialogProps> = ({
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* Regional Manager — all users dropdown */}
|
||||
<div>
|
||||
<Label>Regional Manager</Label>
|
||||
<Select value={regionalManagerId} onValueChange={setRegionalManagerId}>
|
||||
<SelectTrigger className="mt-2 w-full text-slate-900">
|
||||
<SelectValue placeholder="Select Manager" />
|
||||
<SelectValue placeholder="Select from available users" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="max-h-60">
|
||||
{filteredRMUsers.map((user) => (
|
||||
{userAssignedData.map((user) => (
|
||||
<SelectItem key={user.id} value={user.id}>
|
||||
{user.name} ({user.email})
|
||||
{user.name || user.fullName}
|
||||
{user.email ? ` — ${user.email}` : ''}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label>Region Name</Label>
|
||||
<Input placeholder="e.g., Delhi Region" className="mt-2 text-slate-900" value={regionName} onChange={(e) => setRegionName(e.target.value)} />
|
||||
</div>
|
||||
<div>
|
||||
<Label>Region Code</Label>
|
||||
<Input placeholder="e.g., NZ-R1" className="mt-2 text-slate-900" value={regionCode} onChange={(e) => setRegionCode(e.target.value)} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
<div>
|
||||
<Label>Description</Label>
|
||||
<Textarea placeholder="Describe the region..." className="mt-2 text-slate-900" rows={3} value={regionDescription} onChange={(e) => setRegionDescription(e.target.value)} />
|
||||
<Textarea placeholder="Describe the region..." className="mt-2 text-slate-900" rows={2} value={regionDescription} onChange={(e) => setRegionDescription(e.target.value)} />
|
||||
</div>
|
||||
|
||||
{/* States Covered — scoped to selected zone */}
|
||||
<div>
|
||||
<Label>States Covered</Label>
|
||||
{!selectedRegionZone && (
|
||||
<p className="text-xs text-amber-600 mt-1">Select a zone first to see available states</p>
|
||||
)}
|
||||
<div className="mt-2 border rounded-lg p-3 max-h-40 overflow-y-auto bg-slate-50">
|
||||
{statesForZone.length === 0 ? (
|
||||
<p className="text-xs text-slate-400 italic">
|
||||
{selectedRegionZone ? 'No states with available districts in this zone' : 'Select a zone to load states'}
|
||||
</p>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{statesForZone.map((state: any) => (
|
||||
<div key={state.id} className="flex items-center space-x-2">
|
||||
<Checkbox
|
||||
id={`region-state-${state.id}`}
|
||||
checked={selectedStateIds.includes(state.id)}
|
||||
disabled={!selectedRegionZone}
|
||||
onCheckedChange={(checked) => handleStateToggle(state.id, !!checked)}
|
||||
/>
|
||||
<label
|
||||
htmlFor={`region-state-${state.id}`}
|
||||
className={`text-sm cursor-pointer ${!selectedRegionZone ? 'text-slate-400' : 'text-slate-900'}`}
|
||||
>
|
||||
{state.name}
|
||||
</label>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-xs text-slate-500 mt-1">
|
||||
{selectedStateIds.length} {selectedStateIds.length === 1 ? 'state' : 'states'} selected
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Districts Covered — filtered by zone + selected states */}
|
||||
<div>
|
||||
<Label>Districts Covered</Label>
|
||||
<div className="mt-2 border rounded-lg p-3 max-h-48 overflow-y-auto bg-slate-50">
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{allDistricts
|
||||
.map((district) => {
|
||||
const assignedRegionName = districtsAssignedToOthers[district.id];
|
||||
const isDisabled = !!assignedRegionName;
|
||||
<div className="mt-2 border rounded-lg p-3 max-h-56 overflow-y-auto bg-slate-50">
|
||||
{selectedStateIds.length === 0 ? (
|
||||
<p className="text-xs text-slate-400 italic">Select one or more states above to see districts</p>
|
||||
) : districtsByState.length === 0 ? (
|
||||
<p className="text-xs text-slate-400 italic">No available districts in the selected states for this zone</p>
|
||||
) : (
|
||||
<TooltipProvider>
|
||||
{districtsByState.map(({ stateName, districts }) => (
|
||||
<div key={stateName} className="mb-4 last:mb-0">
|
||||
<h4 className="text-xs font-semibold text-amber-700 uppercase tracking-wide mb-2 pb-1 border-b border-slate-200">
|
||||
{stateName}
|
||||
</h4>
|
||||
<div className="space-y-2 ml-1">
|
||||
{districts.map((district) => {
|
||||
const conflictRegion = assignedToOtherRegion[district.id];
|
||||
const inDifferentZone = district.zoneId && district.zoneId !== selectedRegionZone && district.regionId !== editingRegionId;
|
||||
const isDisabled = !!(conflictRegion || inDifferentZone);
|
||||
|
||||
const tooltipText = conflictRegion
|
||||
? `Already assigned to region: ${conflictRegion}`
|
||||
: inDifferentZone
|
||||
? `Belongs to a different zone`
|
||||
: '';
|
||||
|
||||
return (
|
||||
<div key={district.id} className="flex items-center space-x-2">
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="flex items-center space-x-2 w-full">
|
||||
<Checkbox
|
||||
id={`region-district-${district.id}`}
|
||||
disabled={isDisabled}
|
||||
@ -144,26 +276,38 @@ export const RegionDialog: React.FC<RegionDialogProps> = ({
|
||||
/>
|
||||
<label
|
||||
htmlFor={`region-district-${district.id}`}
|
||||
className={`text-sm cursor-pointer ${isDisabled ? 'text-slate-400' : 'text-slate-900'}`}
|
||||
className={`text-sm flex-1 ${isDisabled ? 'text-slate-400 cursor-not-allowed line-through' : 'text-slate-900 cursor-pointer'}`}
|
||||
>
|
||||
{district.name}
|
||||
{conflictRegion && (
|
||||
<span className="ml-2 text-xs text-red-400 font-normal no-underline" style={{ textDecoration: 'none' }}>
|
||||
(in {conflictRegion})
|
||||
</span>
|
||||
)}
|
||||
</label>
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
{isDisabled && (
|
||||
{isDisabled && tooltipText && (
|
||||
<TooltipContent>
|
||||
<p className="text-xs">Already assigned to {assignedRegionName}</p>
|
||||
<p className="text-xs">{tooltipText}</p>
|
||||
</TooltipContent>
|
||||
)}
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</TooltipProvider>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-xs text-slate-500 mt-1">
|
||||
{selectedRegionStates.length} {selectedRegionStates.length === 1 ? 'district' : 'districts'} selected
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex gap-3 pt-4">
|
||||
<Button variant="outline" className="flex-1" onClick={() => onOpenChange(false)}>Cancel</Button>
|
||||
<Button className="flex-1 bg-amber-600 hover:bg-amber-700" onClick={onSave}>Save Regional Office</Button>
|
||||
|
||||
179
src/components/applications/MasterPage/RoleDialog.tsx
Normal file
179
src/components/applications/MasterPage/RoleDialog.tsx
Normal file
@ -0,0 +1,179 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "../../ui/dialog";
|
||||
import { Button } from "../../ui/button";
|
||||
import { Checkbox } from "../../ui/checkbox";
|
||||
import { Badge } from "../../ui/badge";
|
||||
import { Shield, Save } from "lucide-react";
|
||||
|
||||
interface RoleDialogProps {
|
||||
isOpen: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
role: any;
|
||||
onSave: (roleId: string, permissions: string[]) => void;
|
||||
}
|
||||
|
||||
export const RoleDialog: React.FC<RoleDialogProps> = ({
|
||||
isOpen,
|
||||
onOpenChange,
|
||||
role,
|
||||
onSave,
|
||||
}) => {
|
||||
const [selectedPermissions, setSelectedPermissions] = useState<string[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
if (role) {
|
||||
setSelectedPermissions(role.permissions || []);
|
||||
}
|
||||
}, [role, isOpen]);
|
||||
|
||||
const togglePermission = (perm: string) => {
|
||||
setSelectedPermissions(prev =>
|
||||
prev.includes(perm)
|
||||
? prev.filter(p => p !== perm)
|
||||
: [...prev, perm]
|
||||
);
|
||||
};
|
||||
|
||||
if (!role) return null;
|
||||
|
||||
const permissionSections = [
|
||||
{
|
||||
title: "Action Permissions",
|
||||
color: "from-green-50 to-emerald-50 border-green-200",
|
||||
textColor: "text-green-900",
|
||||
permissions: [
|
||||
{ id: "action:approve", label: "Approve Applications" },
|
||||
{ id: "action:reject", label: "Reject Applications" },
|
||||
{ id: "action:upload_docs", label: "Upload Documents" },
|
||||
{ id: "action:request_changes", label: "Request Changes" },
|
||||
{ id: "action:forward", label: "Forward to Others" },
|
||||
{ id: "action:reassign", label: "Reassign Applications" },
|
||||
{ id: "action:schedule_interview", label: "Schedule Interviews" },
|
||||
{ id: "action:add_comments", label: "Add Comments/Notes" },
|
||||
{ id: "action:rank_applicants", label: "Rank Applicants" },
|
||||
{ id: "action:final_approval", label: "Final Approval" },
|
||||
]
|
||||
},
|
||||
{
|
||||
title: "View/Access Permissions",
|
||||
color: "from-blue-50 to-cyan-50 border-blue-200",
|
||||
textColor: "text-blue-900",
|
||||
permissions: [
|
||||
{ id: "view:view_details", label: "Application Details" },
|
||||
{ id: "view:view_financial", label: "Financial Information" },
|
||||
{ id: "view:view_discussions", label: "Discussion Notes" },
|
||||
{ id: "view:view_progress", label: "Progress Tracking" },
|
||||
{ id: "view:view_audit", label: "Audit Logs" },
|
||||
{ id: "view:view_documents", label: "All Documents" },
|
||||
{ id: "view:view_personal", label: "Personal Information" },
|
||||
{ id: "view:view_business", label: "Business Details" },
|
||||
{ id: "view:view_reports", label: "Reports & Analytics" },
|
||||
{ id: "view:view_history", label: "Application History" },
|
||||
]
|
||||
},
|
||||
{
|
||||
title: "Application Stage Access",
|
||||
color: "from-amber-50 to-orange-50 border-amber-200",
|
||||
textColor: "text-amber-900",
|
||||
permissions: [
|
||||
{ id: "stage:initial_review", label: "Initial Review" },
|
||||
{ id: "stage:field_verification", label: "Field Verification" },
|
||||
{ id: "stage:level1_interview", label: "Level 1 Interview" },
|
||||
{ id: "stage:level2_interview", label: "Level 2 Interview" },
|
||||
{ id: "stage:ranking", label: "Ranking & Selection" },
|
||||
{ id: "stage:legal_review", label: "Legal Review" },
|
||||
{ id: "stage:financial_review", label: "Financial Review" },
|
||||
{ id: "stage:final_approval", label: "Final Approval" },
|
||||
{ id: "stage:payment", label: "Payment Verification" },
|
||||
{ id: "stage:onboarding", label: "Onboarding" },
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="sm:max-w-lg max-w-3xl max-h-[90vh] overflow-y-auto p-0 gap-0 border-none shadow-2xl custom-scrollbar">
|
||||
<DialogHeader className="p-6 pb-2 space-y-2 text-left bg-white sticky top-0 z-10 border-b">
|
||||
<DialogTitle className="text-xl font-bold tracking-tight">Edit Role Permissions - {role.name}</DialogTitle>
|
||||
<DialogDescription className="text-slate-500 text-sm">
|
||||
Configure default permissions for all users assigned to the {role.name} role
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="p-6 space-y-6">
|
||||
<div className="bg-gradient-to-br from-purple-50 to-indigo-50 rounded-xl p-5 border border-purple-100 shadow-sm">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-12 h-12 bg-white rounded-xl shadow-sm flex items-center justify-center border border-purple-100">
|
||||
<Shield className="w-6 h-6 text-purple-600" />
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="text-base font-bold text-slate-900">{role.name}</h4>
|
||||
<p className="text-xs font-medium text-slate-500 uppercase tracking-widest">{role.userCount || 0} users currently assigned</p>
|
||||
</div>
|
||||
</div>
|
||||
<Badge className="bg-purple-600 hover:bg-purple-700 px-3 py-1 rounded-full text-[10px] font-bold tracking-wider uppercase">
|
||||
Role Configuration
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-5">
|
||||
<h4 className="text-sm font-bold text-slate-800 flex items-center gap-2">
|
||||
<span className="w-1.5 h-1.5 bg-amber-500 rounded-full"></span>
|
||||
Configure Default Permissions
|
||||
</h4>
|
||||
|
||||
<div className="space-y-4">
|
||||
{permissionSections.map((section, sidx) => (
|
||||
<div key={sidx} className={`border rounded-xl p-5 bg-gradient-to-br ${section.color} shadow-sm transition-all hover:shadow-md`}>
|
||||
<h5 className={`text-sm font-bold ${section.textColor} mb-4 flex items-center justify-between`}>
|
||||
{section.title}
|
||||
<span className="text-[10px] bg-white/50 px-2 py-0.5 rounded-full opacity-70 italic font-normal">Section {sidx + 1}</span>
|
||||
</h5>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-y-3 gap-x-6">
|
||||
{section.permissions.map((perm) => (
|
||||
<div key={perm.id} className="flex items-center space-x-3 group cursor-pointer p-1 rounded-md hover:bg-white/40 transition-colors">
|
||||
<Checkbox
|
||||
id={`perm-${perm.id}`}
|
||||
checked={selectedPermissions.includes(perm.id)}
|
||||
onCheckedChange={() => togglePermission(perm.id)}
|
||||
className="border-slate-300 data-[state=checked]:bg-purple-600 data-[state=checked]:border-purple-600"
|
||||
/>
|
||||
<label
|
||||
htmlFor={`perm-${perm.id}`}
|
||||
className="text-sm font-medium text-slate-700 cursor-pointer group-hover:text-slate-900 transition-colors flex-1"
|
||||
>
|
||||
{perm.label}
|
||||
</label>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3 p-6 pt-4 border-t sticky bottom-0 bg-white/80 backdrop-blur-md z-10">
|
||||
<Button variant="ghost" onClick={() => onOpenChange(false)} className="flex-1 h-11 font-bold text-slate-600 hover:bg-slate-100">
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => onSave(role.id, selectedPermissions)}
|
||||
className="flex-1 h-11 bg-purple-600 hover:bg-purple-700 font-bold text-white shadow-lg shadow-purple-200"
|
||||
>
|
||||
<Save className="w-4 h-4 mr-2" />
|
||||
Save Role Permissions
|
||||
</Button>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
@ -3,7 +3,7 @@ import { useSelector } from 'react-redux';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../../ui/card';
|
||||
import { Button } from '../../ui/button';
|
||||
import { Badge } from '../../ui/badge';
|
||||
import { Shield, Plus, Edit2, Users } from 'lucide-react';
|
||||
import { Shield, Plus, Pen } from 'lucide-react';
|
||||
import { RootState } from '../../../store';
|
||||
|
||||
interface RolePermissionsProps {
|
||||
@ -15,54 +15,59 @@ export const RolePermissions: React.FC<RolePermissionsProps> = ({ onAddRole, onE
|
||||
const { roles } = useSelector((state: RootState) => state.master);
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<div className="flex items-center justify-between">
|
||||
<Card className="bg-card text-card-foreground flex flex-col gap-6 rounded-xl border shadow-sm">
|
||||
<CardHeader className="@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 pt-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6">
|
||||
<div className="flex items-center justify-between w-full">
|
||||
<div>
|
||||
<CardTitle>Roles & Access Controls</CardTitle>
|
||||
<CardDescription>Manage user roles and their associated system permissions</CardDescription>
|
||||
<CardTitle className="leading-none text-xl font-bold">Role Definitions</CardTitle>
|
||||
<CardDescription className="text-muted-foreground mt-1.5">Overview of available roles and their access levels</CardDescription>
|
||||
</div>
|
||||
<Button onClick={onAddRole} className="bg-amber-600 hover:bg-amber-700">
|
||||
<Button onClick={onAddRole} className="bg-amber-600 hover:bg-amber-700 h-9">
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
Add Role
|
||||
</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
<CardContent className="px-6 [&:last-child]:pb-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{roles.map((role) => (
|
||||
<div key={role.id} className="border-2 rounded-xl p-6 bg-white hover:border-amber-200 transition-all group shadow-sm">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div className="w-12 h-12 bg-purple-50 rounded-xl flex items-center justify-center group-hover:bg-purple-100 transition-colors">
|
||||
<Shield className="w-6 h-6 text-purple-700" />
|
||||
<div key={role.id} className="border rounded-lg p-4 space-y-3 bg-gradient-to-br from-white to-slate-50 hover:shadow-md transition-shadow">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<Shield className="w-5 h-5 text-amber-600" />
|
||||
<h3 className="text-slate-900 font-bold">{role.name}</h3>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button variant="ghost" size="sm" onClick={() => onEditRole(role)} className="h-8 w-8 p-0">
|
||||
<Edit2 className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<h3 className="text-lg font-bold text-slate-900 mb-1">{role.name}</h3>
|
||||
<div className="flex items-center gap-2 text-sm text-slate-500 mb-4">
|
||||
<Users className="w-4 h-4" />
|
||||
<span>{role.userCount} Active Users</span>
|
||||
<Badge variant="secondary" className="border-transparent bg-secondary text-secondary-foreground text-xs font-medium">
|
||||
{role.userCount || 0} users
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<p className="text-xs font-semibold text-slate-400 uppercase tracking-wider">Key Permissions</p>
|
||||
<div className="flex flex-wrap gap-1.5">
|
||||
{role.permissions?.slice(0, 6).map((perm: string, idx: number) => (
|
||||
<Badge key={idx} variant="secondary" className="text-[10px] bg-slate-50 border-slate-200 font-medium">
|
||||
{perm.replace(/_/g, ' ')}
|
||||
<div>
|
||||
<label className="items-center gap-2 font-medium text-xs text-slate-600 mb-2 block">Key Permissions</label>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{(role.permissions || []).slice(0, 3).map((perm: string, idx: number) => (
|
||||
<Badge key={idx} variant="outline" className="border px-2 py-0.5 font-medium text-foreground text-[10px] bg-white/50">
|
||||
{perm.replace(/_/g, ' ').toLowerCase()}
|
||||
</Badge>
|
||||
))}
|
||||
{role.permissions?.length > 6 && (
|
||||
{role.permissions?.length > 3 && (
|
||||
<Badge variant="outline" className="text-[10px] border-slate-200">
|
||||
+{role.permissions.length - 6} more
|
||||
+{role.permissions.length - 3}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => onEditRole(role)}
|
||||
className="w-full mt-2 h-8 rounded-md gap-1.5 px-3 border bg-background text-foreground hover:bg-accent hover:text-accent-foreground transition-colors"
|
||||
title="Edit Permissions"
|
||||
>
|
||||
<Pen className="w-3 h-3 mr-1" />
|
||||
Edit Permissions
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@ -182,16 +182,18 @@ export const ZMDialog: React.FC<ZMDialogProps> = ({
|
||||
const selectedUser = userAssignedData.find(u => u.id === value);
|
||||
if (selectedUser) {
|
||||
setZmName(selectedUser.name);
|
||||
setZmCode(selectedUser.zmCode || '');
|
||||
setZmEmployeeId(selectedUser.employeeId || '');
|
||||
// Note: zmCode logic might need to check how it's stored in territoryProfile
|
||||
const zmProfile = selectedUser.territoryProfile?.find((tp: any) => tp.roleCode === 'ZM' || tp.roleCode === 'DD-ZM');
|
||||
if (zmProfile) {
|
||||
setZmCode(zmProfile.managerCode || '');
|
||||
setSelectedZMZone(zmProfile.zoneId || '');
|
||||
|
||||
// Search territoryProfile for ZM/DD-ZM role to find the existing managerCode
|
||||
const zmProfile = (selectedUser.territoryProfile || []).find((tp: any) => tp.roleCode === 'ZM' || tp.roleCode === 'DD-ZM');
|
||||
const existingCode = zmProfile?.managerCode || selectedUser.zmCode || selectedUser.employeeId || '';
|
||||
setZmCode(existingCode);
|
||||
|
||||
if (zmProfile?.zoneId) {
|
||||
setSelectedZMZone(zmProfile.zoneId);
|
||||
}
|
||||
|
||||
setSelectedZMDistricts(selectedUser.districts?.map((d: any) => d.id) || []);
|
||||
// Extract state names from assigned districts if not explicit
|
||||
if (selectedUser.stateNames) setSelectedZMStates(selectedUser.stateNames);
|
||||
}
|
||||
}}
|
||||
|
||||
@ -17,9 +17,13 @@ interface ZMManagementProps {
|
||||
export const ZMManagement: React.FC<ZMManagementProps> = ({
|
||||
selectedZone, onAddZM, onEditZM, onDeleteZM
|
||||
}) => {
|
||||
const { zonalManagerMappings } = useSelector((state: RootState) => state.master);
|
||||
const { zonalManagers } = useSelector((state: RootState) => state.master);
|
||||
|
||||
const filteredZMs = zonalManagerMappings.filter((zm: any) => selectedZone === 'all' || zm.zoneId === selectedZone);
|
||||
const filteredZMs = (zonalManagers || []).filter((zm: any) =>
|
||||
selectedZone === 'all' ||
|
||||
zm.zoneId === selectedZone ||
|
||||
(zm.zones && zm.zones.includes(selectedZone))
|
||||
);
|
||||
|
||||
return (
|
||||
<Card>
|
||||
@ -42,7 +46,6 @@ export const ZMManagement: React.FC<ZMManagementProps> = ({
|
||||
<TableHead>ZM Code</TableHead>
|
||||
<TableHead>Name</TableHead>
|
||||
<TableHead>Zone</TableHead>
|
||||
<TableHead>Region</TableHead>
|
||||
<TableHead>Districts Managed</TableHead>
|
||||
<TableHead>Contact</TableHead>
|
||||
<TableHead>Status</TableHead>
|
||||
@ -55,14 +58,17 @@ export const ZMManagement: React.FC<ZMManagementProps> = ({
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-2">
|
||||
<Users className="w-4 h-4 text-blue-600" />
|
||||
<span className="font-medium">{zm.code}</span>
|
||||
<span className="font-medium">{zm.zmCode || zm.code}</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>{zm.name}</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant="outline">{zm.zoneName}</Badge>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{(zm.zones || [zm.zoneName]).map((z: string, i: number) => (
|
||||
<Badge key={i} variant="outline">{z}</Badge>
|
||||
))}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="text-sm text-slate-600">{zm.regionName}</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{zm.districts.slice(0, 3).map((district: any, idx: number) => (
|
||||
|
||||
@ -5,7 +5,7 @@ import { Button } from '../../ui/button';
|
||||
import { Badge } from '../../ui/badge';
|
||||
import { ScrollArea } from '../../ui/scroll-area';
|
||||
import { Label } from '../../ui/label';
|
||||
import { Globe, Plus, Edit2, UserCog, Mail, Users, MapPin } from 'lucide-react';
|
||||
import { Globe, Plus, Edit2, Mail, Users, MapPin } from 'lucide-react';
|
||||
import { RootState } from '../../../store';
|
||||
|
||||
interface ZoneDetailsProps {
|
||||
@ -49,7 +49,12 @@ export const ZoneDetails: React.FC<ZoneDetailsProps> = ({ selectedZone, onAddZon
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline" size="sm" onClick={() => onEditZone(zone)}>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => onEditZone(zone)}
|
||||
className="h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5"
|
||||
>
|
||||
<Edit2 className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
@ -65,36 +70,17 @@ export const ZoneDetails: React.FC<ZoneDetailsProps> = ({ selectedZone, onAddZon
|
||||
<Label className="text-xs text-slate-600 mb-2 block">States Covered ({zone.states.length})</Label>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{zone.states.map((state: string, idx: number) => (
|
||||
<Badge key={idx} variant="secondary" className="text-xs">
|
||||
<Badge
|
||||
key={idx}
|
||||
variant="secondary"
|
||||
className="text-xs border-transparent bg-secondary text-secondary-foreground"
|
||||
>
|
||||
{state}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{zone.zonalBusinessHead && zone.zonalBusinessHead.name && (
|
||||
<div className="border-t pt-3">
|
||||
<Label className="text-xs text-slate-600 mb-2 block">Zone Business Head (ZBH)</Label>
|
||||
<div className="bg-amber-50 rounded-lg p-3 space-y-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<UserCog className="w-4 h-4 text-amber-600" />
|
||||
<span className="text-sm text-slate-900">{zone.zonalBusinessHead.name}</span>
|
||||
</div>
|
||||
{zone.zonalBusinessHead.email && (
|
||||
<div className="flex items-center gap-2 ml-6">
|
||||
<Mail className="w-3 h-3 text-slate-400" />
|
||||
<span className="text-xs text-slate-600">{zone.zonalBusinessHead.email}</span>
|
||||
</div>
|
||||
)}
|
||||
{zone.zonalBusinessHead.phone && (
|
||||
<div className="flex items-center gap-2 ml-6">
|
||||
<span className="text-xs text-slate-600">{zone.zonalBusinessHead.phone}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{zone.zonalManagers && zone.zonalManagers.length > 0 && (
|
||||
<div className="border-t pt-3">
|
||||
<Label className="text-xs text-slate-600 mb-2 block">
|
||||
@ -108,17 +94,20 @@ export const ZoneDetails: React.FC<ZoneDetailsProps> = ({ selectedZone, onAddZon
|
||||
<span className="text-sm text-slate-900">{zm.name}</span>
|
||||
<Badge variant="outline" className="text-xs ml-auto">ZM-{idx + 1}</Badge>
|
||||
</div>
|
||||
|
||||
{zm.email && (
|
||||
<div className="flex items-center gap-2 ml-6">
|
||||
<Mail className="w-3 h-3 text-slate-400" />
|
||||
<span className="text-xs text-slate-600">{zm.email}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{zm.phone && (
|
||||
<div className="flex items-center gap-2 ml-6">
|
||||
<span className="text-xs text-slate-600">{zm.phone}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{zm.districts && zm.districts.length > 0 && (
|
||||
<div className="ml-6 mt-2">
|
||||
<Label className="text-xs text-slate-500 mb-1 block">
|
||||
@ -126,7 +115,11 @@ export const ZoneDetails: React.FC<ZoneDetailsProps> = ({ selectedZone, onAddZon
|
||||
</Label>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{zm.districts.map((district: string, dIdx: number) => (
|
||||
<Badge key={dIdx} variant="outline" className="text-xs bg-white">
|
||||
<Badge
|
||||
key={dIdx}
|
||||
variant="outline"
|
||||
className="text-xs bg-white text-foreground"
|
||||
>
|
||||
<MapPin className="w-2.5 h-2.5 mr-1" />
|
||||
{district}
|
||||
</Badge>
|
||||
|
||||
@ -36,6 +36,13 @@ export const ZoneDialog: React.FC<ZoneDialogProps> = ({
|
||||
});
|
||||
});
|
||||
|
||||
// PRE-FILLING: When editing an existing zone, find its current ZBH from master data
|
||||
React.useEffect(() => {
|
||||
if (editingZoneId && isOpen) {
|
||||
// we already have the zonalBusinessHeadId passed as prop FROM MasterPage
|
||||
}
|
||||
}, [editingZoneId, isOpen]);
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-md">
|
||||
|
||||
@ -2,7 +2,8 @@ import { useCallback } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { masterService } from '../services/master.service';
|
||||
import {
|
||||
setMasterData, setLoading, setError
|
||||
setMasterData, setLoading, setError,
|
||||
setAreasData, setAreasLoading
|
||||
} from '../store/slices/masterSlice';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
@ -15,7 +16,8 @@ export const useMasterData = () => {
|
||||
|
||||
const [
|
||||
rolesRes, zonesRes, permsRes, regionsRes, usersRes,
|
||||
statesRes, emailTemplatesRes, districtsRes, areasRes, slaRes
|
||||
statesRes, emailTemplatesRes, districtsRes, areasRes, slaRes,
|
||||
asmsRes, zmsRes
|
||||
] = await Promise.all([
|
||||
masterService.getRoles().catch(() => ({ success: false })),
|
||||
masterService.getZones().catch(() => ({ success: false })),
|
||||
@ -24,9 +26,11 @@ export const useMasterData = () => {
|
||||
masterService.getUsers().catch(() => ({ success: false })),
|
||||
masterService.getStates().catch(() => ({ success: false })),
|
||||
masterService.getEmailTemplates().catch(() => ({ success: false })),
|
||||
masterService.getDistricts().catch(() => ({ success: false })),
|
||||
masterService.getAreas().catch(() => ({ success: false })),
|
||||
masterService.getSlaConfigs().catch(() => ({ success: false }))
|
||||
masterService.getDistricts({ limit: 'all' }).catch(() => ({ success: false })),
|
||||
masterService.getAreas({ limit: 'all' }).catch(() => ({ success: false })),
|
||||
masterService.getSlaConfigs().catch(() => ({ success: false })),
|
||||
(masterService as any).getASMs().catch(() => ({ success: false })),
|
||||
(masterService as any).getZonalManagers().catch(() => ({ success: false }))
|
||||
]);
|
||||
|
||||
const getBody = (res: any) => res.success ? res : (res.data ? res.data : res);
|
||||
@ -41,6 +45,8 @@ export const useMasterData = () => {
|
||||
const bodyDistricts = getBody(districtsRes);
|
||||
const bodyAreas = getBody(areasRes);
|
||||
const bodySla = getBody(slaRes);
|
||||
const bodyAsms = getBody(asmsRes);
|
||||
const bodyZms = getBody(zmsRes);
|
||||
|
||||
const users = (bodyUsers?.users || bodyUsers?.data || []).map((u: any) => ({
|
||||
...u,
|
||||
@ -73,11 +79,12 @@ export const useMasterData = () => {
|
||||
email: z.zonalBusinessHead?.email || '',
|
||||
phone: z.zonalBusinessHead?.mobileNumber || ''
|
||||
},
|
||||
zonalManagers: zoneZmUsers.map((m: any) => ({
|
||||
name: m.fullName || 'Unknown',
|
||||
zonalManagers: (z.zonalManagers || []).map((m: any) => ({
|
||||
id: m.id,
|
||||
name: m.name || m.fullName || 'Unknown',
|
||||
email: m.email || '',
|
||||
phone: m.mobileNumber || '',
|
||||
districts: m.territoryProfile?.filter((t: any) => (t.roleCode === 'ZM' || t.role === 'ZONAL MANAGER') && (t.zone === zoneName || t.zoneId === z.id) && t.locationType === 'district').map((t: any) => t.locationName) || []
|
||||
phone: m.phone || m.mobileNumber || '',
|
||||
districts: m.districts || []
|
||||
}))
|
||||
};
|
||||
});
|
||||
@ -90,39 +97,22 @@ export const useMasterData = () => {
|
||||
zoneName: r.zoneName || 'Unknown',
|
||||
states: r.states || [],
|
||||
cities: r.cities || [],
|
||||
districts: (r.districts || []).map((d: any) => ({ id: d.id, name: d.name, stateId: d.stateId })),
|
||||
asmCount: r.asmCount || 0,
|
||||
regionalOfficerCount: r.regionalOfficerCount || 0,
|
||||
regionalManager: r.regionalManager ? {
|
||||
id: r.regionalManager.id,
|
||||
name: r.regionalManager.fullName,
|
||||
name: r.regionalManager.fullName || r.regionalManager.name,
|
||||
email: r.regionalManager.email,
|
||||
phone: r.regionalManager.mobileNumber
|
||||
phone: r.regionalManager.mobileNumber || r.regionalManager.phone
|
||||
} : undefined,
|
||||
status: r.isActive !== false ? 'Active' : 'Inactive'
|
||||
}));
|
||||
|
||||
const asms = users.filter((u: any) => u.allRoles?.some((r: string) => r === 'ASM' || r === 'AREA SALES MANAGER'))
|
||||
.map((u: any) => {
|
||||
const asmT = u.territoryProfile?.find((t: any) => t.roleCode === 'ASM') || {};
|
||||
const asmCode = asmT.managerCode || u.asmCode || '';
|
||||
const areasManaged = u.territoryProfile?.filter((t: any) => t.roleCode === 'ASM' && (t.locationType === 'area' || t.locationType === 'district')).map((t: any) => ({ id: t.locationId, name: t.locationName })) || [];
|
||||
const stateNames = Array.from(new Set(u.territoryProfile?.filter((t: any) => t.roleCode === 'ASM' && t.state).map((t: any) => t.state).filter(Boolean))) as string[];
|
||||
const asms = (bodyAsms?.data || bodyAsms || []);
|
||||
const zonalManagers = (bodyZms?.data || bodyZms || []);
|
||||
|
||||
return {
|
||||
id: u.id, name: u.fullName, code: u.employeeId || 'N/A',
|
||||
asmCode: asmCode, employeeId: u.employeeId || '',
|
||||
email: u.email, phone: u.mobileNumber,
|
||||
zoneId: asmT.zoneId,
|
||||
regionId: asmT.regionId,
|
||||
zoneName: asmT.zone || u.zone || 'Unassigned',
|
||||
regionName: asmT.region || u.region || 'Unassigned',
|
||||
areasManaged: areasManaged, // Now contains {id, name} objects
|
||||
stateNames: stateNames,
|
||||
status: u.status
|
||||
};
|
||||
});
|
||||
|
||||
const zonalManagerMappings = users.filter((u: any) => u.allRoles?.some((r: string) => (r === 'ZM' || r === 'DD-ZM' || r.includes('ZONAL MANAGER')) && !r.includes('HEAD')))
|
||||
const zonalManagerMappings = zonalManagers.length > 0 ? zonalManagers : users.filter((u: any) => u.allRoles?.some((r: string) => (r === 'ZM' || r === 'DD-ZM' || r.includes('ZONAL MANAGER')) && !r.includes('HEAD')))
|
||||
.map((u: any) => {
|
||||
const zmT = u.territoryProfile?.find((t: any) => (t.roleCode === 'ZM' || t.roleCode === 'DD-ZM' || t.role === 'ZONAL MANAGER')) || {};
|
||||
const districts = u.territoryProfile?.filter((t: any) => (t.roleCode === 'ZM' || t.roleCode === 'DD-ZM' || t.role === 'ZONAL MANAGER') && t.locationType === 'district').map((t: any) => ({ id: t.locationId, name: t.locationName })) || [];
|
||||
@ -145,7 +135,10 @@ export const useMasterData = () => {
|
||||
const allDistricts = (bodyDistricts?.districts || bodyDistricts?.data || []).map((d: any) => ({
|
||||
...d,
|
||||
districtName: d.name,
|
||||
stateId: d.stateId // Now populated by backend's deep resolution
|
||||
stateId: d.stateId,
|
||||
asmId: d.asmId,
|
||||
ddAmId: d.ddAmId,
|
||||
zmId: d.zmId
|
||||
}));
|
||||
|
||||
const allAreas = (bodyAreas.areas || bodyAreas.data || []).map((a: any) => ({
|
||||
@ -160,7 +153,7 @@ export const useMasterData = () => {
|
||||
}));
|
||||
|
||||
dispatch(setMasterData({
|
||||
zones, regionalOffices, asms, zonalManagerMappings,
|
||||
zones, regionalOffices, asms, zonalManagerMappings, zonalManagers,
|
||||
roles, allStates, allDistricts, allAreas,
|
||||
availablePermissions: bodyPerms?.permissions || bodyPerms?.data || [],
|
||||
emailTemplates: bodyEmail?.data || [],
|
||||
@ -178,5 +171,20 @@ export const useMasterData = () => {
|
||||
}
|
||||
}, [dispatch]);
|
||||
|
||||
return { fetchInitialData };
|
||||
const fetchAreas = useCallback(async (params?: { search?: string; page?: number; limit?: number }) => {
|
||||
try {
|
||||
dispatch(setAreasLoading(true));
|
||||
const res = await masterService.getAreas(params) as any;
|
||||
if (res.success) {
|
||||
dispatch(setAreasData({ data: res.data, pagination: res.pagination }));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[useMasterData] Error fetching areas:', error);
|
||||
toast.error('Could not load locations');
|
||||
} finally {
|
||||
dispatch(setAreasLoading(false));
|
||||
}
|
||||
}, [dispatch]);
|
||||
|
||||
return { fetchInitialData, fetchAreas };
|
||||
};
|
||||
|
||||
@ -40,16 +40,16 @@ export const masterService = {
|
||||
const response = await API.getRegions();
|
||||
return response.data;
|
||||
},
|
||||
getStates: async (zoneId?: string) => {
|
||||
const response = await API.getStates(zoneId);
|
||||
getStates: async (params?: any) => {
|
||||
const response = await API.getStates(params);
|
||||
return response.data;
|
||||
},
|
||||
getDistricts: async (stateId?: string) => {
|
||||
const response = await API.getDistricts(stateId);
|
||||
getDistricts: async (params?: any) => {
|
||||
const response = await API.getDistricts(params);
|
||||
return response.data;
|
||||
},
|
||||
getAreas: async (districtId?: string) => {
|
||||
const response = await API.getAreas(districtId);
|
||||
getAreas: async (params?: any) => {
|
||||
const response = await API.getAreas(params);
|
||||
return response.data;
|
||||
},
|
||||
updateArea: async (id: string, data: any) => {
|
||||
@ -60,10 +60,25 @@ export const masterService = {
|
||||
const response = await API.createArea(data);
|
||||
return response.data;
|
||||
},
|
||||
deleteArea: async (id: string) => {
|
||||
const response = await (API as any).deleteArea(id);
|
||||
return response.data;
|
||||
},
|
||||
getAreaManagers: async () => {
|
||||
const response = await API.getAreaManagers();
|
||||
return response.data;
|
||||
},
|
||||
getASMs: async () => {
|
||||
const response = await (API as any).getASMs();
|
||||
return response.data;
|
||||
},
|
||||
getZonalManagers: async () => {
|
||||
const response = await (API as any).getZonalManagers();
|
||||
return response.data;
|
||||
},
|
||||
saveZonalManager: async (data: any) => {
|
||||
return (API as any).saveZonalManager(data).then((res: any) => res.data);
|
||||
},
|
||||
|
||||
// User Management
|
||||
getUsers: async () => {
|
||||
|
||||
@ -19,11 +19,12 @@ export interface Region {
|
||||
name: string;
|
||||
zoneId: string;
|
||||
zoneName: string;
|
||||
districts: { id: string; name: string }[];
|
||||
districts: { id: string; name: string, stateId?: string }[];
|
||||
states: string[];
|
||||
status: string;
|
||||
regionalOfficerCount: number;
|
||||
asmCount: number;
|
||||
regionalManager?: { id: string; fullName: string; email: string } | null;
|
||||
regionalManager?: { id: string; fullName: string; email: string; phone?: string } | null;
|
||||
}
|
||||
|
||||
export interface ASM {
|
||||
@ -36,7 +37,7 @@ export interface ASM {
|
||||
regionId: string;
|
||||
zoneName: string;
|
||||
regionName: string;
|
||||
areasManaged: string[];
|
||||
areasManaged: { id: string; name: string }[];
|
||||
districtNames: string[];
|
||||
stateNames: string[];
|
||||
email: string;
|
||||
@ -54,8 +55,11 @@ export interface ZonalManagerMapping {
|
||||
zoneName: string;
|
||||
regionId: string;
|
||||
regionName: string;
|
||||
districts: string[];
|
||||
districts: { id: string; name: string }[];
|
||||
status: string;
|
||||
city?: string;
|
||||
openFrom?: string | Date | null;
|
||||
openTo?: string | Date | null;
|
||||
}
|
||||
|
||||
export interface MasterState {
|
||||
@ -63,6 +67,7 @@ export interface MasterState {
|
||||
regionalOffices: Region[];
|
||||
asms: ASM[];
|
||||
zonalManagerMappings: ZonalManagerMapping[];
|
||||
zonalManagers: any[];
|
||||
roles: any[];
|
||||
allStates: { id: string, name: string, zone?: { name: string } }[];
|
||||
allDistricts: {
|
||||
@ -76,14 +81,24 @@ export interface MasterState {
|
||||
regionName: string,
|
||||
zoneName: string,
|
||||
asmName?: string,
|
||||
isActive: boolean
|
||||
isActive: boolean,
|
||||
city?: string,
|
||||
openFrom?: string | Date | null,
|
||||
openTo?: string | Date | null
|
||||
}[];
|
||||
allAreas: any[];
|
||||
availablePermissions: any[];
|
||||
emailTemplates: any[];
|
||||
slaConfigs: any[];
|
||||
users: any[];
|
||||
areasPagination: {
|
||||
total: number;
|
||||
page: number;
|
||||
limit: number;
|
||||
totalPages: number;
|
||||
};
|
||||
loading: boolean;
|
||||
isAreasLoading: boolean;
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
@ -92,6 +107,7 @@ const initialState: MasterState = {
|
||||
regionalOffices: [],
|
||||
asms: [],
|
||||
zonalManagerMappings: [],
|
||||
zonalManagers: [],
|
||||
roles: [],
|
||||
allStates: [],
|
||||
allDistricts: [],
|
||||
@ -100,7 +116,14 @@ const initialState: MasterState = {
|
||||
emailTemplates: [],
|
||||
slaConfigs: [],
|
||||
users: [],
|
||||
areasPagination: {
|
||||
total: 0,
|
||||
page: 1,
|
||||
limit: 10,
|
||||
totalPages: 0,
|
||||
},
|
||||
loading: false,
|
||||
isAreasLoading: false,
|
||||
error: null,
|
||||
};
|
||||
|
||||
@ -111,6 +134,13 @@ const masterSlice = createSlice({
|
||||
setMasterData: (state, action: PayloadAction<Partial<MasterState>>) => {
|
||||
return { ...state, ...action.payload };
|
||||
},
|
||||
setAreasData: (state, action: PayloadAction<{ data: any[], pagination: any }>) => {
|
||||
state.allDistricts = action.payload.data;
|
||||
state.areasPagination = action.payload.pagination;
|
||||
},
|
||||
setAreasLoading: (state, action: PayloadAction<boolean>) => {
|
||||
state.isAreasLoading = action.payload;
|
||||
},
|
||||
setZones: (state, action: PayloadAction<Zone[]>) => {
|
||||
state.zones = action.payload;
|
||||
},
|
||||
@ -137,7 +167,8 @@ const masterSlice = createSlice({
|
||||
|
||||
export const {
|
||||
setMasterData, setZones, setRegionalOffices, setAsms,
|
||||
setZonalManagerMappings, setUsers, setLoading, setError
|
||||
setZonalManagerMappings, setUsers, setLoading, setError,
|
||||
setAreasData, setAreasLoading
|
||||
} = masterSlice.actions;
|
||||
|
||||
export default masterSlice.reducer;
|
||||
|
||||
Loading…
Reference in New Issue
Block a user