82 lines
2.6 KiB
TypeScript
82 lines
2.6 KiB
TypeScript
export interface ExportColumn<T> {
|
|
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<T>(
|
|
fetchPage: (page: number, pageSize: number) => Promise<{ items: T[]; total: number }>,
|
|
): Promise<T[]> {
|
|
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<T>(name: string, columns: ExportColumn<T>[], 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<T>(name: string, title: string, columns: ExportColumn<T>[], 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`)
|
|
}
|