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'),
|
getRegions: () => client.get('/master/regions'),
|
||||||
getOutlets: () => client.get('/master/outlets'),
|
getOutlets: () => client.get('/master/outlets'),
|
||||||
getOutletByCode: (code: string) => client.get(`/master/outlets/code/${code}`),
|
getOutletByCode: (code: string) => client.get(`/master/outlets/code/${code}`),
|
||||||
getStates: (zoneId?: string) => client.get('/master/states', { params: { zoneId } }),
|
getStates: (params?: any) => client.get('/master/states', typeof params === 'string' ? { zoneId: params } : params),
|
||||||
getDistricts: (stateId?: string) => client.get('/master/districts', { params: { stateId } }),
|
getDistricts: (params?: any) => client.get('/master/districts', typeof params === 'string' ? { stateId: params } : params),
|
||||||
getAreas: (districtId?: string) => client.get('/master/areas', { params: { districtId } }),
|
getAreas: (params?: any) => client.get('/master/areas', params),
|
||||||
updateArea: (id: string, data: any) => client.put(`/master/areas/${id}`, data),
|
updateArea: (id: string, data: any) => client.put(`/master/areas/${id}`, data),
|
||||||
createArea: (data: any) => client.post('/master/areas', data),
|
createArea: (data: any) => client.post('/master/areas', data),
|
||||||
getAreaManagers: () => client.get('/master/area-managers'),
|
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
|
// Onboarding
|
||||||
submitApplication: (data: any) => client.post('/onboarding/apply', data),
|
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),
|
upsertApprovalPolicy: (stageCode: string, data: any) => client.put(`/assessment/approval-policies/${stageCode}`, data),
|
||||||
|
|
||||||
// Collaboration & Participants
|
// 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),
|
addWorknote: (data: any) => client.post('/collaboration/worknotes', data),
|
||||||
addParticipant: (data: any) => client.post('/collaboration/participants', data),
|
addParticipant: (data: any) => client.post('/collaboration/participants', data),
|
||||||
removeParticipant: (id: string) => client.delete(`/collaboration/participants/${id}`),
|
removeParticipant: (id: string) => client.delete(`/collaboration/participants/${id}`),
|
||||||
|
|
||||||
// User management routes
|
// 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),
|
createUser: (data: any) => client.post('/admin/users', data),
|
||||||
updateUser: (id: string, data: any) => client.put(`/admin/users/${id}`, data),
|
updateUser: (id: string, data: any) => client.put(`/admin/users/${id}`, data),
|
||||||
updateUserStatus: (id: string, data: any) => client.patch(`/admin/users/${id}/status`, data),
|
updateUserStatus: (id: string, data: any) => client.patch(`/admin/users/${id}/status`, data),
|
||||||
@ -96,9 +99,9 @@ export const API = {
|
|||||||
|
|
||||||
// Audit Trail
|
// Audit Trail
|
||||||
getAuditLogs: (entityType: string, entityId: string, page: number = 1, limit: number = 50) =>
|
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) =>
|
getAuditSummary: (entityType: string, entityId: string) =>
|
||||||
client.get('/audit/summary', { params: { entityType, entityId } }),
|
client.get('/audit/summary', { entityType, entityId }),
|
||||||
|
|
||||||
// Prospective Login
|
// Prospective Login
|
||||||
sendOtp: (phone: string) => client.post('/prospective-login/send-otp', { phone }),
|
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),
|
finalizeTermination: (id: string, data: any) => client.post(`/termination/${id}/finalize`, data),
|
||||||
|
|
||||||
// Lifecycle Modules (Self-Service)
|
// 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),
|
createResignation: (data: any) => client.post('/resignation', data),
|
||||||
approveResignation: (id: string, data?: any) => client.post(`/resignation/${id}/approve`, data),
|
approveResignation: (id: string, data?: any) => client.post(`/resignation/${id}/approve`, data),
|
||||||
rejectResignation: (id: string, data: any) => client.post(`/resignation/${id}/reject`, 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 { UserManagementTable } from './MasterPage/UserManagementTable';
|
||||||
import { SLAConfiguration } from './MasterPage/SLAConfiguration';
|
import { SLAConfiguration } from './MasterPage/SLAConfiguration';
|
||||||
import { RolePermissions } from './MasterPage/RolePermissions';
|
import { RolePermissions } from './MasterPage/RolePermissions';
|
||||||
|
import { RoleDialog } from './MasterPage/RoleDialog';
|
||||||
import { EmailTemplates } from './MasterPage/EmailTemplates';
|
import { EmailTemplates } from './MasterPage/EmailTemplates';
|
||||||
import { LocationManagement } from './MasterPage/LocationManagement';
|
import { LocationManagement } from './MasterPage/LocationManagement';
|
||||||
import { ASMDialog } from './MasterPage/ASMDialog';
|
import { ASMDialog } from './MasterPage/ASMDialog';
|
||||||
@ -32,15 +33,15 @@ import { ApprovalPoliciesPage } from '../admin/ApprovalPoliciesPage';
|
|||||||
import { RootState } from '../../store';
|
import { RootState } from '../../store';
|
||||||
|
|
||||||
export const MasterPage: React.FC = () => {
|
export const MasterPage: React.FC = () => {
|
||||||
const { fetchInitialData } = useMasterData();
|
const { fetchInitialData, fetchAreas } = useMasterData();
|
||||||
const {
|
const {
|
||||||
asms, zonalManagerMappings,
|
asms, zonalManagerMappings,
|
||||||
allDistricts,
|
allDistricts,
|
||||||
users,
|
users,
|
||||||
|
roles,
|
||||||
loading
|
loading
|
||||||
} = useSelector((state: RootState) => state.master);
|
} = useSelector((state: RootState) => state.master);
|
||||||
|
|
||||||
|
|
||||||
// Tab & Selection State
|
// Tab & Selection State
|
||||||
const [activeTab, setActiveTab] = useState('hierarchy');
|
const [activeTab, setActiveTab] = useState('hierarchy');
|
||||||
const [selectedZone, setSelectedZone] = useState('all');
|
const [selectedZone, setSelectedZone] = useState('all');
|
||||||
@ -63,6 +64,7 @@ export const MasterPage: React.FC = () => {
|
|||||||
const [selectedASMRegion, setSelectedASMRegion] = useState('');
|
const [selectedASMRegion, setSelectedASMRegion] = useState('');
|
||||||
const [selectedASMStates, setSelectedASMStates] = useState<string[]>([]);
|
const [selectedASMStates, setSelectedASMStates] = useState<string[]>([]);
|
||||||
const [selectedASMDistricts, setSelectedASMDistricts] = useState<string[]>([]);
|
const [selectedASMDistricts, setSelectedASMDistricts] = useState<string[]>([]);
|
||||||
|
const [asmRoleCode, setAsmRoleCode] = useState<'ASM' | 'DD-AM'>('ASM');
|
||||||
|
|
||||||
// ZM Management State
|
// ZM Management State
|
||||||
const [showZMDialog, setShowZMDialog] = useState(false);
|
const [showZMDialog, setShowZMDialog] = useState(false);
|
||||||
@ -76,6 +78,10 @@ export const MasterPage: React.FC = () => {
|
|||||||
const [selectedZMStates, setSelectedZMStates] = useState<string[]>([]);
|
const [selectedZMStates, setSelectedZMStates] = useState<string[]>([]);
|
||||||
const [selectedZMDistricts, setSelectedZMDistricts] = useState<string[]>([]);
|
const [selectedZMDistricts, setSelectedZMDistricts] = useState<string[]>([]);
|
||||||
|
|
||||||
|
// Role Management State
|
||||||
|
const [showRoleDialog, setShowRoleDialog] = useState(false);
|
||||||
|
const [editingRole, setEditingRole] = useState<any>(null);
|
||||||
|
|
||||||
// Form State (Zone)
|
// Form State (Zone)
|
||||||
const [editingZoneId, setEditingZoneId] = useState<string | null>(null);
|
const [editingZoneId, setEditingZoneId] = useState<string | null>(null);
|
||||||
const [zoneName, setZoneName] = useState('');
|
const [zoneName, setZoneName] = useState('');
|
||||||
@ -99,20 +105,31 @@ export const MasterPage: React.FC = () => {
|
|||||||
const [previewContent, setPreviewContent] = useState<any>(null);
|
const [previewContent, setPreviewContent] = useState<any>(null);
|
||||||
|
|
||||||
// Form State (Location)
|
// Form State (Location)
|
||||||
const [editingLocationId] = useState<string | null>(null);
|
const [editingLocationId, setEditingLocationId] = useState<string | null>(null);
|
||||||
const [locationState, setLocationState] = useState('');
|
const [locationState, setLocationState] = useState('');
|
||||||
const [locationDistrict, setLocationDistrict] = useState('');
|
const [locationDistrict, setLocationDistrict] = useState('');
|
||||||
const [locationCity, setLocationCity] = useState('');
|
const [locationCity, setLocationCity] = useState('');
|
||||||
const [locationPincode, setLocationPincode] = useState('');
|
|
||||||
const [locationActiveFrom, setLocationActiveFrom] = useState('');
|
const [locationActiveFrom, setLocationActiveFrom] = useState('');
|
||||||
const [locationActiveTo, setLocationActiveTo] = useState('');
|
const [locationActiveTo, setLocationActiveTo] = useState('');
|
||||||
const [locationStatus, setLocationStatus] = useState('active');
|
const [locationStatus, setLocationStatus] = useState('active');
|
||||||
|
|
||||||
|
// Search & Pagination State (Locations)
|
||||||
|
const [districtsSearch, setDistrictsSearch] = useState('');
|
||||||
|
const [districtsPage, setDistrictsPage] = useState(1);
|
||||||
|
|
||||||
// Initial Load
|
// Initial Load
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchInitialData();
|
fetchInitialData();
|
||||||
}, [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
|
// Shared Data Helpers
|
||||||
const districtsAssignedToOthers = useMemo(() => {
|
const districtsAssignedToOthers = useMemo(() => {
|
||||||
const map: Record<string, string[]> = {};
|
const map: Record<string, string[]> = {};
|
||||||
@ -127,9 +144,12 @@ export const MasterPage: React.FC = () => {
|
|||||||
return map;
|
return map;
|
||||||
}, [asms, zonalManagerMappings]);
|
}, [asms, zonalManagerMappings]);
|
||||||
|
|
||||||
const getDistrictsForSelectedState = useCallback((stateName: string) => {
|
const getDistrictsForSelectedState = useCallback((stateName: string, regionId?: string) => {
|
||||||
return allDistricts
|
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 }));
|
.map(d => ({ id: d.id, name: d.name }));
|
||||||
}, [allDistricts]);
|
}, [allDistricts]);
|
||||||
|
|
||||||
@ -140,14 +160,25 @@ export const MasterPage: React.FC = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
try {
|
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;
|
const res = await masterService.saveASM(payload) as any;
|
||||||
if (res.success) {
|
if (res.success) {
|
||||||
toast.success(`ASM ${editingASMId ? 'updated' : 'assigned'} successfully`);
|
toast.success(`${asmRoleCode === 'ASM' ? 'ASM' : 'DD Area Manager'} ${editingASMId ? 'updated' : 'assigned'} successfully`);
|
||||||
setShowASMDialog(false);
|
setShowASMDialog(false);
|
||||||
fetchInitialData();
|
fetchInitialData();
|
||||||
|
} else {
|
||||||
|
toast.error(res.message || 'Failed to save ASM');
|
||||||
}
|
}
|
||||||
} catch (error) { toast.error('Failed to save ASM'); }
|
} catch (error: any) {
|
||||||
|
const msg = error?.response?.data?.message || error?.message || 'Failed to save ASM';
|
||||||
|
toast.error(msg);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleEditASM = (asm: any) => {
|
const handleEditASM = (asm: any) => {
|
||||||
@ -161,6 +192,7 @@ export const MasterPage: React.FC = () => {
|
|||||||
setSelectedASMRegion(asm.regionId);
|
setSelectedASMRegion(asm.regionId);
|
||||||
setSelectedASMStates(asm.stateNames || []);
|
setSelectedASMStates(asm.stateNames || []);
|
||||||
setSelectedASMDistricts(asm.areasManaged?.map((a: any) => a.id) || []);
|
setSelectedASMDistricts(asm.areasManaged?.map((a: any) => a.id) || []);
|
||||||
|
setAsmRoleCode(asm.roleCode === 'DD-AM' ? 'DD-AM' : 'ASM');
|
||||||
setShowASMDialog(true);
|
setShowASMDialog(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -168,9 +200,9 @@ export const MasterPage: React.FC = () => {
|
|||||||
setEditingZMId(zm.id);
|
setEditingZMId(zm.id);
|
||||||
setZmManagerId(zm.id);
|
setZmManagerId(zm.id);
|
||||||
setZmName(zm.name);
|
setZmName(zm.name);
|
||||||
setZmCode(zm.code === 'N/A' ? '' : zm.code);
|
setZmCode(zm.zmCode || zm.code || '');
|
||||||
setZmEmployeeId(zm.code === 'N/A' ? '' : zm.code);
|
setZmEmployeeId(zm.employeeId || '');
|
||||||
setZmStatus(zm.status.toLowerCase() as 'active' | 'inactive');
|
setZmStatus(zm.status?.toLowerCase() === 'active' ? 'active' : 'inactive');
|
||||||
setSelectedZMZone(zm.zoneId || '');
|
setSelectedZMZone(zm.zoneId || '');
|
||||||
setSelectedZMStates(zm.stateNames || []);
|
setSelectedZMStates(zm.stateNames || []);
|
||||||
setSelectedZMDistricts(zm.districts?.map((d: any) => d.id) || []);
|
setSelectedZMDistricts(zm.districts?.map((d: any) => d.id) || []);
|
||||||
@ -185,21 +217,24 @@ export const MasterPage: React.FC = () => {
|
|||||||
try {
|
try {
|
||||||
const payload = {
|
const payload = {
|
||||||
userId: zmManagerId,
|
userId: zmManagerId,
|
||||||
roleCode: 'DD-ZM',
|
|
||||||
zmCode,
|
zmCode,
|
||||||
|
zoneId: selectedZMZone,
|
||||||
districts: selectedZMDistricts,
|
districts: selectedZMDistricts,
|
||||||
status: zmStatus,
|
status: zmStatus
|
||||||
locationId: selectedZMZone // ZM primary location is the zone
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Use the generic saveASM method which I generalized on the backend
|
const res = await (masterService as any).saveZonalManager(payload) as any;
|
||||||
const res = await masterService.saveASM(payload) as any;
|
|
||||||
if (res.success) {
|
if (res.success) {
|
||||||
toast.success(`Zonal Manager ${editingZMId ? 'updated' : 'assigned'} successfully`);
|
toast.success(`Zonal Manager ${editingZMId ? 'updated' : 'assigned'} successfully`);
|
||||||
setShowZMDialog(false);
|
setShowZMDialog(false);
|
||||||
fetchInitialData();
|
fetchInitialData();
|
||||||
|
} else {
|
||||||
|
toast.error(res.message || 'Failed to save Zonal Manager');
|
||||||
}
|
}
|
||||||
} catch (error) { toast.error('Failed to save Zonal Manager'); }
|
} catch (error: any) {
|
||||||
|
const msg = error?.response?.data?.message || error?.message || 'Failed to save Zonal Manager';
|
||||||
|
toast.error(msg);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSaveZone = async () => {
|
const handleSaveZone = async () => {
|
||||||
@ -210,28 +245,39 @@ export const MasterPage: React.FC = () => {
|
|||||||
toast.success('Zone saved successfully');
|
toast.success('Zone saved successfully');
|
||||||
setShowZoneDialog(false);
|
setShowZoneDialog(false);
|
||||||
fetchInitialData();
|
fetchInitialData();
|
||||||
|
} else {
|
||||||
|
toast.error(res.message || 'Error saving zone');
|
||||||
}
|
}
|
||||||
} catch (error) { toast.error('Error saving zone'); }
|
} catch (error: any) {
|
||||||
|
const msg = error?.response?.data?.message || error?.message || 'Error saving zone';
|
||||||
|
toast.error(msg);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSaveRegion = async () => {
|
const handleSaveRegion = async () => {
|
||||||
try {
|
try {
|
||||||
const payload = {
|
const payload = {
|
||||||
name: regionName,
|
...(editingRegionId ? { id: editingRegionId } : {}),
|
||||||
code: regionCode,
|
name: regionName,
|
||||||
description: regionDescription,
|
code: regionCode,
|
||||||
parentId: selectedRegionZone,
|
description: regionDescription,
|
||||||
managerId: regionalManagerId,
|
parentId: selectedRegionZone,
|
||||||
districts: selectedRegionDistricts,
|
managerId: regionalManagerId,
|
||||||
status: 'Active'
|
districts: selectedRegionDistricts,
|
||||||
};
|
status: 'Active'
|
||||||
|
};
|
||||||
const res = await masterService.saveRegion(payload) as any;
|
const res = await masterService.saveRegion(payload) as any;
|
||||||
if (res.success) {
|
if (res.success) {
|
||||||
toast.success('Region saved successfully');
|
toast.success('Region saved successfully');
|
||||||
setShowRegionDialog(false);
|
setShowRegionDialog(false);
|
||||||
fetchInitialData();
|
fetchInitialData();
|
||||||
|
} else {
|
||||||
|
toast.error(res.message || 'Error saving region');
|
||||||
}
|
}
|
||||||
} catch (error) { toast.error('Error saving region'); }
|
} catch (error: any) {
|
||||||
|
const msg = error?.response?.data?.message || error?.message || 'Error saving region';
|
||||||
|
toast.error(msg);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSaveTemplate = async () => {
|
const handleSaveTemplate = async () => {
|
||||||
@ -259,17 +305,49 @@ export const MasterPage: React.FC = () => {
|
|||||||
finally { setPreviewLoading(false); }
|
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 () => {
|
const handleSaveLocation = async () => {
|
||||||
try {
|
try {
|
||||||
const payload = {
|
const payload = {
|
||||||
id: editingLocationId,
|
id: editingLocationId,
|
||||||
stateId: locationState,
|
stateName: locationState,
|
||||||
districtId: locationDistrict,
|
name: locationDistrict,
|
||||||
city: locationCity,
|
city: locationCity,
|
||||||
pincode: locationPincode,
|
|
||||||
status: locationStatus,
|
status: locationStatus,
|
||||||
activeFrom: locationActiveFrom,
|
openFrom: locationActiveFrom,
|
||||||
activeTo: locationActiveTo
|
openTo: locationActiveTo,
|
||||||
|
isActive: locationStatus === 'active'
|
||||||
};
|
};
|
||||||
const res = await (editingLocationId
|
const res = await (editingLocationId
|
||||||
? masterService.updateArea(editingLocationId, payload)
|
? masterService.updateArea(editingLocationId, payload)
|
||||||
@ -277,11 +355,18 @@ export const MasterPage: React.FC = () => {
|
|||||||
if (res.success) {
|
if (res.success) {
|
||||||
toast.success('Location saved');
|
toast.success('Location saved');
|
||||||
setShowLocationDialog(false);
|
setShowLocationDialog(false);
|
||||||
fetchInitialData();
|
fetchAreas({ search: districtsSearch, page: districtsPage });
|
||||||
}
|
}
|
||||||
} catch (error) { toast.error('Error saving location'); }
|
} catch (error) { toast.error('Error saving location'); }
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handler = setTimeout(() => {
|
||||||
|
fetchAreas({ search: districtsSearch, page: districtsPage });
|
||||||
|
}, 500);
|
||||||
|
return () => clearTimeout(handler);
|
||||||
|
}, [districtsSearch, districtsPage, fetchAreas]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div className="flex items-center justify-between">
|
<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">
|
<TabsContent value="roles" className="animate-in fade-in duration-300">
|
||||||
<RolePermissions onAddRole={() => toast.info('Unified Role Management interface being updated')}
|
<RolePermissions onAddRole={() => toast.info('Unified Role Management interface being updated')}
|
||||||
onEditRole={() => toast.info('Unified Role Management interface being updated')} />
|
onEditRole={handleEditRole} />
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="sla" className="animate-in fade-in duration-300">
|
<TabsContent value="sla" className="animate-in fade-in duration-300">
|
||||||
@ -368,8 +453,35 @@ export const MasterPage: React.FC = () => {
|
|||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="locations" className="animate-in fade-in duration-300">
|
<TabsContent value="locations" className="animate-in fade-in duration-300">
|
||||||
<LocationManagement onAddLocation={() => setShowLocationDialog(true)}
|
<LocationManagement
|
||||||
onEditLocation={() => toast.info('Location Editor being updated')} onDeleteLocation={() => toast.error('Delete Location restricted')} />
|
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>
|
||||||
|
|
||||||
<TabsContent value="approvals" className="animate-in fade-in duration-300">
|
<TabsContent value="approvals" className="animate-in fade-in duration-300">
|
||||||
@ -381,7 +493,7 @@ export const MasterPage: React.FC = () => {
|
|||||||
{/* Main Dialogs */}
|
{/* Main Dialogs */}
|
||||||
<ZoneDialog isOpen={showZoneDialog} onOpenChange={setShowZoneDialog} editingZoneId={editingZoneId} zoneName={zoneName} setZoneName={setZoneName} zoneCode={zoneCode} setZoneCode={setZoneCode} zoneDescription={zoneDescription} setZoneDescription={setZoneDescription} zonalBusinessHeadId={zonalBusinessHeadId} setZonalBusinessHeadId={setZonalBusinessHeadId} onSave={handleSaveZone} userAssignedData={users.length > 0 ? users.map(u => ({...u, name: u.fullName || u.name, role: u.role?.roleName, roleCode: u.role?.roleCode, allRoles: u.allRoles})) : asms} />
|
<ZoneDialog isOpen={showZoneDialog} onOpenChange={setShowZoneDialog} editingZoneId={editingZoneId} zoneName={zoneName} setZoneName={setZoneName} zoneCode={zoneCode} setZoneCode={setZoneCode} zoneDescription={zoneDescription} setZoneDescription={setZoneDescription} zonalBusinessHeadId={zonalBusinessHeadId} setZonalBusinessHeadId={setZonalBusinessHeadId} onSave={handleSaveZone} userAssignedData={users.length > 0 ? users.map(u => ({...u, name: u.fullName || u.name, role: u.role?.roleName, roleCode: u.role?.roleCode, allRoles: u.allRoles})) : asms} />
|
||||||
<RegionDialog isOpen={showRegionDialog} onOpenChange={setShowRegionDialog} editingRegionId={editingRegionId} regionName={regionName} setRegionName={setRegionName} 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} />
|
<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
|
<ZMDialog
|
||||||
isOpen={showZMDialog}
|
isOpen={showZMDialog}
|
||||||
onOpenChange={setShowZMDialog}
|
onOpenChange={setShowZMDialog}
|
||||||
@ -408,7 +520,8 @@ export const MasterPage: React.FC = () => {
|
|||||||
getDistrictsForSelectedState={getDistrictsForSelectedState}
|
getDistrictsForSelectedState={getDistrictsForSelectedState}
|
||||||
/>
|
/>
|
||||||
<TemplateDialog isOpen={showTemplateDialog} onOpenChange={setShowTemplateDialog} editingTemplate={editingTemplate} setEditingTemplate={setEditingTemplate} testDataInput={testDataInput} setTestDataInput={setTestDataInput} previewLoading={previewLoading} handlePreviewTemplate={handlePreviewTemplate} previewContent={previewContent} handleSaveTemplate={handleSaveTemplate} />
|
<TemplateDialog isOpen={showTemplateDialog} onOpenChange={setShowTemplateDialog} editingTemplate={editingTemplate} setEditingTemplate={setEditingTemplate} testDataInput={testDataInput} setTestDataInput={setTestDataInput} previewLoading={previewLoading} handlePreviewTemplate={handlePreviewTemplate} previewContent={previewContent} handleSaveTemplate={handleSaveTemplate} />
|
||||||
<LocationDialog isOpen={showLocationDialog} onOpenChange={setShowLocationDialog} editingLocationId={editingLocationId} locationState={locationState} setLocationState={setLocationState} locationDistrict={locationDistrict} setLocationDistrict={setLocationDistrict} locationCity={locationCity} setLocationCity={setLocationCity} 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>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -32,6 +32,8 @@ interface ASMDialogProps {
|
|||||||
selectedASMDistricts: string[];
|
selectedASMDistricts: string[];
|
||||||
setSelectedASMDistricts: (districts: string[]) => void;
|
setSelectedASMDistricts: (districts: string[]) => void;
|
||||||
onSave: () => void;
|
onSave: () => void;
|
||||||
|
asmRoleCode: 'ASM' | 'DD-AM';
|
||||||
|
setAsmRoleCode: (role: 'ASM' | 'DD-AM') => void;
|
||||||
userAssignedData: any[];
|
userAssignedData: any[];
|
||||||
districtsAssignedToOthers: Record<string, string[]>;
|
districtsAssignedToOthers: Record<string, string[]>;
|
||||||
getDistrictsForSelectedState: (state: string) => { id: string; name: string }[];
|
getDistrictsForSelectedState: (state: string) => { id: string; name: string }[];
|
||||||
@ -43,6 +45,7 @@ export const ASMDialog: React.FC<ASMDialogProps> = ({
|
|||||||
asmStatus, setAsmStatus, selectedASMZone, setSelectedASMZone,
|
asmStatus, setAsmStatus, selectedASMZone, setSelectedASMZone,
|
||||||
selectedASMRegion, setSelectedASMRegion, selectedASMStates, setSelectedASMStates,
|
selectedASMRegion, setSelectedASMRegion, selectedASMStates, setSelectedASMStates,
|
||||||
selectedASMDistricts, setSelectedASMDistricts, onSave,
|
selectedASMDistricts, setSelectedASMDistricts, onSave,
|
||||||
|
asmRoleCode, setAsmRoleCode,
|
||||||
userAssignedData, districtsAssignedToOthers, getDistrictsForSelectedState
|
userAssignedData, districtsAssignedToOthers, getDistrictsForSelectedState
|
||||||
}) => {
|
}) => {
|
||||||
const { zones, regionalOffices } = useSelector((state: RootState) => state.master);
|
const { zones, regionalOffices } = useSelector((state: RootState) => state.master);
|
||||||
@ -51,11 +54,27 @@ export const ASMDialog: React.FC<ASMDialogProps> = ({
|
|||||||
const roles = u.allRoles || [];
|
const roles = u.allRoles || [];
|
||||||
return roles.some((r: string) => {
|
return roles.some((r: string) => {
|
||||||
const roleStr = (r || '').toUpperCase();
|
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');
|
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 (
|
return (
|
||||||
<Dialog open={isOpen} onOpenChange={onOpenChange}>
|
<Dialog open={isOpen} onOpenChange={onOpenChange}>
|
||||||
<DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto">
|
<DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto">
|
||||||
@ -84,24 +103,55 @@ export const ASMDialog: React.FC<ASMDialogProps> = ({
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{selectedASMZone && (
|
{selectedASMZone && (
|
||||||
<div>
|
<div className="space-y-4">
|
||||||
<Label>Regional Office</Label>
|
<div>
|
||||||
<Select value={selectedASMRegion} onValueChange={(value) => {
|
<Label>Regional Office</Label>
|
||||||
setSelectedASMRegion(value);
|
<Select value={selectedASMRegion} onValueChange={(value) => {
|
||||||
setSelectedASMStates([]);
|
setSelectedASMRegion(value);
|
||||||
setSelectedASMDistricts([]);
|
setSelectedASMStates([]);
|
||||||
}}>
|
setSelectedASMDistricts([]);
|
||||||
<SelectTrigger className="mt-2 text-slate-900">
|
}}>
|
||||||
<SelectValue placeholder="Select regional office" />
|
<SelectTrigger className="mt-2 text-slate-900">
|
||||||
</SelectTrigger>
|
<SelectValue placeholder="Select regional office" />
|
||||||
<SelectContent>
|
</SelectTrigger>
|
||||||
{regionalOffices
|
<SelectContent>
|
||||||
.filter((office) => office.zoneId === selectedASMZone)
|
{regionalOffices
|
||||||
.map((office) => (
|
.filter((office) => office.zoneId === selectedASMZone)
|
||||||
<SelectItem key={office.id} value={office.id}>{office.name}</SelectItem>
|
.map((office) => (
|
||||||
|
<SelectItem key={office.id} value={office.id}>{office.name}</SelectItem>
|
||||||
|
))}
|
||||||
|
</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>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@ -115,16 +165,16 @@ export const ASMDialog: React.FC<ASMDialogProps> = ({
|
|||||||
|
|
||||||
return availableStates.length > 0 ? (
|
return availableStates.length > 0 ? (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{availableStates.map((state) => (
|
{availableStates.map((state: string) => (
|
||||||
<div key={state} className="flex items-center space-x-2">
|
<div key={state} className="flex items-center space-x-2">
|
||||||
<Checkbox
|
<Checkbox
|
||||||
id={`asm-state-${state}`}
|
id={`asm-state-${state}`}
|
||||||
checked={selectedASMStates.includes(state)}
|
checked={selectedASMStates.some(s => s.toLowerCase() === state.toLowerCase())}
|
||||||
onCheckedChange={(checked) => {
|
onCheckedChange={(checked) => {
|
||||||
if (checked) {
|
if (checked) {
|
||||||
setSelectedASMStates([...selectedASMStates, state]);
|
setSelectedASMStates([...selectedASMStates, state]);
|
||||||
} else {
|
} else {
|
||||||
setSelectedASMStates(selectedASMStates.filter(s => s !== state));
|
setSelectedASMStates(selectedASMStates.filter(s => s.toLowerCase() !== state.toLowerCase()));
|
||||||
const stateDistricts = getDistrictsForSelectedState(state);
|
const stateDistricts = getDistrictsForSelectedState(state);
|
||||||
setSelectedASMDistricts(selectedASMDistricts.filter(dId => !stateDistricts.some(sd => sd.id === dId)));
|
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);
|
const selectedUser = userAssignedData.find(u => u.id === value);
|
||||||
if (selectedUser) {
|
if (selectedUser) {
|
||||||
setAsmName(selectedUser.name);
|
setAsmName(selectedUser.name);
|
||||||
setAsmCode(selectedUser.asmCode || '');
|
|
||||||
setAsmEmployeeId(selectedUser.employeeId || '');
|
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 || []);
|
setSelectedASMStates(selectedUser.stateNames || []);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
|||||||
@ -1,11 +1,9 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useSelector } from 'react-redux';
|
|
||||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '../../ui/dialog';
|
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '../../ui/dialog';
|
||||||
import { Button } from '../../ui/button';
|
import { Button } from '../../ui/button';
|
||||||
import { Label } from '../../ui/label';
|
import { Label } from '../../ui/label';
|
||||||
import { Input } from '../../ui/input';
|
import { Input } from '../../ui/input';
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../../ui/select';
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../../ui/select';
|
||||||
import { RootState } from '../../../store';
|
|
||||||
|
|
||||||
interface LocationDialogProps {
|
interface LocationDialogProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
@ -17,8 +15,6 @@ interface LocationDialogProps {
|
|||||||
setLocationDistrict: (id: string) => void;
|
setLocationDistrict: (id: string) => void;
|
||||||
locationCity: string;
|
locationCity: string;
|
||||||
setLocationCity: (city: string) => void;
|
setLocationCity: (city: string) => void;
|
||||||
locationPincode: string;
|
|
||||||
setLocationPincode: (pincode: string) => void;
|
|
||||||
locationActiveFrom: string;
|
locationActiveFrom: string;
|
||||||
setLocationActiveFrom: (date: string) => void;
|
setLocationActiveFrom: (date: string) => void;
|
||||||
locationActiveTo: string;
|
locationActiveTo: string;
|
||||||
@ -31,11 +27,9 @@ interface LocationDialogProps {
|
|||||||
export const LocationDialog: React.FC<LocationDialogProps> = ({
|
export const LocationDialog: React.FC<LocationDialogProps> = ({
|
||||||
isOpen, onOpenChange, editingLocationId, locationState, setLocationState,
|
isOpen, onOpenChange, editingLocationId, locationState, setLocationState,
|
||||||
locationDistrict, setLocationDistrict, locationCity, setLocationCity,
|
locationDistrict, setLocationDistrict, locationCity, setLocationCity,
|
||||||
locationPincode, setLocationPincode, locationActiveFrom, setLocationActiveFrom,
|
locationActiveFrom, setLocationActiveFrom,
|
||||||
locationActiveTo, setLocationActiveTo, locationStatus, setLocationStatus, onSave
|
locationActiveTo, setLocationActiveTo, locationStatus, setLocationStatus, onSave
|
||||||
}) => {
|
}) => {
|
||||||
const { allStates, allDistricts } = useSelector((state: RootState) => state.master);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={isOpen} onOpenChange={onOpenChange}>
|
<Dialog open={isOpen} onOpenChange={onOpenChange}>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
@ -45,80 +39,62 @@ export const LocationDialog: React.FC<LocationDialogProps> = ({
|
|||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<Label>State</Label>
|
<Label className="flex items-center gap-2 text-sm leading-none font-medium text-slate-700">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>
|
|
||||||
<Input
|
<Input
|
||||||
placeholder="Enter area name (e.g., Connaught Place)"
|
placeholder="Enter state name"
|
||||||
className="mt-2 text-slate-900"
|
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}
|
value={locationCity}
|
||||||
onChange={(e) => setLocationCity(e.target.value)}
|
onChange={(e) => setLocationCity(e.target.value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Label>Pincode</Label>
|
<Label className="flex items-center gap-2 text-sm leading-none font-medium text-slate-700">District</Label>
|
||||||
<Input
|
<Input
|
||||||
placeholder="Enter pincode"
|
placeholder="Enter district name"
|
||||||
className="mt-2 text-slate-900"
|
className="mt-2 text-slate-900 border-slate-200 focus-visible:ring-amber-500/30 focus-visible:border-amber-500"
|
||||||
value={locationPincode}
|
value={locationDistrict}
|
||||||
onChange={(e) => setLocationPincode(e.target.value)}
|
onChange={(e) => setLocationDistrict(e.target.value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="border rounded-lg p-4 bg-gradient-to-br from-blue-50 to-cyan-50 border-blue-200">
|
||||||
<div>
|
<Label className="items-center gap-2 text-sm leading-none font-medium text-blue-900 mb-3 block">Active Period</Label>
|
||||||
<Label>Active From</Label>
|
<p className="text-xs text-blue-700 mb-3">Define the time period when this location will be available for dealership applications</p>
|
||||||
<Input
|
<div className="grid grid-cols-2 gap-4">
|
||||||
type="date"
|
<div>
|
||||||
className="mt-2 text-slate-900"
|
<Label className="flex items-center gap-2 text-sm leading-none font-medium text-slate-700">Start Date</Label>
|
||||||
value={locationActiveFrom}
|
<Input
|
||||||
onChange={(e) => setLocationActiveFrom(e.target.value)}
|
type="date"
|
||||||
/>
|
className="mt-2 text-slate-900 border-slate-200 focus-visible:ring-blue-500/30 focus-visible:border-blue-500"
|
||||||
</div>
|
value={locationActiveFrom}
|
||||||
<div>
|
onChange={(e) => setLocationActiveFrom(e.target.value)}
|
||||||
<Label>Active To</Label>
|
/>
|
||||||
<Input
|
</div>
|
||||||
type="date"
|
<div>
|
||||||
className="mt-2 text-slate-900"
|
<Label className="flex items-center gap-2 text-sm leading-none font-medium text-slate-700">End Date</Label>
|
||||||
value={locationActiveTo}
|
<Input
|
||||||
onChange={(e) => setLocationActiveTo(e.target.value)}
|
type="date"
|
||||||
/>
|
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>
|
</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}>
|
<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" />
|
<SelectValue placeholder="Select Status" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
@ -130,7 +106,7 @@ export const LocationDialog: React.FC<LocationDialogProps> = ({
|
|||||||
|
|
||||||
<div className="flex gap-3 pt-4">
|
<div className="flex gap-3 pt-4">
|
||||||
<Button variant="outline" className="flex-1" onClick={() => onOpenChange(false)}>Cancel</Button>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
|
|||||||
@ -11,79 +11,176 @@ interface LocationManagementProps {
|
|||||||
onAddLocation: () => void;
|
onAddLocation: () => void;
|
||||||
onEditLocation: (location: any) => void;
|
onEditLocation: (location: any) => void;
|
||||||
onDeleteLocation: (id: string) => void;
|
onDeleteLocation: (id: string) => void;
|
||||||
|
onSearch: (term: string) => void;
|
||||||
|
onPageChange: (page: number) => void;
|
||||||
|
searchTerm: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const LocationManagement: React.FC<LocationManagementProps> = ({
|
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 (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
|
||||||
<div>
|
<div>
|
||||||
<CardTitle>Dealership Locations</CardTitle>
|
<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>
|
||||||
|
<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>
|
||||||
<Button onClick={onAddLocation} className="bg-amber-600 hover:bg-amber-700">
|
|
||||||
<Plus className="w-4 h-4 mr-2" />
|
|
||||||
Add Location
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<Table>
|
<div className={`relative ${isAreasLoading ? 'opacity-50 pointer-events-none' : ''}`}>
|
||||||
<TableHeader>
|
<Table>
|
||||||
<TableRow>
|
<TableHeader>
|
||||||
<TableHead>State</TableHead>
|
<TableRow>
|
||||||
<TableHead>Area / City</TableHead>
|
<TableHead>State</TableHead>
|
||||||
<TableHead>District</TableHead>
|
<TableHead>City</TableHead>
|
||||||
<TableHead>Pincode</TableHead>
|
<TableHead>District</TableHead>
|
||||||
<TableHead>Manager</TableHead>
|
<TableHead>Active Period</TableHead>
|
||||||
<TableHead>Status</TableHead>
|
<TableHead>Status</TableHead>
|
||||||
<TableHead className="text-right">Actions</TableHead>
|
<TableHead className="text-right">Actions</TableHead>
|
||||||
</TableRow>
|
|
||||||
</TableHeader>
|
|
||||||
<TableBody>
|
|
||||||
{allDistricts.map((district) => (
|
|
||||||
<TableRow key={district.id}>
|
|
||||||
<TableCell>
|
|
||||||
<div className="flex items-center gap-2">
|
|
||||||
<MapPin className="w-4 h-4 text-amber-600" />
|
|
||||||
<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>
|
|
||||||
{district.asmName ? (
|
|
||||||
<span className="text-slate-700 font-medium">{district.asmName}</span>
|
|
||||||
) : (
|
|
||||||
<span className="text-slate-400 italic text-sm">Unassigned</span>
|
|
||||||
)}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell>
|
|
||||||
<Badge variant={district.isActive ? 'default' : 'secondary'} className={district.isActive ? 'bg-emerald-100 text-emerald-700' : ''}>
|
|
||||||
{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)}>
|
|
||||||
<Edit2 className="w-4 h-4" />
|
|
||||||
</Button>
|
|
||||||
<Button variant="ghost" size="sm" onClick={() => onDeleteLocation(district.id)} className="text-red-500 hover:text-red-600">
|
|
||||||
<Trash2 className="w-4 h-4" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</TableCell>
|
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))}
|
</TableHeader>
|
||||||
</TableBody>
|
<TableBody>
|
||||||
</Table>
|
{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">
|
||||||
|
<MapPin className="w-4 h-4 text-amber-600" />
|
||||||
|
<span className="font-medium">{district.stateName || 'N/A'}</span>
|
||||||
|
</div>
|
||||||
|
</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.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">Not Defined</span>
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<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="outline" size="sm" onClick={() => onEditLocation(district)} className="h-8 w-8 p-0">
|
||||||
|
<Edit2 className="w-4 h-4" />
|
||||||
|
</Button>
|
||||||
|
<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>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
|||||||
@ -24,10 +24,10 @@ interface RegionDialogProps {
|
|||||||
setSelectedRegionZone: (id: string) => void;
|
setSelectedRegionZone: (id: string) => void;
|
||||||
regionalManagerId: string;
|
regionalManagerId: string;
|
||||||
setRegionalManagerId: (id: string) => void;
|
setRegionalManagerId: (id: string) => void;
|
||||||
selectedRegionStates: string[]; // This now contains District IDs
|
selectedRegionStates: string[]; // District IDs selected
|
||||||
setSelectedRegionStates: (districts: string[]) => void;
|
setSelectedRegionStates: (districts: string[]) => void;
|
||||||
onSave: () => void;
|
onSave: () => void;
|
||||||
userAssignedData: any[]; // Used for RM selection
|
userAssignedData: any[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const RegionDialog: React.FC<RegionDialogProps> = ({
|
export const RegionDialog: React.FC<RegionDialogProps> = ({
|
||||||
@ -36,134 +36,278 @@ export const RegionDialog: React.FC<RegionDialogProps> = ({
|
|||||||
selectedRegionZone, setSelectedRegionZone, regionalManagerId, setRegionalManagerId,
|
selectedRegionZone, setSelectedRegionZone, regionalManagerId, setRegionalManagerId,
|
||||||
selectedRegionStates, setSelectedRegionStates, onSave, userAssignedData
|
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
|
// Internal: which states are checked (for filtering districts)
|
||||||
const districtsAssignedToOthers = React.useMemo(() => {
|
const [selectedStateIds, setSelectedStateIds] = React.useState<string[]>([]);
|
||||||
const mapping: Record<string, 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) => {
|
(regionalOffices || []).forEach((r: any) => {
|
||||||
if (r.id !== editingRegionId) {
|
if (r.id !== editingRegionId) {
|
||||||
(r.districts || []).forEach((d: any) => {
|
(r.districts || []).forEach((d: any) => {
|
||||||
mapping[d.id] = r.name;
|
map[d.id] = r.name;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return mapping;
|
return map;
|
||||||
}, [regionalOffices, editingRegionId]);
|
}, [regionalOffices, editingRegionId]);
|
||||||
|
|
||||||
const filteredRMUsers = userAssignedData.filter(u => {
|
// --- States visible for the selected zone (only states that have at least 1 district in this zone OR unassigned) ---
|
||||||
const roles = u.allRoles || [];
|
const statesForZone = React.useMemo(() => {
|
||||||
return roles.some((r: string) => {
|
if (!selectedRegionZone) return allStates;
|
||||||
const roleStr = (r || '').toUpperCase();
|
const stateIdsWithZoneDistricts = new Set(
|
||||||
return ['RM', 'RBM', 'REGIONAL MANAGER', 'ZBH', 'ZONE BUSINESS HEAD', 'ZONAL BUSINESS HEAD', 'ASM', 'AREA SALES MANAGER'].includes(roleStr) ||
|
allDistricts
|
||||||
roleStr.includes('REGIONAL') || roleStr.includes('ZONAL') || roleStr.includes('AREA SALES');
|
.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 (
|
return (
|
||||||
<Dialog open={isOpen} onOpenChange={onOpenChange}>
|
<Dialog open={isOpen} onOpenChange={onOpenChange}>
|
||||||
<DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto">
|
<DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>{editingRegionId ? 'Edit' : 'Add'} Regional Office</DialogTitle>
|
<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>
|
</DialogHeader>
|
||||||
<div className="space-y-4">
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
|
||||||
<div>
|
|
||||||
<Label>Zone</Label>
|
|
||||||
<Select value={selectedRegionZone} onValueChange={setSelectedRegionZone}>
|
|
||||||
<SelectTrigger className="mt-2 text-slate-900">
|
|
||||||
<SelectValue placeholder="Select zone" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent>
|
|
||||||
{zones.map((zone) => (
|
|
||||||
<SelectItem key={zone.id} value={zone.id}>{zone.name}</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Label>Regional Manager</Label>
|
|
||||||
<Select value={regionalManagerId} onValueChange={setRegionalManagerId}>
|
|
||||||
<SelectTrigger className="mt-2 w-full text-slate-900">
|
|
||||||
<SelectValue placeholder="Select Manager" />
|
|
||||||
</SelectTrigger>
|
|
||||||
<SelectContent className="max-h-60">
|
|
||||||
{filteredRMUsers.map((user) => (
|
|
||||||
<SelectItem key={user.id} value={user.id}>
|
|
||||||
{user.name} ({user.email})
|
|
||||||
</SelectItem>
|
|
||||||
))}
|
|
||||||
</SelectContent>
|
|
||||||
</Select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
<div className="space-y-4">
|
||||||
|
{/* Region Code & Name */}
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<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>
|
<div>
|
||||||
<Label>Region Code</Label>
|
<Label>Region Code</Label>
|
||||||
<Input placeholder="e.g., NZ-R1" className="mt-2 text-slate-900" value={regionCode} onChange={(e) => setRegionCode(e.target.value)} />
|
<Input placeholder="e.g., NZ-R1" className="mt-2 text-slate-900" value={regionCode} onChange={(e) => setRegionCode(e.target.value)} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div>
|
||||||
|
<Label>Region Name</Label>
|
||||||
<div>
|
<Input placeholder="e.g., Delhi NCR Region" className="mt-2 text-slate-900" value={regionName} onChange={(e) => setRegionName(e.target.value)} />
|
||||||
<Label>Description</Label>
|
|
||||||
<Textarea placeholder="Describe the region..." className="mt-2 text-slate-900" rows={3} value={regionDescription} onChange={(e) => setRegionDescription(e.target.value)} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<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;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div key={district.id} className="flex items-center space-x-2">
|
|
||||||
<TooltipProvider>
|
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<div className="flex items-center space-x-2">
|
|
||||||
<Checkbox
|
|
||||||
id={`region-district-${district.id}`}
|
|
||||||
disabled={isDisabled}
|
|
||||||
checked={selectedRegionStates.includes(district.id)}
|
|
||||||
onCheckedChange={(checked) => {
|
|
||||||
if (checked) {
|
|
||||||
setSelectedRegionStates([...selectedRegionStates, district.id]);
|
|
||||||
} else {
|
|
||||||
setSelectedRegionStates(selectedRegionStates.filter(id => id !== district.id));
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<label
|
|
||||||
htmlFor={`region-district-${district.id}`}
|
|
||||||
className={`text-sm cursor-pointer ${isDisabled ? 'text-slate-400' : 'text-slate-900'}`}
|
|
||||||
>
|
|
||||||
{district.name}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</TooltipTrigger>
|
|
||||||
{isDisabled && (
|
|
||||||
<TooltipContent>
|
|
||||||
<p className="text-xs">Already assigned to {assignedRegionName}</p>
|
|
||||||
</TooltipContent>
|
|
||||||
)}
|
|
||||||
</Tooltip>
|
|
||||||
</TooltipProvider>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Zone */}
|
||||||
|
<div>
|
||||||
|
<Label>Zone</Label>
|
||||||
|
<Select value={selectedRegionZone} onValueChange={handleZoneChange}>
|
||||||
|
<SelectTrigger className="mt-2 text-slate-900">
|
||||||
|
<SelectValue placeholder="Select zone" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{zones.map((zone) => (
|
||||||
|
<SelectItem key={zone.id} value={zone.id}>{zone.name}</SelectItem>
|
||||||
|
))}
|
||||||
|
</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 from available users" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent className="max-h-60">
|
||||||
|
{userAssignedData.map((user) => (
|
||||||
|
<SelectItem key={user.id} value={user.id}>
|
||||||
|
{user.name || user.fullName}
|
||||||
|
{user.email ? ` — ${user.email}` : ''}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Description */}
|
||||||
|
<div>
|
||||||
|
<Label>Description</Label>
|
||||||
|
<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-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">
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<div className="flex items-center space-x-2 w-full">
|
||||||
|
<Checkbox
|
||||||
|
id={`region-district-${district.id}`}
|
||||||
|
disabled={isDisabled}
|
||||||
|
checked={selectedRegionStates.includes(district.id)}
|
||||||
|
onCheckedChange={(checked) => {
|
||||||
|
if (checked) {
|
||||||
|
setSelectedRegionStates([...selectedRegionStates, district.id]);
|
||||||
|
} else {
|
||||||
|
setSelectedRegionStates(selectedRegionStates.filter(id => id !== district.id));
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
htmlFor={`region-district-${district.id}`}
|
||||||
|
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 && tooltipText && (
|
||||||
|
<TooltipContent>
|
||||||
|
<p className="text-xs">{tooltipText}</p>
|
||||||
|
</TooltipContent>
|
||||||
|
)}
|
||||||
|
</Tooltip>
|
||||||
|
</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">
|
<div className="flex gap-3 pt-4">
|
||||||
<Button variant="outline" className="flex-1" onClick={() => onOpenChange(false)}>Cancel</Button>
|
<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>
|
<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 { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../../ui/card';
|
||||||
import { Button } from '../../ui/button';
|
import { Button } from '../../ui/button';
|
||||||
import { Badge } from '../../ui/badge';
|
import { Badge } from '../../ui/badge';
|
||||||
import { Shield, Plus, Edit2, Users } from 'lucide-react';
|
import { Shield, Plus, Pen } from 'lucide-react';
|
||||||
import { RootState } from '../../../store';
|
import { RootState } from '../../../store';
|
||||||
|
|
||||||
interface RolePermissionsProps {
|
interface RolePermissionsProps {
|
||||||
@ -15,54 +15,59 @@ export const RolePermissions: React.FC<RolePermissionsProps> = ({ onAddRole, onE
|
|||||||
const { roles } = useSelector((state: RootState) => state.master);
|
const { roles } = useSelector((state: RootState) => state.master);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card>
|
<Card className="bg-card text-card-foreground flex flex-col gap-6 rounded-xl border shadow-sm">
|
||||||
<CardHeader>
|
<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">
|
<div className="flex items-center justify-between w-full">
|
||||||
<div>
|
<div>
|
||||||
<CardTitle>Roles & Access Controls</CardTitle>
|
<CardTitle className="leading-none text-xl font-bold">Role Definitions</CardTitle>
|
||||||
<CardDescription>Manage user roles and their associated system permissions</CardDescription>
|
<CardDescription className="text-muted-foreground mt-1.5">Overview of available roles and their access levels</CardDescription>
|
||||||
</div>
|
</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" />
|
<Plus className="w-4 h-4 mr-2" />
|
||||||
Add Role
|
Add Role
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent className="px-6 [&:last-child]:pb-6">
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
{roles.map((role) => (
|
{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 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 mb-4">
|
<div className="flex items-center justify-between">
|
||||||
<div className="w-12 h-12 bg-purple-50 rounded-xl flex items-center justify-center group-hover:bg-purple-100 transition-colors">
|
<div className="flex items-center gap-2">
|
||||||
<Shield className="w-6 h-6 text-purple-700" />
|
<Shield className="w-5 h-5 text-amber-600" />
|
||||||
|
<h3 className="text-slate-900 font-bold">{role.name}</h3>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2">
|
<Badge variant="secondary" className="border-transparent bg-secondary text-secondary-foreground text-xs font-medium">
|
||||||
<Button variant="ghost" size="sm" onClick={() => onEditRole(role)} className="h-8 w-8 p-0">
|
{role.userCount || 0} users
|
||||||
<Edit2 className="w-4 h-4" />
|
</Badge>
|
||||||
</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>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-3">
|
<div>
|
||||||
<p className="text-xs font-semibold text-slate-400 uppercase tracking-wider">Key Permissions</p>
|
<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.5">
|
<div className="flex flex-wrap gap-1">
|
||||||
{role.permissions?.slice(0, 6).map((perm: string, idx: number) => (
|
{(role.permissions || []).slice(0, 3).map((perm: string, idx: number) => (
|
||||||
<Badge key={idx} variant="secondary" className="text-[10px] bg-slate-50 border-slate-200 font-medium">
|
<Badge key={idx} variant="outline" className="border px-2 py-0.5 font-medium text-foreground text-[10px] bg-white/50">
|
||||||
{perm.replace(/_/g, ' ')}
|
{perm.replace(/_/g, ' ').toLowerCase()}
|
||||||
</Badge>
|
</Badge>
|
||||||
))}
|
))}
|
||||||
{role.permissions?.length > 6 && (
|
{role.permissions?.length > 3 && (
|
||||||
<Badge variant="outline" className="text-[10px] border-slate-200">
|
<Badge variant="outline" className="text-[10px] border-slate-200">
|
||||||
+{role.permissions.length - 6} more
|
+{role.permissions.length - 3}
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</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>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -182,16 +182,18 @@ export const ZMDialog: React.FC<ZMDialogProps> = ({
|
|||||||
const selectedUser = userAssignedData.find(u => u.id === value);
|
const selectedUser = userAssignedData.find(u => u.id === value);
|
||||||
if (selectedUser) {
|
if (selectedUser) {
|
||||||
setZmName(selectedUser.name);
|
setZmName(selectedUser.name);
|
||||||
setZmCode(selectedUser.zmCode || '');
|
|
||||||
setZmEmployeeId(selectedUser.employeeId || '');
|
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');
|
// Search territoryProfile for ZM/DD-ZM role to find the existing managerCode
|
||||||
if (zmProfile) {
|
const zmProfile = (selectedUser.territoryProfile || []).find((tp: any) => tp.roleCode === 'ZM' || tp.roleCode === 'DD-ZM');
|
||||||
setZmCode(zmProfile.managerCode || '');
|
const existingCode = zmProfile?.managerCode || selectedUser.zmCode || selectedUser.employeeId || '';
|
||||||
setSelectedZMZone(zmProfile.zoneId || '');
|
setZmCode(existingCode);
|
||||||
|
|
||||||
|
if (zmProfile?.zoneId) {
|
||||||
|
setSelectedZMZone(zmProfile.zoneId);
|
||||||
}
|
}
|
||||||
|
|
||||||
setSelectedZMDistricts(selectedUser.districts?.map((d: any) => d.id) || []);
|
setSelectedZMDistricts(selectedUser.districts?.map((d: any) => d.id) || []);
|
||||||
// Extract state names from assigned districts if not explicit
|
|
||||||
if (selectedUser.stateNames) setSelectedZMStates(selectedUser.stateNames);
|
if (selectedUser.stateNames) setSelectedZMStates(selectedUser.stateNames);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
|||||||
@ -17,9 +17,13 @@ interface ZMManagementProps {
|
|||||||
export const ZMManagement: React.FC<ZMManagementProps> = ({
|
export const ZMManagement: React.FC<ZMManagementProps> = ({
|
||||||
selectedZone, onAddZM, onEditZM, onDeleteZM
|
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 (
|
return (
|
||||||
<Card>
|
<Card>
|
||||||
@ -42,7 +46,6 @@ export const ZMManagement: React.FC<ZMManagementProps> = ({
|
|||||||
<TableHead>ZM Code</TableHead>
|
<TableHead>ZM Code</TableHead>
|
||||||
<TableHead>Name</TableHead>
|
<TableHead>Name</TableHead>
|
||||||
<TableHead>Zone</TableHead>
|
<TableHead>Zone</TableHead>
|
||||||
<TableHead>Region</TableHead>
|
|
||||||
<TableHead>Districts Managed</TableHead>
|
<TableHead>Districts Managed</TableHead>
|
||||||
<TableHead>Contact</TableHead>
|
<TableHead>Contact</TableHead>
|
||||||
<TableHead>Status</TableHead>
|
<TableHead>Status</TableHead>
|
||||||
@ -55,14 +58,17 @@ export const ZMManagement: React.FC<ZMManagementProps> = ({
|
|||||||
<TableCell>
|
<TableCell>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Users className="w-4 h-4 text-blue-600" />
|
<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>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>{zm.name}</TableCell>
|
<TableCell>{zm.name}</TableCell>
|
||||||
<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>
|
||||||
<TableCell className="text-sm text-slate-600">{zm.regionName}</TableCell>
|
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<div className="flex flex-wrap gap-1">
|
<div className="flex flex-wrap gap-1">
|
||||||
{zm.districts.slice(0, 3).map((district: any, idx: number) => (
|
{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 { Badge } from '../../ui/badge';
|
||||||
import { ScrollArea } from '../../ui/scroll-area';
|
import { ScrollArea } from '../../ui/scroll-area';
|
||||||
import { Label } from '../../ui/label';
|
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';
|
import { RootState } from '../../../store';
|
||||||
|
|
||||||
interface ZoneDetailsProps {
|
interface ZoneDetailsProps {
|
||||||
@ -49,7 +49,12 @@ export const ZoneDetails: React.FC<ZoneDetailsProps> = ({ selectedZone, onAddZon
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2">
|
<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" />
|
<Edit2 className="w-4 h-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</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>
|
<Label className="text-xs text-slate-600 mb-2 block">States Covered ({zone.states.length})</Label>
|
||||||
<div className="flex flex-wrap gap-1">
|
<div className="flex flex-wrap gap-1">
|
||||||
{zone.states.map((state: string, idx: number) => (
|
{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}
|
{state}
|
||||||
</Badge>
|
</Badge>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</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 && (
|
{zone.zonalManagers && zone.zonalManagers.length > 0 && (
|
||||||
<div className="border-t pt-3">
|
<div className="border-t pt-3">
|
||||||
<Label className="text-xs text-slate-600 mb-2 block">
|
<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>
|
<span className="text-sm text-slate-900">{zm.name}</span>
|
||||||
<Badge variant="outline" className="text-xs ml-auto">ZM-{idx + 1}</Badge>
|
<Badge variant="outline" className="text-xs ml-auto">ZM-{idx + 1}</Badge>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{zm.email && (
|
{zm.email && (
|
||||||
<div className="flex items-center gap-2 ml-6">
|
<div className="flex items-center gap-2 ml-6">
|
||||||
<Mail className="w-3 h-3 text-slate-400" />
|
<Mail className="w-3 h-3 text-slate-400" />
|
||||||
<span className="text-xs text-slate-600">{zm.email}</span>
|
<span className="text-xs text-slate-600">{zm.email}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{zm.phone && (
|
{zm.phone && (
|
||||||
<div className="flex items-center gap-2 ml-6">
|
<div className="flex items-center gap-2 ml-6">
|
||||||
<span className="text-xs text-slate-600">{zm.phone}</span>
|
<span className="text-xs text-slate-600">{zm.phone}</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{zm.districts && zm.districts.length > 0 && (
|
{zm.districts && zm.districts.length > 0 && (
|
||||||
<div className="ml-6 mt-2">
|
<div className="ml-6 mt-2">
|
||||||
<Label className="text-xs text-slate-500 mb-1 block">
|
<Label className="text-xs text-slate-500 mb-1 block">
|
||||||
@ -126,7 +115,11 @@ export const ZoneDetails: React.FC<ZoneDetailsProps> = ({ selectedZone, onAddZon
|
|||||||
</Label>
|
</Label>
|
||||||
<div className="flex flex-wrap gap-1">
|
<div className="flex flex-wrap gap-1">
|
||||||
{zm.districts.map((district: string, dIdx: number) => (
|
{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" />
|
<MapPin className="w-2.5 h-2.5 mr-1" />
|
||||||
{district}
|
{district}
|
||||||
</Badge>
|
</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 (
|
return (
|
||||||
<Dialog open={isOpen} onOpenChange={onOpenChange}>
|
<Dialog open={isOpen} onOpenChange={onOpenChange}>
|
||||||
<DialogContent className="max-w-md">
|
<DialogContent className="max-w-md">
|
||||||
|
|||||||
@ -2,7 +2,8 @@ import { useCallback } from 'react';
|
|||||||
import { useDispatch } from 'react-redux';
|
import { useDispatch } from 'react-redux';
|
||||||
import { masterService } from '../services/master.service';
|
import { masterService } from '../services/master.service';
|
||||||
import {
|
import {
|
||||||
setMasterData, setLoading, setError
|
setMasterData, setLoading, setError,
|
||||||
|
setAreasData, setAreasLoading
|
||||||
} from '../store/slices/masterSlice';
|
} from '../store/slices/masterSlice';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
|
|
||||||
@ -15,7 +16,8 @@ export const useMasterData = () => {
|
|||||||
|
|
||||||
const [
|
const [
|
||||||
rolesRes, zonesRes, permsRes, regionsRes, usersRes,
|
rolesRes, zonesRes, permsRes, regionsRes, usersRes,
|
||||||
statesRes, emailTemplatesRes, districtsRes, areasRes, slaRes
|
statesRes, emailTemplatesRes, districtsRes, areasRes, slaRes,
|
||||||
|
asmsRes, zmsRes
|
||||||
] = await Promise.all([
|
] = await Promise.all([
|
||||||
masterService.getRoles().catch(() => ({ success: false })),
|
masterService.getRoles().catch(() => ({ success: false })),
|
||||||
masterService.getZones().catch(() => ({ success: false })),
|
masterService.getZones().catch(() => ({ success: false })),
|
||||||
@ -24,9 +26,11 @@ export const useMasterData = () => {
|
|||||||
masterService.getUsers().catch(() => ({ success: false })),
|
masterService.getUsers().catch(() => ({ success: false })),
|
||||||
masterService.getStates().catch(() => ({ success: false })),
|
masterService.getStates().catch(() => ({ success: false })),
|
||||||
masterService.getEmailTemplates().catch(() => ({ success: false })),
|
masterService.getEmailTemplates().catch(() => ({ success: false })),
|
||||||
masterService.getDistricts().catch(() => ({ success: false })),
|
masterService.getDistricts({ limit: 'all' }).catch(() => ({ success: false })),
|
||||||
masterService.getAreas().catch(() => ({ success: false })),
|
masterService.getAreas({ limit: 'all' }).catch(() => ({ success: false })),
|
||||||
masterService.getSlaConfigs().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);
|
const getBody = (res: any) => res.success ? res : (res.data ? res.data : res);
|
||||||
@ -41,6 +45,8 @@ export const useMasterData = () => {
|
|||||||
const bodyDistricts = getBody(districtsRes);
|
const bodyDistricts = getBody(districtsRes);
|
||||||
const bodyAreas = getBody(areasRes);
|
const bodyAreas = getBody(areasRes);
|
||||||
const bodySla = getBody(slaRes);
|
const bodySla = getBody(slaRes);
|
||||||
|
const bodyAsms = getBody(asmsRes);
|
||||||
|
const bodyZms = getBody(zmsRes);
|
||||||
|
|
||||||
const users = (bodyUsers?.users || bodyUsers?.data || []).map((u: any) => ({
|
const users = (bodyUsers?.users || bodyUsers?.data || []).map((u: any) => ({
|
||||||
...u,
|
...u,
|
||||||
@ -73,11 +79,12 @@ export const useMasterData = () => {
|
|||||||
email: z.zonalBusinessHead?.email || '',
|
email: z.zonalBusinessHead?.email || '',
|
||||||
phone: z.zonalBusinessHead?.mobileNumber || ''
|
phone: z.zonalBusinessHead?.mobileNumber || ''
|
||||||
},
|
},
|
||||||
zonalManagers: zoneZmUsers.map((m: any) => ({
|
zonalManagers: (z.zonalManagers || []).map((m: any) => ({
|
||||||
name: m.fullName || 'Unknown',
|
id: m.id,
|
||||||
|
name: m.name || m.fullName || 'Unknown',
|
||||||
email: m.email || '',
|
email: m.email || '',
|
||||||
phone: m.mobileNumber || '',
|
phone: m.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) || []
|
districts: m.districts || []
|
||||||
}))
|
}))
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@ -90,39 +97,22 @@ export const useMasterData = () => {
|
|||||||
zoneName: r.zoneName || 'Unknown',
|
zoneName: r.zoneName || 'Unknown',
|
||||||
states: r.states || [],
|
states: r.states || [],
|
||||||
cities: r.cities || [],
|
cities: r.cities || [],
|
||||||
|
districts: (r.districts || []).map((d: any) => ({ id: d.id, name: d.name, stateId: d.stateId })),
|
||||||
asmCount: r.asmCount || 0,
|
asmCount: r.asmCount || 0,
|
||||||
regionalOfficerCount: r.regionalOfficerCount || 0,
|
regionalOfficerCount: r.regionalOfficerCount || 0,
|
||||||
regionalManager: r.regionalManager ? {
|
regionalManager: r.regionalManager ? {
|
||||||
id: r.regionalManager.id,
|
id: r.regionalManager.id,
|
||||||
name: r.regionalManager.fullName,
|
name: r.regionalManager.fullName || r.regionalManager.name,
|
||||||
email: r.regionalManager.email,
|
email: r.regionalManager.email,
|
||||||
phone: r.regionalManager.mobileNumber
|
phone: r.regionalManager.mobileNumber || r.regionalManager.phone
|
||||||
} : undefined,
|
} : undefined,
|
||||||
status: r.isActive !== false ? 'Active' : 'Inactive'
|
status: r.isActive !== false ? 'Active' : 'Inactive'
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const asms = users.filter((u: any) => u.allRoles?.some((r: string) => r === 'ASM' || r === 'AREA SALES MANAGER'))
|
const asms = (bodyAsms?.data || bodyAsms || []);
|
||||||
.map((u: any) => {
|
const zonalManagers = (bodyZms?.data || bodyZms || []);
|
||||||
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[];
|
|
||||||
|
|
||||||
return {
|
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')))
|
||||||
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')))
|
|
||||||
.map((u: any) => {
|
.map((u: any) => {
|
||||||
const zmT = u.territoryProfile?.find((t: any) => (t.roleCode === 'ZM' || t.roleCode === 'DD-ZM' || t.role === 'ZONAL MANAGER')) || {};
|
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 })) || [];
|
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) => ({
|
const allDistricts = (bodyDistricts?.districts || bodyDistricts?.data || []).map((d: any) => ({
|
||||||
...d,
|
...d,
|
||||||
districtName: d.name,
|
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) => ({
|
const allAreas = (bodyAreas.areas || bodyAreas.data || []).map((a: any) => ({
|
||||||
@ -160,7 +153,7 @@ export const useMasterData = () => {
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
dispatch(setMasterData({
|
dispatch(setMasterData({
|
||||||
zones, regionalOffices, asms, zonalManagerMappings,
|
zones, regionalOffices, asms, zonalManagerMappings, zonalManagers,
|
||||||
roles, allStates, allDistricts, allAreas,
|
roles, allStates, allDistricts, allAreas,
|
||||||
availablePermissions: bodyPerms?.permissions || bodyPerms?.data || [],
|
availablePermissions: bodyPerms?.permissions || bodyPerms?.data || [],
|
||||||
emailTemplates: bodyEmail?.data || [],
|
emailTemplates: bodyEmail?.data || [],
|
||||||
@ -178,5 +171,20 @@ export const useMasterData = () => {
|
|||||||
}
|
}
|
||||||
}, [dispatch]);
|
}, [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();
|
const response = await API.getRegions();
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
getStates: async (zoneId?: string) => {
|
getStates: async (params?: any) => {
|
||||||
const response = await API.getStates(zoneId);
|
const response = await API.getStates(params);
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
getDistricts: async (stateId?: string) => {
|
getDistricts: async (params?: any) => {
|
||||||
const response = await API.getDistricts(stateId);
|
const response = await API.getDistricts(params);
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
getAreas: async (districtId?: string) => {
|
getAreas: async (params?: any) => {
|
||||||
const response = await API.getAreas(districtId);
|
const response = await API.getAreas(params);
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
updateArea: async (id: string, data: any) => {
|
updateArea: async (id: string, data: any) => {
|
||||||
@ -60,10 +60,25 @@ export const masterService = {
|
|||||||
const response = await API.createArea(data);
|
const response = await API.createArea(data);
|
||||||
return response.data;
|
return response.data;
|
||||||
},
|
},
|
||||||
|
deleteArea: async (id: string) => {
|
||||||
|
const response = await (API as any).deleteArea(id);
|
||||||
|
return response.data;
|
||||||
|
},
|
||||||
getAreaManagers: async () => {
|
getAreaManagers: async () => {
|
||||||
const response = await API.getAreaManagers();
|
const response = await API.getAreaManagers();
|
||||||
return response.data;
|
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
|
// User Management
|
||||||
getUsers: async () => {
|
getUsers: async () => {
|
||||||
|
|||||||
@ -19,11 +19,12 @@ export interface Region {
|
|||||||
name: string;
|
name: string;
|
||||||
zoneId: string;
|
zoneId: string;
|
||||||
zoneName: string;
|
zoneName: string;
|
||||||
districts: { id: string; name: string }[];
|
districts: { id: string; name: string, stateId?: string }[];
|
||||||
|
states: string[];
|
||||||
status: string;
|
status: string;
|
||||||
regionalOfficerCount: number;
|
regionalOfficerCount: number;
|
||||||
asmCount: number;
|
asmCount: number;
|
||||||
regionalManager?: { id: string; fullName: string; email: string } | null;
|
regionalManager?: { id: string; fullName: string; email: string; phone?: string } | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ASM {
|
export interface ASM {
|
||||||
@ -36,7 +37,7 @@ export interface ASM {
|
|||||||
regionId: string;
|
regionId: string;
|
||||||
zoneName: string;
|
zoneName: string;
|
||||||
regionName: string;
|
regionName: string;
|
||||||
areasManaged: string[];
|
areasManaged: { id: string; name: string }[];
|
||||||
districtNames: string[];
|
districtNames: string[];
|
||||||
stateNames: string[];
|
stateNames: string[];
|
||||||
email: string;
|
email: string;
|
||||||
@ -54,8 +55,11 @@ export interface ZonalManagerMapping {
|
|||||||
zoneName: string;
|
zoneName: string;
|
||||||
regionId: string;
|
regionId: string;
|
||||||
regionName: string;
|
regionName: string;
|
||||||
districts: string[];
|
districts: { id: string; name: string }[];
|
||||||
status: string;
|
status: string;
|
||||||
|
city?: string;
|
||||||
|
openFrom?: string | Date | null;
|
||||||
|
openTo?: string | Date | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface MasterState {
|
export interface MasterState {
|
||||||
@ -63,6 +67,7 @@ export interface MasterState {
|
|||||||
regionalOffices: Region[];
|
regionalOffices: Region[];
|
||||||
asms: ASM[];
|
asms: ASM[];
|
||||||
zonalManagerMappings: ZonalManagerMapping[];
|
zonalManagerMappings: ZonalManagerMapping[];
|
||||||
|
zonalManagers: any[];
|
||||||
roles: any[];
|
roles: any[];
|
||||||
allStates: { id: string, name: string, zone?: { name: string } }[];
|
allStates: { id: string, name: string, zone?: { name: string } }[];
|
||||||
allDistricts: {
|
allDistricts: {
|
||||||
@ -76,14 +81,24 @@ export interface MasterState {
|
|||||||
regionName: string,
|
regionName: string,
|
||||||
zoneName: string,
|
zoneName: string,
|
||||||
asmName?: string,
|
asmName?: string,
|
||||||
isActive: boolean
|
isActive: boolean,
|
||||||
|
city?: string,
|
||||||
|
openFrom?: string | Date | null,
|
||||||
|
openTo?: string | Date | null
|
||||||
}[];
|
}[];
|
||||||
allAreas: any[];
|
allAreas: any[];
|
||||||
availablePermissions: any[];
|
availablePermissions: any[];
|
||||||
emailTemplates: any[];
|
emailTemplates: any[];
|
||||||
slaConfigs: any[];
|
slaConfigs: any[];
|
||||||
users: any[];
|
users: any[];
|
||||||
|
areasPagination: {
|
||||||
|
total: number;
|
||||||
|
page: number;
|
||||||
|
limit: number;
|
||||||
|
totalPages: number;
|
||||||
|
};
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
|
isAreasLoading: boolean;
|
||||||
error: string | null;
|
error: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -92,6 +107,7 @@ const initialState: MasterState = {
|
|||||||
regionalOffices: [],
|
regionalOffices: [],
|
||||||
asms: [],
|
asms: [],
|
||||||
zonalManagerMappings: [],
|
zonalManagerMappings: [],
|
||||||
|
zonalManagers: [],
|
||||||
roles: [],
|
roles: [],
|
||||||
allStates: [],
|
allStates: [],
|
||||||
allDistricts: [],
|
allDistricts: [],
|
||||||
@ -100,7 +116,14 @@ const initialState: MasterState = {
|
|||||||
emailTemplates: [],
|
emailTemplates: [],
|
||||||
slaConfigs: [],
|
slaConfigs: [],
|
||||||
users: [],
|
users: [],
|
||||||
|
areasPagination: {
|
||||||
|
total: 0,
|
||||||
|
page: 1,
|
||||||
|
limit: 10,
|
||||||
|
totalPages: 0,
|
||||||
|
},
|
||||||
loading: false,
|
loading: false,
|
||||||
|
isAreasLoading: false,
|
||||||
error: null,
|
error: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -111,6 +134,13 @@ const masterSlice = createSlice({
|
|||||||
setMasterData: (state, action: PayloadAction<Partial<MasterState>>) => {
|
setMasterData: (state, action: PayloadAction<Partial<MasterState>>) => {
|
||||||
return { ...state, ...action.payload };
|
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[]>) => {
|
setZones: (state, action: PayloadAction<Zone[]>) => {
|
||||||
state.zones = action.payload;
|
state.zones = action.payload;
|
||||||
},
|
},
|
||||||
@ -137,7 +167,8 @@ const masterSlice = createSlice({
|
|||||||
|
|
||||||
export const {
|
export const {
|
||||||
setMasterData, setZones, setRegionalOffices, setAsms,
|
setMasterData, setZones, setRegionalOffices, setAsms,
|
||||||
setZonalManagerMappings, setUsers, setLoading, setError
|
setZonalManagerMappings, setUsers, setLoading, setError,
|
||||||
|
setAreasData, setAreasLoading
|
||||||
} = masterSlice.actions;
|
} = masterSlice.actions;
|
||||||
|
|
||||||
export default masterSlice.reducer;
|
export default masterSlice.reducer;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user