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

675 lines
27 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';
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' });
}
};