diff --git a/app/(admin)/adminDashboard/page.tsx b/app/(admin)/adminDashboard/page.tsx index 9a26ff7..9d0281e 100644 --- a/app/(admin)/adminDashboard/page.tsx +++ b/app/(admin)/adminDashboard/page.tsx @@ -14,6 +14,7 @@ import KpiBottom from '@/components/dashboards/kpibottom'; import { formatAddress } from '@/app/utils/formatAddress'; import { formatCrmTimestamp } from '@/app/utils/datetime'; import LoggingControlCard from '@/components/dashboards/LoggingControl'; +import { buildExportUrl, getFilenameFromCD } from "@/utils/export"; const EnergyLineChart = dynamic(() => import('@/components/dashboards/EnergyLineChart'), { ssr: false }); const MonthlyBarChart = dynamic(() => import('@/components/dashboards/MonthlyBarChart'), { ssr: false }); @@ -350,24 +351,71 @@ const [downloadDate, setDownloadDate] = useState(ymd(new Date())); // YYYY-MM-DD const [downloading, setDownloading] = useState(false); // action +// util: parse filename from Content-Disposition +function getFilenameFromCD(h: string | null): string | null { + if (!h) return null; + // filename*=UTF-8''name.ext (RFC 5987) + const star = /filename\*\s*=\s*([^']*)''([^;]+)/i.exec(h); + if (star && star[2]) return decodeURIComponent(star[2]); + + // filename="name.ext" or filename=name.ext + const plain = /filename\s*=\s*("?)([^";]+)\1/i.exec(h); + if (plain && plain[2]) return plain[2]; + + return null; +} + const downloadExcel = async () => { if (!selectedProject) return; try { setDownloading(true); - const url = excelUrl(selectedProject.name, meter.trim(), fn, downloadDate); - const resp = await fetch(url, { credentials: 'include' }); + + // Prefer the simple day-based export + const url = buildExportUrl({ + baseUrl: process.env.NEXT_PUBLIC_FASTAPI_URL, + site: selectedProject.name, + suffix: fn, + serial: meter?.trim() || undefined, + day: downloadDate, // "YYYY-MM-DD" + }); + + + const resp = await fetch(url, { credentials: "include" }); + if (!resp.ok) { - const text = await resp.text().catch(() => ''); - throw new Error(text || `HTTP ${resp.status}`); + // server might return JSON error; try to surface it nicely + const ctype = resp.headers.get("Content-Type") || ""; + let msg = `HTTP ${resp.status}`; + if (ctype.includes("application/json")) { + const j = await resp.json().catch(() => null); + if (j?.detail) msg = String(j.detail); + } else { + const t = await resp.text().catch(() => ""); + if (t) msg = t; + } + throw new Error(msg); } + const blob = await resp.blob(); - const a = document.createElement('a'); - a.href = URL.createObjectURL(blob); - a.download = `${meter}_${fn}_${downloadDate}.xlsx`; + + // 1) use server-provided filename if present + const cd = resp.headers.get("Content-Disposition"); + let downloadName = getFilenameFromCD(cd); + + // 2) client-side fallback (date-only as requested) + if (!downloadName) { + const serialPart = meter?.trim() ? meter.trim() : "ALL"; + downloadName = `${selectedProject.name}_${serialPart}_${fn}_${downloadDate}.xlsx`; + } + + const a = document.createElement("a"); + const href = URL.createObjectURL(blob); + a.href = href; + a.download = downloadName; document.body.appendChild(a); a.click(); a.remove(); - URL.revokeObjectURL(a.href); + URL.revokeObjectURL(href); setIsDownloadOpen(false); } catch (e: any) { alert(`Download failed: ${e?.message ?? e}`); @@ -377,6 +425,7 @@ const downloadExcel = async () => { }; + // ---------- RENDER ---------- if (!authChecked) { return
Checking authentication…
; diff --git a/utils/export.ts b/utils/export.ts new file mode 100644 index 0000000..36b7307 --- /dev/null +++ b/utils/export.ts @@ -0,0 +1,70 @@ +// utils/export.ts +export type ExportParams = { + baseUrl?: string; // e.g. process.env.NEXT_PUBLIC_API_URL + site: string; // PROJ-0028 + suffix?: "grid" | "solar"; // default "grid" + serial?: string | null; // device id like "01" + day?: string; // "YYYY-MM-DD" (preferred) + start?: string; // ISO string (if not using day) + end?: string; // ISO string (if not using day) + columns?: string[]; // optional list of columns + localTz?: string; // default "Asia/Kuala_Lumpur" +}; + +export function buildExportUrl(p: ExportParams): string { + const { + baseUrl = "", + site, + suffix = "grid", + serial, + day, + start, + end, + columns, + localTz = "Asia/Kuala_Lumpur", + } = p; + + const params = new URLSearchParams(); + params.set("site", site); + params.set("suffix", suffix); + params.set("local_tz", localTz); + + const s = serial?.trim(); + if (s) params.set("serial", s); + + if (day) { + params.set("day", day); // simple whole-day export + } else { + if (!start || !end) throw new Error("Provide either day=YYYY-MM-DD or both start and end."); + params.set("start", start); // URLSearchParams will encode '+' correctly + params.set("end", end); + } + + if (columns?.length) { + // backend expects ?columns=... repeated; append each + columns.forEach(c => params.append("columns", c)); + } + + // ensure there's a single slash join for /export/xlsx + const root = baseUrl.replace(/\/+$/, ""); + return `${root}/export/xlsx?${params.toString()}`; +} + +/** Parse filename from Content-Disposition (handles RFC5987 filename*) */ +export function getFilenameFromCD(cd: string | null): string | null { + if (!cd) return null; + + // filename*=UTF-8''encoded-name.xlsx + const star = /filename\*\s*=\s*([^']*)''([^;]+)/i.exec(cd); + if (star && star[2]) { + try { + return decodeURIComponent(star[2]); + } catch { + return star[2]; + } + } + + // filename="name.xlsx" OR filename=name.xlsx + const plain = /filename\s*=\s*("?)([^";]+)\1/i.exec(cd); + return plain ? plain[2] : null; +}