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[], headers: Record): 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[], { 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[]); } 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[]; 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; }