/* ============================================================ exportXlsx.js — brand-styled spreadsheet exports. One shared exporter so every "Export" button produces the same dashboard-flavoured file: TalentFlow green title band, mint meta row, ink-teal header, zebra data rows, frozen header + autofilter. exceljs is ~1MB, so it is imported dynamically — Vite splits it into its own chunk that only ever downloads when an export is clicked. ============================================================ */ /** Utopia Brands palette (ARGB, from styles.css brand constants). */ const BRAND = { green: 'FF004D43', // deep green — title band, like the sidebar/action colour ink: 'FF1A3134', // ink teal — header row lime: 'FFCEFF71', // signature lime — title text accent mint: 'FFEAFFF4', // mint white — meta row fill zebra: 'FFF1F7F4', // app background — alternating data rows border: 'FFDBE8E2', // hairline borders text: 'FF10231F', sub: 'FF4A625C', white: 'FFFFFFFF', } const thin = { style: 'thin', color: { argb: BRAND.border } } /** * Build and download a styled .xlsx. * * @param {object} opts * @param {string} opts.filename without extension * @param {string} opts.title big brand-band line, e.g. "Recruitment Inbox" * @param {string} opts.subtitle meta line, e.g. "All Applications · 512 rows · 02/09/2026" * @param {Array<{header: string, key: string, width?: number}>} opts.columns * @param {Array} opts.rows keyed by columns[].key; null/undefined print blank */ export async function exportStyledXlsx({ filename, title, subtitle, columns, rows }) { const ExcelJS = (await import('exceljs')).default const wb = new ExcelJS.Workbook() wb.creator = 'TalentFlow ATS' wb.created = new Date() const ws = wb.addWorksheet(title.slice(0, 31) || 'Export', { views: [{ state: 'frozen', ySplit: 3 }], }) ws.columns = columns.map((c) => ({ key: c.key, width: c.width ?? 18 })) const span = columns.length // Row 1 — brand title band const titleRow = ws.addRow([title]) ws.mergeCells(1, 1, 1, span) titleRow.height = 30 const titleCell = ws.getCell(1, 1) titleCell.font = { name: 'Calibri', size: 14, bold: true, color: { argb: BRAND.lime } } titleCell.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: BRAND.green } } titleCell.alignment = { vertical: 'middle', indent: 1 } // Row 2 — meta line const metaRow = ws.addRow([subtitle]) ws.mergeCells(2, 1, 2, span) metaRow.height = 20 const metaCell = ws.getCell(2, 1) metaCell.font = { name: 'Calibri', size: 10, color: { argb: BRAND.sub } } metaCell.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: BRAND.mint } } metaCell.alignment = { vertical: 'middle', indent: 1 } // Row 3 — column headers const headRow = ws.addRow(columns.map((c) => c.header)) headRow.height = 22 headRow.eachCell((cell) => { cell.font = { name: 'Calibri', size: 10, bold: true, color: { argb: BRAND.white } } cell.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: BRAND.ink } } cell.alignment = { vertical: 'middle' } cell.border = { bottom: { style: 'medium', color: { argb: BRAND.lime } } } }) // Data — zebra rows with hairline borders for (const [i, r] of rows.entries()) { const row = ws.addRow(columns.map((c) => r[c.key] ?? '')) row.eachCell({ includeEmpty: true }, (cell) => { cell.font = { name: 'Calibri', size: 10, color: { argb: BRAND.text } } if (i % 2 === 1) cell.fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: BRAND.zebra } } cell.border = { bottom: thin, right: thin } cell.alignment = { vertical: 'middle', wrapText: false } }) } ws.autoFilter = { from: { row: 3, column: 1 }, to: { row: 3, column: span } } const buf = await wb.xlsx.writeBuffer() const blob = new Blob([buf], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' }) const a = document.createElement('a') a.href = URL.createObjectURL(blob) a.download = `${filename}.xlsx` document.body.appendChild(a) a.click() a.remove() URL.revokeObjectURL(a.href) }