import { unparse } from "papaparse"; /** * Shared CSV serialization for list-view exports (issue #605). * * Quoting/escaping is delegated to papaparse (`unparse`, RFC 4180); this * module only owns the cross-view conventions: column mapping, UTF-8 BOM, * and the filename timestamp — so the export format stays identical on every * dashboard. */ export interface CsvColumn { /** Header label — should match the visible column label in the UI. */ header: string; /** Cell value extractor. `null`/`undefined` render as an empty field. */ value: (row: T) => string | number | null | undefined; } /** Serialize rows to a CSV string with a header row (CRLF line endings). */ export function toCsv(rows: readonly T[], columns: readonly CsvColumn[]): string { return unparse( { fields: columns.map((c) => c.header), data: rows.map((row) => columns.map((c) => c.value(row) ?? "")), }, { newline: "\r\n" }, ); } /** * `YYYY-MM-DD-HHMM` local-time stamp for export filenames * (e.g. `purchase-orders-2026-06-08-1430.csv`). */ export function csvTimestamp(now: Date = new Date()): string { const pad = (n: number) => String(n).padStart(2, "0"); return ( `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}` + `-${pad(now.getHours())}${pad(now.getMinutes())}` ); } /** * Trigger a browser download of a CSV string. Prepends a UTF-8 BOM (U+FEFF) so * Excel detects the encoding and renders non-ASCII characters without mojibake. */ export function downloadCsv(csv: string, filename: string): void { const blob = new Blob(["\uFEFF" + csv], { type: "text/csv;charset=utf-8;" }); const url = URL.createObjectURL(blob); const anchor = document.createElement("a"); anchor.href = url; anchor.download = filename; document.body.appendChild(anchor); anchor.click(); document.body.removeChild(anchor); URL.revokeObjectURL(url); }