feat: refactor

This commit is contained in:
tiennm99 committed 2025-06-27 22:07:15 +07:00
1 parent 1ec1dde0f5
commit 9e08d285d9
2 files changed
+177 -95

No files matched your search

+58 -35
View File
@@ -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" },
});
}
}
}
+119 -60
View File
@@ -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 })
<h1 className="text-2xl font-bold mb-4">Exchange Rate Export</h1>
<div className="controls grid grid-cols-1 sm:grid-cols-2 gap-4 mb-4">
<div className="form-group flex flex-col">
<label htmlFor="bank" className="mb-1">Bank:</label>
<select id="bank" value={selectedBank} onChange={e => setSelectedBank(e.target.value)} className="border rounded p-2">
<label htmlFor="bank" className="mb-1">
Bank:
</label>
<select
id="bank"
value={selectedBank}
onChange={(e) => setSelectedBank(e.target.value)}
className="border rounded p-2"
>
<option value="bidv">BIDV</option>
<option value="tcb">Techcombank</option>
</select>
</div>
<div className="form-group flex flex-col">
<label htmlFor="start-date" className="mb-1">Start Date:</label>
<label htmlFor="start-date" className="mb-1">
Start Date:
</label>
<DatePicker
id="start-date"
selected={startDate}
onChange={date => 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 })
/>
</div>
<div className="form-group flex flex-col">
<label htmlFor="end-date" className="mb-1">End Date:</label>
<label htmlFor="end-date" className="mb-1">
End Date:
</label>
<DatePicker
id="end-date"
selected={endDate}
onChange={date => 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 })
/>
</div>
<div className="button-group flex gap-2 items-end">
<button className="fetch-btn bg-blue-600 text-white px-4 py-2 rounded disabled:opacity-50" onClick={handleFetch} disabled={isLoading}>
{isLoading ? 'Fetching...' : 'Fetch Rates'}
<button
className="fetch-btn bg-blue-600 text-white px-4 py-2 rounded disabled:opacity-50"
onClick={handleFetch}
disabled={isLoading}
>
{isLoading ? "Fetching..." : "Fetch Rates"}
</button>
<button
className="export-btn bg-green-600 text-white px-4 py-2 rounded disabled:opacity-50"
@@ -138,20 +191,22 @@ export default function ExchangeRateViewer({ defaultStartDate, defaultEndDate })
</div>
</div>
{error && <div className="error text-red-600 mb-2">{error}</div>}
{isLoading && <div className="loading mb-2">Loading data, please wait...</div>}
{isLoading && (
<div className="loading mb-2">Loading data, please wait...</div>
)}
{showTable && (
<div className="table-container overflow-x-auto mt-4">
{selectedBank === 'bidv' ? (
{selectedBank === "bidv" ? (
<table className="min-w-full border border-gray-300">
<thead>
<tr>
<th className="border px-2 py-1">Date</th>
<th className="border px-2 py-1">Currency</th>
<th className="border px-2 py-1">Buy Cash</th>
<th className="border px-2 py-1">Buy Transfer</th>
<th className="border px-2 py-1">Sell</th>
<th className="border px-2 py-1">Name (VI)</th>
<th className="border px-2 py-1">Name (EN)</th>
<th className="border px-2 py-1">MuaTm</th>
<th className="border px-2 py-1">MuaCk</th>
<th className="border px-2 py-1">Ban</th>
<th className="border px-2 py-1">NameVI</th>
<th className="border px-2 py-1">NameEN</th>
</tr>
</thead>
<tbody>
@@ -174,12 +229,12 @@ export default function ExchangeRateViewer({ defaultStartDate, defaultEndDate })
<tr>
<th className="border px-2 py-1">Date</th>
<th className="border px-2 py-1">Label</th>
<th className="border px-2 py-1">Ask Rate</th>
<th className="border px-2 py-1">Bid Rate CK</th>
<th className="border px-2 py-1">Bid Rate TM</th>
<th className="border px-2 py-1">Ask Rate TM</th>
<th className="border px-2 py-1">From</th>
<th className="border px-2 py-1">To</th>
<th className="border px-2 py-1">AskRate</th>
<th className="border px-2 py-1">BidRateCK</th>
<th className="border px-2 py-1">BidRateTM</th>
<th className="border px-2 py-1">SourceCurrency</th>
<th className="border px-2 py-1">TargetCurrency</th>
<th className="border px-2 py-1">AskRateTM</th>
</tr>
</thead>
<tbody>
@@ -190,9 +245,13 @@ export default function ExchangeRateViewer({ defaultStartDate, defaultEndDate })
<td className="border px-2 py-1">{result.askRate}</td>
<td className="border px-2 py-1">{result.bidRateCK}</td>
<td className="border px-2 py-1">{result.bidRateTM}</td>
<td className="border px-2 py-1">
{result.sourceCurrency}
</td>
<td className="border px-2 py-1">
{result.targetCurrency}
</td>
<td className="border px-2 py-1">{result.askRateTM}</td>
<td className="border px-2 py-1">{result.sourceCurrency}</td>
<td className="border px-2 py-1">{result.targetCurrency}</td>
</tr>
))}
</tbody>
@@ -202,4 +261,4 @@ export default function ExchangeRateViewer({ defaultStartDate, defaultEndDate })
)}
</div>
);
}
}