909 lines
36 KiB
TypeScript
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;
|
|
|