Safety: - dias-sin-accidente.service: Days without accidents counter - hallazgo.service: Safety findings with workflow - programa-seguridad.service: Annual safety programs Training: - instructor.service: Training instructors management - sesion-capacitacion.service: Training sessions and attendance - constancia-dc3.service: DC-3 certificates (STPS) - cumplimiento.service: Regulatory compliance tracking - comision-seguridad.service: Safety commissions (NOM-019) - horas-trabajadas.service: Work hours for safety indices Environmental: - residuo-peligroso.service: Hazardous waste management - almacen-temporal.service: Temporary storage facilities - proveedor-ambiental.service: Environmental providers - auditoria-ambiental.service: Environmental audits - reporte-programado.service: Scheduled HSE reports Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
662 lines
18 KiB
TypeScript
662 lines
18 KiB
TypeScript
/**
|
|
* ProgramaSeguridadService - Servicio para programas de seguridad
|
|
*
|
|
* RF-MAA017-005: Gestion de programas anuales de seguridad y sus actividades
|
|
*
|
|
* @module HSE
|
|
*/
|
|
|
|
import { Repository, FindOptionsWhere } from 'typeorm';
|
|
import { ProgramaSeguridad, EstadoPrograma } from '../entities/programa-seguridad.entity';
|
|
import { ProgramaActividad, TipoActividadPrograma, EstadoActividad } from '../entities/programa-actividad.entity';
|
|
import { ProgramaInspeccion, EstadoInspeccion } from '../entities/programa-inspeccion.entity';
|
|
import { ServiceContext, PaginatedResult } from '../../../shared/services/base.service';
|
|
|
|
export interface ProgramaFilters {
|
|
fraccionamientoId?: string;
|
|
anio?: number;
|
|
estado?: EstadoPrograma;
|
|
}
|
|
|
|
export interface ActividadFilters {
|
|
programaId?: string;
|
|
tipo?: TipoActividadPrograma;
|
|
estado?: EstadoActividad;
|
|
responsableId?: string;
|
|
dateFrom?: Date;
|
|
dateTo?: Date;
|
|
}
|
|
|
|
export interface InspeccionProgramadaFilters {
|
|
fraccionamientoId?: string;
|
|
tipoInspeccionId?: string;
|
|
inspectorId?: string;
|
|
estado?: EstadoInspeccion;
|
|
dateFrom?: Date;
|
|
dateTo?: Date;
|
|
}
|
|
|
|
export interface CreateProgramaSeguridadDto {
|
|
fraccionamientoId: string;
|
|
anio: number;
|
|
objetivoGeneral?: string;
|
|
metas?: Record<string, unknown>;
|
|
presupuesto?: number;
|
|
}
|
|
|
|
export interface UpdateProgramaSeguridadDto {
|
|
objetivoGeneral?: string;
|
|
metas?: Record<string, unknown>;
|
|
presupuesto?: number;
|
|
}
|
|
|
|
export interface CreateActividadProgramaDto {
|
|
programaId: string;
|
|
actividad: string;
|
|
tipo: TipoActividadPrograma;
|
|
fechaProgramada: Date;
|
|
responsableId?: string;
|
|
recursos?: string;
|
|
}
|
|
|
|
export interface UpdateActividadDto {
|
|
actividad?: string;
|
|
tipo?: TipoActividadPrograma;
|
|
fechaProgramada?: Date;
|
|
responsableId?: string;
|
|
recursos?: string;
|
|
estado?: EstadoActividad;
|
|
evidenciaUrl?: string;
|
|
}
|
|
|
|
export interface CreateInspeccionProgramadaDto {
|
|
fraccionamientoId: string;
|
|
tipoInspeccionId: string;
|
|
inspectorId?: string;
|
|
fechaProgramada: Date;
|
|
horaProgramada?: string;
|
|
zonaArea?: string;
|
|
}
|
|
|
|
export interface ProgramaStats {
|
|
totalProgramas: number;
|
|
programasActivos: number;
|
|
actividadesPendientes: number;
|
|
actividadesCompletadas: number;
|
|
cumplimiento: number;
|
|
}
|
|
|
|
export interface CalendarioActividad {
|
|
id: string;
|
|
fecha: Date;
|
|
titulo: string;
|
|
tipo: TipoActividadPrograma | 'inspeccion';
|
|
estado: string;
|
|
responsable?: string;
|
|
}
|
|
|
|
export class ProgramaSeguridadService {
|
|
constructor(
|
|
private readonly programaRepository: Repository<ProgramaSeguridad>,
|
|
private readonly actividadRepository: Repository<ProgramaActividad>,
|
|
private readonly inspeccionProgramadaRepository: Repository<ProgramaInspeccion>
|
|
) {}
|
|
|
|
// ========== Programas ==========
|
|
|
|
/**
|
|
* Lista programas con filtros
|
|
*/
|
|
async findProgramas(
|
|
ctx: ServiceContext,
|
|
filters: ProgramaFilters = {},
|
|
page: number = 1,
|
|
limit: number = 20
|
|
): Promise<PaginatedResult<ProgramaSeguridad>> {
|
|
const skip = (page - 1) * limit;
|
|
|
|
const queryBuilder = this.programaRepository
|
|
.createQueryBuilder('programa')
|
|
.leftJoinAndSelect('programa.fraccionamiento', 'fraccionamiento')
|
|
.leftJoinAndSelect('programa.aprobadoPor', 'aprobadoPor')
|
|
.where('programa.tenant_id = :tenantId', { tenantId: ctx.tenantId });
|
|
|
|
if (filters.fraccionamientoId) {
|
|
queryBuilder.andWhere('programa.fraccionamiento_id = :fraccionamientoId', {
|
|
fraccionamientoId: filters.fraccionamientoId,
|
|
});
|
|
}
|
|
|
|
if (filters.anio) {
|
|
queryBuilder.andWhere('programa.anio = :anio', { anio: filters.anio });
|
|
}
|
|
|
|
if (filters.estado) {
|
|
queryBuilder.andWhere('programa.estado = :estado', { estado: filters.estado });
|
|
}
|
|
|
|
queryBuilder
|
|
.orderBy('programa.anio', 'DESC')
|
|
.addOrderBy('programa.created_at', 'DESC')
|
|
.skip(skip)
|
|
.take(limit);
|
|
|
|
const [data, total] = await queryBuilder.getManyAndCount();
|
|
|
|
return {
|
|
data,
|
|
total,
|
|
page,
|
|
limit,
|
|
totalPages: Math.ceil(total / limit),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Obtiene programa por ID
|
|
*/
|
|
async findProgramaById(ctx: ServiceContext, id: string): Promise<ProgramaSeguridad | null> {
|
|
return this.programaRepository.findOne({
|
|
where: { id, tenantId: ctx.tenantId } as FindOptionsWhere<ProgramaSeguridad>,
|
|
relations: ['fraccionamiento', 'aprobadoPor'],
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Obtiene programa con todas sus actividades
|
|
*/
|
|
async findProgramaWithActividades(ctx: ServiceContext, id: string): Promise<ProgramaSeguridad | null> {
|
|
return this.programaRepository.findOne({
|
|
where: { id, tenantId: ctx.tenantId } as FindOptionsWhere<ProgramaSeguridad>,
|
|
relations: ['fraccionamiento', 'aprobadoPor', 'actividades', 'actividades.responsable'],
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Crea un nuevo programa
|
|
*/
|
|
async createPrograma(ctx: ServiceContext, dto: CreateProgramaSeguridadDto): Promise<ProgramaSeguridad> {
|
|
// Verificar si ya existe un programa para ese anio y fraccionamiento
|
|
const existing = await this.programaRepository.findOne({
|
|
where: {
|
|
tenantId: ctx.tenantId,
|
|
fraccionamientoId: dto.fraccionamientoId,
|
|
anio: dto.anio,
|
|
} as FindOptionsWhere<ProgramaSeguridad>,
|
|
});
|
|
|
|
if (existing) {
|
|
throw new Error(`Ya existe un programa de seguridad para el anio ${dto.anio} en esta obra`);
|
|
}
|
|
|
|
const programa = this.programaRepository.create({
|
|
tenantId: ctx.tenantId,
|
|
fraccionamientoId: dto.fraccionamientoId,
|
|
anio: dto.anio,
|
|
objetivoGeneral: dto.objetivoGeneral,
|
|
metas: dto.metas,
|
|
presupuesto: dto.presupuesto,
|
|
estado: 'borrador',
|
|
});
|
|
|
|
return this.programaRepository.save(programa);
|
|
}
|
|
|
|
/**
|
|
* Actualiza un programa
|
|
*/
|
|
async updatePrograma(
|
|
ctx: ServiceContext,
|
|
id: string,
|
|
dto: UpdateProgramaSeguridadDto
|
|
): Promise<ProgramaSeguridad | null> {
|
|
const programa = await this.findProgramaById(ctx, id);
|
|
if (!programa) {
|
|
return null;
|
|
}
|
|
|
|
if (programa.estado === 'finalizado') {
|
|
throw new Error('No se puede modificar un programa finalizado');
|
|
}
|
|
|
|
const updated = this.programaRepository.merge(programa, dto);
|
|
return this.programaRepository.save(updated);
|
|
}
|
|
|
|
/**
|
|
* Activa un programa
|
|
*/
|
|
async activarPrograma(ctx: ServiceContext, id: string): Promise<ProgramaSeguridad | null> {
|
|
const programa = await this.findProgramaById(ctx, id);
|
|
if (!programa) {
|
|
return null;
|
|
}
|
|
|
|
if (programa.estado !== 'borrador') {
|
|
throw new Error('Solo se pueden activar programas en borrador');
|
|
}
|
|
|
|
programa.estado = 'activo';
|
|
programa.aprobadoPorId = ctx.userId || '';
|
|
programa.fechaAprobacion = new Date();
|
|
|
|
return this.programaRepository.save(programa);
|
|
}
|
|
|
|
/**
|
|
* Finaliza un programa
|
|
*/
|
|
async finalizarPrograma(ctx: ServiceContext, id: string): Promise<ProgramaSeguridad | null> {
|
|
const programa = await this.findProgramaById(ctx, id);
|
|
if (!programa) {
|
|
return null;
|
|
}
|
|
|
|
if (programa.estado !== 'activo') {
|
|
throw new Error('Solo se pueden finalizar programas activos');
|
|
}
|
|
|
|
programa.estado = 'finalizado';
|
|
return this.programaRepository.save(programa);
|
|
}
|
|
|
|
// ========== Actividades ==========
|
|
|
|
/**
|
|
* Lista actividades con filtros
|
|
*/
|
|
async findActividades(
|
|
ctx: ServiceContext,
|
|
filters: ActividadFilters = {},
|
|
page: number = 1,
|
|
limit: number = 20
|
|
): Promise<PaginatedResult<ProgramaActividad>> {
|
|
const skip = (page - 1) * limit;
|
|
|
|
const queryBuilder = this.actividadRepository
|
|
.createQueryBuilder('actividad')
|
|
.leftJoinAndSelect('actividad.programa', 'programa')
|
|
.leftJoinAndSelect('actividad.responsable', 'responsable')
|
|
.where('programa.tenant_id = :tenantId', { tenantId: ctx.tenantId });
|
|
|
|
if (filters.programaId) {
|
|
queryBuilder.andWhere('actividad.programa_id = :programaId', {
|
|
programaId: filters.programaId,
|
|
});
|
|
}
|
|
|
|
if (filters.tipo) {
|
|
queryBuilder.andWhere('actividad.tipo = :tipo', { tipo: filters.tipo });
|
|
}
|
|
|
|
if (filters.estado) {
|
|
queryBuilder.andWhere('actividad.estado = :estado', { estado: filters.estado });
|
|
}
|
|
|
|
if (filters.responsableId) {
|
|
queryBuilder.andWhere('actividad.responsable_id = :responsableId', {
|
|
responsableId: filters.responsableId,
|
|
});
|
|
}
|
|
|
|
if (filters.dateFrom) {
|
|
queryBuilder.andWhere('actividad.fecha_programada >= :dateFrom', { dateFrom: filters.dateFrom });
|
|
}
|
|
|
|
if (filters.dateTo) {
|
|
queryBuilder.andWhere('actividad.fecha_programada <= :dateTo', { dateTo: filters.dateTo });
|
|
}
|
|
|
|
queryBuilder
|
|
.orderBy('actividad.fecha_programada', 'ASC')
|
|
.skip(skip)
|
|
.take(limit);
|
|
|
|
const [data, total] = await queryBuilder.getManyAndCount();
|
|
|
|
return {
|
|
data,
|
|
total,
|
|
page,
|
|
limit,
|
|
totalPages: Math.ceil(total / limit),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Obtiene actividad por ID
|
|
*/
|
|
async findActividadById(_ctx: ServiceContext, id: string): Promise<ProgramaActividad | null> {
|
|
return this.actividadRepository.findOne({
|
|
where: { id } as FindOptionsWhere<ProgramaActividad>,
|
|
relations: ['programa', 'responsable'],
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Crea una nueva actividad
|
|
*/
|
|
async createActividad(ctx: ServiceContext, dto: CreateActividadProgramaDto): Promise<ProgramaActividad> {
|
|
const programa = await this.findProgramaById(ctx, dto.programaId);
|
|
if (!programa) {
|
|
throw new Error('Programa no encontrado');
|
|
}
|
|
|
|
if (programa.estado === 'finalizado') {
|
|
throw new Error('No se pueden agregar actividades a un programa finalizado');
|
|
}
|
|
|
|
const actividad = this.actividadRepository.create({
|
|
programaId: dto.programaId,
|
|
actividad: dto.actividad,
|
|
tipo: dto.tipo,
|
|
fechaProgramada: dto.fechaProgramada,
|
|
responsableId: dto.responsableId,
|
|
recursos: dto.recursos,
|
|
estado: 'pendiente',
|
|
});
|
|
|
|
return this.actividadRepository.save(actividad);
|
|
}
|
|
|
|
/**
|
|
* Actualiza una actividad
|
|
*/
|
|
async updateActividad(
|
|
ctx: ServiceContext,
|
|
id: string,
|
|
dto: UpdateActividadDto
|
|
): Promise<ProgramaActividad | null> {
|
|
const actividad = await this.findActividadById(ctx, id);
|
|
if (!actividad) {
|
|
return null;
|
|
}
|
|
|
|
const updated = this.actividadRepository.merge(actividad, dto);
|
|
|
|
// Si se completa, registrar fecha
|
|
if (dto.estado === 'completada' && actividad.estado !== 'completada') {
|
|
updated.fechaRealizada = new Date();
|
|
}
|
|
|
|
return this.actividadRepository.save(updated);
|
|
}
|
|
|
|
/**
|
|
* Marca actividad como completada
|
|
*/
|
|
async completarActividad(
|
|
ctx: ServiceContext,
|
|
id: string,
|
|
evidenciaUrl?: string
|
|
): Promise<ProgramaActividad | null> {
|
|
const actividad = await this.findActividadById(ctx, id);
|
|
if (!actividad) {
|
|
return null;
|
|
}
|
|
|
|
actividad.estado = 'completada';
|
|
actividad.fechaRealizada = new Date();
|
|
if (evidenciaUrl) {
|
|
actividad.evidenciaUrl = evidenciaUrl;
|
|
}
|
|
|
|
return this.actividadRepository.save(actividad);
|
|
}
|
|
|
|
/**
|
|
* Cancela una actividad
|
|
*/
|
|
async cancelarActividad(ctx: ServiceContext, id: string): Promise<ProgramaActividad | null> {
|
|
const actividad = await this.findActividadById(ctx, id);
|
|
if (!actividad) {
|
|
return null;
|
|
}
|
|
|
|
if (actividad.estado === 'completada') {
|
|
throw new Error('No se puede cancelar una actividad completada');
|
|
}
|
|
|
|
actividad.estado = 'cancelada';
|
|
return this.actividadRepository.save(actividad);
|
|
}
|
|
|
|
// ========== Inspecciones Programadas ==========
|
|
|
|
/**
|
|
* Lista inspecciones programadas
|
|
*/
|
|
async findInspeccionesProgramadas(
|
|
ctx: ServiceContext,
|
|
filters: InspeccionProgramadaFilters = {},
|
|
page: number = 1,
|
|
limit: number = 20
|
|
): Promise<PaginatedResult<ProgramaInspeccion>> {
|
|
const skip = (page - 1) * limit;
|
|
|
|
const queryBuilder = this.inspeccionProgramadaRepository
|
|
.createQueryBuilder('pi')
|
|
.leftJoinAndSelect('pi.fraccionamiento', 'fraccionamiento')
|
|
.leftJoinAndSelect('pi.tipoInspeccion', 'tipoInspeccion')
|
|
.leftJoinAndSelect('pi.inspector', 'inspector')
|
|
.where('pi.tenant_id = :tenantId', { tenantId: ctx.tenantId });
|
|
|
|
if (filters.fraccionamientoId) {
|
|
queryBuilder.andWhere('pi.fraccionamiento_id = :fraccionamientoId', {
|
|
fraccionamientoId: filters.fraccionamientoId,
|
|
});
|
|
}
|
|
|
|
if (filters.tipoInspeccionId) {
|
|
queryBuilder.andWhere('pi.tipo_inspeccion_id = :tipoInspeccionId', {
|
|
tipoInspeccionId: filters.tipoInspeccionId,
|
|
});
|
|
}
|
|
|
|
if (filters.inspectorId) {
|
|
queryBuilder.andWhere('pi.inspector_id = :inspectorId', {
|
|
inspectorId: filters.inspectorId,
|
|
});
|
|
}
|
|
|
|
if (filters.estado) {
|
|
queryBuilder.andWhere('pi.estado = :estado', { estado: filters.estado });
|
|
}
|
|
|
|
if (filters.dateFrom) {
|
|
queryBuilder.andWhere('pi.fecha_programada >= :dateFrom', { dateFrom: filters.dateFrom });
|
|
}
|
|
|
|
if (filters.dateTo) {
|
|
queryBuilder.andWhere('pi.fecha_programada <= :dateTo', { dateTo: filters.dateTo });
|
|
}
|
|
|
|
queryBuilder
|
|
.orderBy('pi.fecha_programada', 'ASC')
|
|
.skip(skip)
|
|
.take(limit);
|
|
|
|
const [data, total] = await queryBuilder.getManyAndCount();
|
|
|
|
return {
|
|
data,
|
|
total,
|
|
page,
|
|
limit,
|
|
totalPages: Math.ceil(total / limit),
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Crea una inspeccion programada
|
|
*/
|
|
async createInspeccionProgramada(
|
|
ctx: ServiceContext,
|
|
dto: CreateInspeccionProgramadaDto
|
|
): Promise<ProgramaInspeccion> {
|
|
const inspeccion = this.inspeccionProgramadaRepository.create({
|
|
tenantId: ctx.tenantId,
|
|
fraccionamientoId: dto.fraccionamientoId,
|
|
tipoInspeccionId: dto.tipoInspeccionId,
|
|
inspectorId: dto.inspectorId,
|
|
fechaProgramada: dto.fechaProgramada,
|
|
horaProgramada: dto.horaProgramada,
|
|
zonaArea: dto.zonaArea,
|
|
estado: 'programada',
|
|
});
|
|
|
|
return this.inspeccionProgramadaRepository.save(inspeccion);
|
|
}
|
|
|
|
/**
|
|
* Actualiza estado de inspeccion programada
|
|
*/
|
|
async updateInspeccionProgramadaEstado(
|
|
ctx: ServiceContext,
|
|
id: string,
|
|
estado: EstadoInspeccion,
|
|
motivoCancelacion?: string
|
|
): Promise<ProgramaInspeccion | null> {
|
|
const inspeccion = await this.inspeccionProgramadaRepository.findOne({
|
|
where: { id, tenantId: ctx.tenantId } as FindOptionsWhere<ProgramaInspeccion>,
|
|
});
|
|
|
|
if (!inspeccion) {
|
|
return null;
|
|
}
|
|
|
|
inspeccion.estado = estado;
|
|
if (estado === 'cancelada' && motivoCancelacion) {
|
|
inspeccion.motivoCancelacion = motivoCancelacion;
|
|
}
|
|
|
|
return this.inspeccionProgramadaRepository.save(inspeccion);
|
|
}
|
|
|
|
// ========== Calendario y Estadisticas ==========
|
|
|
|
/**
|
|
* Obtiene calendario de actividades e inspecciones
|
|
*/
|
|
async getCalendario(
|
|
ctx: ServiceContext,
|
|
fraccionamientoId: string,
|
|
dateFrom: Date,
|
|
dateTo: Date
|
|
): Promise<CalendarioActividad[]> {
|
|
const calendario: CalendarioActividad[] = [];
|
|
|
|
// Obtener actividades del programa
|
|
const actividades = await this.actividadRepository
|
|
.createQueryBuilder('a')
|
|
.leftJoinAndSelect('a.programa', 'p')
|
|
.leftJoinAndSelect('a.responsable', 'r')
|
|
.where('p.tenant_id = :tenantId', { tenantId: ctx.tenantId })
|
|
.andWhere('p.fraccionamiento_id = :fraccionamientoId', { fraccionamientoId })
|
|
.andWhere('a.fecha_programada >= :dateFrom', { dateFrom })
|
|
.andWhere('a.fecha_programada <= :dateTo', { dateTo })
|
|
.getMany();
|
|
|
|
for (const act of actividades) {
|
|
calendario.push({
|
|
id: act.id,
|
|
fecha: act.fechaProgramada,
|
|
titulo: act.actividad,
|
|
tipo: act.tipo,
|
|
estado: act.estado,
|
|
responsable: act.responsable?.id,
|
|
});
|
|
}
|
|
|
|
// Obtener inspecciones programadas
|
|
const inspecciones = await this.inspeccionProgramadaRepository
|
|
.createQueryBuilder('i')
|
|
.leftJoinAndSelect('i.tipoInspeccion', 't')
|
|
.leftJoinAndSelect('i.inspector', 'ins')
|
|
.where('i.tenant_id = :tenantId', { tenantId: ctx.tenantId })
|
|
.andWhere('i.fraccionamiento_id = :fraccionamientoId', { fraccionamientoId })
|
|
.andWhere('i.fecha_programada >= :dateFrom', { dateFrom })
|
|
.andWhere('i.fecha_programada <= :dateTo', { dateTo })
|
|
.getMany();
|
|
|
|
for (const insp of inspecciones) {
|
|
calendario.push({
|
|
id: insp.id,
|
|
fecha: insp.fechaProgramada,
|
|
titulo: `Inspeccion: ${insp.tipoInspeccion?.nombre || 'General'}`,
|
|
tipo: 'inspeccion',
|
|
estado: insp.estado,
|
|
responsable: insp.inspectorId,
|
|
});
|
|
}
|
|
|
|
// Ordenar por fecha
|
|
calendario.sort((a, b) => new Date(a.fecha).getTime() - new Date(b.fecha).getTime());
|
|
|
|
return calendario;
|
|
}
|
|
|
|
/**
|
|
* Obtiene estadisticas del programa
|
|
*/
|
|
async getStats(ctx: ServiceContext, fraccionamientoId?: string, anio?: number): Promise<ProgramaStats> {
|
|
const queryBuilder = this.programaRepository
|
|
.createQueryBuilder('p')
|
|
.where('p.tenant_id = :tenantId', { tenantId: ctx.tenantId });
|
|
|
|
if (fraccionamientoId) {
|
|
queryBuilder.andWhere('p.fraccionamiento_id = :fraccionamientoId', { fraccionamientoId });
|
|
}
|
|
|
|
if (anio) {
|
|
queryBuilder.andWhere('p.anio = :anio', { anio });
|
|
}
|
|
|
|
const totalProgramas = await queryBuilder.getCount();
|
|
|
|
const programasActivos = await this.programaRepository
|
|
.createQueryBuilder('p')
|
|
.where('p.tenant_id = :tenantId', { tenantId: ctx.tenantId })
|
|
.andWhere('p.estado = :estado', { estado: 'activo' })
|
|
.getCount();
|
|
|
|
// Estadisticas de actividades
|
|
const actividadesQuery = this.actividadRepository
|
|
.createQueryBuilder('a')
|
|
.leftJoin('a.programa', 'p')
|
|
.where('p.tenant_id = :tenantId', { tenantId: ctx.tenantId });
|
|
|
|
if (fraccionamientoId) {
|
|
actividadesQuery.andWhere('p.fraccionamiento_id = :fraccionamientoId', { fraccionamientoId });
|
|
}
|
|
|
|
if (anio) {
|
|
actividadesQuery.andWhere('p.anio = :anio', { anio });
|
|
}
|
|
|
|
const totalActividades = await actividadesQuery.getCount();
|
|
|
|
const actividadesCompletadas = await actividadesQuery
|
|
.clone()
|
|
.andWhere('a.estado = :estado', { estado: 'completada' })
|
|
.getCount();
|
|
|
|
const actividadesPendientes = await actividadesQuery
|
|
.clone()
|
|
.andWhere('a.estado IN (:...estados)', { estados: ['pendiente', 'en_progreso'] })
|
|
.getCount();
|
|
|
|
const cumplimiento = totalActividades > 0
|
|
? Math.round((actividadesCompletadas / totalActividades) * 100)
|
|
: 0;
|
|
|
|
return {
|
|
totalProgramas,
|
|
programasActivos,
|
|
actividadesPendientes,
|
|
actividadesCompletadas,
|
|
cumplimiento,
|
|
};
|
|
}
|
|
}
|