/ frontend / src / app / utils / csvExport.js
csvExport.js
 1  // RFC 4180 CSV field escape: wrap in quotes when the value contains a
 2  // quote, comma, or newline; escape inner quotes by doubling them.
 3  function escapeCell(v) {
 4    if (v == null) return "";
 5    const s = typeof v === "string" ? v : (typeof v === "object" ? JSON.stringify(v) : String(v));
 6    if (/[",\n\r]/.test(s)) {
 7      return `"${s.replace(/"/g, '""')}"`;
 8    }
 9    return s;
10  }
11  
12  /**
13   * Build a CSV string from an array of row objects.
14   *
15   * @param {object[]} rows — data rows
16   * @param {Array<{key: string, header?: string, get?: (row) => any}>} columns
17   *   `get` takes precedence over `row[key]`; `header` falls back to `key`.
18   * @returns {string} CSV with CRLF line endings (Excel-friendly).
19   */
20  export function toCsv(rows, columns) {
21    const headerLine = columns.map((c) => escapeCell(c.header || c.key)).join(",");
22    const dataLines = rows.map((r) =>
23      columns.map((c) => escapeCell(c.get ? c.get(r) : r[c.key])).join(",")
24    );
25    // Excel on Windows reads CRLF more reliably than LF, and Excel-on-Mac
26    // handles either — CRLF is the safe default.
27    return [headerLine, ...dataLines].join("\r\n");
28  }
29  
30  /**
31   * Trigger a browser download of the given CSV content. `filename` gets a
32   * `.csv` suffix if not already present.
33   */
34  export function downloadCsv(filename, csv) {
35    const name = filename.endsWith(".csv") ? filename : `${filename}.csv`;
36    // `` BOM so Excel on Windows auto-detects UTF-8. Harmless for
37    // other tooling — pandas / Numbers / Sheets strip the BOM on read.
38    const blob = new Blob(["", csv], { type: "text/csv;charset=utf-8;" });
39    const href = URL.createObjectURL(blob);
40    const a = document.createElement("a");
41    a.href = href;
42    a.download = name;
43    document.body.appendChild(a);
44    a.click();
45    document.body.removeChild(a);
46    // Give the browser a tick before revoking so the download actually fires.
47    setTimeout(() => URL.revokeObjectURL(href), 0);
48  }