"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