From 9e08d285d977480a8895b5f30d762be2f8445ac2 Mon Sep 17 00:00:00 2001 From: tiennm99 Date: Fri, 27 Jun 2025 22:07:15 +0700 Subject: [PATCH] feat: refactor --- src/app/api/exchange-rates/route.js | 93 +++++++----- src/app/components/ExchangeRateViewer.js | 179 +++++++++++++++-------- 2 files changed, 177 insertions(+), 95 deletions(-) diff --git a/src/app/api/exchange-rates/route.js b/src/app/api/exchange-rates/route.js index 244e747..d37f4d4 100644 --- a/src/app/api/exchange-rates/route.js +++ b/src/app/api/exchange-rates/route.js @@ -1,71 +1,83 @@ -import { format } from 'date-fns'; -import axios from 'axios'; -import https from 'https'; -import crypto from 'crypto'; +import { format } from "date-fns"; +import axios from "axios"; +import https from "https"; +import crypto from "crypto"; // Configure axios with legacy SSL support const axiosInstance = axios.create({ httpsAgent: new https.Agent({ secureOptions: crypto.constants.SSL_OP_LEGACY_SERVER_CONNECT, - }) + }), }); // Helper function to fetch BIDV exchange rates async function fetchBIDVRates(dateObj) { - const dateStr = format(dateObj, 'dd/MM/yyyy'); + const dateStr = format(dateObj, "dd/MM/yyyy"); try { // First API call to get namerecord - const timeRes = await axiosInstance.get(`https://bidv.com.vn/ServicesBIDV/ExchangeDetailSearchTimeServlet?date=${dateStr}`); + const timeUrl = `https://bidv.com.vn/ServicesBIDV/ExchangeDetailSearchTimeServlet?date=${dateStr}`; + console.log("[BIDV] Request URL:", timeUrl); + const timeRes = await axiosInstance.get(timeUrl); + console.log("[BIDV] Response:", JSON.stringify(timeRes.data)); const timeData = timeRes.data; if (timeData.status !== 1 || !timeData.data?.length) return null; // Get the latest record - const latest = timeData.data.reduce((latest, current) => - current.time > latest.time ? current : latest, timeData.data[0]); + const latest = timeData.data.reduce( + (latest, current) => (current.time > latest.time ? current : latest), + timeData.data[0] + ); // Second API call to get exchange rates - const rateRes = await axiosInstance.get(`https://bidv.com.vn/ServicesBIDV/ExchangeDetailServlet?date=${dateStr}&time=${latest.namerecord}`); + const rateUrl = `https://bidv.com.vn/ServicesBIDV/ExchangeDetailServlet?date=${dateStr}&time=${latest.namerecord}`; + console.log("[BIDV] Request URL:", rateUrl); + const rateRes = await axiosInstance.get(rateUrl); + console.log("[BIDV] Response:", JSON.stringify(rateRes.data)); const rateData = rateRes.data; if (rateData.status !== 1 || !rateData.data) return null; // Find USD data - const usd = rateData.data.find(item => item.currency === 'USD'); + const usd = rateData.data.find((item) => item.currency === "USD"); if (!usd) return null; return { date: dateStr, - nameVI: usd.nameVI || '', - muaTm: usd.muaTm || '', - muaCk: usd.muaCk || '', - currency: usd.currency || 'USD', - nameEN: usd.nameEN || '', - ban: usd.ban || '' + nameVI: usd.nameVI || "", + muaTm: usd.muaTm || "", + muaCk: usd.muaCk || "", + currency: usd.currency || "USD", + nameEN: usd.nameEN || "", + ban: usd.ban || "", }; } catch (error) { - console.error('Error fetching BIDV rates:', error); + console.error("Error fetching BIDV rates:", error); return null; } } // Helper function to fetch TCB exchange rates async function fetchTCBRates(dateObj) { - const dateStr = format(dateObj, 'yyyy-MM-dd'); + 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 { + console.log("[TCB] Request URL:", url); const res = await axiosInstance.get(url); + console.log("[TCB] Response:", JSON.stringify(res.data)); const data = res.data; if (!data.exchangeRate?.data) return null; // Find USD data - const usd = data.exchangeRate.data.find(item => item.label === 'USD (50,100)'); + const usd = data.exchangeRate.data.find( + (item) => item.label === "USD (50,100)" + ); if (!usd) return null; return { date: dateStr, - label: usd.label || '', - askRate: usd.askRate || '', - bidRateCK: usd.bidRateCK || '', - bidRateTM: usd.bidRateTM || '', - askRateTM: usd.askRateTM || '', - sourceCurrency: usd.sourceCurrency || 'USD', - targetCurrency: usd.targetCurrency || 'VND' + label: usd.label || "", + askRate: usd.askRate || "", + bidRateCK: usd.bidRateCK || "", + bidRateTM: usd.bidRateTM || "", + sourceCurrency: usd.sourceCurrency || "", + targetCurrency: usd.targetCurrency || "", + askRateTM: usd.askRateTM || "", }; } catch (error) { - console.error('Error fetching TCB rates:', error); + console.error("Error fetching TCB rates:", error); return null; } } @@ -85,18 +97,22 @@ function getDateRange(start, end) { export async function POST(request) { try { const body = await request.json(); + console.log("[POST] Payload:", JSON.stringify(body)); const { startDate, endDate, bank } = body; if (!startDate || !endDate || !bank) { - return new Response(JSON.stringify({ error: 'Missing required parameters' }), { status: 400, headers: { 'Content-Type': 'application/json' } }); + return new Response( + JSON.stringify({ error: "Missing required parameters" }), + { status: 400, headers: { "Content-Type": "application/json" } } + ); } const dates = getDateRange(new Date(startDate), new Date(endDate)); const results = []; for (const date of dates) { let rate = null; try { - if (bank === 'bidv') { + if (bank === "bidv") { rate = await fetchBIDVRates(date); - } else if (bank === 'tcb') { + } else if (bank === "tcb") { rate = await fetchTCBRates(date); } if (rate) { @@ -106,9 +122,16 @@ export async function POST(request) { console.error(`Error fetching rates for ${date}:`, error); } } - return new Response(JSON.stringify({ data: results }), { status: 200, headers: { 'Content-Type': 'application/json' } }); + console.log("[POST] Response:", JSON.stringify({ data: results })); + return new Response(JSON.stringify({ data: results }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); } catch (error) { - console.error('Server error:', error); - return new Response(JSON.stringify({ error: 'Internal server error' }), { status: 500, headers: { 'Content-Type': 'application/json' } }); + console.error("Server error:", error); + return new Response(JSON.stringify({ error: "Internal server error" }), { + status: 500, + headers: { "Content-Type": "application/json" }, + }); } -} \ No newline at end of file +} diff --git a/src/app/components/ExchangeRateViewer.js b/src/app/components/ExchangeRateViewer.js index aeac624..3753256 100644 --- a/src/app/components/ExchangeRateViewer.js +++ b/src/app/components/ExchangeRateViewer.js @@ -1,29 +1,40 @@ "use client"; -import React, { useState } from 'react'; -import DatePicker from 'react-datepicker'; -import 'react-datepicker/dist/react-datepicker.css'; -import { parse, isDate } from 'date-fns'; +import React, { useState } from "react"; +import DatePicker from "react-datepicker"; +import "react-datepicker/dist/react-datepicker.css"; +import { parse, isDate } from "date-fns"; -export default function ExchangeRateViewer({ defaultStartDate, defaultEndDate }) { +export default function ExchangeRateViewer({ + defaultStartDate, + defaultEndDate, +}) { const [startDate, setStartDate] = useState( - defaultStartDate ? (isDate(defaultStartDate) ? defaultStartDate : parse(defaultStartDate, 'yyyy-MM-dd', new Date())) : null + defaultStartDate + ? isDate(defaultStartDate) + ? defaultStartDate + : parse(defaultStartDate, "yyyy-MM-dd", new Date()) + : null ); const [endDate, setEndDate] = useState( - defaultEndDate ? (isDate(defaultEndDate) ? defaultEndDate : parse(defaultEndDate, 'yyyy-MM-dd', new Date())) : null + defaultEndDate + ? isDate(defaultEndDate) + ? defaultEndDate + : parse(defaultEndDate, "yyyy-MM-dd", new Date()) + : null ); - const [selectedBank, setSelectedBank] = useState('bidv'); + const [selectedBank, setSelectedBank] = useState("bidv"); const [isLoading, setIsLoading] = useState(false); - const [error, setError] = useState(''); + const [error, setError] = useState(""); const [results, setResults] = useState([]); const [showTable, setShowTable] = useState(false); const handleFetch = async () => { if (!startDate || !endDate) { - setError('Please select both start and end dates'); + setError("Please select both start and end dates"); return; } - setError(''); + setError(""); setIsLoading(true); setResults([]); setShowTable(false); @@ -31,30 +42,32 @@ export default function ExchangeRateViewer({ defaultStartDate, defaultEndDate }) const start = startDate; const end = endDate; if (start > end) { - setError('Start date cannot be after end date'); + setError("Start date cannot be after end date"); setIsLoading(false); return; } - const response = await fetch('/api/exchange-rates', { - method: 'POST', + const response = await fetch("/api/exchange-rates", { + method: "POST", headers: { - 'Content-Type': 'application/json', + "Content-Type": "application/json", }, body: JSON.stringify({ - startDate: start.toISOString().split('T')[0], - endDate: end.toISOString().split('T')[0], - bank: selectedBank - }) + startDate: start.toISOString().split("T")[0], + endDate: end.toISOString().split("T")[0], + bank: selectedBank, + }), }); if (!response.ok) { const errorData = await response.json(); - throw new Error(errorData.error || 'Failed to fetch exchange rates'); + throw new Error(errorData.error || "Failed to fetch exchange rates"); } const data = await response.json(); setResults(data.data || []); setShowTable(true); } catch (err) { - setError(`An error occurred while fetching exchange rates: ${err.message}`); + setError( + `An error occurred while fetching exchange rates: ${err.message}` + ); console.error(err); } finally { setIsLoading(false); @@ -63,26 +76,51 @@ export default function ExchangeRateViewer({ defaultStartDate, defaultEndDate }) const handleExport = () => { if (results.length === 0) return; - let csvContent = ''; + let csvContent = ""; let headers = []; - if (selectedBank === 'bidv') { - headers = ['Date', 'Currency', 'Buy Cash', 'Buy Transfer', 'Sell', 'Name VI', 'Name EN']; - csvContent = headers.join(',') + '\n'; - csvContent += results.map(row => - `"${row.date}","${row.currency}","${row.muaTm}","${row.muaCk}","${row.ban}","${row.nameVI}","${row.nameEN}"` - ).join('\n'); + if (selectedBank === "bidv") { + headers = [ + "Date", + "Currency", + "MuaTm", + "MuaCk", + "Ban", + "NameVI", + "NameEN", + ]; + csvContent = headers.join(",") + "\n"; + csvContent += results + .map( + (row) => + `"${row.date}","${row.currency}","${row.muaTm}","${row.muaCk}","${row.ban}","${row.nameVI}","${row.nameEN}"` + ) + .join("\n"); } else { - headers = ['Date', 'Label', 'Ask Rate', 'Bid Rate CK', 'Bid Rate TM', 'Ask Rate TM', 'From', 'To']; - csvContent = headers.join(',') + '\n'; - csvContent += results.map(row => - `"${row.date}","${row.label}","${row.askRate}","${row.bidRateCK}","${row.bidRateTM}","${row.askRateTM}","${row.sourceCurrency}","${row.targetCurrency}"` - ).join('\n'); + headers = [ + "Date", + "Label", + "AskRate", + "BidRateCK", + "BidRateTM", + "SourceCurrency", + "TargetCurrency", + "AskRateTM", + ]; + csvContent = headers.join(",") + "\n"; + csvContent += results + .map( + (row) => + `"${row.date}","${row.label}","${row.askRate}","${row.bidRateCK}","${row.bidRateTM}","${row.sourceCurrency}","${row.targetCurrency}","${row.askRateTM}"` + ) + .join("\n"); } - const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' }); + const blob = new Blob([csvContent], { type: "text/csv;charset=utf-8;" }); const url = URL.createObjectURL(blob); - const link = document.createElement('a'); + 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}_${ + new Date().toISOString().split("T")[0] + }.csv`; document.body.appendChild(link); link.click(); document.body.removeChild(link); @@ -93,18 +131,27 @@ export default function ExchangeRateViewer({ defaultStartDate, defaultEndDate })

