HR-ATS-Portal/frontend/src/lib/exportXlsx.js

101 lines
4.1 KiB
JavaScript

/* ============================================================
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<object>} 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)
}