/** * Exportación de reportes a Excel (.xlsx) con formato de marca SKEEN. * Usa exceljs cargado on-demand (dynamic import) para no inflar el bundle inicial. */ export interface ExportColumn { header: string; key: string; width?: number; format?: 'currency' | 'number' | 'date' | 'text' | 'percent'; } export interface ExportOptions { filename: string; sheetName: string; title: string; subtitle?: string; columns: ExportColumn[]; rows: Record[]; totals?: Record; } const NUM_FORMATS: Record = { currency: '"$"#,##0.00', number: '#,##0', date: 'DD/MM/YYYY', percent: '0%', }; const toCellValue = (value: unknown, format?: string) => { if (value === null || value === undefined || value === '') return ''; if (format === 'currency' || format === 'number') { const n = typeof value === 'number' ? value : parseFloat(String(value)); return Number.isNaN(n) ? String(value) : n; } if (format === 'percent') { const n = typeof value === 'number' ? value : parseFloat(String(value)); return Number.isNaN(n) ? String(value) : (n > 1 ? n / 100 : n); } if (format === 'date') { const s = String(value); const d = new Date(s.length === 10 ? `${s}T12:00:00` : s.replace(' ', 'T')); return Number.isNaN(d.getTime()) ? s : d; } return String(value); }; export const buildWorkbook = async (opts: ExportOptions) => { const mod: unknown = await import('exceljs'); const ExcelJS = ((mod as { default?: unknown }).default ?? mod) as typeof import('exceljs'); const wb = new ExcelJS.Workbook(); const ws = wb.addWorksheet(opts.sheetName.slice(0, 31)); const lastCol = Math.max(opts.columns.length, 1); const lastColLetter = ws.getColumn(lastCol).letter; // Título (marca) ws.mergeCells(`A1:${lastColLetter}1`); const titleCell = ws.getCell('A1'); titleCell.value = opts.title; titleCell.font = { bold: true, size: 14, color: { argb: 'FF1A1A1A' } }; titleCell.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFEDA588' } }; titleCell.alignment = { vertical: 'middle' }; ws.getRow(1).height = 24; let rowIdx = 2; if (opts.subtitle) { ws.mergeCells(`A2:${lastColLetter}2`); const sub = ws.getCell('A2'); sub.value = opts.subtitle; sub.font = { size: 10, color: { argb: 'FF6B6B6B' } }; rowIdx = 3; } rowIdx += 1; // línea en blanco // Encabezados const headerRow = ws.getRow(rowIdx); opts.columns.forEach((col, i) => { const cell = headerRow.getCell(i + 1); cell.value = col.header; cell.font = { bold: true, color: { argb: 'FFFFFFFF' } }; cell.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FF1A1A1A' } }; cell.border = { bottom: { style: 'thin', color: { argb: 'FFD2D6DE' } } }; cell.alignment = { vertical: 'middle' }; }); headerRow.height = 18; rowIdx += 1; // Datos for (const row of opts.rows) { const r = ws.getRow(rowIdx); opts.columns.forEach((col, i) => { const cell = r.getCell(i + 1); cell.value = toCellValue(row[col.key], col.format) as never; if (col.format && NUM_FORMATS[col.format]) { cell.numFmt = NUM_FORMATS[col.format]; } }); rowIdx += 1; } // Totales if (opts.totals) { const r = ws.getRow(rowIdx); opts.columns.forEach((col, i) => { const cell = r.getCell(i + 1); const v = opts.totals![col.key]; cell.value = (i === 0 && (v === undefined || v === '')) ? 'Totales' : toCellValue(v, col.format) as never; cell.font = { bold: true }; cell.border = { top: { style: 'thin', color: { argb: 'FF1A1A1A' } } }; if (col.format && NUM_FORMATS[col.format]) { cell.numFmt = NUM_FORMATS[col.format]; } }); } // Anchos: explícito o estimado por contenido (tope 50) opts.columns.forEach((col, i) => { if (col.width) { ws.getColumn(i + 1).width = col.width; return; } let max = col.header.length; for (const row of opts.rows.slice(0, 500)) { const v = row[col.key]; if (v !== null && v !== undefined) max = Math.max(max, String(v).length); } if (opts.totals && opts.totals[col.key] !== undefined) { max = Math.max(max, String(opts.totals[col.key]).length); } ws.getColumn(i + 1).width = Math.min(max + 3, 50); }); return wb; }; export const exportToExcel = async (opts: ExportOptions): Promise => { const wb = await buildWorkbook(opts); const buffer = await wb.xlsx.writeBuffer(); const blob = new Blob([buffer], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = opts.filename.endsWith('.xlsx') ? opts.filename : `${opts.filename}.xlsx`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); };