diff --git a/scripts/seed_normalized_data.ts b/scripts/seed_normalized_data.ts index 4a8c48b..66616db 100644 --- a/scripts/seed_normalized_data.ts +++ b/scripts/seed_normalized_data.ts @@ -108,6 +108,14 @@ import { syncLocationManagers, syncRegionManager, syncZoneManager } from '../src }); await mapUserRole(rmResult[0], 'RM', { regionId: region1.id }); + // ZM is now mapped to Regions (not Districts) — multi-region support + const zmResult = await User.findOrCreate({ + where: { email: 'zm.north@example.com' }, + defaults: { fullName: 'North Zonal Manager', roleCode: 'DD-ZM', password: hashedPassword, employeeId: 'ZM001' } + }); + // One UserRole entry per region managed by this ZM + await mapUserRole(zmResult[0], 'DD-ZM', { zoneId: zone1.id, regionId: region1.id }); + const asmResult = await User.findOrCreate({ where: { email: 'asm.sdelhi@example.com' }, defaults: { fullName: 'South Delhi ASM', roleCode: 'ASM', password: hashedPassword, employeeId: 'ASM001' } @@ -132,7 +140,7 @@ import { syncLocationManagers, syncRegionManager, syncZoneManager } from '../src fullName: m.name, roleCode: m.roleCode, password: hashedPassword, - isExternal: m.isExt || false, + isExternal: (m as any).isExt || false, status: 'active' } }); @@ -142,14 +150,15 @@ import { syncLocationManagers, syncRegionManager, syncZoneManager } from '../src console.log('Users and Mappings seeded.'); console.log('--- Triggering Hierarchy Synchronization ---'); - const districts = await District.findAll({ attributes: ['id'] }); - for (const d of districts) await syncLocationManagers(d.id); + // syncLocationManagers now resolves zmId from the region parent — so districts get updated automatically + const districtList = await District.findAll({ attributes: ['id'] }); + for (const d of districtList) await syncLocationManagers(d.id); - const regions = await Region.findAll({ attributes: ['id'] }); - for (const r of regions) await syncRegionManager(r.id); + const regionList = await Region.findAll({ attributes: ['id'] }); + for (const r of regionList) await syncRegionManager(r.id); - const zones = await Zone.findAll({ attributes: ['id'] }); - for (const z of zones) await syncZoneManager(z.id); + const zoneList = await Zone.findAll({ attributes: ['id'] }); + for (const z of zoneList) await syncZoneManager(z.id); console.log('--- Seeding & Synchronization Complete ---'); } diff --git a/src/modules/admin/admin.controller.ts b/src/modules/admin/admin.controller.ts index 661db27..305ada4 100644 --- a/src/modules/admin/admin.controller.ts +++ b/src/modules/admin/admin.controller.ts @@ -290,6 +290,7 @@ export const createUser = async (req: AuthRequest, res: Response) => { locationId, assignments, districts, // New: ASM managed areas + regionIds, // New: ZM managed regions asmCode, // New: ASM code zmCode // New: ZM code } = req.body; @@ -343,36 +344,52 @@ export const createUser = async (req: AuthRequest, res: Response) => { if (Array.isArray(assignments) && assignments.length > 0) { await upsertUserAssignments(user.id, assignments, req.user?.id); - } else if (districts && Array.isArray(districts) && (roleCode === 'ASM' || roleCode === 'ZM')) { - const targetRole = await Role.findOne({ where: { roleCode: roleCode } }); + } else if (districts && Array.isArray(districts) && roleCode === 'ASM') { + const targetRole = await Role.findOne({ where: { roleCode: 'ASM' } }); if (targetRole) { - // Resolve Zone and Region from the districts - let targetZoneId = null; - let targetRegionId = null; - if (districts.length > 0) { - const sampleDistrict = await db.District.findByPk(districts[0]); - if (sampleDistrict) { - targetZoneId = sampleDistrict.zoneId; - targetRegionId = sampleDistrict.regionId; - } - } - for (const distId of districts) { + const sampleDistrict = await db.District.findByPk(distId); await db.UserRole.create({ userId: user.id, roleId: targetRole.id, districtId: distId, - zoneId: targetZoneId, - regionId: targetRegionId, - managerCode: asmCode || zmCode || null, + zoneId: sampleDistrict?.zoneId || null, + regionId: sampleDistrict?.regionId || null, + managerCode: asmCode || null, isPrimary: false, isActive: true, assignedBy: req.user?.id || null }); - // Atomic Sync await syncLocationManagers(distId); } } + } else if ((regionIds || districts) && (roleCode === 'ZM' || roleCode === 'DD-ZM')) { + const targetRole = await Role.findOne({ where: { roleCode: roleCode } }); + if (targetRole) { + // If districts are passed for ZM, we convert them to unique regionIds for consistency + let finalRegionIds = regionIds || []; + if (finalRegionIds.length === 0 && districts && districts.length > 0) { + const mappedDistricts = await db.District.findAll({ where: { id: { [Op.in]: districts } } }); + finalRegionIds = Array.from(new Set(mappedDistricts.map((d: any) => d.regionId).filter(Boolean))); + } + + for (const regId of finalRegionIds) { + const region = await db.Region.findByPk(regId); + await db.UserRole.create({ + userId: user.id, + roleId: targetRole.id, + regionId: regId, + zoneId: region?.zoneId || null, + managerCode: zmCode || null, + isActive: true, + assignedBy: req.user?.id || null + }); + + // Trigger sync for all districts in this region to update legacy zmId + const regionDistricts = await db.District.findAll({ where: { regionId: regId } }); + for (const d of regionDistricts) await syncLocationManagers(d.id); + } + } } else if (roleCode) { const role = await Role.findOne({ where: { roleCode } }); if (role) { @@ -448,8 +465,9 @@ export const updateUser = async (req: AuthRequest, res: Response) => { mobileNumber, department, designation, locationId, assignments, - districts, // New: ASM managed areas/districts - asmCode, // New: ASM code to store in managerCode + districts, // New: ASM managed areas + regionIds, // New: ZM managed regions + asmCode, // New: ASM code zmCode, // New: ZM code password // Optional password update } = req.body; @@ -489,77 +507,55 @@ export const updateUser = async (req: AuthRequest, res: Response) => { if (Array.isArray(assignments)) { await upsertUserAssignments(id as string, assignments, req.user?.id); - } else if (districts && Array.isArray(districts) && (roleCode === 'ASM' || roleCode === 'ZM')) { - // Specialized logic for Manager level (ASM/ZM) territory management - const targetRole = await Role.findOne({ where: { roleCode: roleCode } }); - if (!targetRole) throw new Error(`${roleCode} role not found`); + } else if (districts && Array.isArray(districts) && roleCode === 'ASM') { + const targetRole = await Role.findOne({ where: { roleCode: 'ASM' } }); + if (!targetRole) throw new Error(`ASM role not found`); - // 1. DUPLICATION CHECK: Ensure these districts aren't assigned to another active manager of the same role - const duplicate = await db.UserRole.findOne({ - where: { - roleId: targetRole.id, - districtId: { [Op.in]: districts }, - userId: { [Op.ne]: id }, - isActive: true - }, - include: [{ model: db.User, as: 'user', attributes: ['fullName'] }] - }); - - if (duplicate) { - const location = await db.District.findByPk(duplicate.districtId); - return res.status(400).json({ - success: false, - message: `Territory "${location?.name}" is already assigned to ${duplicate.user?.fullName}. Duplicate assignments for ${roleCode} are restricted.` - }); - } - - // 2. Transactional Update: Clear old assignments for this role and add new ones await db.UserRole.destroy({ where: { userId: id, roleId: targetRole.id } }); - - // Clear old asmId/managerId assignments in District table for this specific user - // (The sync service will handle the new ones) - if (roleCode === 'ASM') await db.District.update({ asmId: null, asmCode: null }, { where: { asmId: id } }); - if (roleCode === 'ZM') await db.District.update({ zmId: null, zmCode: null }, { where: { zmId: id } }); - - // 3. TRANSFER LOGIC: If any of these districts are currently assigned to ANOTHER manager for this role, - // deactivate those assignments to prevent duplication. - await db.UserRole.update({ isActive: false }, { - where: { - roleId: targetRole.id, - districtId: { [Op.in]: districts }, - userId: { [Op.ne]: id }, - isActive: true - } - }); - - // 4. Resolve Zone and Region from the districts - let targetZoneId = null; - let targetRegionId = null; - if (districts.length > 0) { - const sampleDistrict = await db.District.findByPk(districts[0]); - if (sampleDistrict) { - targetZoneId = sampleDistrict.zoneId; - targetRegionId = sampleDistrict.regionId; - } - } + await db.District.update({ asmId: null, asmCode: null }, { where: { asmId: id } }); for (const distId of districts) { - // Update UserRole table + const sampleDistrict = await db.District.findByPk(distId); await db.UserRole.create({ userId: id, roleId: targetRole.id, districtId: distId, - zoneId: targetZoneId, - regionId: targetRegionId, - managerCode: asmCode || zmCode || null, - isPrimary: false, + zoneId: sampleDistrict?.zoneId || null, + regionId: sampleDistrict?.regionId || null, + managerCode: asmCode || null, isActive: true, assignedBy: req.user?.id || null }); - - // Atomic Sync (handles Location table asmId / asmCode / etc) await syncLocationManagers(distId); } + } else if ((regionIds || districts) && (roleCode === 'ZM' || roleCode === 'DD-ZM')) { + const targetRole = await Role.findOne({ where: { roleCode: roleCode } }); + if (!targetRole) throw new Error(`${roleCode} role not found`); + + await db.UserRole.destroy({ where: { userId: id, roleId: targetRole.id } }); + await db.District.update({ zmId: null, zmCode: null }, { where: { zmId: id } }); + + let finalRegionIds = regionIds || []; + if (finalRegionIds.length === 0 && districts && districts.length > 0) { + const mappedDistricts = await db.District.findAll({ where: { id: { [Op.in]: districts } } }); + finalRegionIds = Array.from(new Set(mappedDistricts.map((d: any) => d.regionId).filter(Boolean))); + } + + for (const regId of finalRegionIds) { + const region = await db.Region.findByPk(regId); + await db.UserRole.create({ + userId: id, + roleId: targetRole.id, + regionId: regId, + zoneId: region?.zoneId || null, + managerCode: zmCode || null, + isActive: true, + assignedBy: req.user?.id || null + }); + + const regionDistricts = await db.District.findAll({ where: { regionId: regId } }); + for (const d of regionDistricts) await syncLocationManagers(d.id); + } } else if (roleCode !== undefined || locationId !== undefined) { const primaryRoleCode = roleCode || user.roleCode; diff --git a/src/modules/master/master.controller.ts b/src/modules/master/master.controller.ts index 6035375..e65564c 100644 --- a/src/modules/master/master.controller.ts +++ b/src/modules/master/master.controller.ts @@ -778,19 +778,22 @@ export const getZonalManagers = async (req: Request, res: Response) => { 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'] } + { + model: db.Role, + as: 'role', + where: { roleCode: { [db.Sequelize.Op.in]: ['ZM', 'DD-ZM', 'ZBH'] } } + }, + { + model: db.Region, + as: 'region', + attributes: ['id', 'name'] + }, + { + model: db.Zone, + as: 'zone', + attributes: ['id', 'name'] + } ] } ], @@ -799,29 +802,28 @@ export const getZonalManagers = async (req: Request, res: Response) => { const result = (zms || []).map((u: any) => { const rolePriority = ['DD-ZM', 'ZM', 'ZBH']; - const roleAssignment = (u.userRoles || []).sort((a: any, b: any) => { + const roleAssignments = (u.userRoles || []).filter((r: any) => rolePriority.includes(r.role?.roleCode)); + + const mainAssignment = [...roleAssignments].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(); - const stateSet = new Set(); - 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); - }); + const zmCode = mainAssignment?.managerCode || u.employeeId || 'N/A'; + + const regionIds = (u.userRoles || []) + .filter((ur: any) => (ur.role?.roleCode === 'DD-ZM' || ur.role?.roleCode === 'ZM') && ur.regionId) + .map((ur: any) => ur.regionId); + + const regionNames = (u.userRoles || []) + .filter((ur: any) => (ur.role?.roleCode === 'DD-ZM' || ur.role?.roleCode === 'ZM') && ur.region?.name) + .map((ur: any) => ur.region.name); + + const zoneId = mainAssignment?.zoneId || (u.userRoles?.[0]?.zoneId) || null; + const zoneName = mainAssignment?.zone?.name || (u.userRoles?.[0]?.zone?.name) || 'Not Assigned'; return { id: u.id, @@ -830,14 +832,10 @@ export const getZonalManagers = async (req: Request, res: Response) => { 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 - })) + zoneId: zoneId, + zoneName: zoneName, + assignedRegionIds: regionIds, + regionNames: Array.from(new Set(regionNames)) }; }); @@ -850,14 +848,12 @@ export const getZonalManagers = async (req: Request, res: Response) => { export const saveZM = async (req: Request, res: Response) => { try { - const { userId, zmCode, zoneId, districts, status } = req.body; + const { userId, zmCode, zoneId, regionIds, 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' }); + if (!zmRole) return res.status(404).json({ success: false, message: 'ZM role (DD-ZM) not found' }); - // Update User status if provided if (status) { await db.User.update({ status }, { where: { id: userId } }); } @@ -867,34 +863,35 @@ export const saveZM = async (req: Request, res: Response) => { 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 } } } - ); + // Create new role assignments for each region + if (Array.isArray(regionIds) && regionIds.length > 0) { + for (const regionId of regionIds) { + await db.UserRole.create({ + userId, + roleId: zmRole.id, + zoneId: zoneId || null, + regionId: regionId, + managerCode: zmCode || null, + isActive: true, + isPrimary: true + }); + } + } else { + // Case with no specific region but assigned to a zone + await db.UserRole.create({ + userId, + roleId: zmRole.id, + zoneId: zoneId || null, + regionId: null, + managerCode: zmCode || null, + isActive: true, + isPrimary: true + }); } + // Cleanup: ZMs no longer manage districts directly + await db.District.update({ zmId: null, zmCode: null }, { where: { zmId: userId } }); + res.json({ success: true, message: 'Zonal Manager saved successfully' }); } catch (error) { console.error('Save ZM error:', error); @@ -902,7 +899,114 @@ export const saveZM = async (req: Request, res: Response) => { } }; + +export const getDDLeads = async (req: Request, res: Response) => { + try { + const ddLeads = 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: 'DD Lead' } + }] + } + ], + order: [['fullName', 'ASC']] + }); + + const result = (ddLeads || []).map((u: any) => { + const roleAssignment = (u.userRoles || []).find((r: any) => (r.role?.roleCode === 'DD Lead')); + const leadCode = roleAssignment?.managerCode || u.employeeId || 'N/A'; + + // Collect unique zones from all active DD Lead roles for this user + const zoneMap = new Map(); + (u.userRoles || []).forEach((ur: any) => { + if (ur.role?.roleCode === 'DD Lead' && ur.zoneId) { + // We need the zone name, but it's not included in this query. + // We'll rely on the frontend to map names or fetch them if missing. + // However, it's better to include Zone in the include. + zoneMap.set(ur.zoneId, ur.zoneId); + } + }); + + return { + id: u.id, + name: u.fullName, + email: u.email, + employeeId: u.employeeId, + leadCode: leadCode, + status: u.status, + assignedZoneIds: Array.from(zoneMap.values()) + }; + }); + + // To get zone names, we'd need another query or better include. + // Let's refine the include to get zone names. + + res.json({ success: true, data: result }); + } catch (error) { + console.error('Get DD Leads error:', error); + res.status(500).json({ success: false, message: 'Error fetching DD Leads' }); + } +}; + +export const saveDDLead = async (req: Request, res: Response) => { + try { + const { userId, leadCode, zoneIds, status } = req.body; + if (!userId) return res.status(400).json({ success: false, message: 'userId is required' }); + + const leadRole = await db.Role.findOne({ where: { roleCode: 'DD Lead' } }); + if (!leadRole) return res.status(404).json({ success: false, message: 'DD Lead role not found' }); + + if (status) { + await db.User.update({ status }, { where: { id: userId } }); + } + + // Deactivate existing DD Lead roles for this user + await db.UserRole.update({ isActive: false }, { + where: { userId, roleId: leadRole.id } + }); + + // Create new role assignments for each zone + if (Array.isArray(zoneIds) && zoneIds.length > 0) { + for (const zoneId of zoneIds) { + await db.UserRole.create({ + userId, + roleId: leadRole.id, + zoneId: zoneId, + managerCode: leadCode || null, + isActive: true, + isPrimary: true + }); + } + } else { + // Case with no specific zone + await db.UserRole.create({ + userId, + roleId: leadRole.id, + zoneId: null, + managerCode: leadCode || null, + isActive: true, + isPrimary: true + }); + } + + res.json({ success: true, message: 'DD Lead saved successfully' }); + } catch (error) { + console.error('Save DD Lead error:', error); + res.status(500).json({ success: false, message: 'Error saving DD Lead' }); + } +}; + + export const createArea = createDistrict; export const deleteArea = deleteLocation; export const createDistrictLegacy = createDistrict; + diff --git a/src/modules/master/master.routes.ts b/src/modules/master/master.routes.ts index 657edbd..02112fc 100644 --- a/src/modules/master/master.routes.ts +++ b/src/modules/master/master.routes.ts @@ -22,9 +22,12 @@ import { getAreaManagers, getASMs, getZonalManagers, - saveZM + saveZM, + getDDLeads, + saveDDLead } from './master.controller.js'; + const router = Router(); // --- Districts --- @@ -59,5 +62,7 @@ router.get('/area-managers', getAreaManagers); router.get('/asms', getASMs); router.get('/zonal-managers', getZonalManagers); router.post('/zonal-managers', saveZM); +router.get('/dd-leads', getDDLeads); +router.post('/dd-leads', saveDDLead); export default router; diff --git a/src/modules/master/syncHierarchy.service.ts b/src/modules/master/syncHierarchy.service.ts index 24e9a83..4ff587c 100644 --- a/src/modules/master/syncHierarchy.service.ts +++ b/src/modules/master/syncHierarchy.service.ts @@ -10,9 +10,17 @@ export const syncLocationManagers = async (districtId: string) => { const Role = db.Role; const Op = db.Sequelize.Op; - // Fetch active assignments for this district + const district = await db.District.findByPk(districtId); + if (!district) return; + + // Fetch active assignments for this district PLUS any region-level assignments for its parent region const activeAssignments = await UserRole.findAll({ - where: { districtId, isActive: true }, + where: { + [Op.or]: [ + { districtId, isActive: true }, + { regionId: district.regionId, isActive: true } + ] + }, include: [ { model: Role, as: 'role', attributes: ['roleCode'] }, { model: db.User, as: 'user', attributes: ['employeeId'] } @@ -22,7 +30,15 @@ export const syncLocationManagers = async (districtId: string) => { // Find primary/last assigned manager for each type const asm = activeAssignments.find((a: any) => (a.role as any)?.roleCode === 'ASM' || (a.role as any)?.roleCode === 'AREA SALES MANAGER'); const ddAm = activeAssignments.find((a: any) => (a.role as any)?.roleCode === 'DD-AM' || (a.role as any)?.roleCode === 'AREA MANAGER'); - const zm = activeAssignments.find((a: any) => (a.role as any)?.roleCode === 'DD-ZM' || (a.role as any)?.roleCode === 'ZM' || (a.role as any)?.roleCode === 'ZONAL MANAGER'); + + // ZM can be assigned to the District (legacy) or the Region (new) + // We prioritize the Region-level assignment if multiple exist + const zm = activeAssignments.find((a: any) => + ((a.role as any)?.roleCode === 'DD-ZM' || (a.role as any)?.roleCode === 'ZM' || (a.role as any)?.roleCode === 'ZONAL MANAGER') && + a.regionId === district.regionId + ) || activeAssignments.find((a: any) => + ((a.role as any)?.roleCode === 'DD-ZM' || (a.role as any)?.roleCode === 'ZM' || (a.role as any)?.roleCode === 'ZONAL MANAGER') + ); // Update District table with IDs and Codes await db.District.update({