/* |-------------------------------------------------------------------------- | Agência PSI |-------------------------------------------------------------------------- | Criado e desenvolvido por Leonardo Nohama | | Tecnologia aplicada à escuta. | Estrutura para o cuidado. | | Arquivo: src/utils/excelExport.js | Data: 2026 | Local: São Carlos/SP — Brasil |-------------------------------------------------------------------------- | © 2026 — Todos os direitos reservados |-------------------------------------------------------------------------- */ import ExcelJS from 'exceljs'; /** * Tipos de coluna suportados: * text — string (default) * money — cents (divide por 100 + formata BRL) * date — ISO/Date (formata dd/mm/yyyy hh:mm) * number — inteiro/decimal */ function fmtDateCell(value) { if (!value) return null; const d = value instanceof Date ? value : new Date(value); if (isNaN(d.getTime())) return String(value); return d; } function sanitizeText(value) { if (value === null || value === undefined) return ''; const s = String(value).replace(/[\r\n]+/g, ' ').trim(); // Previne fórmula Excel: prefixa com espaço se começar com =, +, -, @ return /^[=+\-@]/.test(s) ? ' ' + s : s; } /** * Gera workbook Excel a partir de headers e rows. * headers: [{ key, label, type: 'text'|'money'|'date'|'number', width?: number }] * rows: array de objetos */ export async function buildExcelBlob({ sheetName = 'Dados', headers, rows, footerRows = [] }) { const wb = new ExcelJS.Workbook(); wb.creator = 'AgenciaPSI'; wb.created = new Date(); const ws = wb.addWorksheet(sheetName, { views: [{ state: 'frozen', ySplit: 1 }] }); // ── Cabeçalho ────────────────────────────────────────────── ws.columns = headers.map((h) => ({ header: h.label, key: h.key, width: h.width || 18 })); const headerRow = ws.getRow(1); headerRow.font = { bold: true, color: { argb: 'FFFFFFFF' } }; headerRow.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FF374151' } }; headerRow.alignment = { vertical: 'middle', horizontal: 'left' }; headerRow.height = 20; // ── Rows ─────────────────────────────────────────────────── for (const row of rows) { const cells = {}; for (const h of headers) { const raw = row[h.key]; switch (h.type) { case 'money': { const cents = Number(raw) || 0; cells[h.key] = cents / 100; break; } case 'date': cells[h.key] = fmtDateCell(raw); break; case 'number': cells[h.key] = Number(raw) || 0; break; default: cells[h.key] = sanitizeText(raw); } } const r = ws.addRow(cells); // Formatação por tipo headers.forEach((h, idx) => { const cell = r.getCell(idx + 1); if (h.type === 'money') { cell.numFmt = '"R$" #,##0.00;[Red]-"R$" #,##0.00'; } else if (h.type === 'date') { cell.numFmt = 'dd/mm/yyyy hh:mm'; } else if (h.type === 'number') { cell.numFmt = '#,##0'; } }); } // ── Zebra (linhas pares) ─────────────────────────────────── for (let i = 2; i <= ws.rowCount; i++) { if (i % 2 === 0) { ws.getRow(i).fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFF9FAFB' } }; } } // ── Footer com resumo ────────────────────────────────────── if (footerRows.length) { ws.addRow([]); for (const fr of footerRows) { const r = ws.addRow(fr); r.font = { bold: true }; } } // ── Borders ──────────────────────────────────────────────── const borderStyle = { style: 'thin', color: { argb: 'FFE5E7EB' } }; ws.eachRow((row) => { row.eachCell((cell) => { cell.border = { top: borderStyle, left: borderStyle, bottom: borderStyle, right: borderStyle }; }); }); const buf = await wb.xlsx.writeBuffer(); return new Blob([buf], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' }); } export async function downloadExcel({ filename, ...opts }) { const blob = await buildExcelBlob(opts); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filename || `export-${Date.now()}.xlsx`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); }