download excel
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 2m51s

This commit is contained in:
Syasya 2025-08-29 15:29:59 +08:00
parent eac2bb51e2
commit ed131acab4

View File

@ -336,6 +336,47 @@ useEffect(() => {
}
};
// helpers
// helpers
const ymd = (d: Date) => d.toISOString().slice(0, 10);
const excelUrl = (site: string, device: string, fn: 'grid' | 'solar', dateYMD: string) =>
`${API}/excel-fs/${encodeURIComponent(site)}/${encodeURIComponent(device)}/${fn}/${dateYMD}.xlsx`;
// popup state
const [isDownloadOpen, setIsDownloadOpen] = useState(false);
const [meter, setMeter] = useState('01'); // ADW300 device id
const [fn, setFn] = useState<'grid' | 'solar'>('grid'); // which function
const [downloadDate, setDownloadDate] = useState(ymd(new Date())); // YYYY-MM-DD
const [downloading, setDownloading] = useState(false);
// action
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' });
if (!resp.ok) {
const text = await resp.text().catch(() => '');
throw new Error(text || `HTTP ${resp.status}`);
}
const blob = await resp.blob();
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = `${meter}_${fn}_${downloadDate}.xlsx`;
document.body.appendChild(a);
a.click();
a.remove();
URL.revokeObjectURL(a.href);
setIsDownloadOpen(false);
} catch (e: any) {
alert(`Download failed: ${e?.message ?? e}`);
} finally {
setDownloading(false);
}
};
// ---------- RENDER ----------
if (!authChecked) {
return <div>Checking authentication</div>;
@ -459,25 +500,154 @@ useEffect(() => {
/>
<div className="flex flex-col md:flex-row gap-4 justify-center">
<button
onClick={handlePDFExport}
className="text-sm lg:text-lg btn-primary"
>
<button onClick={handlePDFExport} className="text-sm lg:text-lg btn-primary">
Export Chart Images to PDF
</button>
<a
href={`https://drive.google.com/drive/folders/${process.env.NEXT_PUBLIC_GOOGLE_DRIVE_FOLDER_ID}`}
target="_blank"
rel="noopener noreferrer"
<button
onClick={() => setIsDownloadOpen(true)}
className="text-sm lg:text-lg btn-primary"
>
View Excel Logs
</a>
Download Excel Log
</button>
</div>
</>
)}
{isDownloadOpen && (
<div
className="fixed inset-0 z-50 flex items-center justify-center"
aria-modal="true"
role="dialog"
onKeyDown={(e) => e.key === 'Escape' && setIsDownloadOpen(false)}
>
{/* Backdrop */}
<div
className="absolute inset-0 bg-black/50"
onClick={() => setIsDownloadOpen(false)}
/>
{/* Modal */}
<div className="relative w-full max-w-lg mx-4 rounded-2xl bg-white dark:bg-rtgray-800 shadow-2xl">
<div className="p-5 sm:p-6 border-b border-black/5 dark:border-white/10">
<h3 className="text-lg font-semibold text-black/90 dark:text-white">
Download Excel Log
</h3>
<p className="mt-1 text-sm text-black/60 dark:text-white/60">
Choose device, function, and date to export the .xlsx generated by the logger.
</p>
</div>
<div className="p-5 sm:p-6 space-y-5">
{/* Site (read-only preview) */}
<div>
<label className="block text-sm opacity-80 mb-1 dark:text-white">Site</label>
<div className="px-3 py-2 rounded-lg bg-black/5 dark:bg-white/5 text-sm truncate dark:text-white">
{selectedProject?.project_name || selectedProject?.name}
</div>
</div>
{/* Device + Function */}
<div className="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div>
<label className="block text-sm opacity-80 mb-1 dark:text-white">Meter (Device)</label>
<input
value={meter}
onChange={(e) => setMeter(e.target.value)}
placeholder="01"
className="input input-bordered w-full pl-2 rounded-lg"
/>
<p className="mt-1 text-xs opacity-70 dark:text-white">
Matches topic: <code>ADW300/&lt;site&gt;/<b>{meter || '01'}</b>/</code>
</p>
</div>
<div>
<label className="block text-sm opacity-80 mb-1 dark:text-white">Function</label>
<div className="flex rounded-xl overflow-hidden border border-black/10 dark:border-white/10">
<button
type="button"
onClick={() => setFn('grid')}
className={`flex-1 px-3 py-2 text-sm ${
fn === 'grid'
? 'bg-rtyellow-200 text-black'
: 'bg-black/5 dark:bg-white/5 hover:bg-black/10 dark:hover:bg-white/10 dark:text-white'
}`}
>
Grid
</button>
<button
type="button"
onClick={() => setFn('solar')}
className={`flex-1 px-3 py-2 text-sm ${
fn === 'solar'
? 'bg-rtyellow-200 text-black'
: 'bg-black/5 dark:bg-white/5 hover:bg-black/10 dark:hover:bg-white/10 dark:text-white'
}`}
>
Solar
</button>
</div>
</div>
</div>
{/* Date + quick picks */}
<div>
<label className="block text-sm opacity-80 mb-1 dark:text-white">Date</label>
<div className="flex items-center gap-3">
<input
type="date"
value={downloadDate}
onChange={(e) => setDownloadDate(e.target.value)}
className="input input-bordered w-48 pl-2 rounded-lg"
/>
<div className="flex gap-2">
<button
type="button"
className="px-3 py-1 rounded-full text-xs border border-black/10 dark:border-white/15 hover:bg-black/5 dark:hover:bg-white/10 dark:text-white"
onClick={() => setDownloadDate(ymd(new Date()))}
>
Today
</button>
<button
type="button"
className="px-3 py-1 rounded-full text-xs border border-black/10 dark:border-white/15 hover:bg-black/5 dark:hover:bg:white/10 dark:text-white"
onClick={() => {
const d = new Date();
d.setDate(d.getDate() - 1);
setDownloadDate(ymd(d));
}}
>
Yesterday
</button>
</div>
</div>
</div>
</div>
{/* Footer */}
<div className="p-5 sm:p-6 flex justify-end gap-3 border-t border-black/5 dark:border-white/10">
<button
type="button"
className="btn btn-primary bg-red-500 hover:bg-red-600 border-transparent"
onClick={() => setIsDownloadOpen(false)}
disabled={downloading}
>
Cancel
</button>
<button
type="button"
className="btn btn-primary border-transparent"
onClick={downloadExcel}
disabled={downloading || !meter || !downloadDate}
>
{downloading ? 'Preparing…' : 'Download'}
</button>
</div>
</div>
</div>
)}
</div>
</DashboardLayout>