- Remove meta wrapper from PaginatedResult returns in findWithFilters and findAll - Return flat format with total, page, limit, totalPages at top level - Align with base.service.ts interface definition - Controllers already updated to access flat pagination structure
377 lines
12 KiB
TypeScript
377 lines
12 KiB
TypeScript
/**
|
|
* IndicadorController - Controller de indicadores HSE
|
|
*
|
|
* Endpoints REST para gestión de indicadores y reportes HSE.
|
|
*
|
|
* @module HSE
|
|
*/
|
|
|
|
import { Router, Request, Response, NextFunction } from 'express';
|
|
import { DataSource } from 'typeorm';
|
|
import {
|
|
IndicadorService,
|
|
CreateIndicadorDto,
|
|
IndicadorFilters,
|
|
} from '../services/indicador.service';
|
|
import { AuthMiddleware } from '../../auth/middleware/auth.middleware';
|
|
import { AuthService } from '../../auth/services/auth.service';
|
|
import { IndicadorConfig } from '../entities/indicador-config.entity';
|
|
import { IndicadorMetaObra } from '../entities/indicador-meta-obra.entity';
|
|
import { IndicadorValor } from '../entities/indicador-valor.entity';
|
|
import { HorasTrabajadas } from '../entities/horas-trabajadas.entity';
|
|
import { DiasSinAccidente } from '../entities/dias-sin-accidente.entity';
|
|
import { ReporteProgramado } from '../entities/reporte-programado.entity';
|
|
import { AlertaIndicador } from '../entities/alerta-indicador.entity';
|
|
import { User } from '../../core/entities/user.entity';
|
|
import { Tenant } from '../../core/entities/tenant.entity';
|
|
import { RefreshToken } from '../../auth/entities/refresh-token.entity';
|
|
|
|
/**
|
|
* Service context for multi-tenant operations
|
|
*/
|
|
interface ServiceContext {
|
|
tenantId: string;
|
|
userId?: string;
|
|
}
|
|
|
|
/**
|
|
* Crear router de indicadores
|
|
*/
|
|
export function createIndicadorController(dataSource: DataSource): Router {
|
|
const router = Router();
|
|
|
|
// Repositorios
|
|
const indicadorRepository = dataSource.getRepository(IndicadorConfig);
|
|
const metaRepository = dataSource.getRepository(IndicadorMetaObra);
|
|
const valorRepository = dataSource.getRepository(IndicadorValor);
|
|
const horasRepository = dataSource.getRepository(HorasTrabajadas);
|
|
const diasRepository = dataSource.getRepository(DiasSinAccidente);
|
|
const reporteRepository = dataSource.getRepository(ReporteProgramado);
|
|
const alertaRepository = dataSource.getRepository(AlertaIndicador);
|
|
const userRepository = dataSource.getRepository(User);
|
|
const tenantRepository = dataSource.getRepository(Tenant);
|
|
const refreshTokenRepository = dataSource.getRepository(RefreshToken);
|
|
|
|
// Servicios
|
|
const indicadorService = new IndicadorService(
|
|
indicadorRepository,
|
|
metaRepository,
|
|
valorRepository,
|
|
horasRepository,
|
|
diasRepository,
|
|
reporteRepository,
|
|
alertaRepository
|
|
);
|
|
const authService = new AuthService(userRepository, tenantRepository, refreshTokenRepository as any);
|
|
const authMiddleware = new AuthMiddleware(authService, dataSource);
|
|
|
|
// Helper para crear contexto de servicio
|
|
const getContext = (req: Request): ServiceContext => {
|
|
if (!req.tenantId) {
|
|
throw new Error('Tenant ID is required');
|
|
}
|
|
return {
|
|
tenantId: req.tenantId,
|
|
userId: req.user?.sub,
|
|
};
|
|
};
|
|
|
|
// ========== Configuración de Indicadores ==========
|
|
|
|
/**
|
|
* GET /indicadores
|
|
* Listar indicadores con filtros
|
|
*/
|
|
router.get('/', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise<void> => {
|
|
try {
|
|
const tenantId = req.tenantId;
|
|
if (!tenantId) {
|
|
res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' });
|
|
return;
|
|
}
|
|
|
|
const page = parseInt(req.query.page as string) || 1;
|
|
const limit = Math.min(parseInt(req.query.limit as string) || 20, 100);
|
|
|
|
const filters: IndicadorFilters = {
|
|
tipo: req.query.tipo as any,
|
|
activo: req.query.activo === 'true' ? true : req.query.activo === 'false' ? false : undefined,
|
|
search: req.query.search as string,
|
|
};
|
|
|
|
const result = await indicadorService.findIndicadores(getContext(req), filters, page, limit);
|
|
|
|
res.status(200).json({
|
|
success: true,
|
|
data: result.data,
|
|
pagination: {
|
|
total: result.total,
|
|
page: result.page,
|
|
limit: result.limit,
|
|
totalPages: result.totalPages,
|
|
},
|
|
});
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /indicadores/stats
|
|
* Obtener estadísticas de indicadores
|
|
*/
|
|
router.get('/stats', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise<void> => {
|
|
try {
|
|
const tenantId = req.tenantId;
|
|
if (!tenantId) {
|
|
res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' });
|
|
return;
|
|
}
|
|
|
|
const stats = await indicadorService.getStats(getContext(req));
|
|
res.status(200).json({ success: true, data: stats });
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
});
|
|
|
|
/**
|
|
* GET /indicadores/:id
|
|
* Obtener indicador por ID
|
|
*/
|
|
router.get('/:id', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise<void> => {
|
|
try {
|
|
const indicador = await indicadorService.findIndicadorById(getContext(req), req.params.id);
|
|
if (!indicador) {
|
|
res.status(404).json({ error: 'Not Found', message: 'Indicator not found' });
|
|
return;
|
|
}
|
|
res.status(200).json({ success: true, data: indicador });
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
});
|
|
|
|
/**
|
|
* POST /indicadores
|
|
* Crear indicador
|
|
*/
|
|
router.post('/', authMiddleware.authenticate, authMiddleware.authorize('admin'), async (req: Request, res: Response, next: NextFunction): Promise<void> => {
|
|
try {
|
|
const tenantId = req.tenantId;
|
|
if (!tenantId) {
|
|
res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' });
|
|
return;
|
|
}
|
|
|
|
const dto: CreateIndicadorDto = req.body;
|
|
|
|
if (!dto.codigo || !dto.nombre || !dto.tipo) {
|
|
res.status(400).json({
|
|
error: 'Bad Request',
|
|
message: 'codigo, nombre and tipo are required',
|
|
});
|
|
return;
|
|
}
|
|
|
|
const indicador = await indicadorService.createIndicador(getContext(req), dto);
|
|
res.status(201).json({ success: true, data: indicador });
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
});
|
|
|
|
/**
|
|
* PATCH /indicadores/:id
|
|
* Actualizar indicador
|
|
*/
|
|
router.patch('/:id', authMiddleware.authenticate, authMiddleware.authorize('admin'), async (req: Request, res: Response, next: NextFunction): Promise<void> => {
|
|
try {
|
|
const dto: Partial<CreateIndicadorDto> = req.body;
|
|
const indicador = await indicadorService.updateIndicador(getContext(req), req.params.id, dto);
|
|
|
|
if (!indicador) {
|
|
res.status(404).json({ error: 'Not Found', message: 'Indicator not found' });
|
|
return;
|
|
}
|
|
|
|
res.status(200).json({ success: true, data: indicador });
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
});
|
|
|
|
/**
|
|
* POST /indicadores/:id/toggle
|
|
* Activar/desactivar indicador
|
|
*/
|
|
router.post('/:id/toggle', authMiddleware.authenticate, authMiddleware.authorize('admin'), async (req: Request, res: Response, next: NextFunction): Promise<void> => {
|
|
try {
|
|
const indicador = await indicadorService.toggleIndicadorActivo(getContext(req), req.params.id);
|
|
|
|
if (!indicador) {
|
|
res.status(404).json({ error: 'Not Found', message: 'Indicator not found' });
|
|
return;
|
|
}
|
|
|
|
res.status(200).json({
|
|
success: true,
|
|
data: indicador,
|
|
message: indicador.activo ? 'Indicator activated' : 'Indicator deactivated',
|
|
});
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
});
|
|
|
|
// ========== Metas por Obra ==========
|
|
|
|
/**
|
|
* GET /indicadores/metas
|
|
* Listar metas por obra
|
|
*/
|
|
router.get('/metas/list', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise<void> => {
|
|
try {
|
|
const fraccionamientoId = req.query.fraccionamientoId as string;
|
|
const indicadorId = req.query.indicadorId as string;
|
|
const metas = await indicadorService.findMetas(getContext(req), fraccionamientoId, indicadorId);
|
|
res.status(200).json({ success: true, data: metas });
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
});
|
|
|
|
// ========== Valores de Indicadores ==========
|
|
|
|
/**
|
|
* GET /indicadores/:id/valores
|
|
* Listar valores de un indicador
|
|
*/
|
|
router.get('/:id/valores', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise<void> => {
|
|
try {
|
|
const tenantId = req.tenantId;
|
|
if (!tenantId) {
|
|
res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' });
|
|
return;
|
|
}
|
|
|
|
const page = parseInt(req.query.page as string) || 1;
|
|
const limit = Math.min(parseInt(req.query.limit as string) || 50, 200);
|
|
const fraccionamientoId = req.query.fraccionamientoId as string;
|
|
|
|
const result = await indicadorService.findValores(
|
|
getContext(req),
|
|
req.params.id,
|
|
fraccionamientoId,
|
|
page,
|
|
limit
|
|
);
|
|
|
|
res.status(200).json({
|
|
success: true,
|
|
data: result.data,
|
|
pagination: {
|
|
total: result.total,
|
|
page: result.page,
|
|
limit: result.limit,
|
|
totalPages: result.totalPages,
|
|
},
|
|
});
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
});
|
|
|
|
// ========== Horas Trabajadas ==========
|
|
|
|
/**
|
|
* GET /indicadores/horas-trabajadas/:fraccionamientoId
|
|
* Obtener horas trabajadas por obra
|
|
*/
|
|
router.get('/horas-trabajadas/:fraccionamientoId', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise<void> => {
|
|
try {
|
|
const year = req.query.year ? parseInt(req.query.year as string) : undefined;
|
|
const horas = await indicadorService.findHorasTrabajadas(
|
|
getContext(req),
|
|
req.params.fraccionamientoId,
|
|
year
|
|
);
|
|
res.status(200).json({ success: true, data: horas });
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
});
|
|
|
|
// ========== Días Sin Accidente ==========
|
|
|
|
/**
|
|
* GET /indicadores/dias-sin-accidente/:fraccionamientoId
|
|
* Obtener días sin accidente por obra
|
|
*/
|
|
router.get('/dias-sin-accidente/:fraccionamientoId', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise<void> => {
|
|
try {
|
|
const dias = await indicadorService.findDiasSinAccidente(
|
|
getContext(req),
|
|
req.params.fraccionamientoId
|
|
);
|
|
res.status(200).json({ success: true, data: dias });
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
});
|
|
|
|
// ========== Reportes Programados ==========
|
|
|
|
/**
|
|
* GET /indicadores/reportes
|
|
* Listar reportes programados
|
|
*/
|
|
router.get('/reportes/list', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise<void> => {
|
|
try {
|
|
const reportes = await indicadorService.findReportes(getContext(req));
|
|
res.status(200).json({ success: true, data: reportes });
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
});
|
|
|
|
// ========== Alertas ==========
|
|
|
|
/**
|
|
* GET /indicadores/alertas
|
|
* Listar alertas de indicadores
|
|
*/
|
|
router.get('/alertas/list', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise<void> => {
|
|
try {
|
|
const tenantId = req.tenantId;
|
|
if (!tenantId) {
|
|
res.status(400).json({ error: 'Bad Request', message: 'Tenant ID required' });
|
|
return;
|
|
}
|
|
|
|
const page = parseInt(req.query.page as string) || 1;
|
|
const limit = Math.min(parseInt(req.query.limit as string) || 20, 100);
|
|
const fraccionamientoId = req.query.fraccionamientoId as string;
|
|
|
|
const result = await indicadorService.findAlertas(getContext(req), fraccionamientoId, page, limit);
|
|
|
|
res.status(200).json({
|
|
success: true,
|
|
data: result.data,
|
|
pagination: {
|
|
total: result.total,
|
|
page: result.page,
|
|
limit: result.limit,
|
|
totalPages: result.totalPages,
|
|
},
|
|
});
|
|
} catch (error) {
|
|
next(error);
|
|
}
|
|
});
|
|
|
|
return router;
|
|
}
|
|
|
|
export default createIndicadorController;
|