Dealer_Onboarding_Backend/src/modules/master/master.controller.ts

909 lines
36 KiB
TypeScript

import { Request, Response } from 'express';
import { Op } from 'sequelize';
import { syncLocationManagers, syncRegionManager, syncZoneManager } from './syncHierarchy.service.js';
import db from '../../database/models/index.js';
const { User } = db;
// --- Areas (Granular Locations) ---
export const getAreas = async (req: Request, res: Response) => {
try {
let search = req.query.search as string;
let page = (req.query.page || 1) as any;
let limit = (req.query.limit || 10) as any;
const checkNested = (obj: any) => {
if (!obj || typeof obj !== 'object') return;
if (!search && obj.search) search = obj.search;
if (page === 1 && obj.page) page = obj.page;
if (limit === 10 && obj.limit) limit = obj.limit;
};
checkNested(req.query.params);
const isAll = limit === 'all' || limit === -1 || limit === '-1';
page = Number(page || 1);
limit = isAll ? null : Number(limit || 10);
const offset = isAll ? null : (page - 1) * limit;
const where: any = {};
if (search) {
where[Op.or] = [
{ name: { [Op.iLike]: `%${search}%` } },
{ city: { [Op.iLike]: `%${search}%` } },
{ '$district.name$': { [Op.iLike]: `%${search}%` } }
];
}
const { count, rows: areas } = await db.Location.findAndCountAll({
where,
include: [
{
model: db.District,
as: 'district',
include: [
{ model: db.Zone, as: 'zone', attributes: ['name'] },
{ model: db.Region, as: 'region', attributes: ['name'] },
{ model: db.State, as: 'state', attributes: ['name'] }
]
}
],
order: [['name', 'ASC']],
limit: limit === null ? undefined : Number(limit),
offset: offset === null ? undefined : Number(offset),
distinct: true,
subQuery: false
});
const result = areas.map((a: any) => {
const d = a.district || {};
return {
...a.toJSON(),
districtName: d.name || 'N/A',
zoneName: d.zone?.name || 'UNKNOWN',
regionName: d.region?.name || 'UNKNOWN',
stateName: d.state?.name || 'UNKNOWN'
};
});
res.json({
success: true,
data: result,
pagination: {
total: count,
page: Number(page),
limit: isAll ? count : Number(limit),
totalPages: isAll ? 1 : Math.ceil(count / Number(limit))
}
});
} catch (error) {
console.error('Get areas error:', error);
res.status(500).json({ success: false, message: 'Error fetching areas' });
}
};
// --- Districts (Territory Entities) ---
export const getDistricts = async (req: Request, res: Response) => {
try {
let search = req.query.search as string;
let limit = (req.query.limit || 10) as any;
const stateId = req.query.stateId as string;
const zoneId = req.query.zoneId as string;
const regionId = req.query.regionId as string;
const isAll = limit === 'all' || limit === -1 || limit === '-1';
const where: any = {};
if (search) {
where.name = { [Op.iLike]: `%${search}%` };
}
if (stateId) where.stateId = stateId;
if (zoneId) where.zoneId = zoneId;
if (regionId) where.regionId = regionId;
const { count, rows: districts } = await db.District.findAndCountAll({
where,
include: [
{ model: db.Zone, as: 'zone', attributes: ['id', 'name'] },
{ model: db.Region, as: 'region', attributes: ['id', 'name'] },
{ model: db.State, as: 'state', attributes: ['id', 'name'] },
{ model: db.User, as: 'asm', attributes: ['id', 'fullName', 'email', 'employeeId'] },
{ model: db.User, as: 'zonalManager', attributes: ['id', 'fullName', 'email', 'employeeId'] }
],
order: [['name', 'ASC']],
limit: isAll ? undefined : Number(limit),
distinct: true
});
const result = districts.map((d: any) => ({
...d.toJSON(),
zoneName: d.zone?.name || 'UNKNOWN',
regionName: d.region?.name || 'UNKNOWN',
stateName: d.state?.name || 'UNKNOWN',
asmName: d.asm?.fullName || 'UNASSIGNED',
zmName: d.zonalManager?.fullName || 'UNASSIGNED'
}));
res.json({ success: true, data: result, total: count });
} catch (error) {
console.error('Get districts error:', error);
res.status(500).json({ success: false, message: 'Error fetching districts' });
}
};
export const createDistrict = async (req: Request, res: Response) => {
try {
const { name, code, stateName, city, openFrom, openTo, status, isActive, description } = req.body;
if (!name) return res.status(400).json({ success: false, message: 'District name is required' });
// Find or Create state if stateName provided
let stateId = req.body.stateId;
if (stateName && !stateId) {
const [state] = await db.State.findOrCreate({
where: { name: stateName },
defaults: { name: stateName }
});
stateId = state.id;
}
const district = await db.District.create({
name,
code,
stateId,
isActive: isActive !== undefined ? isActive : true
});
const area = await db.Location.create({
name,
districtId: district.id,
city: city || name,
isActive: true,
openFrom: openFrom || null,
openTo: openTo || null,
description: description || null
});
// Create associated Opportunity for "Active Period" and "City"
await db.Opportunity.create({
districtId: district.id,
areaId: area.id,
city: city || name,
openFrom: openFrom || null,
openTo: openTo || null,
status: status || 'inactive',
opportunityType: 'New Dealership',
capacity: 'Standard',
priority: 'Medium'
});
res.status(201).json({ success: true, data: area });
} catch (error) {
console.error('Create area error:', error);
res.status(500).json({ success: false, message: 'Error creating area' });
}
};
// --- Regions ---
export const getRegions = async (req: Request, res: Response) => {
try {
const regions = await db.Region.findAll({
include: [
{ model: db.Zone, as: 'zone', attributes: ['name'] },
{
model: db.District,
as: 'districts',
attributes: ['id', 'name', 'stateId'],
include: [{ model: db.State, as: 'state', attributes: ['id', 'name'] }]
}
],
order: [['name', 'ASC']]
});
const roles = await db.Role.findAll({
where: {
roleCode: { [db.Sequelize.Op.in]: ['ASM', 'RM', 'RBM'] }
}
});
const asmRoleIds = roles.filter((r: any) => r.roleCode === 'ASM').map((r: any) => r.id);
const rmRoleIds = roles.filter((r: any) => ['RM', 'RBM'].includes(r.roleCode)).map((r: any) => r.id);
const result = await Promise.all(regions.map(async (region: any) => {
const regionJson = region.toJSON();
regionJson.zoneName = region.zone?.name || 'UNKNOWN';
const districtIds = (region.districts || []).map((d: any) => d.id);
const [asmCount, rmCount, rmAssignment] = await Promise.all([
db.UserRole.count({ where: { roleId: { [db.Sequelize.Op.in]: asmRoleIds }, districtId: { [db.Sequelize.Op.in]: districtIds }, isActive: true }, distinct: true, col: 'userId' }),
db.UserRole.count({ where: { roleId: { [db.Sequelize.Op.in]: rmRoleIds }, regionId: region.id, isActive: true }, distinct: true, col: 'userId' }),
db.UserRole.findOne({
where: { roleId: { [db.Sequelize.Op.in]: rmRoleIds }, regionId: region.id, isActive: true },
include: [{ model: db.User, as: 'user' }]
})
]);
regionJson.asmCount = asmCount;
regionJson.regionalOfficerCount = rmCount;
regionJson.regionalManager = rmAssignment?.user || null;
regionJson.rbmCode = region.rbmCode || 'N/A';
// Extract unique states for this region
const statesMap = new Map();
(region.districts || []).forEach((d: any) => {
if (d.state) {
statesMap.set(d.state.id, d.state.name);
}
});
regionJson.states = Array.from(statesMap.values());
regionJson.districts = (region.districts || []).map((d: any) => ({
id: d.id,
name: d.name.toUpperCase(),
stateId: d.stateId
}));
return regionJson;
}));
res.json({ success: true, data: result });
} catch (error) {
console.error('Get regions error:', error);
res.status(500).json({ success: false, message: 'Error fetching regions' });
}
};
export const createRegion = async (req: Request, res: Response) => {
try {
const { name, code, parentId, zoneId, managerId, districts, districtIds } = req.body;
const targetZoneId = zoneId || parentId;
if (!name) return res.status(400).json({ success: false, message: 'Region name is required' });
const region = await db.Region.create({ name, code, zoneId: targetZoneId });
// 1. Assign Manager
if (managerId) {
const rmRole = await db.Role.findOne({ where: { roleCode: 'RM' } });
if (rmRole) {
await db.UserRole.update({ isActive: false }, { where: { regionId: region.id, roleId: rmRole.id } });
await db.UserRole.create({
userId: managerId,
roleId: rmRole.id,
regionId: region.id,
zoneId: targetZoneId,
isActive: true,
isPrimary: true
});
}
}
// 2. Assign Districts (with conflict check)
const targetDistrictIds = districts || districtIds;
if (Array.isArray(targetDistrictIds) && targetDistrictIds.length > 0) {
const conflicts = await db.District.findAll({
where: {
id: { [db.Sequelize.Op.in]: targetDistrictIds },
regionId: { [db.Sequelize.Op.and]: [{ [db.Sequelize.Op.ne]: null }, { [db.Sequelize.Op.ne]: region.id }] }
}
});
if (conflicts.length > 0) {
return res.status(409).json({
success: false,
message: `Districts already assigned to another region: ${conflicts.map((c: any) => c.name).join(', ')}`
});
}
await db.District.update(
{ regionId: region.id, zoneId: targetZoneId },
{ where: { id: { [db.Sequelize.Op.in]: targetDistrictIds } } }
);
}
res.status(201).json({ success: true, message: 'Region created', data: region });
} catch (error: any) {
console.error('Create region error:', error);
if (error.name === 'SequelizeUniqueConstraintError') {
const field = error.errors?.[0]?.path || 'field';
const value = error.errors?.[0]?.value || '';
return res.status(409).json({
success: false,
message: `A region with this ${field} "${value}" already exists. Please use a different name.`
});
}
res.status(500).json({ success: false, message: 'Error creating region' });
}
};
export const updateRegion = async (req: Request, res: Response) => {
try {
const { id } = req.params;
const { name, code, parentId, zoneId, managerId, districts, districtIds } = req.body;
const targetZoneId = zoneId || parentId;
const region = await db.Region.findByPk(id);
if (!region) return res.status(404).json({ success: false, message: 'Region not found' });
await region.update({
name,
code,
zoneId: targetZoneId || region.zoneId
});
// 1. Update Manager
if (managerId) {
const rmRole = await db.Role.findOne({ where: { roleCode: 'RM' } });
if (rmRole) {
// Deactivate old RMs for this region
await db.UserRole.update({ isActive: false }, {
where: { regionId: id, roleId: rmRole.id }
});
// Assign new RM
await db.UserRole.create({
userId: managerId,
roleId: rmRole.id,
regionId: id,
zoneId: region.zoneId,
isActive: true,
isPrimary: true
});
}
}
// 2. Update Districts (with conflict check)
const targetDistrictIds = districts || districtIds;
if (Array.isArray(targetDistrictIds)) {
if (targetDistrictIds.length > 0) {
const conflicts = await db.District.findAll({
where: {
id: { [db.Sequelize.Op.in]: targetDistrictIds },
regionId: { [db.Sequelize.Op.and]: [{ [db.Sequelize.Op.ne]: null }, { [db.Sequelize.Op.ne]: id }] }
}
});
if (conflicts.length > 0) {
return res.status(409).json({
success: false,
message: `Districts already assigned to another region: ${conflicts.map((c: any) => c.name).join(', ')}`
});
}
}
await db.District.update({ regionId: null }, { where: { regionId: id } });
if (targetDistrictIds.length > 0) {
await db.District.update(
{ regionId: id, zoneId: region.zoneId },
{ where: { id: { [db.Sequelize.Op.in]: targetDistrictIds } } }
);
}
}
res.json({ success: true, message: 'Region updated' });
} catch (error: any) {
console.error('Update region error:', error);
if (error.name === 'SequelizeUniqueConstraintError') {
const field = error.errors?.[0]?.path || 'field';
const value = error.errors?.[0]?.value || '';
return res.status(409).json({
success: false,
message: `A region with this ${field} "${value}" already exists. Please use a different name.`
});
}
res.status(500).json({ success: false, message: 'Error updating region' });
}
};
// --- Zones ---
export const getZones = async (req: Request, res: Response) => {
try {
const zones = await db.Zone.findAll({
include: [
{ model: db.Region, as: 'regions', attributes: ['id', 'name'] },
{ model: db.State, as: 'states', attributes: ['id', 'name'] },
{ model: db.District, as: 'districts', attributes: ['id'] }
],
order: [['name', 'ASC']]
});
const roles = await db.Role.findAll({
where: { roleCode: { [db.Sequelize.Op.in]: ['ZBH', 'DD-ZM'] } }
});
const zbhRoleIds = roles.filter((r: any) => r.roleCode === 'ZBH').map((r: any) => r.id);
const zmRoleIds = roles.filter((r: any) => r.roleCode === 'DD-ZM').map((r: any) => r.id);
const result = await Promise.all(zones.map(async (zone: any) => {
const zoneJson = zone.toJSON();
const [zbhAssignment, zms] = await Promise.all([
db.UserRole.findOne({
where: { roleId: { [db.Sequelize.Op.in]: zbhRoleIds }, zoneId: zone.id, isPrimary: true, isActive: true },
include: [{ model: db.User, as: 'user' }]
}),
db.UserRole.findAll({
where: { roleId: { [db.Sequelize.Op.in]: zmRoleIds }, zoneId: zone.id, isActive: true },
include: [{ model: db.User, as: 'user' }]
})
]);
// For each ZM, fetch their assigned districts in this zone
const zonalManagers = await Promise.all(zms.map(async (zmRole: any) => {
const districts = await db.District.findAll({
where: { zmId: zmRole.user.id, zoneId: zone.id }, // ZMs usually managed regions or specific district sets
attributes: ['name']
});
return {
id: zmRole.user.id,
name: zmRole.user.fullName || zmRole.user.name,
email: zmRole.user.email,
phone: zmRole.user.mobileNumber || 'N/A',
code: zmRole.managerCode || zmRole.user.employeeId || 'N/A',
districts: districts.map((d: any) => d.name)
};
}));
zoneJson.regionCount = (zone.regions || []).length;
zoneJson.districtCount = (zone.districts || []).length;
zoneJson.states = (zone.states || []).map((s: any) => s.name.toUpperCase());
zoneJson.zmCount = zms.length;
zoneJson.zonalBusinessHead = zbhAssignment ? {
id: zbhAssignment.user.id,
name: zbhAssignment.user.fullName || zbhAssignment.user.name,
email: zbhAssignment.user.email,
phone: zbhAssignment.user.mobileNumber || 'N/A',
code: zone.zbhCode || 'N/A'
} : null;
zoneJson.zonalManagers = zonalManagers;
return zoneJson;
}));
res.json({ success: true, data: result });
} catch (error) {
console.error('Get zones error:', error);
res.status(500).json({ success: false, message: 'Error fetching zones' });
}
};
export const createZone = async (req: Request, res: Response) => {
try {
const { name, code, stateIds, managerId } = req.body;
if (!name) return res.status(400).json({ success: false, message: 'Zone name is required' });
const zone = await db.Zone.create({ name, code });
// 1. Assign ZBH
if (managerId) {
const zbhRole = await db.Role.findOne({ where: { roleCode: 'ZBH' } });
if (zbhRole) {
await db.UserRole.update({ isActive: false }, { where: { zoneId: zone.id, roleId: zbhRole.id } });
await db.UserRole.create({
userId: managerId,
roleId: zbhRole.id,
zoneId: zone.id,
isActive: true,
isPrimary: true
});
}
}
if (Array.isArray(stateIds) && stateIds.length > 0) {
await db.State.update({ zoneId: zone.id }, { where: { id: { [db.Sequelize.Op.in]: stateIds } } });
await db.District.update({ zoneId: zone.id }, { where: { stateId: { [db.Sequelize.Op.in]: stateIds } } });
}
res.status(201).json({ success: true, message: 'Zone created', data: zone });
} catch (error) {
console.error('Create zone error:', error);
res.status(500).json({ success: false, message: 'Error creating zone' });
}
};
export const updateZone = async (req: Request, res: Response) => {
try {
const { id } = req.params;
const { name, code, stateIds, managerId } = req.body;
const zone = await db.Zone.findByPk(id);
if (!zone) return res.status(404).json({ success: false, message: 'Zone not found' });
await zone.update({ name, code });
// 1. Update ZBH
if (managerId) {
const zbhRole = await db.Role.findOne({ where: { roleCode: 'ZBH' } });
if (zbhRole) {
// Deactivate old ZBHs for this zone
await db.UserRole.update({ isActive: false }, {
where: { zoneId: id, roleId: zbhRole.id }
});
// Assign new ZBH
await db.UserRole.create({
userId: managerId,
roleId: zbhRole.id,
zoneId: id,
isActive: true,
isPrimary: true
});
}
}
if (Array.isArray(stateIds)) {
await db.State.update({ zoneId: null }, { where: { zoneId: id } });
await db.District.update({ zoneId: null }, { where: { zoneId: id } });
if (stateIds.length > 0) {
await db.State.update({ zoneId: id }, { where: { id: { [db.Sequelize.Op.in]: stateIds } } });
await db.District.update({ zoneId: id }, { where: { stateId: { [db.Sequelize.Op.in]: stateIds } } });
}
}
res.json({ success: true, message: 'Zone updated' });
} catch (error) {
console.error('Update zone error:', error);
res.status(500).json({ success: false, message: 'Error updating zone' });
}
};
// --- States ---
export const getStates = async (req: Request, res: Response) => {
try {
const states = await db.State.findAll({
include: [{ model: db.Zone, as: 'zone', attributes: ['name'] }],
order: [['name', 'ASC']]
});
res.json({ success: true, data: states });
} catch (error) {
console.error('Get states error:', error);
res.status(500).json({ success: false, message: 'Error fetching states' });
}
};
export const createState = async (req: Request, res: Response) => {
try {
const { name, zoneId } = req.body;
if (!name) return res.status(400).json({ success: false, message: 'State name is required' });
const state = await db.State.create({ name, zoneId });
if (zoneId) {
await db.District.update({ zoneId }, { where: { stateId: state.id } });
}
res.status(201).json({ success: true, data: state });
} catch (error) {
console.error('Create state error:', error);
res.status(500).json({ success: false, message: 'Error creating state' });
}
};
// --- Managers ---
export const getManagersByRole = async (req: Request, res: Response) => {
try {
const { roleCode, locationId } = req.query as any;
const managers = await User.findAll({
attributes: ['id', 'fullName', 'email', 'mobileNumber', 'roleCode'],
include: [
{
model: db.UserRole,
as: 'userRoles',
include: [{ model: db.Role, as: 'role' }]
}
]
});
const filteredManagers = managers.filter((m: any) => {
const hasRole = !roleCode || m.roleCode === roleCode || (m.userRoles || []).some((a: any) => a.role?.roleCode === roleCode);
const hasLocation = !locationId || (m.userRoles || []).some((a: any) =>
a.districtId === locationId ||
a.zoneId === locationId ||
a.regionId === locationId
);
return hasRole && hasLocation;
});
res.json({ success: true, data: filteredManagers });
} catch (error) {
console.error('Get managers error:', error);
res.status(500).json({ success: false, message: 'Error fetching managers' });
}
};
export const getAreaManagers = async (req: Request, res: Response) => {
req.query.roleCode = 'ASM';
return getManagersByRole(req, res);
};
// --- Delete Area (Location) ---
export const deleteLocation = async (req: Request, res: Response) => {
try {
const { id } = req.params;
const area = await db.Location.findByPk(id);
if (!area) return res.status(404).json({ success: false, message: 'Area not found' });
// Delete associated opportunities if they belong to this granular location
await db.Opportunity.destroy({ where: { areaId: id } });
// Delete the location itself
await area.destroy();
res.json({ success: true, message: 'Area deleted successfully' });
} catch (error) {
console.error('Delete area error:', error);
res.status(500).json({ success: false, message: 'Error deleting area' });
}
};
export const updateLocation = async (req: Request, res: Response) => {
try {
const { id } = req.params; // This is the Area ID
const { name, code, stateName, city, openFrom, openTo, status, isActive, description } = req.body;
const area = await db.Location.findByPk(id, {
include: [{ model: db.District, as: 'district' }]
});
if (!area) return res.status(404).json({ success: false, message: 'Area not found' });
const district = area.district;
// 1. Update District
if (district) {
let stateId = req.body.stateId;
if (stateName && !stateId) {
const [state] = await db.State.findOrCreate({
where: { name: stateName },
defaults: { name: stateName }
});
stateId = state.id;
}
await district.update({
name: name || district.name,
code: code || district.code,
stateId: stateId || district.stateId,
isActive: isActive !== undefined ? isActive : district.isActive
});
}
// 2. Update Area
await area.update({
name: name || area.name,
city: city || area.city,
isActive: isActive !== undefined ? isActive : area.isActive,
openFrom: openFrom !== undefined ? (openFrom || null) : area.openFrom,
openTo: openTo !== undefined ? (openTo || null) : area.openTo,
description: description || area.description
});
// 3. Update or Create associated Opportunity
const [opportunity] = await db.Opportunity.findOrBuild({
where: { areaId: id }
});
opportunity.set({
districtId: district?.id || opportunity.districtId,
city: city || opportunity.city || name || area.name,
openFrom: openFrom !== undefined ? (openFrom || null) : opportunity.openFrom,
openTo: openTo !== undefined ? (openTo || null) : opportunity.openTo,
status: status || opportunity.status || 'inactive'
});
await opportunity.save();
res.json({ success: true, message: 'Area updated' });
if (district && typeof district.id === 'string') {
await syncLocationManagers(district.id);
}
} catch (error) {
console.error('Update area error:', error);
res.status(500).json({ success: false, message: 'Error updating area' });
}
};
// --- Managers ---
export const getASMs = async (req: Request, res: Response) => {
try {
const asms = await db.User.findAll({
where: {
roleCode: { [db.Sequelize.Op.in]: ['ASM', 'AREA SALES MANAGER'] },
isActive: true
},
include: [
{
model: db.UserRole,
as: 'userRoles',
where: { isActive: true },
required: false,
include: [{ model: db.Role, as: 'role', where: { roleCode: 'ASM' } }]
},
{
model: db.District,
as: 'managedAsmDistricts',
include: [
{ model: db.State, as: 'state', attributes: ['id', 'name'] },
{ model: db.Region, as: 'region', attributes: ['id', 'name'] },
{ model: db.Zone, as: 'zone', attributes: ['id', 'name'] }
]
}
],
order: [['fullName', 'ASC']]
});
const result = (asms || []).map((u: any) => {
const districts = u.managedAsmDistricts || [];
const asmRoleAssignment = (u.userRoles || []).find((r: any) => r.role?.roleCode === 'ASM');
const asmCode = asmRoleAssignment?.managerCode || u.employeeId;
const zoneSet = new Set();
const regionSet = new Set();
const stateSet = new Set();
const territoryInfo = districts.map((d: any) => {
if (d.zone) zoneSet.add(JSON.stringify({ id: d.zone.id, name: d.zone.name }));
if (d.region) regionSet.add(JSON.stringify({ id: d.region.id, name: d.region.name }));
if (d.state) stateSet.add(d.state.name);
return {
id: d.id,
name: d.name,
stateId: d.stateId,
regionId: d.regionId,
zoneId: d.zoneId
};
});
const zones = Array.from(zoneSet).map((s: any) => JSON.parse(s));
const regions = Array.from(regionSet).map((s: any) => JSON.parse(s));
return {
id: u.id,
name: u.fullName,
email: u.email,
phone: u.mobileNumber,
employeeId: u.employeeId,
asmCode: asmCode || 'N/A',
status: u.status,
zoneId: zones[0]?.id || '',
zoneName: zones[0]?.name || 'Unassigned',
regionId: regions[0]?.id || '',
regionName: regions[0]?.name || 'Unassigned',
areasManaged: territoryInfo,
stateNames: Array.from(stateSet),
totalDistricts: territoryInfo.length
};
});
res.json({ success: true, data: result });
} catch (error) {
console.error('Get ASMs error:', error);
res.status(500).json({ success: false, message: 'Error fetching ASMs' });
}
};
export const getZonalManagers = async (req: Request, res: Response) => {
try {
const zms = await db.User.findAll({
attributes: ['id', 'fullName', 'email', 'employeeId', 'status'],
include: [
{
model: db.UserRole,
as: 'userRoles',
where: { isActive: true },
required: true,
include: [{
model: db.Role,
as: 'role',
where: { roleCode: { [Op.in]: ['ZM', 'DD-ZM', 'ZBH'] } }
}]
},
{
model: db.District,
as: 'managedZmDistricts',
include: [
{ model: db.Zone, as: 'zone', attributes: ['id', 'name'] },
{ model: db.Region, as: 'region', attributes: ['id', 'name'] },
{ model: db.State, as: 'state', attributes: ['id', 'name'] }
]
}
],
order: [['fullName', 'ASC']]
});
const result = (zms || []).map((u: any) => {
const rolePriority = ['DD-ZM', 'ZM', 'ZBH'];
const roleAssignment = (u.userRoles || []).sort((a: any, b: any) => {
const aIndex = rolePriority.indexOf(a.role?.roleCode || '');
const bIndex = rolePriority.indexOf(b.role?.roleCode || '');
if (aIndex !== bIndex) return aIndex - bIndex;
// If same role type, prefer the one with a code
if (a.managerCode && !b.managerCode) return -1;
if (!a.managerCode && b.managerCode) return 1;
return 0;
})[0];
const zmCode = roleAssignment?.managerCode || u.employeeId || 'N/A';
// Collect unique zones and states
const zoneSet = new Set<string>();
const stateSet = new Set<string>();
let inferredZoneId = roleAssignment?.zoneId || null;
(u.managedZmDistricts || []).forEach((d: any) => {
if (d.zone) {
zoneSet.add(d.zone.name);
if (!inferredZoneId) inferredZoneId = d.zone.id; // Fallback to first district's zone if role zone is missing
}
if (d.state) stateSet.add(d.state.name);
});
return {
id: u.id,
name: u.fullName,
email: u.email,
employeeId: u.employeeId,
zmCode: zmCode,
status: u.status,
zoneId: inferredZoneId,
zones: Array.from(zoneSet).length > 0 ? Array.from(zoneSet) : ["Assigned Zone"],
stateNames: Array.from(stateSet),
districts: (u.managedZmDistricts || []).map((d: any) => ({
id: d.id,
name: d.name,
state: d.state?.name
}))
};
});
res.json({ success: true, data: result });
} catch (error) {
console.error('Get ZMs error:', error);
res.status(500).json({ success: false, message: 'Error fetching Zonal Managers' });
}
};
export const saveZM = async (req: Request, res: Response) => {
try {
const { userId, zmCode, zoneId, districts, status } = req.body;
if (!userId) return res.status(400).json({ success: false, message: 'userId is required' });
// Find the ZM role (DD-ZM)
const zmRole = await db.Role.findOne({ where: { roleCode: 'DD-ZM' } });
if (!zmRole) return res.status(404).json({ success: false, message: 'ZM role (DD-ZM) not found in roles table' });
// Update User status if provided
if (status) {
await db.User.update({ status }, { where: { id: userId } });
}
// Deactivate existing ZM role assignments for this user
await db.UserRole.update({ isActive: false }, {
where: { userId, roleId: zmRole.id }
});
// Create new active UserRole with managerCode = zmCode
await db.UserRole.create({
userId,
roleId: zmRole.id,
zoneId: zoneId || null,
managerCode: zmCode || null,
isActive: true,
isPrimary: true
});
// Assign districts to this user if provided
// First, clear this ZM from any other districts they might have had
await db.District.update({ zmId: null, zmCode: null }, { where: { zmId: userId } });
if (Array.isArray(districts) && districts.length > 0) {
// Then assign new ones
const updateProps: any = {
zmId: userId,
zmCode: zmCode || null
};
if (zoneId) updateProps.zoneId = zoneId;
await db.District.update(
updateProps,
{ where: { id: { [db.Sequelize.Op.in]: districts } } }
);
}
res.json({ success: true, message: 'Zonal Manager saved successfully' });
} catch (error) {
console.error('Save ZM error:', error);
res.status(500).json({ success: false, message: 'Error saving Zonal Manager' });
}
};
export const createArea = createDistrict;
export const deleteArea = deleteLocation;
export const createDistrictLegacy = createDistrict;