import { Request, Response } from 'express'; import bcrypt from 'bcryptjs'; import { Op } from 'sequelize'; import db from '../../database/models/index.js'; const { Role, Permission, RolePermission, User, DealerCode, AuditLog } = db; import { AUDIT_ACTIONS } from '../../common/config/constants.js'; import { AuthRequest } from '../../types/express.types.js'; import { syncLocationManagers, syncRegionManager, syncZoneManager } from '../master/syncHierarchy.service.js'; import { resolveManagerCode } from '../../services/userRoleCode.service.js'; const upsertUserAssignments = async ( userId: string, assignments: any[], actorUserId?: string ) => { if (!Array.isArray(assignments)) return; await db.UserRole.destroy({ where: { userId } }); for (let i = 0; i < assignments.length; i++) { const assignment = assignments[i] || {}; const roleCode = assignment.roleCode || assignment.role; if (!roleCode) continue; const role = await Role.findOne({ where: { roleCode } }); if (!role) continue; const managerCode = await resolveManagerCode(role.id, roleCode, null); const createdRole = await db.UserRole.create({ userId, roleId: role.id, districtId: assignment.locationId || assignment.districtId || null, zoneId: assignment.zoneId || null, regionId: assignment.regionId || null, managerCode, isPrimary: assignment.isPrimary !== undefined ? Boolean(assignment.isPrimary) : i === 0, isActive: assignment.isActive !== undefined ? Boolean(assignment.isActive) : true, effectiveFrom: assignment.effectiveFrom || null, effectiveTo: assignment.effectiveTo || null, assignedBy: actorUserId || null }); // Trigger Sync const targetId = assignment.locationId || assignment.districtId; if (targetId) await syncLocationManagers(targetId); if (assignment.regionId) await syncRegionManager(assignment.regionId); if (assignment.zoneId) await syncZoneManager(assignment.zoneId); } }; // --- Roles Management --- export const getRoles = async (req: Request, res: Response) => { try { const roles = await Role.findAll({ include: [ { model: Permission, as: 'permissions', through: { attributes: [] } }, { model: User, as: 'users', attributes: ['id'] } ], order: [['roleName', 'ASC']] }); // Map to include userCount const result = roles.map((r: any) => ({ ...r.toJSON(), userCount: r.users?.length || 0 })); res.json({ success: true, data: result }); } catch (error) { console.error('Get roles error:', error); res.status(500).json({ success: false, message: 'Error fetching roles' }); } }; export const createRole = async (req: AuthRequest, res: Response) => { try { const { roleCode, roleName, description, permissionIds } = req.body; // permissionIds: string[] const role = await Role.create({ roleCode, roleName, description }); if (permissionIds && permissionIds.length > 0) { for (const pid of permissionIds) { await RolePermission.create({ roleId: role.id, permissionId: pid }); } } await AuditLog.create({ userId: req.user?.id, action: AUDIT_ACTIONS.CREATED, entityType: 'role', entityId: role.id, newData: req.body }); res.status(201).json({ success: true, data: role, message: 'Role created successfully' }); } catch (error) { console.error('Create role error:', error); res.status(500).json({ success: false, message: 'Error creating role' }); } }; export const updateRole = async (req: AuthRequest, res: Response) => { try { const { id } = req.params; const { roleName, description, permissionIds, permissions, isActive } = req.body; const permsToUpdate = permissionIds || permissions; const role = await Role.findByPk(id); if (!role) return res.status(404).json({ success: false, message: 'Role not found' }); await role.update({ roleName, description, isActive }); if (permsToUpdate && Array.isArray(permsToUpdate)) { // Resolve IDs if they are passed as codes const resolvedIds: string[] = []; for (const pid of permsToUpdate) { if (pid.includes(':') || /^[A-Z_]+$/.test(pid)) { const perm = await Permission.findOne({ where: { permissionCode: pid } }); if (perm) resolvedIds.push(perm.id); } else { resolvedIds.push(pid); } } // Remove existing permissions and re-add new ones await RolePermission.destroy({ where: { roleId: id } }); for (const resolvedId of resolvedIds) { await RolePermission.create({ roleId: id, permissionId: resolvedId }); } } await AuditLog.create({ userId: req.user?.id, action: AUDIT_ACTIONS.UPDATED, entityType: 'role', entityId: id, newData: req.body }); res.json({ success: true, message: 'Role updated successfully' }); } catch (error) { console.error('Update role error:', error); res.status(500).json({ success: false, message: 'Error updating role' }); } }; // --- Permissions Management --- export const getPermissions = async (req: Request, res: Response) => { try { const permissions = await Permission.findAll({ order: [['module', 'ASC']] }); res.json({ success: true, data: permissions }); } catch (error) { console.error('Get permissions error:', error); res.status(500).json({ success: false, message: 'Error fetching permissions' }); } }; // --- User Management (Admin) --- export const getAllUsers = async (req: Request, res: Response) => { try { const { roleCode, locationId, search, page = 1, limit = 100, isExternal } = req.query as any; const whereClause: any = {}; // 0. External filter if (isExternal !== undefined) { whereClause.isExternal = isExternal === 'true'; } // 1. Search filter if (search) { whereClause[Op.or] = [ { fullName: { [Op.iLike]: `%${search}%` } }, { email: { [Op.iLike]: `%${search}%` } }, { employeeId: { [Op.iLike]: `%${search}%` } } ]; } // 2. Role filter let rawRoleCode: any = roleCode || req.query['roleCode[]']; let finalRoleCodes: string[] = []; if (rawRoleCode) { if (Array.isArray(rawRoleCode)) { finalRoleCodes = rawRoleCode; } else if (typeof rawRoleCode === 'string') { finalRoleCodes = rawRoleCode.split(',').map(r => r.trim()); } } if (finalRoleCodes.length > 0) { whereClause.roleCode = { [Op.in]: finalRoleCodes }; } const nationalRoles = ['NBH', 'DD Head', 'Super Admin']; const isNationalRole = finalRoleCodes.some(r => nationalRoles.includes(r)); if (!isNationalRole && locationId) { const district: any = await db.District.findByPk(locationId as string, { attributes: ['id', 'zoneId', 'regionId', 'stateId'] }); if (district) { const relevantIds = [district.id, district.zoneId, district.regionId, district.stateId].filter(Boolean); whereClause[Op.or] = [ { districtId: { [Op.in]: relevantIds } }, { zoneId: { [Op.in]: relevantIds } }, { regionId: { [Op.in]: relevantIds } }, { stateId: { [Op.in]: relevantIds } }, { '$userRoles.districtId$': { [Op.in]: relevantIds } }, { '$userRoles.zoneId$': { [Op.in]: relevantIds } }, { '$userRoles.regionId$': { [Op.in]: relevantIds } } ]; } } const { count, rows: users } = await User.findAndCountAll({ where: whereClause, attributes: { exclude: ['password'] }, limit: Number(limit), offset: (Number(page) - 1) * Number(limit), include: [ { model: Role, as: 'role', include: [ { model: Permission, as: 'permissions', through: { attributes: [] } } ] }, { model: db.District, as: 'district' }, { model: db.UserRole, as: 'userRoles', include: [ { model: db.District, as: 'district', attributes: ['id', 'name', 'stateId', 'regionId', 'zoneId'], 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'] } ] }, { model: db.Zone, as: 'zone', attributes: ['id', 'name'] }, { model: db.Region, as: 'region', attributes: ['id', 'name'] } ] } ], order: [['createdAt', 'DESC']], distinct: true }); const result = users.map((u: any) => { const userJson = u.toJSON(); const assignments = userJson.userRoles || []; // territories mapping — provide fallbacks to the nested location fields const territories = assignments.map((a: any) => ({ role: a.role?.roleName, roleCode: a.role?.roleCode, districtId: a.districtId, districtName: a.district?.name, locationType: 'district', managerCode: a.managerCode, zoneId: a.zoneId || a.district?.zoneId, zone: a.zone?.name || a.district?.zone?.name, regionId: a.regionId || a.district?.regionId, region: a.region?.name || a.district?.region?.name, stateId: a.district?.state?.id || a.district?.stateId, state: a.district?.state?.name, isActive: a.isActive })); userJson.territoryProfile = territories; userJson.allRoles = Array.from(new Set([ u.role?.roleCode, u.role?.roleName, ...assignments.flatMap((a: any) => [a.role?.roleCode, a.role?.roleName]) ].filter(Boolean))); userJson.allZones = Array.from(new Set( territories.map((t: any) => t.zone).filter(Boolean).map((z: string) => z.toUpperCase()) )); userJson.allRegions = Array.from(new Set( territories.map((t: any) => t.region).filter(Boolean).map((r: string) => r.toUpperCase()) )); return userJson; }); res.json({ success: true, data: result, total: count }); } catch (error) { console.error('Get users error:', error); res.status(500).json({ success: false, message: 'Error fetching users' }); } } export const createUser = async (req: AuthRequest, res: Response) => { try { const { fullName, email, roleCode, employeeId, mobileNumber, department, designation, locationId, assignments, districts, // New: ASM managed areas regionIds, // New: ZM managed regions asmCode, // New: ASM code zmCode // New: ZM code } = req.body; // Validate required fields if (!fullName || !email || !roleCode) { return res.status(400).json({ success: false, message: 'Full Name, Email, and Role are required' }); } // Check if user already exists (Email) const existingEmail = await User.findOne({ where: { email } }); if (existingEmail) { return res.status(400).json({ success: false, message: 'User with this email already exists' }); } // Check if user already exists (Employee ID) if (employeeId) { const existingEmpId = await User.findOne({ where: { employeeId } }); if (existingEmpId) { return res.status(400).json({ success: false, message: `User with Employee ID ${employeeId} already exists` }); } } // Hash default password const hashedPassword = await bcrypt.hash('Admin@123', 10); // Location is optional. Only persist to user.districtId if the value is a valid district UUID. let safeDistrictId: string | null = null; if (locationId) { const districtExists = await db.District.findByPk(locationId); safeDistrictId = districtExists ? locationId : null; } // Create user const user = await User.create({ fullName, email, password: hashedPassword, roleCode, status: 'active', isActive: true, employeeId, mobileNumber, department, designation, districtId: safeDistrictId }); if (Array.isArray(assignments) && assignments.length > 0) { await upsertUserAssignments(user.id, assignments, req.user?.id); } else if (districts && Array.isArray(districts) && roleCode === 'ASM') { const targetRole = await Role.findOne({ where: { roleCode: 'ASM' } }); if (targetRole) { for (const distId of districts) { const sampleDistrict = await db.District.findByPk(distId); const managerCode = await resolveManagerCode(targetRole.id, 'ASM', null); await db.UserRole.create({ userId: user.id, roleId: targetRole.id, districtId: distId, zoneId: sampleDistrict?.zoneId || null, regionId: sampleDistrict?.regionId || null, managerCode, isPrimary: false, isActive: true, assignedBy: req.user?.id || null }); 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); const managerCode = await resolveManagerCode(targetRole.id, roleCode, null); await db.UserRole.create({ userId: user.id, roleId: targetRole.id, regionId: regId, zoneId: region?.zoneId || null, managerCode, 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) { const managerCode = await resolveManagerCode(role.id, roleCode, null); await db.UserRole.create({ userId: user.id, roleId: role.id, districtId: safeDistrictId, managerCode, isPrimary: true, isActive: true, assignedBy: req.user?.id || null }); } } await AuditLog.create({ userId: req.user?.id, action: AUDIT_ACTIONS.CREATED, entityType: 'user', entityId: user.id, newData: req.body }); res.status(201).json({ success: true, message: 'User created successfully', data: user }); } catch (error: any) { console.error('Create user error:', error); if (error.name === 'SequelizeUniqueConstraintError') { const field = error.errors[0]?.path || 'field'; const value = error.errors[0]?.value || ''; return res.status(400).json({ success: false, message: `${field} "${value}" already exists. Please use a unique value.` }); } res.status(500).json({ success: false, message: 'Error creating user' }); } }; export const updateUserStatus = async (req: AuthRequest, res: Response) => { try { const { id } = req.params; const { status, isActive } = req.body; const user = await User.findByPk(id); if (!user) return res.status(404).json({ success: false, message: 'User not found' }); await user.update({ status, isActive }); // If user is deactivated, clear their ASM assignments in District table if (isActive === false) { await db.District.update({ asmId: null }, { where: { asmId: id } }); } await AuditLog.create({ userId: req.user?.id, action: AUDIT_ACTIONS.UPDATED, entityType: 'user', entityId: id, newData: { status, isActive } }); res.json({ success: true, message: 'User status updated' }); } catch (error) { console.error('Update user status error:', error); res.status(500).json({ success: false, message: 'Error updating user status' }); } }; export const updateUser = async (req: AuthRequest, res: Response) => { try { const { id } = req.params; const { fullName, email, roleCode, status, isActive, employeeId, mobileNumber, department, designation, locationId, assignments, districts, // New: ASM managed areas regionIds, // New: ZM managed regions asmCode, // New: ASM code zmCode, // New: ZM code password // Optional password update } = req.body; const user = await User.findByPk(id); if (!user) return res.status(404).json({ success: false, message: 'User not found' }); const oldData = user.toJSON(); const updates: any = { fullName: fullName || user.fullName, email: email || user.email, roleCode: roleCode || user.roleCode, status: status || user.status, isActive: isActive !== undefined ? isActive : user.isActive, employeeId: employeeId || user.employeeId, mobileNumber: mobileNumber || user.mobileNumber, department: department || user.department, designation: designation || user.designation }; // NEW: Validate locationId if provided (must exist in districts table, otherwise set to null on User record) if (locationId !== undefined) { if (locationId === '' || locationId === null) { updates.districtId = null; } else { const districtExists = await db.District.findByPk(locationId); updates.districtId = districtExists ? locationId : null; } } // If password is provided, hash it and update if (password && password.trim() !== '') { updates.password = await bcrypt.hash(password, 10); } await user.update(updates); if (Array.isArray(assignments)) { await upsertUserAssignments(id as string, assignments, req.user?.id); } 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`); await db.UserRole.destroy({ where: { userId: id, roleId: targetRole.id } }); await db.District.update({ asmId: null, asmCode: null }, { where: { asmId: id } }); for (const distId of districts) { const sampleDistrict = await db.District.findByPk(distId); const managerCode = await resolveManagerCode(targetRole.id, 'ASM', null); await db.UserRole.create({ userId: id, roleId: targetRole.id, districtId: distId, zoneId: sampleDistrict?.zoneId || null, regionId: sampleDistrict?.regionId || null, managerCode, isActive: true, assignedBy: req.user?.id || null }); 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); const managerCode = await resolveManagerCode(targetRole.id, roleCode, null); await db.UserRole.create({ userId: id, roleId: targetRole.id, regionId: regId, zoneId: region?.zoneId || null, managerCode, 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; if (primaryRoleCode) { const role = await Role.findOne({ where: { roleCode: primaryRoleCode } }); if (role) { await db.UserRole.destroy({ where: { userId: id, isPrimary: true } }); const created = await db.UserRole.create({ userId: id, roleId: role.id, districtId: updates.districtId ?? user.districtId ?? null, isPrimary: true, isActive: updates.isActive, assignedBy: req.user?.id || null }); // Sync primary location if exists if (created.districtId) await syncLocationManagers(created.districtId); if (created.regionId) await syncRegionManager(created.regionId); if (created.zoneId) await syncZoneManager(created.zoneId); } } } await AuditLog.create({ userId: req.user?.id, action: AUDIT_ACTIONS.UPDATED, entityType: 'user', entityId: id, oldData, newData: req.body }); res.json({ success: true, message: 'User updated successfully', data: user }); } catch (error: any) { console.error('Update user error:', error); if (error.name === 'SequelizeUniqueConstraintError') { const field = error.errors[0]?.path || 'field'; const value = error.errors[0]?.value || ''; return res.status(400).json({ success: false, message: `${field} "${value}" already exists. Please use a unique value.` }); } res.status(500).json({ success: false, message: 'Error updating user' }); } }; // --- Dealer Codes --- export const generateDealerCode = async (req: AuthRequest, res: Response) => { try { const { locationId, channel } = req.body; // Logic to generate unique code based on format (e.g., RE-[Region]-[State]-[Seq]) // This is a placeholder for the actual business logic const timestamp = Date.now().toString().slice(-6); const code = `DLR-${timestamp}`; const dealerCode = await DealerCode.create({ code, isUsed: false, generatedBy: req.user?.id }); res.status(201).json({ success: true, data: dealerCode }); } catch (error) { console.error('Generate dealer code error:', error); res.status(500).json({ success: false, message: 'Error generating dealer code' }); } };