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
167 lines
4.8 KiB
TypeScript
167 lines
4.8 KiB
TypeScript
import { Response } from 'express';
|
|
import * as xlsx from 'xlsx';
|
|
import { CSV_SEPARATOR, ExportFormat } from '../constants/export.constants';
|
|
import { ExcelWorkbookData } from '../types/analytics.types';
|
|
|
|
/**
|
|
* Convierte datos a formato CSV
|
|
* @param data - Array de objetos a convertir
|
|
* @param headers - Mapeo de { clave: nombreColumna }
|
|
* @returns string en formato CSV
|
|
*/
|
|
export function toCSV(data: Record<string, unknown>[], headers: Record<string, string>): string {
|
|
if (data.length === 0) {
|
|
return Object.values(headers).join(CSV_SEPARATOR);
|
|
}
|
|
|
|
const headerRow = Object.values(headers).join(CSV_SEPARATOR);
|
|
const keys = Object.keys(headers);
|
|
|
|
const rows = data.map(item => {
|
|
return keys.map(key => {
|
|
const value = item[key];
|
|
if (value === null || value === undefined) {
|
|
return '';
|
|
}
|
|
if (typeof value === 'string' && (value.includes(CSV_SEPARATOR) || value.includes('"') || value.includes('\n'))) {
|
|
return `"${value.replace(/"/g, '""')}"`;
|
|
}
|
|
return String(value);
|
|
}).join(CSV_SEPARATOR);
|
|
});
|
|
|
|
return [headerRow, ...rows].join('\n');
|
|
}
|
|
|
|
/**
|
|
* Convierte datos a formato JSON
|
|
* @param data - Array de objetos a convertir
|
|
* @returns string en formato JSON
|
|
*/
|
|
export function toJSON(data: unknown[]): string {
|
|
return JSON.stringify(data, null, 2);
|
|
}
|
|
|
|
/**
|
|
* Genera un archivo Excel con múltiples hojas
|
|
* @param workbookData - Datos del workbook con múltiples hojas
|
|
* @returns Buffer con el archivo Excel
|
|
*/
|
|
export function generateExcel(workbookData: ExcelWorkbookData): Buffer {
|
|
const workbook = xlsx.utils.book_new();
|
|
|
|
workbookData.sheets.forEach(sheet => {
|
|
let worksheet: xlsx.WorkSheet;
|
|
|
|
if (sheet.headers && sheet.data.length > 0) {
|
|
// Si tenemos headers definidos, usamos json_to_sheet
|
|
worksheet = xlsx.utils.json_to_sheet(sheet.data as Record<string, unknown>[], {
|
|
header: sheet.headers
|
|
});
|
|
} else if (sheet.data.length > 0) {
|
|
// Sin headers explícitos, convertimos los objetos
|
|
worksheet = xlsx.utils.json_to_sheet(sheet.data as Record<string, unknown>[]);
|
|
} else {
|
|
// Hoja vacía
|
|
worksheet = xlsx.utils.aoa_to_sheet([[]]);
|
|
}
|
|
|
|
// Ajustar anchos de columna automáticamente
|
|
const colWidths: { wch: number }[] = [];
|
|
const data = sheet.data as Record<string, unknown>[];
|
|
|
|
if (data.length > 0) {
|
|
const keys = sheet.headers || Object.keys(data[0]);
|
|
keys.forEach((key, idx) => {
|
|
const maxLength = Math.max(
|
|
String(key).length,
|
|
...data.map(row => String(row[key] || '').length)
|
|
);
|
|
colWidths[idx] = { wch: Math.min(maxLength + 2, 50) };
|
|
});
|
|
worksheet['!cols'] = colWidths;
|
|
}
|
|
|
|
xlsx.utils.book_append_sheet(workbook, worksheet, sheet.name);
|
|
});
|
|
|
|
return xlsx.write(workbook, { type: 'buffer', bookType: 'xlsx' });
|
|
}
|
|
|
|
/**
|
|
* Configura los headers HTTP para la descarga de archivos
|
|
* @param res - Response de Express
|
|
* @param filename - Nombre del archivo
|
|
* @param format - Formato de exportación
|
|
*/
|
|
export function setExportHeaders(res: Response, filename: string, format: ExportFormat): void {
|
|
let contentType: string;
|
|
let extension: string;
|
|
|
|
switch (format) {
|
|
case ExportFormat.CSV:
|
|
contentType = 'text/csv; charset=utf-8';
|
|
extension = 'csv';
|
|
break;
|
|
case ExportFormat.JSON:
|
|
contentType = 'application/json; charset=utf-8';
|
|
extension = 'json';
|
|
break;
|
|
case ExportFormat.EXCEL:
|
|
contentType = 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
|
|
extension = 'xlsx';
|
|
break;
|
|
default:
|
|
contentType = 'application/octet-stream';
|
|
extension = 'txt';
|
|
}
|
|
|
|
// BOM para Excel español
|
|
if (format === ExportFormat.CSV) {
|
|
res.setHeader('Content-Type', `${contentType}; BOM=`);
|
|
} else {
|
|
res.setHeader('Content-Type', contentType);
|
|
}
|
|
|
|
res.setHeader(
|
|
'Content-Disposition',
|
|
`attachment; filename="${filename}_${new Date().toISOString().split('T')[0]}.${extension}"`
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Formatea una fecha para exportación
|
|
* @param date - Fecha a formatear
|
|
* @returns string en formato ISO 8601
|
|
*/
|
|
export function formatDateForExport(date: Date | null): string {
|
|
if (!date) return '';
|
|
return date.toISOString();
|
|
}
|
|
|
|
/**
|
|
* Formatea una fecha para display
|
|
* @param date - Fecha a formatear
|
|
* @returns string formateada
|
|
*/
|
|
export function formatDateForDisplay(date: Date | null): string {
|
|
if (!date) return '';
|
|
return date.toISOString().split('T')[0];
|
|
}
|
|
|
|
/**
|
|
* Escapa valores para CSV
|
|
* @param value - Valor a escapar
|
|
* @returns string escapado
|
|
*/
|
|
export function escapeCSVValue(value: unknown): string {
|
|
if (value === null || value === undefined) {
|
|
return '';
|
|
}
|
|
const str = String(value);
|
|
if (str.includes(CSV_SEPARATOR) || str.includes('"') || str.includes('\n') || str.includes('\r')) {
|
|
return `"${str.replace(/"/g, '""')}"`;
|
|
}
|
|
return str;
|
|
}
|