✅ 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:
166
src/utils/export.ts
Normal file
166
src/utils/export.ts
Normal file
@@ -0,0 +1,166 @@
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user