erp_be/src/modules/assets/assets-maintenance.service.js

300 lines
9.4 KiB
JavaScript

const prisma = require('../../config/prisma');
const ApiError = require('../../utils/ApiError');
const auditLog = require('../../utils/auditLog');
const { getPagination, isDropdownCall } = require('../../utils/pagination');
const { MAINTENANCE_CHECKLIST_STATUSES } = require('./assets.constants');
const { toDateOnly, getAssetOrThrow } = require('./assets.helpers');
const { sanitizeAsset } = require('./assets.service');
const logInclude = {
users_asset_maintenance_logs_performed_byTousers: {
select: { id: true, full_name: true, employee_code: true },
},
users_asset_maintenance_logs_created_byTousers: {
select: { id: true, full_name: true },
},
};
const assetListInclude = {
item_categories: { select: { id: true, code: true, name: true } },
item_subcategories: { select: { id: true, code: true, name: true } },
location: { select: { id: true, code: true, name: true, type: true } },
departments: { select: { id: true, name: true } },
users_assets_assigned_to_user_idTousers: {
select: { id: true, full_name: true, employee_code: true },
},
users_assets_maintenance_incharge_idTousers: {
select: { id: true, full_name: true, employee_code: true },
},
asset_maintenance_logs: {
where: { deleted_at: null },
orderBy: [{ performed_date: 'desc' }, { created_at: 'desc' }],
take: 1,
select: { id: true, performed_date: true, next_due_date: true },
},
};
const sanitizeLog = (row) => {
if (!row) return null;
const {
users_asset_maintenance_logs_performed_byTousers,
users_asset_maintenance_logs_created_byTousers,
...rest
} = row;
return {
...rest,
performed_by_user: users_asset_maintenance_logs_performed_byTousers || null,
created_by_user: users_asset_maintenance_logs_created_byTousers || null,
};
};
const startOfUtcDay = (value) => {
const d = new Date(value);
if (Number.isNaN(d.getTime())) return null;
return new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate()));
};
const addDays = (value, days) => {
const base = startOfUtcDay(value);
if (!base || days === null || days === undefined) return null;
base.setUTCDate(base.getUTCDate() + Number(days));
return base;
};
const normalizeChecklistTemplate = (template) => {
if (template === null || template === undefined) return null;
if (!Array.isArray(template)) {
throw new ApiError(422, 'maintenance_checklist_json must be an array');
}
const keys = new Set();
return template.map((item, index) => {
const key = String(item.key || '').trim();
const label = String(item.label || '').trim();
if (!key) throw new ApiError(422, `Checklist item ${index + 1}: key is required`);
if (!label) throw new ApiError(422, `Checklist item ${index + 1}: label is required`);
if (keys.has(key)) throw new ApiError(422, `Duplicate checklist key: ${key}`);
keys.add(key);
return {
key,
label,
required: item.required !== false,
};
});
};
const validateChecklistResults = (template, results) => {
if (!Array.isArray(results) || !results.length) {
throw new ApiError(422, 'checklist_json is required and must contain at least one item');
}
const templateItems = Array.isArray(template) ? template : [];
const templateMap = new Map(templateItems.map((item) => [item.key, item]));
const resultKeys = new Set();
const normalized = [];
for (const row of results) {
const key = String(row.key || '').trim();
if (!key) throw new ApiError(422, 'Each checklist result requires a key');
if (resultKeys.has(key)) throw new ApiError(422, `Duplicate checklist result key: ${key}`);
resultKeys.add(key);
if (!MAINTENANCE_CHECKLIST_STATUSES.includes(row.status)) {
throw new ApiError(422, `Invalid checklist status for ${key}`);
}
const templateItem = templateMap.get(key);
normalized.push({
key,
label: row.label || templateItem?.label || key,
status: row.status,
remarks: row.remarks || null,
});
}
for (const item of templateItems) {
if (item.required !== false && !resultKeys.has(item.key)) {
throw new ApiError(422, `Required checklist item missing: ${item.key}`);
}
}
return normalized;
};
const assertInchargeOrThrow = (asset, userId) => {
if (!userId) throw new ApiError(403, 'Authentication required');
if (
!asset.maintenance_incharge_user_id ||
asset.maintenance_incharge_user_id.toString() !== BigInt(userId).toString()
) {
throw new ApiError(403, 'Only the maintenance incharge can perform this action');
}
};
/**
* Assets assigned to the logged-in user as maintenance incharge.
* Optional due_only=true returns only assets whose next maintenance is due/overdue.
*/
const listMyMaintenanceAssets = async (query, userId) => {
if (!userId) throw new ApiError(403, 'Authentication required');
const where = {
deleted_at: null,
maintenance_incharge_user_id: BigInt(userId),
...(query.status ? { status: query.status } : {}),
...(query.location_id ? { location_id: BigInt(query.location_id) } : {}),
...(query.is_active !== undefined ? { is_active: query.is_active } : { is_active: true }),
...(query.search
? {
OR: [
{ asset_code: { contains: query.search, mode: 'insensitive' } },
{ asset_name: { contains: query.search, mode: 'insensitive' } },
{ serial_number: { contains: query.search, mode: 'insensitive' } },
],
}
: {}),
};
const rows = await prisma.assets.findMany({
where,
include: assetListInclude,
orderBy: { asset_code: 'asc' },
});
let data = rows.map(sanitizeAsset);
if (query.due_only === true) {
data = data.filter((row) => row.maintenance?.is_due === true);
}
if (isDropdownCall(query)) {
return { data, meta: { page: 1, limit: data.length, total: data.length, dropdown: true } };
}
const { page, limit, skip } = getPagination(query);
const total = data.length;
const paged = data.slice(skip, skip + limit);
return { data: paged, meta: { page, limit, total } };
};
const listMaintenanceLogs = async (assetId, query) => {
await getAssetOrThrow(assetId);
const { page, limit, skip } = getPagination(query);
const where = { asset_id: BigInt(assetId), deleted_at: null };
const [rows, total] = await Promise.all([
prisma.asset_maintenance_logs.findMany({
where,
include: logInclude,
orderBy: [{ performed_date: 'desc' }, { created_at: 'desc' }],
skip,
take: limit,
}),
prisma.asset_maintenance_logs.count({ where }),
]);
return { data: rows.map(sanitizeLog), meta: { page, limit, total } };
};
const getMaintenanceLog = async (assetId, logId) => {
await getAssetOrThrow(assetId);
const row = await prisma.asset_maintenance_logs.findFirst({
where: {
id: BigInt(logId),
asset_id: BigInt(assetId),
deleted_at: null,
},
include: logInclude,
});
if (!row) throw new ApiError(404, 'Maintenance log not found');
return sanitizeLog(row);
};
const createMaintenanceLog = async (assetId, payload, userId, requestId) => {
const asset = await getAssetOrThrow(assetId);
assertInchargeOrThrow(asset, userId);
if (!asset.maintenance_frequency_in_days) {
throw new ApiError(422, 'Asset has no maintenance_frequency_in_days configured');
}
const template = normalizeChecklistTemplate(asset.maintenance_checklist_json);
if (!template || !template.length) {
throw new ApiError(422, 'Asset has no maintenance_checklist_json configured');
}
const checklist = validateChecklistResults(template, payload.checklist_json);
const performedDate = toDateOnly(payload.performed_date);
const nextDueDate = payload.next_due_date
? toDateOnly(payload.next_due_date)
: addDays(performedDate, asset.maintenance_frequency_in_days);
const created = await prisma.asset_maintenance_logs.create({
data: {
asset_id: BigInt(assetId),
performed_by: userId ? BigInt(userId) : null,
performed_date: performedDate,
next_due_date: nextDueDate,
checklist_json: checklist,
remarks: payload.remarks || null,
created_by: userId ? BigInt(userId) : null,
updated_by: userId ? BigInt(userId) : null,
},
include: logInclude,
});
const sanitized = sanitizeLog(created);
await auditLog({
tableName: 'asset_maintenance_logs',
recordId: created.id,
action: 'CREATE',
oldValue: null,
newValue: sanitized,
userId,
requestId,
});
return sanitized;
};
const deleteMaintenanceLog = async (assetId, logId, userId, requestId) => {
const asset = await getAssetOrThrow(assetId);
assertInchargeOrThrow(asset, userId);
const existing = await prisma.asset_maintenance_logs.findFirst({
where: {
id: BigInt(logId),
asset_id: BigInt(assetId),
deleted_at: null,
},
include: logInclude,
});
if (!existing) throw new ApiError(404, 'Maintenance log not found');
const deleted = await prisma.asset_maintenance_logs.update({
where: { id: existing.id },
data: {
deleted_at: new Date(),
updated_by: userId ? BigInt(userId) : null,
},
});
await auditLog({
tableName: 'asset_maintenance_logs',
recordId: logId,
action: 'DELETE',
oldValue: sanitizeLog(existing),
newValue: deleted,
userId,
requestId,
});
};
module.exports = {
listMyMaintenanceAssets,
listMaintenanceLogs,
getMaintenanceLog,
createMaintenanceLog,
deleteMaintenanceLog,
normalizeChecklistTemplate,
};