/** * CSV helpers untuk admin export. Simple string-building — bukan streaming — * karena admin export jarang lebih dari 10k row di MVP. * * Escape rule (RFC 4180): * - Field yang berisi koma, quote, CR, atau LF → bungkus quote, escape quote * internal dengan dobel quote. * - Field lain biarkan apa adanya. */ /** Escape satu cell sesuai aturan RFC 4180. */ export function escapeCsvCell(value: unknown): string { if (value == null) return ""; const str = String(value); if (/[",\r\n]/.test(str)) { return `"${str.replace(/"/g, '""')}"`; } return str; } /** * Bangun string CSV lengkap dari headers + rows. Pakai CRLF (RFC 4180) supaya * Excel di Windows happy. */ export function buildCsv(headers: string[], rows: unknown[][]): string { const lines = [headers.map(escapeCsvCell).join(",")]; for (const row of rows) { lines.push(row.map(escapeCsvCell).join(",")); } return lines.join("\r\n") + "\r\n"; } /** * Bikin Response CSV siap pakai dari Next route handler. * BOM ditambahkan supaya Excel auto-detect UTF-8 untuk karakter non-ASCII * (mis. nama Indonesia dengan diakritik). */ export function csvResponse(filename: string, csv: string): Response { const bom = ""; return new Response(bom + csv, { status: 200, headers: { "Content-Type": "text/csv; charset=utf-8", "Content-Disposition": `attachment; filename="${filename}"`, "Cache-Control": "no-store", }, }); } /** Format ISO untuk CSV (UTC, sortable). */ export function csvDate(d: Date | null | undefined): string { if (!d) return ""; return d.toISOString(); } /** Tanggal Jakarta yang readable di Excel. */ export function csvDateJakarta(d: Date | null | undefined): string { if (!d) return ""; return d.toLocaleString("id-ID", { day: "2-digit", month: "2-digit", year: "numeric", hour: "2-digit", minute: "2-digit", timeZone: "Asia/Jakarta", }); }