Dealer_Onboarding_Backend/src/modules/admin/admin.controller.ts

633 lines
24 KiB
TypeScript

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';
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 createdRole = await db.UserRole.create({
userId,
roleId: role.id,
districtId: assignment.locationId || assignment.districtId || null,
zoneId: assignment.zoneId || null,
regionId: assignment.regionId || null,
managerCode: assignment.managerCode || assignment.asmCode || null,
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 } = req.query;
const whereClause: any = {};
if (roleCode) {
// Handle both single string and array of role codes (if passed as multiple params)
if (Array.isArray(roleCode)) {
whereClause.roleCode = { [Op.in]: roleCode };
} else {
whereClause.roleCode = roleCode;
}
}
const nationalRoles = ['NBH', 'DD Head', 'Super Admin'];
const isNationalRole = (typeof roleCode === 'string' && nationalRoles.includes(roleCode)) ||
(Array.isArray(roleCode) && roleCode.some(r => typeof r === 'string' && 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.districtId = { [Op.in]: relevantIds };
}
}
const users = await User.findAll({
where: whereClause,
attributes: { exclude: ['password'] },
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']]
});
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 });
} 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
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);
// Create user
const user = await User.create({
fullName,
email,
password: hashedPassword,
roleCode,
status: 'active',
isActive: true,
employeeId,
mobileNumber,
department,
designation,
districtId: locationId
});
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 } });
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) {
await db.UserRole.create({
userId: user.id,
roleId: targetRole.id,
districtId: distId,
zoneId: targetZoneId,
regionId: targetRegionId,
managerCode: asmCode || zmCode || null,
isPrimary: false,
isActive: true,
assignedBy: req.user?.id || null
});
// Atomic Sync
await syncLocationManagers(distId);
}
}
} else if (roleCode) {
const role = await Role.findOne({ where: { roleCode } });
if (role) {
await db.UserRole.create({
userId: user.id,
roleId: role.id,
locationId: locationId || null,
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/districts
asmCode, // New: ASM code to store in managerCode
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' || 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`);
// 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;
}
}
for (const distId of districts) {
// Update UserRole table
await db.UserRole.create({
userId: id,
roleId: targetRole.id,
districtId: distId,
zoneId: targetZoneId,
regionId: targetRegionId,
managerCode: asmCode || zmCode || null,
isPrimary: false,
isActive: true,
assignedBy: req.user?.id || null
});
// Atomic Sync (handles Location table asmId / asmCode / etc)
await syncLocationManagers(distId);
}
}
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' });
}
};