-
+
@@ -512,8 +529,8 @@ export function UserManagementPage() {
- {regions.filter(r => r.zoneId === formData.zoneId).map(region => (
- {region.regionName}
+ {regions.filter(r => (r.parents && r.parents.some((p:any) => p.id === formData.zoneId)) || r.zoneId === formData.zoneId).map(region => (
+ {region.name || region.regionName}
))}
@@ -529,8 +546,8 @@ export function UserManagementPage() {
- {states.filter(s => s.zoneId === formData.zoneId).map(state => (
- {state.stateName}
+ {states.filter(s => (s.parents && s.parents.some((p:any) => p.id === formData.zoneId)) || s.zoneId === formData.zoneId).map(state => (
+ {state.name || state.stateName}
))}
@@ -546,8 +563,8 @@ export function UserManagementPage() {
- {districts.map(district => (
- {district.districtName}
+ {districts.filter(d => (d.parents && d.parents.some((p:any) => p.id === formData.stateId)) || d.stateId === formData.stateId).map(district => (
+ {district.name || district.districtName}
))}
@@ -563,8 +580,8 @@ export function UserManagementPage() {
- {areas.map(area => (
- {area.areaName}
+ {areas.filter(a => (a.parents && a.parents.some((p:any) => p.id === formData.districtId)) || a.districtId === formData.districtId).map(area => (
+ {area.name || area.areaName}
))}
diff --git a/src/components/applications/MasterPage.tsx b/src/components/applications/MasterPage.tsx
index 6f2eda1..ded2cee 100644
--- a/src/components/applications/MasterPage.tsx
+++ b/src/components/applications/MasterPage.tsx
@@ -1,5 +1,7 @@
import { useState, useEffect, useCallback } from 'react';
import { masterService } from '../../services/master.service';
+import { adminService } from '../../services/admin.service';
+import { ApprovalPoliciesPage } from '../admin/ApprovalPoliciesPage';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../ui/card';
import { Badge } from '../ui/badge';
import { Button } from '../ui/button';
@@ -30,7 +32,8 @@ import {
Play,
UserCog,
CheckCircle,
- XCircle
+ XCircle,
+ SlidersHorizontal
} from 'lucide-react';
import { toast } from 'sonner';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from '../ui/dialog';
@@ -53,6 +56,24 @@ const getBody = (res: ApiResponse | unknown): ApiResponse => {
return r;
};
+const flattenLocationParents = (location: any): any[] => {
+ const collected: any[] = [];
+ const walk = (node: any) => {
+ const parents = Array.isArray(node?.parents) ? node.parents : [];
+ for (const parent of parents) {
+ collected.push(parent);
+ walk(parent);
+ }
+ };
+ walk(location);
+ return collected;
+};
+
+const getAncestorByType = (location: any, type: string): any | undefined => {
+ if (location?.type === type) return location;
+ return flattenLocationParents(location).find((p: any) => p?.type === type);
+};
+
// State management interfaces
// Unused Location interface removed
@@ -230,6 +251,7 @@ interface UserAssignment {
name: string;
role: string;
roleCode: string;
+ locationId?: string;
region: string;
regionId: string;
zone: string;
@@ -383,6 +405,16 @@ export function MasterPage() {
const bodyAreas = getBody(areasRes);
const bodySla = getBody(slaRes);
+ const users = bodyUsers?.users || bodyUsers?.data || [];
+ const usersByLocation: Record = {};
+ users.forEach((u: any) => {
+ const locId = u.location?.id || u.locationId;
+ if (locId) {
+ if (!usersByLocation[locId]) usersByLocation[locId] = [];
+ usersByLocation[locId].push(u);
+ }
+ });
+
if (bodyRoles?.success) {
setRoles((bodyRoles.roles || bodyRoles.data || []).map((r: any) => ({
id: r.id,
@@ -394,25 +426,40 @@ export function MasterPage() {
if (bodyZones?.success) {
const rawZones = bodyZones.zones || bodyZones.data || [];
- setZones(rawZones.map((z: any) => ({
- id: z.id,
- code: z.zoneCode,
- name: z.zoneName,
- description: z.description || '',
- states: Array.isArray(z.states) ? z.states.map((s: any) => s.stateName) : [],
- zmCount: Array.isArray(z.managers) ? z.managers.length : 0,
- zbh: {
- name: z.zonalBusinessHead?.fullName || 'Not Assigned',
- email: z.zonalBusinessHead?.email || '',
- phone: z.zonalBusinessHead?.mobileNumber || ''
- },
- zonalManagers: Array.isArray(z.managers) ? z.managers.map((m: any) => ({
- name: m.user?.fullName || 'Unknown',
- email: m.user?.email || '',
- phone: m.user?.mobileNumber || '',
- districts: []
- })) : []
- })));
+ setZones(rawZones.map((z: any) => {
+ const zoneUsers = usersByLocation[z.id] || [];
+ const zbhUser = zoneUsers.find((u: any) => {
+ const code = (u.roleCode || '').toLowerCase();
+ const name = (u.role?.roleName || '').toLowerCase();
+ return code === 'zbh' || name.includes('zonal business head') || code.includes('business head');
+ }) || ({} as any);
+
+ const zmUsers = zoneUsers.filter((u: any) => {
+ const code = (u.roleCode || '').toLowerCase();
+ const name = (u.role?.roleName || '').toLowerCase();
+ return (code === 'zm' || name.includes('zonal manager')) && !name.includes('head');
+ });
+
+ return {
+ id: z.id,
+ code: z.name ? z.name.substring(0, 3).toUpperCase() : 'ZON',
+ name: z.name || z.zoneName,
+ description: z.description || '',
+ states: Array.isArray(z.children) ? z.children.filter((child: any) => child.type === 'state').map((c: any) => c.name) : [],
+ zmCount: zmUsers.length,
+ zbh: {
+ name: zbhUser.fullName || 'Not Assigned',
+ email: zbhUser.email || '',
+ phone: zbhUser.mobileNumber || ''
+ },
+ zonalManagers: zmUsers.map((m: any) => ({
+ name: m.fullName || 'Unknown',
+ email: m.email || '',
+ phone: m.mobileNumber || '',
+ districts: []
+ }))
+ };
+ }));
}
if (bodyPerms?.success) {
@@ -420,86 +467,178 @@ export function MasterPage() {
}
if (bodyRegions?.success) {
- setRegionalOffices((bodyRegions.regions || bodyRegions.data || []).map((r: any) => ({
- id: r.id,
- code: r.regionCode,
- name: r.regionName,
- zoneId: r.zoneId,
- zoneName: r.zone?.zoneName || 'Unknown',
- states: Array.isArray(r.states) ? r.states.map((s: any) => s.stateName) : [],
- cities: [],
- regionalOfficerCount: 0,
- asmCount: 0,
- status: (r.isActive !== false) ? 'Active' : 'Inactive',
- regionalManager: r.regionalManager ? {
- id: r.regionalManager.id,
- name: r.regionalManager.fullName,
- email: r.regionalManager.email,
- phone: r.regionalManager.mobileNumber
- } : undefined
- })));
+ setRegionalOffices((bodyRegions.regions || bodyRegions.data || []).map((r: any) => {
+ const regionUsers = usersByLocation[r.id] || [];
+ const rmUser = regionUsers.find(u => u.roleCode === 'RM' || u.roleCode === 'Regional Manager' || u.role?.roleName === 'Regional Manager') || ({} as any);
+ const asmUsers = regionUsers.filter(u => u.roleCode === 'ASM' || u.roleCode === 'Area Sales Manager' || u.role?.roleName === 'Area Sales Manager');
+ const zoneParent = getAncestorByType(r, 'zone');
+
+ return {
+ id: r.id,
+ code: r.name ? r.name.substring(0, 3).toUpperCase() : 'REG',
+ name: r.name || r.regionName,
+ zoneId: zoneParent?.id || r.zoneId,
+ zoneName: zoneParent?.name || r.zone?.zoneName || 'Unknown',
+ states: Array.isArray(r.children) ? r.children.filter((c: any) => c.type === 'state').map((c: any) => c.name) : [],
+ cities: [],
+ regionalOfficerCount: rmUser.id ? 1 : 0,
+ asmCount: asmUsers.length,
+ status: (r.isActive !== false) ? 'Active' : 'Inactive',
+ regionalManager: rmUser.id ? {
+ id: rmUser.id,
+ name: rmUser.fullName,
+ email: rmUser.email,
+ phone: rmUser.mobileNumber
+ } : undefined
+ };
+ }));
}
if (bodyUsers?.success) {
- const users = bodyUsers.users || bodyUsers.data || [];
- setUserAssignedData(users.map((u: any) => ({
- id: u.id,
- name: u.fullName,
- role: u.role?.roleName || u.roleCode || 'User',
- roleCode: u.roleCode || '',
- region: u.region?.regionName || 'Not Assigned',
- regionId: u.regionId || '',
- zone: u.zone?.zoneName || 'Not Assigned',
- zoneId: u.zoneId || '',
- email: u.email,
- phone: u.mobileNumber || 'N/A',
- status: (u.isActive !== false) ? 'Active' : 'Inactive',
- employeeId: u.employeeId || '',
- asmCode: u.asmCode || '',
- permissions: u.role?.permissions?.map((p: any) => p.permissionCode) || []
- })));
+ setUserAssignedData(users.map((u: any) => {
+ const zone = getAncestorByType(u.location, 'zone');
+ const region = getAncestorByType(u.location, 'region');
+
+ return {
+ id: u.id,
+ name: u.fullName,
+ role: u.role?.roleName || u.roleCode || 'User',
+ roleCode: u.roleCode || '',
+ locationId: u.location?.id || '',
+ region: region?.name || 'Not Assigned',
+ regionId: region?.id || '',
+ zone: zone?.name || 'Not Assigned',
+ zoneId: zone?.id || '',
+ email: u.email,
+ phone: u.mobileNumber || 'N/A',
+ status: (u.isActive !== false) ? 'Active' : 'Inactive',
+ employeeId: u.employeeId || '',
+ asmCode: u.asmCode || '',
+ permissions: u.role?.permissions?.map((p: any) => p.permissionCode) || []
+ };
+ }));
// Populate Zonal Manager Mappings from user assignments
- const zmUsers = users.filter((u: any) => u.roleCode === 'ZM' || u.role?.roleName === 'Zonal Manager');
- setZonalManagerMappings(zmUsers.map((u: any) => ({
+ const globalZmUsers = users.filter((u: any) => {
+ const code = (u.roleCode || '').toLowerCase();
+ const name = (u.role?.roleName || '').toLowerCase();
+ return code === 'zm' || name.includes('zonal manager') && !name.includes('head');
+ });
+
+ setZonalManagerMappings(globalZmUsers.map((u: any) => ({
id: u.id,
name: u.fullName,
code: u.employeeId || 'N/A',
email: u.email,
phone: u.mobileNumber || 'N/A',
- zoneId: u.zoneId || '',
- zoneName: u.zone?.zoneName || 'Not Assigned',
- regionId: u.regionId || '',
- regionName: u.region?.regionName || 'Not Assigned',
+ zoneId: u.location?.type === 'zone' ? u.location?.id : '',
+ zoneName: u.location?.type === 'zone' ? u.location?.name : 'Not Assigned',
+ regionId: u.location?.type === 'region' ? u.location?.id : '',
+ regionName: u.location?.type === 'region' ? u.location?.name : 'Not Assigned',
districts: u.districts || [],
status: (u.isActive !== false) ? 'Active' : 'Inactive'
})));
}
- if (bodyStates?.success) setAllStates(bodyStates.states || bodyStates.data || []);
- if (bodyDistricts?.success) setAllDistricts(bodyDistricts.districts || bodyDistricts.data || []);
- if (bodyAreas?.success) setAllAreas(bodyAreas.areas || bodyAreas.data || []);
+ let parsedStates: any[] = [];
+ if (bodyStates?.success) {
+ const rawStates = bodyStates.states || bodyStates.data || [];
+ parsedStates = rawStates.map((s: any) => ({
+ ...s,
+ stateName: s.name,
+ zoneId: getAncestorByType(s, 'zone')?.id || s.zoneId
+ }));
+ setAllStates(parsedStates);
+ }
+
+ let parsedDistricts: any[] = [];
+ if (bodyDistricts?.success) {
+ const rawDistricts = bodyDistricts.districts || bodyDistricts.data || [];
+ parsedDistricts = rawDistricts.map((d: any) => ({
+ ...d,
+ districtName: d.name,
+ stateId: getAncestorByType(d, 'state')?.id || d.stateId
+ }));
+ setAllDistricts(parsedDistricts);
+ }
+
+ if (bodyAreas?.success) {
+ const rawAreas = bodyAreas.areas || bodyAreas.data || [];
+ setAllAreas(rawAreas.map((a: any) => {
+ const districtId = getAncestorByType(a, 'district')?.id || a.districtId;
+ const districtObj = parsedDistricts.find(d => d.id === districtId);
+ const stateObj = parsedStates.find(s => s.id === districtObj?.stateId);
+
+ return {
+ ...a,
+ areaName: a.name,
+ districtId: districtId,
+ district: {
+ districtName: districtObj?.districtName || 'Unknown',
+ stateId: districtObj?.stateId,
+ state: {
+ stateName: stateObj?.stateName || 'Unknown'
+ }
+ }
+ };
+ }));
+ }
+
if (bodyEmail?.success) setEmailTemplates(bodyEmail.data || []);
if (bodyAsm?.success) {
const rawAsms = bodyAsm.data || bodyAsm.users || [];
- setAsms(rawAsms.map((u: any) => ({
- id: u.id,
- code: u.employeeId || 'N/A',
- asmCode: u.asmCode || '',
- employeeId: u.employeeId || '',
- name: u.fullName,
- zoneId: u.zoneId || '',
- regionId: u.regionId || '',
- zoneName: u.zone?.zoneName || 'Unassigned',
- regionName: u.region?.regionName || 'Unassigned',
- areasManaged: u.areaManagers ? u.areaManagers.map((am: any) => am.area?.areaName).filter(Boolean) : [],
- districtNames: u.areaManagers ? Array.from(new Set(u.areaManagers.map((am: any) => am.area?.district?.districtName).filter(Boolean))) : [],
- stateNames: u.areaManagers ? Array.from(new Set(u.areaManagers.map((am: any) => am.area?.state?.stateName).filter(Boolean))) : [],
- email: u.email,
- phone: u.mobileNumber,
- status: (u.isActive !== false) ? 'Active' : 'Inactive'
- })));
+ setAsms(rawAsms.map((u: any) => {
+ const location = u.location;
+ const zone = getAncestorByType(location, 'zone');
+ const region = getAncestorByType(location, 'region');
+ const district = getAncestorByType(location, 'district');
+ const state = getAncestorByType(location, 'state');
+ const areaNamesFromAreaManagers = Array.isArray(u.areaManagers)
+ ? u.areaManagers.map((am: any) => am.area?.areaName || am.area?.name).filter(Boolean)
+ : [];
+ const areaNamesFromUserRoles = Array.isArray(u.userRoles)
+ ? u.userRoles
+ .filter((ur: any) => ur?.role?.roleCode === 'ASM' && ur?.location?.type === 'area')
+ .map((ur: any) => ur?.location?.name)
+ .filter(Boolean)
+ : [];
+ const mergedAreaNames = Array.from(new Set([...areaNamesFromAreaManagers, ...areaNamesFromUserRoles]));
+ const assignmentAsmCode = Array.isArray(u.userRoles)
+ ? (u.userRoles.find((ur: any) =>
+ ur?.role?.roleCode === 'ASM' && (ur?.managerCode || '').trim() !== ''
+ )?.managerCode || '')
+ : '';
+
+ return {
+ id: u.id,
+ code: u.employeeId || 'N/A',
+ asmCode: u.asmCode || assignmentAsmCode || '',
+ employeeId: u.employeeId || '',
+ name: u.fullName,
+ zoneId: u.zoneId || zone?.id || '',
+ regionId: u.regionId || region?.id || '',
+ zoneName: u.zone?.zoneName || zone?.name || 'Unassigned',
+ regionName: u.region?.regionName || region?.name || 'Unassigned',
+ areasManaged: mergedAreaNames.length > 0
+ ? mergedAreaNames
+ : (location?.type === 'area' ? [location?.name] : []),
+ districtNames: u.areaManagers
+ ? Array.from(new Set(u.areaManagers.map((am: any) => am.area?.district?.districtName || am.area?.district?.name).filter(Boolean)))
+ : (district?.name ? [district?.name] : []),
+ stateNames: u.areaManagers
+ ? Array.from(new Set(u.areaManagers.map((am: any) =>
+ am.area?.state?.stateName ||
+ am.area?.state?.name ||
+ am.area?.district?.state?.stateName ||
+ am.area?.district?.state?.name
+ ).filter(Boolean)))
+ : (state?.name ? [state?.name] : []),
+ email: u.email,
+ phone: u.mobileNumber,
+ status: (u.isActive !== false) ? 'Active' : 'Inactive'
+ };
+ }));
}
if (bodySla?.success) {
@@ -760,10 +899,10 @@ export function MasterPage() {
if (editingZoneId) {
// Update existing zone
const updateData = {
- zoneName,
+ name: zoneName,
description: zoneDescription,
zonalBusinessHeadId: zbhId || null,
- stateIds // Send IDs to backend
+ childrenIds: stateIds
};
const res = (await masterService.updateZone(editingZoneId, updateData)) as { success: boolean; message?: string };
@@ -776,35 +915,22 @@ export function MasterPage() {
toast.error(res.message || 'Failed to update zone');
}
} else {
- // Create new zone (Mock implementation for now as create is not fully hooked up in this task)
- // Build zonal managers array
- const zonalManagers: ZonalManager[] = [];
- for (let i = 0; i < zonalManagersCount; i++) {
- if (zonalManagersData[i]) {
- zonalManagers.push(zonalManagersData[i]);
- }
- }
-
- // Create new zone object
- const newZone: Zone = {
- id: zoneCode.toLowerCase().replace(/\s+/g, '-'),
- code: zoneCode,
+ // Create new zone
+ const createData = {
name: zoneName,
- states: selectedZoneStates,
- zmCount: zonalManagersCount,
description: zoneDescription,
- zbh: {
- name: zbhName,
- email: zbhEmail,
- phone: zbhPhone
- },
- zonalManagers
+ zonalBusinessHeadId: zbhId || null,
+ childrenIds: stateIds
};
- // Add to zones array
- setZones([...zones, newZone]);
- toast.success('Zone saved successfully!');
- setShowZoneDialog(false);
+ const res = (await masterService.createZone(createData)) as { success: boolean; message?: string };
+ if (res.success) {
+ toast.success('Zone created successfully');
+ setShowZoneDialog(false);
+ fetchInitialData();
+ } else {
+ toast.error(res.message || 'Failed to create zone');
+ }
}
// Reset form (common)
@@ -865,11 +991,10 @@ export function MasterPage() {
.map(s => s.id);
const regionData = {
- zoneId: selectedRegionZone, // This is expected to be ID now
- regionCode,
- regionName,
+ parentIds: [selectedRegionZone], // This is expected to be array of IDs now
+ name: regionName,
description: regionDescription,
- stateIds,
+ childrenIds: stateIds,
regionalManagerId: regionalManagerId || null
};
@@ -951,7 +1076,8 @@ export function MasterPage() {
const handleSaveASM = async () => {
try {
- if (!asmManagerId) {
+ const targetAsmUserId = asmManagerId || editingASMId || '';
+ if (!targetAsmUserId) {
toast.error('Please select an Area Sales Manager');
return;
}
@@ -960,55 +1086,56 @@ export function MasterPage() {
toast.error('Please select at least one district');
return;
}
+ if (!selectedASMRegion && !selectedASMZone) {
+ toast.error('Please select zone and region');
+ return;
+ }
// Map selected district names to IDs
const selectedDistrictIds = allDistricts
.filter((d: District) => selectedASMDistricts.includes(d.districtName))
.map((d: District) => d.id);
-
- if (selectedDistrictIds.length === 0) {
- // Fallback if we can't match names (e.g. mock data mismatch)
- // But for now let's hope names match since they come from STATE_DISTRICTS_MAP which aligns with seed?
- // Actually STATE_DISTRICTS_MAP is hardcoded. backend seed might differ.
- // We warn if no match.
- console.warn('No matching district IDs found for names:', selectedASMDistricts);
- // Attempt to find areas by matching districtName in allAreas if linked?
- // But let's rely on allDistricts for now.
- }
-
- // Find all areas in these districts
+ const selectedStateIds = allStates
+ .filter((s: State) => selectedASMStates.includes(s.stateName))
+ .map((s: State) => s.id);
const targetAreas = allAreas.filter((a: Area) => a.districtId && selectedDistrictIds.includes(a.districtId));
-
if (targetAreas.length === 0) {
- console.warn('Debugging ASM Save:');
- console.warn('Selected District Names:', selectedASMDistricts);
- console.warn('All Districts Count:', allDistricts.length);
- console.warn('Matched District IDs:', selectedDistrictIds);
- console.warn('All Areas Count:', allAreas.length);
-
- const hasMissingDistricts = selectedASMDistricts.length > selectedDistrictIds.length;
-
- if (hasMissingDistricts) {
- toast.error('Some selected districts do not exist in the database. Please create them first from the "Locations" tab.');
- } else {
- toast.error('No Areas found in the selected districts. You must create Areas (Locations) in these districts before assigning an ASM.');
- }
+ toast.error('No areas found for selected districts');
return;
}
- // Assign manager to all found areas and update ASM Code
- // We process each area update
- const updatePromises = targetAreas.map((area: Area) => {
- console.log(`Updating area ${area.id} with managerId ${asmManagerId} and asmCode ${asmCode || 'null'}`);
- return ((masterService as unknown) as { updateArea: (id: string, data: { managerId: string; asmCode: string | null }) => Promise }).updateArea(area.id, {
- managerId: asmManagerId,
- asmCode: asmCode || null
- });
+ const asmAssignments = targetAreas.map((area: Area, index: number) => ({
+ roleCode: 'ASM',
+ locationId: area.id,
+ managerCode: asmCode || null,
+ isPrimary: index === 0,
+ isActive: asmStatus === 'active'
+ }));
+
+ if (selectedDistrictIds.length === 0) {
+ toast.error('Selected districts are not mapped in database');
+ return;
+ }
+
+ // Update ASM user mapping itself (not area reporting manager)
+ const updateRes = await adminService.updateUser(targetAsmUserId, {
+ locationId: selectedASMRegion || selectedASMZone || null,
+ status: asmStatus,
+ isActive: asmStatus === 'active',
+ asmCode: asmCode || null,
+ zoneId: selectedASMZone || null,
+ regionId: selectedASMRegion || null,
+ stateIds: selectedStateIds,
+ districtIds: selectedDistrictIds,
+ assignments: asmAssignments
});
- await Promise.all(updatePromises);
+ if (!updateRes?.success) {
+ toast.error(updateRes?.message || 'Failed to update ASM mapping');
+ return;
+ }
- toast.success(`ASM assigned to ${targetAreas.length} areas successfully!`);
+ toast.success('ASM mapping updated successfully');
setShowASMDialog(false);
setEditingASMId(null);
setAsmStatus('active');
@@ -1028,6 +1155,31 @@ export function MasterPage() {
}
};
+ const getDistrictsForSelectedState = (stateName: string): string[] => {
+ const stateIds = allStates
+ .filter((s) => s.stateName === stateName || s.name === stateName)
+ .map((s) => s.id);
+
+ if (stateIds.length === 0) return [];
+
+ return allDistricts
+ .filter((d) => stateIds.includes(d.stateId || d.state?.id))
+ .map((d) => d.districtName);
+ };
+
+ const filteredASMUsers = userAssignedData.filter((u) => {
+ const code = (u.roleCode || '').toLowerCase();
+ const name = (u.role || '').toLowerCase();
+ const isAsm = code === 'asm' || name.includes('area sales manager') || name.includes('asm');
+ if (!isAsm) return false;
+
+ if (editingASMId && u.id === editingASMId) return true;
+ if (selectedASMRegion && u.regionId) return u.regionId === selectedASMRegion;
+ if (selectedASMZone && u.zoneId) return u.zoneId === selectedASMZone;
+
+ return true;
+ });
+
const handleEditRole = (role: Role) => {
setSelectedRoleForEdit(role);
@@ -1105,7 +1257,7 @@ export function MasterPage() {
) : (
-
+
Organisation
@@ -1126,6 +1278,10 @@ export function MasterPage() {
Locations
+
+
+ Approval Policies
+
{/* Regional Hierarchy Tab */}
@@ -1915,7 +2071,7 @@ export function MasterPage() {