Files

68 lines
1.9 KiB
TypeScript
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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",
});
}