264 lines
10 KiB
JavaScript
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
|