Exchange Rate Export

- - setSelectedBank(e.target.value)} + className="border rounded p-2" + >
- + setStartDate(date)} + onChange={(date) => setStartDate(date)} maxDate={endDate || new Date()} dateFormat="yyyy-MM-dd" className="border rounded p-2" @@ -112,11 +159,13 @@ export default function ExchangeRateViewer({ defaultStartDate, defaultEndDate }) />
- + setEndDate(date)} + onChange={(date) => setEndDate(date)} minDate={startDate} maxDate={new Date()} dateFormat="yyyy-MM-dd" @@ -125,8 +174,12 @@ export default function ExchangeRateViewer({ defaultStartDate, defaultEndDate }) />
-
{error &&
{error}
} - {isLoading &&
Loading data, please wait...
} + {isLoading && ( +
Loading data, please wait...
+ )} {showTable && (
- {selectedBank === 'bidv' ? ( + {selectedBank === "bidv" ? ( - - - - - + + + + + @@ -174,12 +229,12 @@ export default function ExchangeRateViewer({ defaultStartDate, defaultEndDate }) - - - - - - + + + + + + @@ -190,9 +245,13 @@ export default function ExchangeRateViewer({ defaultStartDate, defaultEndDate }) + + - - ))} @@ -202,4 +261,4 @@ export default function ExchangeRateViewer({ defaultStartDate, defaultEndDate }) )} ); -} \ No newline at end of file +}
Date CurrencyBuy CashBuy TransferSellName (VI)Name (EN)MuaTmMuaCkBanNameVINameEN
Date LabelAsk RateBid Rate CKBid Rate TMAsk Rate TMFromToAskRateBidRateCKBidRateTMSourceCurrencyTargetCurrencyAskRateTM
{result.askRate} {result.bidRateCK} {result.bidRateTM} + {result.sourceCurrency} + + {result.targetCurrency} + {result.askRateTM}{result.sourceCurrency}{result.targetCurrency}