hirrachy enhanced nd creatded distric as centric and added dealer location stable ui made for the hirarchy

This commit is contained in:
laxmanhalaki 2026-03-30 02:59:13 +05:30
parent c210f640d8
commit 2fab9c5c2d
16 changed files with 1060 additions and 420 deletions

2
.env.example Normal file
View File

@ -0,0 +1,2 @@
# API Server URL
VITE_API_URL=http://localhost:5000/api

View File

@ -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),

View File

@ -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>
);
};

View File

@ -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 || []);
}
}}

View File

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

View File

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

View File

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

View 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>
);
};

View File

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

View File

@ -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);
}
}}

View File

@ -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) => (

View File

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

View File

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

View File

@ -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 };
};

View File

@ -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 () => {

View File

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