erp-construccion-backend/dist/modules/hse/services/incidente.service.js

264 lines
10 KiB
JavaScript

"use strict";
/**
* IncidenteService - Servicio para gestión de incidentes HSE
*
* Gestión de incidentes de seguridad con workflow y relaciones.
* Workflow: abierto -> en_investigacion -> cerrado
*
* @module HSE
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.IncidenteService = void 0;
class IncidenteService {
incidenteRepository;
involucradoRepository;
accionRepository;
constructor(incidenteRepository, involucradoRepository, accionRepository) {
this.incidenteRepository = incidenteRepository;
this.involucradoRepository = involucradoRepository;
this.accionRepository = accionRepository;
}
generateFolio() {
const now = new Date();
const year = now.getFullYear().toString().slice(-2);
const month = (now.getMonth() + 1).toString().padStart(2, '0');
const random = Math.random().toString(36).substring(2, 6).toUpperCase();
return `INC-${year}${month}-${random}`;
}
async findWithFilters(ctx, filters = {}, page = 1, limit = 20) {
const skip = (page - 1) * limit;
const queryBuilder = this.incidenteRepository
.createQueryBuilder('incidente')
.leftJoinAndSelect('incidente.fraccionamiento', 'fraccionamiento')
.leftJoinAndSelect('incidente.createdBy', 'createdBy')
.where('incidente.tenant_id = :tenantId', { tenantId: ctx.tenantId });
if (filters.fraccionamientoId) {
queryBuilder.andWhere('incidente.fraccionamiento_id = :fraccionamientoId', {
fraccionamientoId: filters.fraccionamientoId,
});
}
if (filters.tipo) {
queryBuilder.andWhere('incidente.tipo = :tipo', { tipo: filters.tipo });
}
if (filters.gravedad) {
queryBuilder.andWhere('incidente.gravedad = :gravedad', { gravedad: filters.gravedad });
}
if (filters.estado) {
queryBuilder.andWhere('incidente.estado = :estado', { estado: filters.estado });
}
if (filters.dateFrom) {
queryBuilder.andWhere('incidente.fecha_hora >= :dateFrom', { dateFrom: filters.dateFrom });
}
if (filters.dateTo) {
queryBuilder.andWhere('incidente.fecha_hora <= :dateTo', { dateTo: filters.dateTo });
}
queryBuilder
.orderBy('incidente.fecha_hora', 'DESC')
.skip(skip)
.take(limit);
const [data, total] = await queryBuilder.getManyAndCount();
return {
data,
meta: {
total,
page,
limit,
totalPages: Math.ceil(total / limit),
},
};
}
async findById(ctx, id) {
return this.incidenteRepository.findOne({
where: {
id,
tenantId: ctx.tenantId,
},
});
}
async findWithDetails(ctx, id) {
return this.incidenteRepository.findOne({
where: {
id,
tenantId: ctx.tenantId,
},
relations: [
'fraccionamiento',
'createdBy',
'involucrados',
'involucrados.employee',
'acciones',
'acciones.responsable',
],
});
}
async create(ctx, dto) {
const incidente = this.incidenteRepository.create({
tenantId: ctx.tenantId,
createdById: ctx.userId,
folio: this.generateFolio(),
fechaHora: dto.fechaHora,
fraccionamientoId: dto.fraccionamientoId,
ubicacionDescripcion: dto.ubicacionDescripcion,
tipo: dto.tipo,
gravedad: dto.gravedad,
descripcion: dto.descripcion,
causaInmediata: dto.causaInmediata,
causaBasica: dto.causaBasica,
estado: 'abierto',
});
return this.incidenteRepository.save(incidente);
}
async update(ctx, id, dto) {
const existing = await this.findById(ctx, id);
if (!existing) {
return null;
}
if (existing.estado === 'cerrado') {
throw new Error('Cannot update a closed incident');
}
const updated = this.incidenteRepository.merge(existing, dto);
return this.incidenteRepository.save(updated);
}
async startInvestigation(ctx, id) {
const existing = await this.findById(ctx, id);
if (!existing) {
return null;
}
if (existing.estado !== 'abierto') {
throw new Error('Can only start investigation on open incidents');
}
existing.estado = 'en_investigacion';
return this.incidenteRepository.save(existing);
}
async closeIncident(ctx, id) {
const existing = await this.findWithDetails(ctx, id);
if (!existing) {
return null;
}
if (existing.estado === 'cerrado') {
throw new Error('Incident is already closed');
}
// Check if all actions are completed or verified
const pendingActions = existing.acciones?.filter((a) => a.estado !== 'completada' && a.estado !== 'verificada');
if (pendingActions && pendingActions.length > 0) {
throw new Error('Cannot close incident with pending actions');
}
existing.estado = 'cerrado';
return this.incidenteRepository.save(existing);
}
async addInvolucrado(ctx, incidenteId, dto) {
const incidente = await this.findById(ctx, incidenteId);
if (!incidente) {
throw new Error('Incidente not found');
}
const involucrado = this.involucradoRepository.create({
incidenteId,
employeeId: dto.employeeId,
rol: dto.rol,
descripcionLesion: dto.descripcionLesion,
parteCuerpo: dto.parteCuerpo,
diasIncapacidad: dto.diasIncapacidad || 0,
});
return this.involucradoRepository.save(involucrado);
}
async removeInvolucrado(ctx, incidenteId, involucradoId) {
const incidente = await this.findById(ctx, incidenteId);
if (!incidente) {
return false;
}
const result = await this.involucradoRepository.delete({
id: involucradoId,
incidenteId,
});
return (result.affected ?? 0) > 0;
}
async addAccion(ctx, incidenteId, dto) {
const incidente = await this.findById(ctx, incidenteId);
if (!incidente) {
throw new Error('Incidente not found');
}
if (incidente.estado === 'cerrado') {
throw new Error('Cannot add actions to a closed incident');
}
const accion = this.accionRepository.create({
incidenteId,
descripcion: dto.descripcion,
tipo: dto.tipo,
responsableId: dto.responsableId,
fechaCompromiso: dto.fechaCompromiso,
estado: 'pendiente',
});
return this.accionRepository.save(accion);
}
async updateAccion(ctx, incidenteId, accionId, dto) {
const incidente = await this.findById(ctx, incidenteId);
if (!incidente) {
return null;
}
const accion = await this.accionRepository.findOne({
where: { id: accionId, incidenteId },
});
if (!accion) {
return null;
}
// If completing the action, set the close date
if (dto.estado === 'completada' && accion.estado !== 'completada') {
dto.fechaCompromiso = dto.fechaCompromiso; // keep existing
accion.fechaCierre = new Date();
}
const updated = this.accionRepository.merge(accion, dto);
return this.accionRepository.save(updated);
}
async getStats(ctx, fraccionamientoId) {
const queryBuilder = this.incidenteRepository
.createQueryBuilder('incidente')
.where('incidente.tenant_id = :tenantId', { tenantId: ctx.tenantId });
if (fraccionamientoId) {
queryBuilder.andWhere('incidente.fraccionamiento_id = :fraccionamientoId', { fraccionamientoId });
}
const total = await queryBuilder.getCount();
const porTipo = await this.incidenteRepository
.createQueryBuilder('incidente')
.select('incidente.tipo', 'tipo')
.addSelect('COUNT(*)', 'count')
.where('incidente.tenant_id = :tenantId', { tenantId: ctx.tenantId })
.groupBy('incidente.tipo')
.getRawMany();
const porGravedad = await this.incidenteRepository
.createQueryBuilder('incidente')
.select('incidente.gravedad', 'gravedad')
.addSelect('COUNT(*)', 'count')
.where('incidente.tenant_id = :tenantId', { tenantId: ctx.tenantId })
.groupBy('incidente.gravedad')
.getRawMany();
const porEstado = await this.incidenteRepository
.createQueryBuilder('incidente')
.select('incidente.estado', 'estado')
.addSelect('COUNT(*)', 'count')
.where('incidente.tenant_id = :tenantId', { tenantId: ctx.tenantId })
.groupBy('incidente.estado')
.getRawMany();
// Calculate days since last accident
const lastAccident = await this.incidenteRepository.findOne({
where: {
tenantId: ctx.tenantId,
tipo: 'accidente',
},
order: { fechaHora: 'DESC' },
});
let diasSinAccidente = 0;
if (lastAccident) {
const diff = Date.now() - new Date(lastAccident.fechaHora).getTime();
diasSinAccidente = Math.floor(diff / (1000 * 60 * 60 * 24));
}
return {
total,
porTipo: porTipo.map((p) => ({ tipo: p.tipo, count: parseInt(p.count, 10) })),
porGravedad: porGravedad.map((p) => ({ gravedad: p.gravedad, count: parseInt(p.count, 10) })),
porEstado: porEstado.map((p) => ({ estado: p.estado, count: parseInt(p.count, 10) })),
diasSinAccidente,
};
}
}
exports.IncidenteService = IncidenteService;
//# sourceMappingURL=incidente.service.js.map