From 5c683f4c47ff354330271fc22d5139798db3c141 Mon Sep 17 00:00:00 2001 From: tiennm99 Date: Fri, 3 Apr 2026 23:21:25 +0700 Subject: [PATCH] feat: add multi-currency support with currency selector API now accepts currency param (USD, EUR, JPY, ALL, etc.). Returns array of rates per date instead of single object. Frontend adds currency dropdown with 12 common currencies plus "All". --- src/app/api/exchange-rates/route.js | 94 +++++++++++++----------- src/app/components/ExchangeRateViewer.js | 53 ++++++++++--- 2 files changed, 93 insertions(+), 54 deletions(-) diff --git a/src/app/api/exchange-rates/route.js b/src/app/api/exchange-rates/route.js index f0610d0..ba46c5f 100644 --- a/src/app/api/exchange-rates/route.js +++ b/src/app/api/exchange-rates/route.js @@ -12,7 +12,7 @@ const axiosInstance = axios.create({ const DISPLAY_DATE_FORMAT = "yyyy-MM-dd"; const PARALLEL_CHUNK_SIZE = 5; -async function fetchBIDVRates(dateObj) { +async function fetchBIDVRates(dateObj, currency) { const dateStr = format(dateObj, "dd/MM/yyyy"); try { const timeUrl = `https://bidv.com.vn/ServicesBIDV/ExchangeDetailSearchTimeServlet?date=${dateStr}`; @@ -30,24 +30,28 @@ async function fetchBIDVRates(dateObj) { const rateData = rateRes.data; if (rateData.status !== 1 || !rateData.data) return null; - const usd = rateData.data.find((item) => item.currency === "USD"); - if (!usd) return null; - return { - date: format(dateObj, DISPLAY_DATE_FORMAT), - nameVI: usd.nameVI || "", - muaTm: usd.muaTm || "", - muaCk: usd.muaCk || "", - currency: usd.currency || "USD", - nameEN: usd.nameEN || "", - ban: usd.ban || "", - }; + const displayDate = format(dateObj, DISPLAY_DATE_FORMAT); + const items = currency === "ALL" + ? rateData.data + : rateData.data.filter((item) => item.currency === currency); + if (items.length === 0) return null; + + return items.map((item) => ({ + date: displayDate, + nameVI: item.nameVI || "", + muaTm: item.muaTm || "", + muaCk: item.muaCk || "", + currency: item.currency || "", + nameEN: item.nameEN || "", + ban: item.ban || "", + })); } catch (error) { console.error("Error fetching BIDV rates:", error.message); return null; } } -async function fetchTCBRates(dateObj) { +async function fetchTCBRates(dateObj, currency) { const dateStr = format(dateObj, "yyyy-MM-dd"); const url = `https://techcombank.com/content/techcombank/web/vn/vi/cong-cu-tien-ich/ty-gia/_jcr_content.exchange-rates.${dateStr}.integration.json`; try { @@ -55,28 +59,29 @@ async function fetchTCBRates(dateObj) { const data = res.data; if (!data.exchangeRate?.data) return null; - const usd = data.exchangeRate.data.find( - (item) => item.label === "USD (50,100)" - ); - if (!usd) return null; - return { + const items = currency === "ALL" + ? data.exchangeRate.data + : data.exchangeRate.data.filter((item) => item.sourceCurrency === currency); + if (items.length === 0) return null; + + return items.map((item) => ({ date: dateStr, - label: usd.label || "", - askRate: usd.askRate || "", - bidRateCK: usd.bidRateCK || "", - bidRateTM: usd.bidRateTM || "", - sourceCurrency: usd.sourceCurrency || "", - targetCurrency: usd.targetCurrency || "", - askRateTM: usd.askRateTM || "", - }; + label: item.label || "", + askRate: item.askRate || "", + bidRateCK: item.bidRateCK || "", + bidRateTM: item.bidRateTM || "", + sourceCurrency: item.sourceCurrency || "", + targetCurrency: item.targetCurrency || "", + askRateTM: item.askRateTM || "", + })); } catch (error) { console.error("Error fetching TCB rates:", error.message); return null; } } -function fetchRateForDate(dateObj, bank) { - return bank === "bidv" ? fetchBIDVRates(dateObj) : fetchTCBRates(dateObj); +function fetchRateForDate(dateObj, bank, currency) { + return bank === "bidv" ? fetchBIDVRates(dateObj, currency) : fetchTCBRates(dateObj, currency); } function getDateRange(start, end) { @@ -91,7 +96,7 @@ function getDateRange(start, end) { } // Search backwards for the most recent available rate -async function getPreviousDayRate(date, bank, rateCache, maxAttempts = 30) { +async function getPreviousDayRate(date, bank, currency, rateCache, maxAttempts = 30) { let currentDate = new Date(date); for (let attempt = 0; attempt < maxAttempts; attempt++) { currentDate.setDate(currentDate.getDate() - 1); @@ -101,19 +106,19 @@ async function getPreviousDayRate(date, bank, rateCache, maxAttempts = 30) { return rateCache.get(dateStr); } - const rate = await fetchRateForDate(currentDate, bank); - if (rate) { - rateCache.set(dateStr, rate); - return rate; + const rates = await fetchRateForDate(currentDate, bank, currency); + if (rates) { + rateCache.set(dateStr, rates); + return rates; } } return null; } // Fetch a chunk of dates in parallel, filling gaps with previous day data -async function fetchChunk(dates, bank, rateCache) { +async function fetchChunk(dates, bank, currency, rateCache) { const settled = await Promise.allSettled( - dates.map((date) => fetchRateForDate(date, bank)) + dates.map((date) => fetchRateForDate(date, bank, currency)) ); const results = []; @@ -121,16 +126,17 @@ async function fetchChunk(dates, bank, rateCache) { const date = dates[i]; const dateStr = format(date, "yyyy-MM-dd"); const outcome = settled[i]; - let rate = outcome.status === "fulfilled" ? outcome.value : null; + let rates = outcome.status === "fulfilled" ? outcome.value : null; - if (rate) { - rateCache.set(dateStr, rate); - results.push(rate); + if (rates) { + rateCache.set(dateStr, rates); + results.push(...rates); } else { // Weekend/holiday/error — fill with most recent available rate - const prevRate = await getPreviousDayRate(date, bank, rateCache); - if (prevRate) { - results.push({ ...prevRate, date: format(date, DISPLAY_DATE_FORMAT) }); + const prevRates = await getPreviousDayRate(date, bank, currency, rateCache); + if (prevRates) { + const displayDate = format(date, DISPLAY_DATE_FORMAT); + results.push(...prevRates.map((r) => ({ ...r, date: displayDate }))); } } } @@ -140,7 +146,7 @@ async function fetchChunk(dates, bank, rateCache) { export async function POST(request) { try { const body = await request.json(); - const { startDate, endDate, bank } = body; + const { startDate, endDate, bank, currency = "USD" } = body; if (!startDate || !endDate || !bank) { return Response.json( { error: "Missing required parameters" }, @@ -155,7 +161,7 @@ export async function POST(request) { // Process dates in parallel chunks to avoid overwhelming upstream APIs for (let i = 0; i < dates.length; i += PARALLEL_CHUNK_SIZE) { const chunk = dates.slice(i, i + PARALLEL_CHUNK_SIZE); - const chunkResults = await fetchChunk(chunk, bank, rateCache); + const chunkResults = await fetchChunk(chunk, bank, currency, rateCache); results.push(...chunkResults); } diff --git a/src/app/components/ExchangeRateViewer.js b/src/app/components/ExchangeRateViewer.js index 77c9575..fda817c 100644 --- a/src/app/components/ExchangeRateViewer.js +++ b/src/app/components/ExchangeRateViewer.js @@ -103,6 +103,7 @@ function loadSavedSettings() { 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, }; @@ -111,10 +112,11 @@ function loadSavedSettings() { } } -function saveSettings(bank, startDate, endDate) { +function saveSettings(bank, currency, startDate, endDate) { try { localStorage.setItem("exchangeRateSettings", JSON.stringify({ bank, + currency, startDate: startDate?.toISOString(), endDate: endDate?.toISOString(), })); @@ -134,6 +136,10 @@ export default function ExchangeRateViewer() { 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([]); @@ -142,8 +148,8 @@ export default function ExchangeRateViewer() { // Persist settings to localStorage useEffect(() => { - saveSettings(selectedBank, startDate, endDate); - }, [selectedBank, startDate, endDate]); + saveSettings(selectedBank, selectedCurrency, startDate, endDate); + }, [selectedBank, selectedCurrency, startDate, endDate]); const handleFetch = useCallback(async () => { if (!startDate || !endDate) { @@ -167,6 +173,7 @@ export default function ExchangeRateViewer() { startDate: format(startDate, "yyyy-MM-dd"), endDate: format(endDate, "yyyy-MM-dd"), bank: selectedBank, + currency: selectedCurrency, }), }); if (!response.ok) { @@ -188,7 +195,7 @@ export default function ExchangeRateViewer() { setIsLoading(false); setProgress(null); } - }, [startDate, endDate, selectedBank]); + }, [startDate, endDate, selectedBank, selectedCurrency]); // Auto-fetch on initial load useEffect(() => { @@ -223,7 +230,7 @@ export default function ExchangeRateViewer() { const url = URL.createObjectURL(blob); const link = document.createElement("a"); link.href = url; - link.download = `exchange_rates_${selectedBank}_${new Date().toISOString().split("T")[0]}.csv`; + link.download = `exchange_rates_${selectedBank}_${selectedCurrency}_${new Date().toISOString().split("T")[0]}.csv`; document.body.appendChild(link); link.click(); document.body.removeChild(link); @@ -242,7 +249,7 @@ export default function ExchangeRateViewer() {

Exchange Rate Export

- Fetch USD exchange rates from Vietnamese banks and export to CSV. + Fetch exchange rates from Vietnamese banks and export to CSV.

@@ -254,8 +261,8 @@ export default function ExchangeRateViewer() { >
{/* Inputs row */} -
-
+
+
@@ -270,7 +277,33 @@ export default function ExchangeRateViewer() {
-
+
+ + +
+
@@ -284,7 +317,7 @@ export default function ExchangeRateViewer() { placeholderText="Select start date" />
-
+