300 lines
9.4 KiB
JavaScript
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,
|
|
};
|