"use client"; import React, { useState, useEffect, useCallback } from "react"; import DatePicker from "react-datepicker"; import "react-datepicker/dist/react-datepicker.css"; import { format, subDays, startOfMonth } from "date-fns"; import * as XLSX from "xlsx"; function LoadingSpinner({ progress }) { return (

{progress ? `Fetching ${progress.current}/${progress.total} days...` : "Fetching exchange rates..."}

); } function ErrorMessage({ message }) { return (
⚠ {message}
); } function EmptyState() { return (

No results yet

Select a bank and date range, then click Fetch Rates.

); } function RateTable({ results, selectedBank }) { const bidvHeaders = ["Date", "Name (VI)", "Buy TM", "Buy CK", "Currency", "Name (EN)", "Sell"]; const tcbHeaders = ["Date", "Label", "Ask Rate", "Bid CK", "Bid TM", "Source", "Target", "Ask TM"]; const headers = selectedBank === "bidv" ? bidvHeaders : tcbHeaders; const getRowCells = (row) => { if (selectedBank === "bidv") { return [row.date, row.nameVI, row.muaTm, row.muaCk, row.currency, row.nameEN, row.ban]; } return [row.date, row.label, row.askRate, row.bidRateCK, row.bidRateTM, row.sourceCurrency, row.targetCurrency, row.askRateTM]; }; return (
{headers.map((h) => ( ))} {results.map((row, idx) => ( {getRowCells(row).map((cell, i) => ( ))} ))}
{h}
{cell}
); } function loadSavedSettings() { if (typeof window === "undefined") return null; try { const saved = localStorage.getItem("exchangeRateSettings"); if (!saved) return null; const parsed = JSON.parse(saved); return { bank: parsed.bank || "bidv", currency: parsed.currency || "USD", startDate: parsed.startDate ? new Date(parsed.startDate) : null, endDate: parsed.endDate ? new Date(parsed.endDate) : null, }; } catch { return null; } } function saveSettings(bank, currency, startDate, endDate) { try { localStorage.setItem("exchangeRateSettings", JSON.stringify({ bank, currency, startDate: startDate?.toISOString(), endDate: endDate?.toISOString(), })); } catch { /* ignore quota errors */ } } export default function ExchangeRateViewer() { const [startDate, setStartDate] = useState(() => { const saved = loadSavedSettings(); return saved?.startDate || subDays(new Date(), 7); }); const [endDate, setEndDate] = useState(() => { const saved = loadSavedSettings(); return saved?.endDate || new Date(); }); const [selectedBank, setSelectedBank] = useState(() => { const saved = loadSavedSettings(); return saved?.bank || "bidv"; }); const [selectedCurrency, setSelectedCurrency] = useState(() => { const saved = loadSavedSettings(); return saved?.currency || "USD"; }); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(""); const [results, setResults] = useState([]); const [hasFetched, setHasFetched] = useState(false); const [progress, setProgress] = useState(null); // Persist settings to localStorage useEffect(() => { saveSettings(selectedBank, selectedCurrency, startDate, endDate); }, [selectedBank, selectedCurrency, startDate, endDate]); const handleFetch = useCallback(async () => { if (!startDate || !endDate) { setError("Please select both start and end dates"); return; } if (startDate > endDate) { setError("Start date cannot be after end date"); return; } setError(""); setIsLoading(true); setResults([]); setHasFetched(false); setProgress(null); try { const response = await fetch("/api/exchange-rates", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ startDate: format(startDate, "yyyy-MM-dd"), endDate: format(endDate, "yyyy-MM-dd"), bank: selectedBank, currency: selectedCurrency, }), }); if (!response.ok) { const errorData = await response.json(); throw new Error(errorData.error || "Failed to fetch exchange rates"); } // Stream progress from response const data = await response.json(); if (data.total) { setProgress({ current: data.total, total: data.total }); } setResults(data.data || []); setHasFetched(true); } catch (err) { setError(`An error occurred while fetching exchange rates: ${err.message}`); console.error(err); } finally { setIsLoading(false); setProgress(null); } }, [startDate, endDate, selectedBank, selectedCurrency]); // Auto-fetch on initial load useEffect(() => { handleFetch(); }, []); // eslint-disable-line react-hooks/exhaustive-deps const getExportRows = () => { if (selectedBank === "bidv") { const headers = ["Date", "NameVI", "MuaTm", "MuaCk", "Currency", "NameEN", "Ban"]; const rows = results.map((row) => [row.date, row.nameVI, row.muaTm, row.muaCk, row.currency, row.nameEN, row.ban]); return [headers, ...rows]; } const headers = ["Date", "Label", "AskRate", "BidRateCK", "BidRateTM", "SourceCurrency", "TargetCurrency", "AskRateTM"]; const rows = results.map((row) => [row.date, row.label, row.askRate, row.bidRateCK, row.bidRateTM, row.sourceCurrency, row.targetCurrency, row.askRateTM]); return [headers, ...rows]; }; const baseFilename = `exchange_rates_${selectedBank}_${selectedCurrency}_${new Date().toISOString().split("T")[0]}`; const handleExportExcel = () => { if (results.length === 0) return; const ws = XLSX.utils.aoa_to_sheet(getExportRows()); const wb = XLSX.utils.book_new(); XLSX.utils.book_append_sheet(wb, ws, "Exchange Rates"); XLSX.writeFile(wb, `${baseFilename}.xlsx`); }; const handleExportCsv = () => { if (results.length === 0) return; const ws = XLSX.utils.aoa_to_sheet(getExportRows()); const csv = XLSX.utils.sheet_to_csv(ws); const blob = new Blob([csv], { type: "text/csv;charset=utf-8;" }); const url = URL.createObjectURL(blob); const link = document.createElement("a"); link.href = url; link.download = `${baseFilename}.csv`; document.body.appendChild(link); link.click(); document.body.removeChild(link); URL.revokeObjectURL(url); }; const inputStyle = { background: "var(--input-bg)", color: "var(--foreground)", borderColor: "var(--input-border)", }; return (
{/* Header */}

Exchange Rate Export

Fetch exchange rates from Vietnamese banks and export to CSV.

{/* Controls card */}
{ if (e.key === "Enter" && !isLoading) handleFetch(); }} >
{/* Inputs row */}
setStartDate(date)} maxDate={endDate || new Date()} dateFormat="yyyy-MM-dd" className="border rounded-lg px-3 py-2 text-sm w-full transition-colors focus:outline-none focus:ring-2 focus:ring-blue-500/30" placeholderText="Select start date" />
setEndDate(date)} minDate={startDate} maxDate={new Date()} dateFormat="yyyy-MM-dd" className="border rounded-lg px-3 py-2 text-sm w-full transition-colors focus:outline-none focus:ring-2 focus:ring-blue-500/30" placeholderText="Select end date" />
{/* Date presets */}
{[ { label: "Today", start: new Date(), end: new Date() }, { label: "Last 7 days", start: subDays(new Date(), 7), end: new Date() }, { label: "Last 30 days", start: subDays(new Date(), 30), end: new Date() }, { label: "This month", start: startOfMonth(new Date()), end: new Date() }, ].map((preset) => ( ))}
{/* Buttons row */}
{/* Status area - announced to screen readers */}
{error && } {isLoading && } {!isLoading && hasFetched && results.length === 0 && } {!isLoading && hasFetched && results.length > 0 && (

{results.length} {results.length === 1 ? "record" : "records"} found

)}
); }