mirror of
https://github.com/tiennm99/exchange-rate-export.git
synced 2026-10-11 03:13:21 +00:00
feat: refactor
This commit is contained in:
1 parent
1ec1dde0f5
commit
9e08d285d9
2 files changed
+177
-95
No files matched your search
@@ -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" },
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user