admin roadmap csv export, adminactionlog, global search

This commit is contained in:
2026-05-18 20:09:22 +07:00
parent 244a6da9bb
commit ea63f56e97
25 changed files with 1330 additions and 158 deletions
+67
View File
@@ -0,0 +1,67 @@
/**
* 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",
});
}