diff --git a/src/modules/reports/controllers/report.controller.ts b/src/modules/reports/controllers/report.controller.ts index 5b3fad9..1e60aa0 100644 --- a/src/modules/reports/controllers/report.controller.ts +++ b/src/modules/reports/controllers/report.controller.ts @@ -9,6 +9,7 @@ import { Router, Request, Response, NextFunction } from 'express'; import { DataSource } from 'typeorm'; import { ReportService, CreateReportDto, UpdateReportDto, ReportFilters, ExecuteReportDto } from '../services/report.service'; +import { EarnedValueService } from '../services/earned-value.service'; import { AuthMiddleware } from '../../auth/middleware/auth.middleware'; import { AuthService } from '../../auth/services/auth.service'; import { User } from '../../core/entities/user.entity'; @@ -30,6 +31,7 @@ export function createReportController(dataSource: DataSource): Router { // Servicios const reportService = new ReportService(dataSource); + const evmService = new EarnedValueService(dataSource); const authService = new AuthService(userRepository, tenantRepository, refreshTokenRepository as any); const authMiddleware = new AuthMiddleware(authService, dataSource); @@ -44,6 +46,213 @@ export function createReportController(dataSource: DataSource): Router { }; }; + // ==================== Dashboard Endpoints (for frontend integration) ==================== + + /** + * GET /reports/dashboard/stats + * Get aggregate dashboard statistics + */ + router.get('/dashboard/stats', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { + try { + const ctx = getContext(req); + const portfolio = await evmService.getPortfolioSummary(ctx); + + res.status(200).json({ + totalProyectos: portfolio.projects.length, + proyectosActivos: portfolio.projects.filter(p => p.percentComplete < 100).length, + presupuestoTotal: portfolio.totals.totalBac, + avancePromedio: portfolio.projects.length > 0 + ? portfolio.projects.reduce((sum, p) => sum + p.percentComplete, 0) / portfolio.projects.length + : 0, + alertasActivas: portfolio.totals.projectsCritical + portfolio.totals.projectsAtRisk, + }); + } catch (error) { + next(error); + } + }); + + /** + * GET /reports/projects/summary + * Get projects summary with KPIs + */ + router.get('/projects/summary', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { + try { + const ctx = getContext(req); + const status = req.query.status as string | undefined; + const portfolio = await evmService.getPortfolioSummary(ctx); + + let items = portfolio.projects.map(p => ({ + id: p.fraccionamientoId, + nombre: p.name, + presupuesto: p.bac, + avanceReal: p.percentComplete, + avanceProgramado: p.percentComplete * (p.spi || 1), // Estimate planned progress + spi: p.spi, + cpi: p.cpi, + status: p.status, + })); + + // Filter by status if provided + if (status) { + items = items.filter(p => p.status === status); + } + + res.status(200).json({ + items, + total: items.length, + page: 1, + limit: items.length, + }); + } catch (error) { + next(error); + } + }); + + /** + * GET /reports/projects/:projectId/kpis + * Get KPIs for a specific project + */ + router.get('/projects/:projectId/kpis', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { + try { + const ctx = getContext(req); + const { projectId } = req.params; + const metrics = await evmService.getCurrentMetrics(ctx, projectId); + + res.status(200).json({ + ...metrics, + date: metrics.date.toISOString(), + }); + } catch (error) { + next(error); + } + }); + + /** + * GET /reports/projects/:projectId/earned-value + * Get Earned Value metrics for a project + */ + router.get('/projects/:projectId/earned-value', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { + try { + const ctx = getContext(req); + const { projectId } = req.params; + const metrics = await evmService.getCurrentMetrics(ctx, projectId); + + res.status(200).json({ + ...metrics, + date: metrics.date.toISOString(), + }); + } catch (error) { + next(error); + } + }); + + /** + * GET /reports/projects/:projectId/s-curve + * Get S-Curve data for a project + */ + router.get('/projects/:projectId/s-curve', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { + try { + const ctx = getContext(req); + const { projectId } = req.params; + const periodType = (req.query.periodType as 'daily' | 'weekly' | 'monthly') || 'weekly'; + + const curvaS = await evmService.getCurvaS(ctx, projectId, { periodType }); + + res.status(200).json(curvaS.map(point => ({ + date: point.date.toISOString(), + plannedValue: point.pv, + earnedValue: point.ev, + actualCost: point.ac, + plannedCumulative: point.pv, + earnedCumulative: point.ev, + actualCumulative: point.ac, + }))); + } catch (error) { + next(error); + } + }); + + /** + * GET /reports/alerts + * Get active alerts + */ + router.get('/alerts', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { + try { + const ctx = getContext(req); + const acknowledged = req.query.acknowledged === 'true'; + + // Get projects at risk or critical status as alerts + const portfolio = await evmService.getPortfolioSummary(ctx); + const alerts = portfolio.projects + .filter(p => p.status !== 'green') + .map((p, index) => ({ + id: `alert-${p.fraccionamientoId}-${index}`, + projectId: p.fraccionamientoId, + projectName: p.name, + type: p.spi < p.cpi ? 'schedule' : 'cost', + severity: p.status === 'red' ? 'critical' : 'warning', + title: p.status === 'red' + ? `Proyecto ${p.name} en estado crítico` + : `Proyecto ${p.name} requiere atención`, + message: p.spi < 0.95 + ? `SPI: ${p.spi.toFixed(2)} - Atraso en cronograma` + : `CPI: ${p.cpi.toFixed(2)} - Desviación en costo`, + metric: p.spi < p.cpi ? 'SPI' : 'CPI', + value: p.spi < p.cpi ? p.spi : p.cpi, + threshold: 0.95, + createdAt: new Date().toISOString(), + })); + + res.status(200).json({ + items: acknowledged ? [] : alerts, + total: acknowledged ? 0 : alerts.length, + page: 1, + limit: alerts.length, + }); + } catch (error) { + next(error); + } + }); + + /** + * PATCH /reports/alerts/:alertId/acknowledge + * Acknowledge an alert + */ + router.patch('/alerts/:alertId/acknowledge', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { + try { + const { alertId } = req.params; + + // For now, return success (alerts are derived from project status) + res.status(200).json({ + id: alertId, + acknowledgedAt: new Date().toISOString(), + message: 'Alert acknowledged', + }); + } catch (error) { + next(error); + } + }); + + /** + * PATCH /reports/alerts/:alertId/resolve + * Resolve an alert + */ + router.patch('/alerts/:alertId/resolve', authMiddleware.authenticate, async (req: Request, res: Response, next: NextFunction): Promise => { + try { + const { alertId } = req.params; + + res.status(200).json({ + id: alertId, + resolvedAt: new Date().toISOString(), + message: 'Alert resolved', + }); + } catch (error) { + next(error); + } + }); + + // ==================== Report Management Endpoints ==================== + /** * GET /reports * Listar reportes con filtros