erp_be/src/modules/users/users.service.js
2026-06-20 14:00:46 +05:30

371 lines
10 KiB
JavaScript

const bcrypt = require('bcrypt');
const prisma = require('../../config/prisma');
const env = require('../../config/env');
const ApiError = require('../../utils/ApiError');
const auditLog = require('../../utils/auditLog');
const { getPagination } = require('../../utils/pagination');
const { encrypt, decrypt, blindIndex } = require('../../utils/encryption');
const userInclude = {
roles_users_role_idToroles: { select: { id: true, name: true } },
departments: { select: { id: true, name: true } },
designations: { select: { id: true, name: true } },
plants_users_plant_idToplants: { select: { id: true, code: true, name: true } },
users_users_reporting_toTousers: { select: { id: true, full_name: true, employee_code: true } },
};
const sanitizeUser = (user) => {
if (!user) return null;
const {
roles_users_role_idToroles,
plants_users_plant_idToplants,
users_users_reporting_toTousers,
departments,
designations,
...rest
} = user;
delete rest.password_hash;
delete rest.mobile_index;
return {
...rest,
mobile: rest.mobile ? decrypt(rest.mobile) : null,
role: roles_users_role_idToroles || null,
department: departments || null,
designation: designations || null,
plant: plants_users_plant_idToplants || null,
reporting_manager: users_users_reporting_toTousers || null,
};
};
const buildUsersWhere = (query) => ({
deleted_at: null,
...(query.status ? { status: query.status } : {}),
...(query.is_active !== undefined ? { is_active: query.is_active } : {}),
...(query.role_id ? { role_id: BigInt(query.role_id) } : {}),
...(query.department_id ? { department_id: BigInt(query.department_id) } : {}),
...(query.search
? {
OR: [
{ full_name: { contains: query.search, mode: 'insensitive' } },
{ email: { contains: query.search, mode: 'insensitive' } },
{ employee_code: { contains: query.search, mode: 'insensitive' } },
],
}
: {}),
});
const toInitials = (name) => {
if (!name) return '';
return name
.trim()
.split(/\s+/)
.slice(0, 2)
.map((part) => part[0]?.toUpperCase() || '')
.join('');
};
const toUserListItem = (user) => {
const row = sanitizeUser(user);
return {
id: row.id,
full_name: row.full_name,
email: row.email,
initials: toInitials(row.full_name),
employee_code: row.employee_code,
role: row.role,
department: row.department,
plant: row.plant,
last_login_at: row.last_login_at,
status: row.status,
is_active: row.is_active,
};
};
const assertFk = async (payload) => {
if (payload.role_id) {
const role = await prisma.roles.findFirst({
where: { id: BigInt(payload.role_id), deleted_at: null, is_active: true },
});
if (!role) throw new ApiError(422, 'Invalid role_id');
}
if (payload.department_id) {
const row = await prisma.departments.findFirst({
where: { id: BigInt(payload.department_id), deleted_at: null },
});
if (!row) throw new ApiError(422, 'Invalid department_id');
}
if (payload.designation_id) {
const row = await prisma.designations.findFirst({
where: { id: BigInt(payload.designation_id), deleted_at: null },
});
if (!row) throw new ApiError(422, 'Invalid designation_id');
}
if (payload.plant_id) {
const row = await prisma.plants.findFirst({
where: { id: BigInt(payload.plant_id), deleted_at: null },
});
if (!row) throw new ApiError(422, 'Invalid plant_id');
}
if (payload.reporting_to) {
const row = await prisma.users.findFirst({
where: { id: BigInt(payload.reporting_to), deleted_at: null },
});
if (!row) throw new ApiError(422, 'Invalid reporting_to');
}
};
const buildUserData = async (payload, { hashPassword = false } = {}) => {
const data = { ...payload };
if (hashPassword && data.password) {
data.password_hash = await bcrypt.hash(data.password, env.BCRYPT_SALT_ROUNDS);
delete data.password;
} else {
delete data.password;
}
if (Object.prototype.hasOwnProperty.call(data, 'mobile')) {
if (data.mobile) {
data.mobile_index = blindIndex(data.mobile);
data.mobile = encrypt(data.mobile);
} else {
data.mobile = null;
data.mobile_index = null;
}
}
for (const key of ['role_id', 'department_id', 'designation_id', 'plant_id', 'reporting_to']) {
if (data[key] !== undefined && data[key] !== null) data[key] = BigInt(data[key]);
}
return data;
};
const createUser = async (payload, userId, requestId) => {
await assertFk(payload);
const existing = await prisma.users.findFirst({
where: {
deleted_at: null,
OR: [{ email: payload.email }, { employee_code: payload.employee_code }],
},
});
if (existing) throw new ApiError(409, 'User with this email or employee code already exists');
const data = await buildUserData(payload, { hashPassword: true });
data.created_by = userId ? BigInt(userId) : null;
data.updated_by = userId ? BigInt(userId) : null;
const created = await prisma.users.create({ data, include: userInclude });
await auditLog({
tableName: 'users',
recordId: created.id,
action: 'CREATE',
oldValue: null,
newValue: sanitizeUser(created),
userId,
requestId,
});
return sanitizeUser(created);
};
const listUsers = async (query) => {
const { page, limit, skip } = getPagination(query);
const where = buildUsersWhere(query);
const [rows, total] = await Promise.all([
prisma.users.findMany({
where,
include: userInclude,
orderBy: { created_at: 'desc' },
skip,
take: limit,
}),
prisma.users.count({ where }),
]);
return { data: rows.map(toUserListItem), meta: { page, limit, total } };
};
const getScreenSummary = async () => {
const baseWhere = { deleted_at: null };
const [total, active, inactive, locked, rolesTotal] = await Promise.all([
prisma.users.count({ where: baseWhere }),
prisma.users.count({ where: { ...baseWhere, status: 'active' } }),
prisma.users.count({ where: { ...baseWhere, status: 'inactive' } }),
prisma.users.count({ where: { ...baseWhere, status: 'locked' } }),
prisma.roles.count({ where: { deleted_at: null } }),
]);
return {
users: { total, active, inactive, locked },
roles: { total: rolesTotal },
tabs: { users: total, roles: rolesTotal },
};
};
const getFilterOptions = async () => {
const [roles, departments] = await Promise.all([
prisma.roles.findMany({
where: { deleted_at: null, is_active: true },
select: { id: true, name: true },
orderBy: { name: 'asc' },
}),
prisma.departments.findMany({
where: { deleted_at: null, is_active: true },
select: { id: true, name: true },
orderBy: { name: 'asc' },
}),
]);
return {
roles,
departments,
statuses: [
{ value: 'active', label: 'Active' },
{ value: 'inactive', label: 'Inactive' },
{ value: 'locked', label: 'Locked' },
],
};
};
const escapeCsv = (value) => {
const text = String(value ?? '');
if (/[",\n]/.test(text)) return `"${text.replace(/"/g, '""')}"`;
return text;
};
const exportUsers = async (query) => {
const where = buildUsersWhere(query);
const rows = await prisma.users.findMany({
where,
include: userInclude,
orderBy: { full_name: 'asc' },
});
const header = [
'Full Name',
'Email',
'Employee Code',
'Role',
'Department',
'Plant',
'Last Login',
'Status',
];
const lines = rows.map((user) => {
const item = toUserListItem(user);
return [
item.full_name,
item.email,
item.employee_code,
item.role?.name || '',
item.department?.name || '',
item.plant?.name || '',
item.last_login_at ? new Date(item.last_login_at).toISOString() : '',
item.status,
]
.map(escapeCsv)
.join(',');
});
return [header.join(','), ...lines].join('\n');
};
const getUserById = async (id) => {
const user = await prisma.users.findFirst({
where: { id: BigInt(id), deleted_at: null },
include: userInclude,
});
if (!user) throw new ApiError(404, 'User not found');
return sanitizeUser(user);
};
const updateUser = async (id, payload, userId, requestId) => {
const existing = await prisma.users.findFirst({
where: { id: BigInt(id), deleted_at: null },
include: userInclude,
});
if (!existing) throw new ApiError(404, 'User not found');
await assertFk(payload);
if (payload.email || payload.employee_code) {
const duplicate = await prisma.users.findFirst({
where: {
deleted_at: null,
id: { not: BigInt(id) },
OR: [
...(payload.email ? [{ email: payload.email }] : []),
...(payload.employee_code ? [{ employee_code: payload.employee_code }] : []),
],
},
});
if (duplicate) throw new ApiError(409, 'User with this email or employee code already exists');
}
const data = await buildUserData(payload, { hashPassword: Boolean(payload.password) });
data.updated_by = userId ? BigInt(userId) : null;
const updated = await prisma.users.update({
where: { id: BigInt(id) },
data,
include: userInclude,
});
await auditLog({
tableName: 'users',
recordId: id,
action: 'UPDATE',
oldValue: sanitizeUser(existing),
newValue: sanitizeUser(updated),
userId,
requestId,
});
return sanitizeUser(updated);
};
const deleteUser = async (id, userId, requestId) => {
const existing = await prisma.users.findFirst({
where: { id: BigInt(id), deleted_at: null },
include: userInclude,
});
if (!existing) throw new ApiError(404, 'User not found');
if (userId && String(existing.id) === String(userId)) {
throw new ApiError(400, 'You cannot delete your own account');
}
await prisma.users.update({
where: { id: BigInt(id) },
data: { deleted_at: new Date(), updated_by: userId ? BigInt(userId) : null, is_active: false },
});
await auditLog({
tableName: 'users',
recordId: id,
action: 'DELETE',
oldValue: sanitizeUser(existing),
newValue: { deleted_at: new Date() },
userId,
requestId,
});
};
module.exports = {
createUser,
listUsers,
getUserById,
updateUser,
deleteUser,
getScreenSummary,
getFilterOptions,
exportUsers,
};