572 lines
22 KiB
TypeScript
572 lines
22 KiB
TypeScript
import { Request, Response } from 'express';
|
|
import db from '../../database/models/index.js';
|
|
const { Region, Zone, State, District, Area, User, AreaManager } = db;
|
|
|
|
// --- Regions ---
|
|
export const getRegions = async (req: Request, res: Response) => {
|
|
try {
|
|
const regions = await Region.findAll({
|
|
include: [
|
|
{
|
|
model: State,
|
|
as: 'states',
|
|
attributes: ['id', 'stateName']
|
|
},
|
|
{
|
|
model: Zone,
|
|
as: 'zone',
|
|
attributes: ['id', 'zoneName']
|
|
},
|
|
{
|
|
model: User,
|
|
as: 'regionalManager',
|
|
attributes: ['id', 'fullName', 'email', 'mobileNumber']
|
|
}
|
|
],
|
|
order: [['regionName', 'ASC']]
|
|
});
|
|
|
|
res.json({ success: true, data: regions });
|
|
} 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 { zoneId, regionCode, regionName, description, stateIds, regionalManagerId } = req.body;
|
|
|
|
if (!zoneId || !regionName || !regionCode) {
|
|
return res.status(400).json({ success: false, message: 'Zone ID, region name and code are required' });
|
|
}
|
|
|
|
const region = await Region.create({
|
|
zoneId,
|
|
regionCode,
|
|
regionName,
|
|
description,
|
|
regionalManagerId: regionalManagerId || null
|
|
});
|
|
|
|
// Assign states if provided
|
|
if (stateIds && Array.isArray(stateIds) && stateIds.length > 0) {
|
|
await State.update(
|
|
{ regionId: region.id, zoneId }, // Also ensure State belongs to the Zone (hierarchy)
|
|
{ where: { id: stateIds } }
|
|
);
|
|
}
|
|
|
|
res.status(201).json({ success: true, message: 'Region created successfully', data: region });
|
|
} catch (error) {
|
|
console.error('Create region error:', error);
|
|
res.status(500).json({ success: false, message: 'Error creating region' });
|
|
}
|
|
};
|
|
|
|
export const updateRegion = async (req: Request, res: Response) => {
|
|
try {
|
|
const { id } = req.params;
|
|
const { zoneId, regionCode, regionName, description, isActive, stateIds, regionalManagerId } = req.body;
|
|
|
|
const region = await Region.findByPk(id);
|
|
if (!region) {
|
|
return res.status(404).json({ success: false, message: 'Region not found' });
|
|
}
|
|
|
|
const updates: any = {};
|
|
if (zoneId) updates.zoneId = zoneId;
|
|
if (regionCode) updates.regionCode = regionCode;
|
|
if (regionName) updates.regionName = regionName;
|
|
if (description !== undefined) updates.description = description;
|
|
if (isActive !== undefined) updates.isActive = isActive;
|
|
if (regionalManagerId !== undefined) updates.regionalManagerId = regionalManagerId;
|
|
|
|
await region.update(updates);
|
|
|
|
// Handle State reassignment
|
|
if (stateIds && Array.isArray(stateIds)) {
|
|
// 1. Unassign states currently assigned to this region but NOT in the new list?
|
|
// Or just simpler: Assign the new ones. Old ones stay?
|
|
// Standard behavior for "List of items in a container": Sync list.
|
|
// We should set regionId=null for states previously in this region but not in stateIds.
|
|
// But let's check safety. If I uncheck a state, I want it removed from the region.
|
|
|
|
// First, find states currently in this region
|
|
// Actually, simplest 'Reset and Set' approach:
|
|
// 1. Set regionId=null for all states where regionId = this.id
|
|
// 2. Set regionId=this.id for states in stateIds.
|
|
|
|
// Note: We should probably also enforce zoneId match?
|
|
// If a user moves a state to this Region, the State must conceptually belong to the Region's Zone.
|
|
// So we update both regionId and zoneId for the target states.
|
|
|
|
// Step 1: Remove States from this Region (if they are NOT in the new list)
|
|
// We can do this by:
|
|
// await State.update({ regionId: null }, { where: { regionId: id } });
|
|
// But wait, if I am only ADDING, I don't want to nuke everything.
|
|
// But "update" implies "this is the new state of the world".
|
|
// Assuming frontend sends the FULL list of selected states.
|
|
|
|
await State.update({ regionId: null }, { where: { regionId: id } });
|
|
|
|
if (stateIds.length > 0) {
|
|
await State.update(
|
|
{
|
|
regionId: id,
|
|
zoneId: zoneId || region.zoneId // Ensure state moves to the region's zone
|
|
},
|
|
{ where: { id: stateIds } }
|
|
);
|
|
}
|
|
}
|
|
|
|
res.json({ success: true, message: 'Region updated successfully' });
|
|
} catch (error) {
|
|
console.error('Update region error:', error);
|
|
res.status(500).json({ success: false, message: 'Error updating region' });
|
|
}
|
|
};
|
|
|
|
// --- Zones ---
|
|
export const getZones = async (req: Request, res: Response) => {
|
|
try {
|
|
const { regionId } = req.query as { regionId?: string };
|
|
|
|
const where: any = {};
|
|
if (regionId) {
|
|
where.regionId = regionId;
|
|
}
|
|
|
|
const zones = await Zone.findAll({
|
|
where,
|
|
include: [
|
|
{
|
|
model: Region,
|
|
as: 'regions',
|
|
attributes: ['regionName']
|
|
},
|
|
{
|
|
model: User,
|
|
as: 'zonalBusinessHead',
|
|
attributes: ['fullName', 'email', 'mobileNumber']
|
|
},
|
|
{
|
|
model: State,
|
|
as: 'states',
|
|
attributes: ['stateName']
|
|
}
|
|
],
|
|
order: [['zoneName', 'ASC']]
|
|
});
|
|
|
|
res.json({ success: true, data: zones });
|
|
} 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 { regionId, zoneName } = req.body;
|
|
|
|
if (!regionId || !zoneName) {
|
|
return res.status(400).json({ success: false, message: 'Region ID and zone name are required' });
|
|
}
|
|
|
|
const zone = await Zone.create({
|
|
regionId, // Wait, Zone Model doesn't have regionId. It's the other way around?
|
|
zoneName
|
|
});
|
|
|
|
res.status(201).json({ success: true, message: 'Zone created successfully', 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 { zoneName, description, isActive, zonalBusinessHeadId, stateIds } = req.body;
|
|
|
|
const zone = await Zone.findByPk(id);
|
|
if (!zone) {
|
|
return res.status(404).json({ success: false, message: 'Zone not found' });
|
|
}
|
|
|
|
const updates: any = {};
|
|
if (zoneName) updates.zoneName = zoneName;
|
|
if (description !== undefined) updates.description = description;
|
|
if (isActive !== undefined) updates.isActive = isActive;
|
|
if (zonalBusinessHeadId !== undefined) updates.zonalBusinessHeadId = zonalBusinessHeadId;
|
|
|
|
await zone.update(updates);
|
|
|
|
// Handle State assignment
|
|
if (stateIds && Array.isArray(stateIds) && stateIds.length > 0) {
|
|
// Update all provided states to belong to this zone
|
|
// We can't easily "remove" states because zoneId is non-nullable.
|
|
// States must be moved TO another zone to be removed from this one.
|
|
// So we primarily handle "bringing states into this zone".
|
|
// However, we should check if they exist first.
|
|
await State.update(
|
|
{ zoneId: zone.id },
|
|
{ where: { id: stateIds } }
|
|
);
|
|
}
|
|
|
|
res.json({ success: true, message: 'Zone updated successfully' });
|
|
} 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 { zoneId } = req.query as { zoneId?: string };
|
|
const where: any = {};
|
|
if (zoneId) where.zoneId = zoneId;
|
|
|
|
const states = await State.findAll({
|
|
where,
|
|
include: [{ model: Zone, as: 'zone', attributes: ['zoneName'] }],
|
|
order: [['stateName', 'ASC']]
|
|
});
|
|
res.json({ success: true, 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 { zoneId, stateName } = req.body;
|
|
if (!zoneId || !stateName) return res.status(400).json({ success: false, message: 'Zone ID and state name required' });
|
|
|
|
const state = await State.create({ zoneId, stateName });
|
|
res.status(201).json({ success: true, message: 'State created', data: state });
|
|
} catch (error) {
|
|
console.error('Create state error:', error);
|
|
res.status(500).json({ success: false, message: 'Error creating state' });
|
|
}
|
|
};
|
|
|
|
export const updateState = async (req: Request, res: Response) => {
|
|
try {
|
|
const { id } = req.params;
|
|
const { stateName, isActive } = req.body;
|
|
const state = await State.findByPk(id);
|
|
if (!state) return res.status(404).json({ success: false, message: 'State not found' });
|
|
|
|
await state.update({ stateName, isActive });
|
|
res.json({ success: true, message: 'State updated' });
|
|
} catch (error) {
|
|
console.error('Update state error:', error);
|
|
res.status(500).json({ success: false, message: 'Error updating state' });
|
|
}
|
|
};
|
|
|
|
// --- Districts ---
|
|
export const getDistricts = async (req: Request, res: Response) => {
|
|
try {
|
|
const { stateId } = req.query as { stateId?: string };
|
|
const where: any = {};
|
|
if (stateId) where.stateId = stateId;
|
|
|
|
const districts = await District.findAll({
|
|
where,
|
|
include: [{ model: State, as: 'state', attributes: ['stateName'] }],
|
|
order: [['districtName', 'ASC']]
|
|
});
|
|
res.json({ success: true, districts });
|
|
} 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 { stateId, districtName } = req.body;
|
|
if (!stateId || !districtName) return res.status(400).json({ success: false, message: 'State ID and district name required' });
|
|
|
|
const district = await District.create({ stateId, districtName });
|
|
res.status(201).json({ success: true, message: 'District created', data: district });
|
|
} catch (error) {
|
|
console.error('Create district error:', error);
|
|
res.status(500).json({ success: false, message: 'Error creating district' });
|
|
}
|
|
};
|
|
|
|
export const updateDistrict = async (req: Request, res: Response) => {
|
|
try {
|
|
const { id } = req.params;
|
|
const { districtName, isActive } = req.body;
|
|
const district = await District.findByPk(id);
|
|
if (!district) return res.status(404).json({ success: false, message: 'District not found' });
|
|
|
|
await district.update({ districtName, isActive });
|
|
res.json({ success: true, message: 'District updated' });
|
|
} catch (error) {
|
|
console.error('Update district error:', error);
|
|
res.status(500).json({ success: false, message: 'Error updating district' });
|
|
}
|
|
};
|
|
|
|
// --- Areas ---
|
|
export const getAreas = async (req: Request, res: Response) => {
|
|
try {
|
|
const { districtId } = req.query as { districtId?: string };
|
|
const where: any = {};
|
|
if (districtId) where.districtId = districtId;
|
|
|
|
const areas = await Area.findAll({
|
|
where,
|
|
include: [
|
|
{ model: District, as: 'district', attributes: ['districtName'] },
|
|
{ model: State, as: 'state', attributes: ['stateName'] },
|
|
{ model: Region, as: 'region', attributes: ['regionName'] },
|
|
{ model: Zone, as: 'zone', attributes: ['zoneName'] },
|
|
// Include explicit manager column (legacy/fallback)
|
|
{ model: User, as: 'manager', attributes: ['id', 'fullName', 'email', 'mobileNumber'] },
|
|
// Include active managers from dedicated table
|
|
{
|
|
model: AreaManager,
|
|
as: 'areaManagers',
|
|
where: { isActive: true },
|
|
required: false, // Left join, so we get areas even without managers
|
|
include: [{
|
|
model: User,
|
|
as: 'user',
|
|
attributes: ['id', 'fullName', 'email', 'mobileNumber']
|
|
}]
|
|
}
|
|
],
|
|
order: [['areaName', 'ASC']]
|
|
});
|
|
res.json({ success: true, areas });
|
|
} catch (error) {
|
|
console.error('Get areas error:', error);
|
|
res.status(500).json({ success: false, message: 'Error fetching areas' });
|
|
}
|
|
};
|
|
|
|
export const createArea = async (req: Request, res: Response) => {
|
|
try {
|
|
const { districtId, areaCode, areaName, city, pincode, managerId } = req.body;
|
|
if (!districtId || !areaName || !pincode) return res.status(400).json({ success: false, message: 'District ID, area name, and pincode required' });
|
|
|
|
// Need to fetch regionId from district -> state -> zone -> region?
|
|
// Or user provides it?
|
|
// The Area model has regionId, districtId.
|
|
// It's safer to fetch relationships.
|
|
const district = await District.findByPk(districtId, {
|
|
include: [{
|
|
model: State,
|
|
as: 'state',
|
|
include: [
|
|
{ model: Zone, as: 'zone' },
|
|
{ model: Region, as: 'region' }
|
|
]
|
|
}]
|
|
});
|
|
|
|
let regionId = null;
|
|
let zoneId = null;
|
|
let stateId = null;
|
|
|
|
if (district) {
|
|
stateId = district.stateId;
|
|
// Access associations using the logical structure (District -> State -> Zone/Region)
|
|
if (district.state) {
|
|
if (district.state.zone) {
|
|
zoneId = district.state.zone.id;
|
|
}
|
|
if (district.state.region) {
|
|
regionId = district.state.region.id;
|
|
}
|
|
}
|
|
}
|
|
|
|
const area = await Area.create({
|
|
districtId,
|
|
stateId,
|
|
zoneId,
|
|
regionId,
|
|
areaCode,
|
|
areaName,
|
|
city,
|
|
pincode,
|
|
managerId: managerId || null, // Legacy support
|
|
isActive: req.body.isActive ?? true,
|
|
activeFrom: req.body.activeFrom || null,
|
|
activeTo: req.body.activeTo || null
|
|
});
|
|
|
|
// Create AreaManager record if manager assigned
|
|
if (managerId) {
|
|
await AreaManager.create({
|
|
areaId: area.id,
|
|
userId: managerId,
|
|
managerType: 'ASM',
|
|
isActive: true,
|
|
assignedAt: new Date(),
|
|
asmCode: req.body.asmCode || null
|
|
});
|
|
}
|
|
|
|
res.status(201).json({ success: true, message: 'Area created', data: area });
|
|
} catch (error) {
|
|
console.error('Create area error:', error);
|
|
res.status(500).json({ success: false, message: 'Error creating area' });
|
|
}
|
|
};
|
|
|
|
// --- Area Managers ---
|
|
export const getAreaManagers = async (req: Request, res: Response) => {
|
|
try {
|
|
// Fetch Users who have active AreaManager assignments
|
|
// We use the User model as the primary so we get the User details naturally
|
|
const managers = await User.findAll({
|
|
attributes: ['id', 'fullName', 'email', 'mobileNumber', 'employeeId', 'roleCode', 'zoneId', 'regionId'],
|
|
include: [
|
|
{
|
|
model: AreaManager,
|
|
as: 'areaManagers',
|
|
where: { isActive: true },
|
|
required: true, // Only return users who ARE active managers
|
|
attributes: ['asmCode'],
|
|
include: [
|
|
{
|
|
model: Area,
|
|
as: 'area',
|
|
attributes: ['id', 'areaName', 'areaCode'],
|
|
include: [
|
|
{ model: District, as: 'district', attributes: ['districtName'] },
|
|
{ model: State, as: 'state', attributes: ['stateName'] },
|
|
{ model: Region, as: 'region', attributes: ['id', 'regionName'] },
|
|
{ model: Zone, as: 'zone', attributes: ['id', 'zoneName'] }
|
|
]
|
|
}
|
|
]
|
|
},
|
|
{ model: Zone, as: 'zone', attributes: ['id', 'zoneName'] },
|
|
{ model: Region, as: 'region', attributes: ['id', 'regionName'] }
|
|
],
|
|
order: [['fullName', 'ASC']]
|
|
});
|
|
|
|
// Transform if necessary to flatten the structure for the frontend
|
|
// But the user asked for "straightforward", so a clean nested JSON is usually best
|
|
// We can double check if they want a flat list of (User, Area) pairs or User -> [Areas]
|
|
// "Arean mangers" implies the People. So User -> [Areas] is the best entity representation.
|
|
|
|
res.json({ success: true, data: managers });
|
|
} catch (error) {
|
|
console.error('Get area managers error:', error);
|
|
res.status(500).json({ success: false, message: 'Error fetching area managers' });
|
|
}
|
|
};
|
|
|
|
export const updateArea = async (req: Request, res: Response) => {
|
|
try {
|
|
const { id } = req.params;
|
|
const { areaName, city, pincode, isActive, managerId, districtId, activeFrom, activeTo } = req.body;
|
|
const area = await Area.findByPk(id);
|
|
if (!area) return res.status(404).json({ success: false, message: 'Area not found' });
|
|
|
|
const updates: any = {};
|
|
if (areaName) updates.areaName = areaName;
|
|
if (city) updates.city = city;
|
|
if (pincode) updates.pincode = pincode;
|
|
if (isActive !== undefined) updates.isActive = isActive;
|
|
if (activeFrom !== undefined) updates.activeFrom = activeFrom || null;
|
|
if (activeTo !== undefined) updates.activeTo = activeTo || null;
|
|
if (managerId !== undefined) updates.managerId = managerId; // Legacy support
|
|
|
|
// If district is changed, update the entire hierarchy (State, Zone, Region)
|
|
if (districtId && districtId !== area.districtId) {
|
|
updates.districtId = districtId;
|
|
|
|
const district = await District.findByPk(districtId, {
|
|
include: [{
|
|
model: State,
|
|
as: 'state',
|
|
include: [
|
|
{ model: Zone, as: 'zone' },
|
|
{ model: Region, as: 'region' }
|
|
]
|
|
}]
|
|
});
|
|
|
|
if (district) {
|
|
updates.stateId = district.stateId;
|
|
if (district.state) {
|
|
if (district.state.zone) {
|
|
updates.zoneId = district.state.zone.id;
|
|
}
|
|
if (district.state.region) {
|
|
updates.regionId = district.state.region.id;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
await area.update(updates);
|
|
|
|
// Handle AreaManager Table Update
|
|
if (managerId !== undefined) {
|
|
const asmCode = req.body.asmCode;
|
|
|
|
// 1. Find currently active manager for this area
|
|
const currentActiveManager = await AreaManager.findOne({
|
|
where: {
|
|
areaId: id,
|
|
isActive: true
|
|
}
|
|
});
|
|
|
|
// If there is an active manager
|
|
if (currentActiveManager) {
|
|
// If the new managerId is different (or null, meaning unassign), deactivate the old one
|
|
if (currentActiveManager.userId !== managerId) {
|
|
await currentActiveManager.update({ isActive: false });
|
|
} else {
|
|
// If SAME user, update asmCode if provided
|
|
if (asmCode !== undefined) {
|
|
await currentActiveManager.update({ asmCode });
|
|
}
|
|
}
|
|
}
|
|
|
|
// 2. If a new manager is being assigned (and it's not null)
|
|
if (managerId) {
|
|
// Check if this specific user is already active (to avoid duplicates if logic above missed it)
|
|
const isAlreadyActive = currentActiveManager && currentActiveManager.userId === managerId;
|
|
|
|
if (!isAlreadyActive) {
|
|
await AreaManager.create({
|
|
areaId: id,
|
|
userId: managerId,
|
|
managerType: 'ASM', // Default type
|
|
isActive: true,
|
|
assignedAt: new Date(),
|
|
asmCode: asmCode || null
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
res.json({ success: true, message: 'Area updated successfully' });
|
|
} catch (error) {
|
|
console.error('Update area error:', error);
|
|
res.status(500).json({ success: false, message: 'Error updating area' });
|
|
}
|
|
};
|