export type CsvColumn = { header: string value: (row: T) => string | number } /** Escapes a single CSV cell (quotes wrapping + doubled inner quotes). */ function escapeCell(value: string | number): string { const str = String(value ?? '') if (/[",\n;]/.test(str)) { return `"${str.replace(/"/g, '""')}"` } return str } export function useCsvExport() { /** Builds a CSV string from rows + column definitions (semicolon separator, FR-friendly). */ function toCsv(rows: T[], columns: CsvColumn[]): string { const header = columns.map(c => escapeCell(c.header)).join(';') const lines = rows.map(row => columns.map(c => escapeCell(c.value(row))).join(';'), ) return [header, ...lines].join('\n') } /** Triggers a browser download of the given CSV content. */ function downloadCsv(filename: string, rows: T[], columns: CsvColumn[]): void { if (typeof window === 'undefined') { return } const csv = toCsv(rows, columns) // Prepend BOM so Excel detects UTF-8. const blob = new Blob([`${csv}`], { type: 'text/csv;charset=utf-8;' }) const url = URL.createObjectURL(blob) const link = document.createElement('a') link.href = url link.download = filename.endsWith('.csv') ? filename : `${filename}.csv` document.body.appendChild(link) link.click() document.body.removeChild(link) URL.revokeObjectURL(url) } return { toCsv, downloadCsv } }