export interface ExportColumn { header: string value: (row: T) => string | number | null | undefined /** Right-align and format as a number in Excel. */ numeric?: boolean } const MAX_PAGE_SIZE = 200 /** Walks every page of a server-paged endpoint and returns all rows. */ export async function fetchAllPages( fetchPage: (page: number, pageSize: number) => Promise<{ items: T[]; total: number }>, ): Promise { const rows: T[] = [] for (let page = 1; ; page++) { const res = await fetchPage(page, MAX_PAGE_SIZE) rows.push(...res.items) if (res.items.length === 0 || rows.length >= res.total) return rows } } function stamp() { return new Date().toISOString().slice(0, 10) } function download(blob: Blob, filename: string) { const url = URL.createObjectURL(blob) const a = document.createElement('a') a.href = url a.download = filename a.click() URL.revokeObjectURL(url) } export async function exportExcel(name: string, columns: ExportColumn[], rows: T[]) { const { Workbook } = await import('exceljs') const wb = new Workbook() const ws = wb.addWorksheet(name.slice(0, 31)) ws.columns = columns.map(c => ({ header: c.header, width: Math.max(c.header.length + 4, 14) })) ws.getRow(1).font = { bold: true } ws.views = [{ state: 'frozen', ySplit: 1 }] rows.forEach(r => ws.addRow(columns.map(c => c.value(r) ?? ''))) columns.forEach((c, i) => { if (c.numeric) { const col = ws.getColumn(i + 1) col.numFmt = '#,##0.##' col.alignment = { horizontal: 'right' } } }) const buffer = await wb.xlsx.writeBuffer() download( new Blob([buffer], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' }), `${name}-${stamp()}.xlsx`, ) } export async function exportPdf(name: string, title: string, columns: ExportColumn[], rows: T[], subtitle?: string) { const [{ jsPDF }, { default: autoTable }] = await Promise.all([import('jspdf'), import('jspdf-autotable')]) const doc = new jsPDF({ orientation: columns.length > 6 ? 'landscape' : 'portrait', unit: 'pt', format: 'a4' }) doc.setFontSize(14) doc.text(title, 40, 40) doc.setFontSize(9) doc.text(subtitle ? `${subtitle} ยท Generated ${stamp()}` : `Generated ${stamp()}`, 40, 56) autoTable(doc, { startY: 68, head: [columns.map(c => c.header)], body: rows.map(r => columns.map(c => String(c.value(r) ?? ''))), styles: { fontSize: 8 }, headStyles: { fillColor: [55, 65, 81] }, columnStyles: Object.fromEntries( columns.map((c, i) => [i, c.numeric ? { halign: 'right' as const } : {}]), ), }) doc.save(`${name}-${stamp()}.pdf`) }