68 lines
1.9 KiB
TypeScript
68 lines
1.9 KiB
TypeScript
/**
|
||
* 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",
|
||
});
|
||
}
|