From 2fab9c5c2dda32c25fe30dc4e3b3ebe547542b20 Mon Sep 17 00:00:00 2001 From: laxmanhalaki Date: Mon, 30 Mar 2026 02:59:13 +0530 Subject: [PATCH] hirrachy enhanced nd creatded distric as centric and added dealer location stable ui made for the hirarchy --- .env.example | 2 + src/api/API.ts | 19 +- src/components/applications/MasterPage.tsx | 193 ++++++++-- .../applications/MasterPage/ASMDialog.tsx | 105 ++++-- .../MasterPage/LocationDialog.tsx | 110 +++--- .../MasterPage/LocationManagement.tsx | 211 ++++++++--- .../applications/MasterPage/RegionDialog.tsx | 356 ++++++++++++------ .../applications/MasterPage/RoleDialog.tsx | 179 +++++++++ .../MasterPage/RolePermissions.tsx | 69 ++-- .../applications/MasterPage/ZMDialog.tsx | 16 +- .../applications/MasterPage/ZMManagement.tsx | 18 +- .../applications/MasterPage/ZoneDetails.tsx | 47 +-- .../applications/MasterPage/ZoneDialog.tsx | 7 + src/hooks/useMasterData.ts | 78 ++-- src/services/master.service.ts | 27 +- src/store/slices/masterSlice.ts | 43 ++- 16 files changed, 1060 insertions(+), 420 deletions(-) create mode 100644 .env.example create mode 100644 src/components/applications/MasterPage/RoleDialog.tsx diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..da98740 --- /dev/null +++ b/.env.example @@ -0,0 +1,2 @@ +# API Server URL +VITE_API_URL=http://localhost:5000/api diff --git a/src/api/API.ts b/src/api/API.ts index b2004d2..2c38b41 100644 --- a/src/api/API.ts +++ b/src/api/API.ts @@ -20,12 +20,15 @@ export const API = { getRegions: () => client.get('/master/regions'), getOutlets: () => client.get('/master/outlets'), getOutletByCode: (code: string) => client.get(`/master/outlets/code/${code}`), - getStates: (zoneId?: string) => client.get('/master/states', { params: { zoneId } }), - getDistricts: (stateId?: string) => client.get('/master/districts', { params: { stateId } }), - getAreas: (districtId?: string) => client.get('/master/areas', { params: { districtId } }), + getStates: (params?: any) => client.get('/master/states', typeof params === 'string' ? { zoneId: params } : params), + getDistricts: (params?: any) => client.get('/master/districts', typeof params === 'string' ? { stateId: params } : params), + getAreas: (params?: any) => client.get('/master/areas', params), updateArea: (id: string, data: any) => client.put(`/master/areas/${id}`, data), createArea: (data: any) => client.post('/master/areas', data), getAreaManagers: () => client.get('/master/area-managers'), + getASMs: () => client.get('/master/asms'), + getZonalManagers: () => client.get('/master/zonal-managers'), + saveZonalManager: (data: any) => client.post('/master/zonal-managers', data), // Onboarding submitApplication: (data: any) => client.post('/onboarding/apply', data), @@ -67,13 +70,13 @@ export const API = { upsertApprovalPolicy: (stageCode: string, data: any) => client.put(`/assessment/approval-policies/${stageCode}`, data), // Collaboration & Participants - getWorknotes: (requestId: string, requestType: string) => client.get('/collaboration/worknotes', { params: { requestId, requestType } }), + getWorknotes: (requestId: string, requestType: string) => client.get('/collaboration/worknotes', { requestId, requestType }), addWorknote: (data: any) => client.post('/collaboration/worknotes', data), addParticipant: (data: any) => client.post('/collaboration/participants', data), removeParticipant: (id: string) => client.delete(`/collaboration/participants/${id}`), // User management routes - getUsers: (params?: any) => client.get('/admin/users', { params }), + getUsers: (params?: any) => client.get('/admin/users', params), createUser: (data: any) => client.post('/admin/users', data), updateUser: (id: string, data: any) => client.put(`/admin/users/${id}`, data), updateUserStatus: (id: string, data: any) => client.patch(`/admin/users/${id}/status`, data), @@ -96,9 +99,9 @@ export const API = { // Audit Trail getAuditLogs: (entityType: string, entityId: string, page: number = 1, limit: number = 50) => - client.get('/audit/logs', { params: { entityType, entityId, page, limit } }), + client.get('/audit/logs', { entityType, entityId, page, limit }), getAuditSummary: (entityType: string, entityId: string) => - client.get('/audit/summary', { params: { entityType, entityId } }), + client.get('/audit/summary', { entityType, entityId }), // Prospective Login sendOtp: (phone: string) => client.post('/prospective-login/send-otp', { phone }), @@ -119,7 +122,7 @@ export const API = { finalizeTermination: (id: string, data: any) => client.post(`/termination/${id}/finalize`, data), // Lifecycle Modules (Self-Service) - getResignations: (params?: any) => client.get('/resignation', { params }), + getResignations: (params?: any) => client.get('/resignation', params), createResignation: (data: any) => client.post('/resignation', data), approveResignation: (id: string, data?: any) => client.post(`/resignation/${id}/approve`, data), rejectResignation: (id: string, data: any) => client.post(`/resignation/${id}/reject`, data), diff --git a/src/components/applications/MasterPage.tsx b/src/components/applications/MasterPage.tsx index 8176586..279be54 100644 --- a/src/components/applications/MasterPage.tsx +++ b/src/components/applications/MasterPage.tsx @@ -20,6 +20,7 @@ import { ZMManagement } from './MasterPage/ZMManagement'; import { UserManagementTable } from './MasterPage/UserManagementTable'; import { SLAConfiguration } from './MasterPage/SLAConfiguration'; import { RolePermissions } from './MasterPage/RolePermissions'; +import { RoleDialog } from './MasterPage/RoleDialog'; import { EmailTemplates } from './MasterPage/EmailTemplates'; import { LocationManagement } from './MasterPage/LocationManagement'; import { ASMDialog } from './MasterPage/ASMDialog'; @@ -32,15 +33,15 @@ import { ApprovalPoliciesPage } from '../admin/ApprovalPoliciesPage'; import { RootState } from '../../store'; export const MasterPage: React.FC = () => { - const { fetchInitialData } = useMasterData(); + const { fetchInitialData, fetchAreas } = useMasterData(); const { asms, zonalManagerMappings, allDistricts, users, + roles, loading } = useSelector((state: RootState) => state.master); - // Tab & Selection State const [activeTab, setActiveTab] = useState('hierarchy'); const [selectedZone, setSelectedZone] = useState('all'); @@ -63,6 +64,7 @@ export const MasterPage: React.FC = () => { const [selectedASMRegion, setSelectedASMRegion] = useState(''); const [selectedASMStates, setSelectedASMStates] = useState([]); const [selectedASMDistricts, setSelectedASMDistricts] = useState([]); + const [asmRoleCode, setAsmRoleCode] = useState<'ASM' | 'DD-AM'>('ASM'); // ZM Management State const [showZMDialog, setShowZMDialog] = useState(false); @@ -76,6 +78,10 @@ export const MasterPage: React.FC = () => { const [selectedZMStates, setSelectedZMStates] = useState([]); const [selectedZMDistricts, setSelectedZMDistricts] = useState([]); + // Role Management State + const [showRoleDialog, setShowRoleDialog] = useState(false); + const [editingRole, setEditingRole] = useState(null); + // Form State (Zone) const [editingZoneId, setEditingZoneId] = useState(null); const [zoneName, setZoneName] = useState(''); @@ -99,20 +105,31 @@ export const MasterPage: React.FC = () => { const [previewContent, setPreviewContent] = useState(null); // Form State (Location) - const [editingLocationId] = useState(null); + const [editingLocationId, setEditingLocationId] = useState(null); const [locationState, setLocationState] = useState(''); const [locationDistrict, setLocationDistrict] = useState(''); const [locationCity, setLocationCity] = useState(''); - const [locationPincode, setLocationPincode] = useState(''); const [locationActiveFrom, setLocationActiveFrom] = useState(''); const [locationActiveTo, setLocationActiveTo] = useState(''); const [locationStatus, setLocationStatus] = useState('active'); + // Search & Pagination State (Locations) + const [districtsSearch, setDistrictsSearch] = useState(''); + const [districtsPage, setDistrictsPage] = useState(1); + // Initial Load useEffect(() => { fetchInitialData(); }, [fetchInitialData]); + // Sync editingRole with latest data from Redux + useEffect(() => { + if (editingRole && roles.length > 0) { + const latest = roles.find((r: any) => r.id === editingRole.id); + if (latest) setEditingRole(latest); + } + }, [roles, editingRole?.id]); + // Shared Data Helpers const districtsAssignedToOthers = useMemo(() => { const map: Record = {}; @@ -127,9 +144,12 @@ export const MasterPage: React.FC = () => { return map; }, [asms, zonalManagerMappings]); - const getDistrictsForSelectedState = useCallback((stateName: string) => { + const getDistrictsForSelectedState = useCallback((stateName: string, regionId?: string) => { return allDistricts - .filter(d => d.stateName?.toUpperCase() === stateName?.toUpperCase()) + .filter(d => + d.stateName?.toUpperCase() === stateName?.toUpperCase() && + (!regionId || d.regionId === regionId) + ) .map(d => ({ id: d.id, name: d.name })); }, [allDistricts]); @@ -140,14 +160,25 @@ export const MasterPage: React.FC = () => { return; } try { - const payload = { userId: asmManagerId, asmCode, districts: selectedASMDistricts, status: asmStatus }; + const payload = { + userId: asmManagerId, + roleCode: asmRoleCode, + asmCode, + districts: selectedASMDistricts, + status: asmStatus + }; const res = await masterService.saveASM(payload) as any; if (res.success) { - toast.success(`ASM ${editingASMId ? 'updated' : 'assigned'} successfully`); + toast.success(`${asmRoleCode === 'ASM' ? 'ASM' : 'DD Area Manager'} ${editingASMId ? 'updated' : 'assigned'} successfully`); setShowASMDialog(false); fetchInitialData(); + } else { + toast.error(res.message || 'Failed to save ASM'); } - } catch (error) { 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) => { @@ -161,6 +192,7 @@ export const MasterPage: React.FC = () => { setSelectedASMRegion(asm.regionId); setSelectedASMStates(asm.stateNames || []); setSelectedASMDistricts(asm.areasManaged?.map((a: any) => a.id) || []); + setAsmRoleCode(asm.roleCode === 'DD-AM' ? 'DD-AM' : 'ASM'); setShowASMDialog(true); }; @@ -168,9 +200,9 @@ export const MasterPage: React.FC = () => { setEditingZMId(zm.id); setZmManagerId(zm.id); setZmName(zm.name); - setZmCode(zm.code === 'N/A' ? '' : zm.code); - setZmEmployeeId(zm.code === 'N/A' ? '' : zm.code); - setZmStatus(zm.status.toLowerCase() as 'active' | 'inactive'); + setZmCode(zm.zmCode || zm.code || ''); + setZmEmployeeId(zm.employeeId || ''); + setZmStatus(zm.status?.toLowerCase() === 'active' ? 'active' : 'inactive'); setSelectedZMZone(zm.zoneId || ''); setSelectedZMStates(zm.stateNames || []); setSelectedZMDistricts(zm.districts?.map((d: any) => d.id) || []); @@ -185,21 +217,24 @@ export const MasterPage: React.FC = () => { try { const payload = { userId: zmManagerId, - roleCode: 'DD-ZM', - zmCode, + zmCode, + zoneId: selectedZMZone, districts: selectedZMDistricts, - status: zmStatus, - locationId: selectedZMZone // ZM primary location is the zone + status: zmStatus }; - // Use the generic saveASM method which I generalized on the backend - const res = await masterService.saveASM(payload) as any; + const res = await (masterService as any).saveZonalManager(payload) as any; if (res.success) { toast.success(`Zonal Manager ${editingZMId ? 'updated' : 'assigned'} successfully`); setShowZMDialog(false); fetchInitialData(); + } else { + toast.error(res.message || 'Failed to save Zonal Manager'); } - } catch (error) { 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 () => { @@ -210,28 +245,39 @@ export const MasterPage: React.FC = () => { toast.success('Zone saved successfully'); setShowZoneDialog(false); 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 () => { try { const payload = { - name: regionName, - code: regionCode, - description: regionDescription, - parentId: selectedRegionZone, - managerId: regionalManagerId, - districts: selectedRegionDistricts, - status: 'Active' - }; + ...(editingRegionId ? { id: editingRegionId } : {}), + name: regionName, + code: regionCode, + description: regionDescription, + parentId: selectedRegionZone, + managerId: regionalManagerId, + districts: selectedRegionDistricts, + status: 'Active' + }; const res = await masterService.saveRegion(payload) as any; if (res.success) { toast.success('Region saved successfully'); setShowRegionDialog(false); fetchInitialData(); + } else { + toast.error(res.message || 'Error saving region'); } - } catch (error) { 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 () => { @@ -259,17 +305,49 @@ export const MasterPage: React.FC = () => { finally { setPreviewLoading(false); } }; + const handleSaveRole = async (roleId: string, permissions: string[]) => { + try { + const res = await masterService.updateRole(roleId, { permissions }) as any; + if (res.success) { + toast.success('Role permissions updated successfully'); + setShowRoleDialog(false); + fetchInitialData(); + } else { + toast.error(res.message || 'Error saving role permissions'); + } + } catch (error: any) { + const msg = error?.response?.data?.message || error?.message || 'Error saving role permissions'; + toast.error(msg); + } + }; + + const handleEditRole = (role: any) => { + setEditingRole(role); + setShowRoleDialog(true); + }; + + const handleEditLocation = (loc: any) => { + setEditingLocationId(loc.id); + setLocationState(loc.stateName || ''); + setLocationCity(loc.city || ''); + setLocationDistrict(loc.name || ''); + setLocationActiveFrom(loc.openFrom ? new Date(loc.openFrom).toISOString().split('T')[0] : ''); + setLocationActiveTo(loc.openTo ? new Date(loc.openTo).toISOString().split('T')[0] : ''); + setLocationStatus(loc.isActive ? 'active' : 'inactive'); + setShowLocationDialog(true); + }; + const handleSaveLocation = async () => { try { const payload = { id: editingLocationId, - stateId: locationState, - districtId: locationDistrict, + stateName: locationState, + name: locationDistrict, city: locationCity, - pincode: locationPincode, status: locationStatus, - activeFrom: locationActiveFrom, - activeTo: locationActiveTo + openFrom: locationActiveFrom, + openTo: locationActiveTo, + isActive: locationStatus === 'active' }; const res = await (editingLocationId ? masterService.updateArea(editingLocationId, payload) @@ -277,11 +355,18 @@ export const MasterPage: React.FC = () => { if (res.success) { toast.success('Location saved'); setShowLocationDialog(false); - fetchInitialData(); + fetchAreas({ search: districtsSearch, page: districtsPage }); } } catch (error) { toast.error('Error saving location'); } }; + useEffect(() => { + const handler = setTimeout(() => { + fetchAreas({ search: districtsSearch, page: districtsPage }); + }, 500); + return () => clearTimeout(handler); + }, [districtsSearch, districtsPage, fetchAreas]); + return (
@@ -355,7 +440,7 @@ export const MasterPage: React.FC = () => { toast.info('Unified Role Management interface being updated')} - onEditRole={() => toast.info('Unified Role Management interface being updated')} /> + onEditRole={handleEditRole} /> @@ -368,8 +453,35 @@ export const MasterPage: React.FC = () => { - setShowLocationDialog(true)} - onEditLocation={() => toast.info('Location Editor being updated')} onDeleteLocation={() => toast.error('Delete Location restricted')} /> + { + 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} + /> @@ -381,7 +493,7 @@ export const MasterPage: React.FC = () => { {/* Main Dialogs */} 0 ? users.map(u => ({...u, name: u.fullName || u.name, role: u.role?.roleName, roleCode: u.role?.roleCode, allRoles: u.allRoles})) : asms} /> 0 ? users.map(u => ({...u, name: u.fullName || u.name, role: u.role?.roleName, roleCode: u.role?.roleCode, allRoles: u.allRoles})) : asms} /> - 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} /> + 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)} /> { getDistrictsForSelectedState={getDistrictsForSelectedState} /> - + +
); }; diff --git a/src/components/applications/MasterPage/ASMDialog.tsx b/src/components/applications/MasterPage/ASMDialog.tsx index 4174e44..4aad4a4 100644 --- a/src/components/applications/MasterPage/ASMDialog.tsx +++ b/src/components/applications/MasterPage/ASMDialog.tsx @@ -32,6 +32,8 @@ interface ASMDialogProps { selectedASMDistricts: string[]; setSelectedASMDistricts: (districts: string[]) => void; onSave: () => void; + asmRoleCode: 'ASM' | 'DD-AM'; + setAsmRoleCode: (role: 'ASM' | 'DD-AM') => void; userAssignedData: any[]; districtsAssignedToOthers: Record; getDistrictsForSelectedState: (state: string) => { id: string; name: string }[]; @@ -43,6 +45,7 @@ export const ASMDialog: React.FC = ({ asmStatus, setAsmStatus, selectedASMZone, setSelectedASMZone, selectedASMRegion, setSelectedASMRegion, selectedASMStates, setSelectedASMStates, selectedASMDistricts, setSelectedASMDistricts, onSave, + asmRoleCode, setAsmRoleCode, userAssignedData, districtsAssignedToOthers, getDistrictsForSelectedState }) => { const { zones, regionalOffices } = useSelector((state: RootState) => state.master); @@ -51,11 +54,27 @@ export const ASMDialog: React.FC = ({ const roles = u.allRoles || []; return roles.some((r: string) => { const roleStr = (r || '').toUpperCase(); - return ['ASM', 'AREA SALES MANAGER', 'RM', 'RBM', 'REGIONAL MANAGER', 'ZBH', 'ZONE BUSINESS HEAD', 'ZONAL BUSINESS HEAD'].includes(roleStr) || + return ['ASM', 'AREA SALES MANAGER', 'DD-AM', 'AREA MANAGER', 'RM', 'RBM', 'REGIONAL MANAGER', 'ZBH', 'ZONE BUSINESS HEAD', 'ZONAL BUSINESS HEAD'].includes(roleStr) || roleStr.includes('AREA SALES') || roleStr.includes('REGIONAL') || roleStr.includes('ZONAL'); }); }); + // PRE-FILLING LOGIC: When manager or role changes, pre-select their districts + React.useEffect(() => { + if (asmManagerId && isOpen) { + const manager = userAssignedData.find(u => u.id === asmManagerId); + if (manager && manager.territoryProfile) { + const assignedDistricts = manager.territoryProfile + .filter((t: any) => t.roleCode === asmRoleCode && t.locationType === 'district') + .map((t: any) => t.locationId); + + if (assignedDistricts.length > 0) { + setSelectedASMDistricts(assignedDistricts); + } + } + } + }, [asmManagerId, asmRoleCode, isOpen, userAssignedData, setSelectedASMDistricts]); + return ( @@ -84,24 +103,55 @@ export const ASMDialog: React.FC = ({
{selectedASMZone && ( -
- - { + setSelectedASMRegion(value); + setSelectedASMStates([]); + setSelectedASMDistricts([]); + }}> + + + + + {regionalOffices + .filter((office) => office.zoneId === selectedASMZone) + .map((office) => ( + {office.name} + ))} + + +
+ +
+ + +
+ +
+ + + + +
)} @@ -115,16 +165,16 @@ export const ASMDialog: React.FC = ({ return availableStates.length > 0 ? (
- {availableStates.map((state) => ( + {availableStates.map((state: string) => (
s.toLowerCase() === state.toLowerCase())} onCheckedChange={(checked) => { if (checked) { setSelectedASMStates([...selectedASMStates, state]); } else { - setSelectedASMStates(selectedASMStates.filter(s => s !== state)); + setSelectedASMStates(selectedASMStates.filter(s => s.toLowerCase() !== state.toLowerCase())); const stateDistricts = getDistrictsForSelectedState(state); setSelectedASMDistricts(selectedASMDistricts.filter(dId => !stateDistricts.some(sd => sd.id === dId))); } @@ -208,9 +258,18 @@ export const ASMDialog: React.FC = ({ const selectedUser = userAssignedData.find(u => u.id === value); if (selectedUser) { setAsmName(selectedUser.name); - setAsmCode(selectedUser.asmCode || ''); setAsmEmployeeId(selectedUser.employeeId || ''); - setSelectedASMDistricts(selectedUser.areasManaged?.map((a: any) => a.id) || []); + + // Extraction logic: Look for managerCode in the territoryProfile matching the current role + const roleProfile = (selectedUser.territoryProfile || []).find((t: any) => t.roleCode === asmRoleCode); + const existingCode = roleProfile?.managerCode || selectedUser.asmCode || selectedUser.employeeId || ''; + setAsmCode(existingCode); + + // Extract zone/region from assignments if present + if (roleProfile?.zoneId) setSelectedASMZone(roleProfile.zoneId); + if (roleProfile?.regionId) setSelectedASMRegion(roleProfile.regionId); + + setSelectedASMDistricts(selectedUser.areasManaged?.filter((a: any) => a.roleCode === asmRoleCode).map((a: any) => a.id) || []); setSelectedASMStates(selectedUser.stateNames || []); } }} diff --git a/src/components/applications/MasterPage/LocationDialog.tsx b/src/components/applications/MasterPage/LocationDialog.tsx index 6e492c7..24c1b41 100644 --- a/src/components/applications/MasterPage/LocationDialog.tsx +++ b/src/components/applications/MasterPage/LocationDialog.tsx @@ -1,11 +1,9 @@ import React from 'react'; -import { useSelector } from 'react-redux'; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '../../ui/dialog'; import { Button } from '../../ui/button'; import { Label } from '../../ui/label'; import { Input } from '../../ui/input'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '../../ui/select'; -import { RootState } from '../../../store'; interface LocationDialogProps { isOpen: boolean; @@ -17,8 +15,6 @@ interface LocationDialogProps { setLocationDistrict: (id: string) => void; locationCity: string; setLocationCity: (city: string) => void; - locationPincode: string; - setLocationPincode: (pincode: string) => void; locationActiveFrom: string; setLocationActiveFrom: (date: string) => void; locationActiveTo: string; @@ -31,11 +27,9 @@ interface LocationDialogProps { export const LocationDialog: React.FC = ({ isOpen, onOpenChange, editingLocationId, locationState, setLocationState, locationDistrict, setLocationDistrict, locationCity, setLocationCity, - locationPincode, setLocationPincode, locationActiveFrom, setLocationActiveFrom, + locationActiveFrom, setLocationActiveFrom, locationActiveTo, setLocationActiveTo, locationStatus, setLocationStatus, onSave }) => { - const { allStates, allDistricts } = useSelector((state: RootState) => state.master); - return ( @@ -45,80 +39,62 @@ export const LocationDialog: React.FC = ({
- - -
-
- - -
-
- + setLocationState(e.target.value)} + /> +
+
+ + setLocationCity(e.target.value)} />
- + setLocationPincode(e.target.value)} + placeholder="Enter district name" + className="mt-2 text-slate-900 border-slate-200 focus-visible:ring-amber-500/30 focus-visible:border-amber-500" + value={locationDistrict} + onChange={(e) => setLocationDistrict(e.target.value)} />
-
-
- - setLocationActiveFrom(e.target.value)} - /> -
-
- - setLocationActiveTo(e.target.value)} - /> +
+ +

Define the time period when this location will be available for dealership applications

+
+
+ + setLocationActiveFrom(e.target.value)} + /> +
+
+ + setLocationActiveTo(e.target.value)} + /> +
- + 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" + /> +
+
-
- - - - State - Area / City - District - Pincode - Manager - Status - Actions - - - - {allDistricts.map((district) => ( - - -
- - {district.stateName || 'N/A'} -
-
- {district.name} - {district.regionName || 'N/A'} - {district.code || 'N/A'} - - {district.asmName ? ( - {district.asmName} - ) : ( - Unassigned - )} - - - - {district.isActive ? 'Active' : 'Inactive'} - - - -
- - -
-
+
+
+ + + State + City + District + Active Period + Status + Actions - ))} - -
+ + + {allDistricts.length === 0 ? ( + + + {searchTerm ? 'No locations found matching your search' : 'No locations available'} + + + ) : ( + allDistricts.map((district) => ( + + +
+ + {district.stateName || 'N/A'} +
+
+ {district.city || 'N/A'} + {district.name} + + {district.openFrom && district.openTo ? ( +
+
+ From: + + {new Date(district.openFrom).toLocaleDateString('en-GB', { day: 'numeric', month: 'short', year: 'numeric' })} + +
+
+ To: + + {new Date(district.openTo).toLocaleDateString('en-GB', { day: 'numeric', month: 'short', year: 'numeric' })} + +
+
+ ) : ( + Not Defined + )} +
+ + + {district.isActive ? 'Active' : 'Inactive'} + + + +
+ + +
+
+
+ )) + )} +
+ + {isAreasLoading && ( +
+
+
+ )} +
+ + {/* Pagination Controls */} + {areasPagination.totalPages > 1 && ( +
+
+ Showing {(areasPagination.page - 1) * areasPagination.limit + 1} to {Math.min(areasPagination.page * areasPagination.limit, areasPagination.total)} of {areasPagination.total} results +
+
+ +
+ {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 ( + + ); + })} +
+ +
+
+ )} diff --git a/src/components/applications/MasterPage/RegionDialog.tsx b/src/components/applications/MasterPage/RegionDialog.tsx index a6be6a2..2fb9a4d 100644 --- a/src/components/applications/MasterPage/RegionDialog.tsx +++ b/src/components/applications/MasterPage/RegionDialog.tsx @@ -24,146 +24,290 @@ interface RegionDialogProps { setSelectedRegionZone: (id: string) => void; regionalManagerId: string; setRegionalManagerId: (id: string) => void; - selectedRegionStates: string[]; // This now contains District IDs - setSelectedRegionStates: (districts: string[]) => void; + selectedRegionStates: string[]; // District IDs selected + setSelectedRegionStates: (districts: string[]) => void; onSave: () => void; - userAssignedData: any[]; // Used for RM selection + userAssignedData: any[]; } export const RegionDialog: React.FC = ({ - isOpen, onOpenChange, editingRegionId, regionName, setRegionName, + isOpen, onOpenChange, editingRegionId, regionName, setRegionName, regionCode, setRegionCode, regionDescription, setRegionDescription, selectedRegionZone, setSelectedRegionZone, regionalManagerId, setRegionalManagerId, selectedRegionStates, setSelectedRegionStates, onSave, userAssignedData }) => { - const { zones, allDistricts, regionalOffices } = useSelector((state: RootState) => state.master); + const { zones, allStates, allDistricts, regionalOffices } = useSelector((state: RootState) => state.master); - // Map of District ID -> Region Name for districts assigned to other regions - const districtsAssignedToOthers = React.useMemo(() => { - const mapping: Record = {}; + // Internal: which states are checked (for filtering districts) + const [selectedStateIds, setSelectedStateIds] = React.useState([]); + + // --- Build conflict map: districtId -> region name (for districts in OTHER regions) --- + const assignedToOtherRegion = React.useMemo(() => { + const map: Record = {}; (regionalOffices || []).forEach((r: any) => { if (r.id !== editingRegionId) { (r.districts || []).forEach((d: any) => { - mapping[d.id] = r.name; + map[d.id] = r.name; }); } }); - return mapping; + return map; }, [regionalOffices, editingRegionId]); - const filteredRMUsers = userAssignedData.filter(u => { - const roles = u.allRoles || []; - return roles.some((r: string) => { - const roleStr = (r || '').toUpperCase(); - return ['RM', 'RBM', 'REGIONAL MANAGER', 'ZBH', 'ZONE BUSINESS HEAD', 'ZONAL BUSINESS HEAD', 'ASM', 'AREA SALES MANAGER'].includes(roleStr) || - roleStr.includes('REGIONAL') || roleStr.includes('ZONAL') || roleStr.includes('AREA SALES'); + // --- States visible for the selected zone (only states that have at least 1 district in this zone OR unassigned) --- + const statesForZone = React.useMemo(() => { + if (!selectedRegionZone) return allStates; + const stateIdsWithZoneDistricts = new Set( + allDistricts + .filter(d => + d.zoneId === selectedRegionZone || + !d.zoneId || + selectedRegionStates.includes(d.id) + ) + .map(d => d.stateId) + .filter(Boolean) + ); + return allStates.filter((s: any) => + s.zoneId === selectedRegionZone || + stateIdsWithZoneDistricts.has(s.id) || + !s.zoneId + ); + }, [allStates, allDistricts, selectedRegionZone, selectedRegionStates]); + + // --- Districts shown = only those in selected states + (in this zone or unassigned) --- + const availableDistricts = React.useMemo(() => { + if (selectedStateIds.length === 0) return []; + return allDistricts.filter(d => + selectedStateIds.includes(d.stateId as string) && + (!d.zoneId || d.zoneId === selectedRegionZone || d.regionId === editingRegionId) + ); + }, [allDistricts, selectedStateIds, selectedRegionZone, editingRegionId]); + + // --- Group districts by state for rendering --- + const districtsByState = React.useMemo(() => { + const map: Record = {}; + 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 ( {editingRegionId ? 'Edit' : 'Add'} Regional Office - Configure regional office and assign to a zone + Configure regional office details and coverage area -
-
-
- - -
-
- - -
-
+
+ {/* Region Code & Name */}
-
- - setRegionName(e.target.value)} /> -
setRegionCode(e.target.value)} />
-
- -
- -