FASE 5 COMPLETADA: Analytics y Administración

Implementados 5 módulos de analytics con agent swarm:

1. DASHBOARD ADMINISTRATIVO
   - Resumen ejecutivo (reservas, ingresos, usuarios)
   - Vista del día con alertas
   - Calendario semanal de ocupación

2. MÉTRICAS DE OCUPACIÓN
   - Ocupación por fecha, cancha, franja horaria
   - Horas pico (top 5 demandados)
   - Comparativa entre períodos
   - Tendencias de uso

3. MÉTRICAS FINANCIERAS
   - Ingresos por período, cancha, tipo
   - Métodos de pago más usados
   - Estadísticas de reembolsos
   - Tendencias de crecimiento
   - Top días de ingresos

4. MÉTRICAS DE USUARIOS
   - Stats generales y actividad
   - Top jugadores (por partidos/victorias/puntos)
   - Detección de churn (riesgo de abandono)
   - Tasa de retención
   - Crecimiento mensual

5. EXPORTACIÓN DE DATOS
   - Exportar a CSV (separado por ;)
   - Exportar a JSON
   - Exportar a Excel (múltiples hojas)
   - Reportes completos descargables

Endpoints nuevos (solo admin):
- /analytics/dashboard/*
- /analytics/occupancy/*
- /analytics/revenue/*
- /analytics/reports/*
- /analytics/users/*
- /analytics/exports/*

Dependencias:
- xlsx - Generación de archivos Excel

Utilidades:
- Cálculo de crecimiento porcentual
- Formateo de moneda
- Agrupación por fechas
- Relleno de fechas faltantes
This commit is contained in:
2026-01-31 09:13:03 +00:00
parent b8a964dc2c
commit 5e50dd766f
31 changed files with 6068 additions and 3 deletions

View File

@@ -0,0 +1,142 @@
import { Request, Response, NextFunction } from 'express';
import { ReportService } from '../../services/analytics/report.service';
import { ApiError } from '../../middleware/errorHandler';
// Validar fechas
const validateDates = (startDateStr: string, endDateStr: string): { startDate: Date; endDate: Date } => {
const startDate = new Date(startDateStr);
const endDate = new Date(endDateStr);
if (isNaN(startDate.getTime()) || isNaN(endDate.getTime())) {
throw new ApiError('Fechas inválidas', 400);
}
if (startDate > endDate) {
throw new ApiError('La fecha de inicio debe ser anterior a la fecha de fin', 400);
}
// Ajustar endDate para incluir todo el día
endDate.setHours(23, 59, 59, 999);
return { startDate, endDate };
};
export class ReportController {
/**
* GET /analytics/reports/revenue
* Reporte completo de ingresos
*/
static async getRevenueReport(req: Request, res: Response, next: NextFunction) {
try {
const { startDate: startDateStr, endDate: endDateStr } = req.query;
if (!startDateStr || !endDateStr) {
throw new ApiError('Se requieren fechas de inicio y fin', 400);
}
const { startDate, endDate } = validateDates(startDateStr as string, endDateStr as string);
const report = await ReportService.generateRevenueReport(startDate, endDate);
res.status(200).json({
success: true,
data: report,
meta: {
reportType: 'revenue',
generatedAt: new Date().toISOString(),
},
});
} catch (error) {
next(error);
}
}
/**
* GET /analytics/reports/occupancy
* Reporte de ocupación
*/
static async getOccupancyReport(req: Request, res: Response, next: NextFunction) {
try {
const { startDate: startDateStr, endDate: endDateStr } = req.query;
if (!startDateStr || !endDateStr) {
throw new ApiError('Se requieren fechas de inicio y fin', 400);
}
const { startDate, endDate } = validateDates(startDateStr as string, endDateStr as string);
const report = await ReportService.generateOccupancyReport(startDate, endDate);
res.status(200).json({
success: true,
data: report,
meta: {
reportType: 'occupancy',
generatedAt: new Date().toISOString(),
},
});
} catch (error) {
next(error);
}
}
/**
* GET /analytics/reports/users
* Reporte de usuarios
*/
static async getUserReport(req: Request, res: Response, next: NextFunction) {
try {
const { startDate: startDateStr, endDate: endDateStr } = req.query;
if (!startDateStr || !endDateStr) {
throw new ApiError('Se requieren fechas de inicio y fin', 400);
}
const { startDate, endDate } = validateDates(startDateStr as string, endDateStr as string);
const report = await ReportService.generateUserReport(startDate, endDate);
res.status(200).json({
success: true,
data: report,
meta: {
reportType: 'users',
generatedAt: new Date().toISOString(),
},
});
} catch (error) {
next(error);
}
}
/**
* GET /analytics/reports/summary
* Resumen ejecutivo
*/
static async getExecutiveSummary(req: Request, res: Response, next: NextFunction) {
try {
const { startDate: startDateStr, endDate: endDateStr } = req.query;
if (!startDateStr || !endDateStr) {
throw new ApiError('Se requieren fechas de inicio y fin', 400);
}
const { startDate, endDate } = validateDates(startDateStr as string, endDateStr as string);
const summary = await ReportService.getReportSummary(startDate, endDate);
res.status(200).json({
success: true,
data: summary,
meta: {
reportType: 'executive-summary',
generatedAt: new Date().toISOString(),
},
});
} catch (error) {
next(error);
}
}
}
export default ReportController;