mirror of
https://github.com/tiennm99/exchange-rate-export.git
synced 2026-10-12 20:15:43 +00:00
feat(svelte): migrate to svelte
This commit is contained in:
1 parent
da3ebb5a45
commit
e8d82e0120
8 files changed
+546
-791
No files matched your search
@@ -1,266 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Exchange Rate Export</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
margin: 40px;
|
||||
background: #f7f7f7;
|
||||
}
|
||||
.container {
|
||||
background: #fff;
|
||||
padding: 24px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.07);
|
||||
max-width: 700px;
|
||||
margin: auto;
|
||||
}
|
||||
h1 {
|
||||
text-align: center;
|
||||
}
|
||||
label {
|
||||
margin-right: 8px;
|
||||
}
|
||||
input[type="date"] {
|
||||
margin-right: 16px;
|
||||
padding: 4px 8px;
|
||||
}
|
||||
button {
|
||||
padding: 8px 16px;
|
||||
margin-right: 8px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
background: #007bff;
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
}
|
||||
button:disabled {
|
||||
background: #aaa;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-top: 24px;
|
||||
}
|
||||
th, td {
|
||||
border: 1px solid #ddd;
|
||||
padding: 8px;
|
||||
text-align: center;
|
||||
}
|
||||
th {
|
||||
background: #007bff;
|
||||
color: #fff;
|
||||
}
|
||||
.loading {
|
||||
text-align: center;
|
||||
margin-top: 20px;
|
||||
color: #007bff;
|
||||
}
|
||||
.error {
|
||||
color: red;
|
||||
text-align: center;
|
||||
margin-top: 20px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>Exchange Rate Export (USD)</h1>
|
||||
<div style="margin-bottom: 20px;">
|
||||
<label for="start-date">Start Date:</label>
|
||||
<input type="date" id="start-date">
|
||||
<label for="end-date">End Date:</label>
|
||||
<input type="date" id="end-date">
|
||||
<button id="fetch-btn">Fetch</button>
|
||||
<button id="export-btn" disabled>Export CSV</button>
|
||||
</div>
|
||||
<div id="status"></div>
|
||||
<table id="result-table" style="display:none;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Date</th>
|
||||
<th>nameVI</th>
|
||||
<th>muaTm</th>
|
||||
<th>muaCk</th>
|
||||
<th>currency</th>
|
||||
<th>nameEN</th>
|
||||
<th>ban</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<script>
|
||||
const fetchBtn = document.getElementById('fetch-btn');
|
||||
const exportBtn = document.getElementById('export-btn');
|
||||
const startDateInput = document.getElementById('start-date');
|
||||
const endDateInput = document.getElementById('end-date');
|
||||
const statusDiv = document.getElementById('status');
|
||||
const table = document.getElementById('result-table');
|
||||
const tbody = table.querySelector('tbody');
|
||||
let results = [];
|
||||
|
||||
function formatDate(date) {
|
||||
// Format as dd/MM/yyyy
|
||||
const d = new Date(date);
|
||||
const day = String(d.getDate()).padStart(2, '0');
|
||||
const month = String(d.getMonth() + 1).padStart(2, '0');
|
||||
const year = d.getFullYear();
|
||||
return `${day}/${month}/${year}`;
|
||||
}
|
||||
|
||||
function getDateRange(start, end) {
|
||||
const dates = [];
|
||||
let current = new Date(start);
|
||||
const last = new Date(end);
|
||||
while (current <= last) {
|
||||
dates.push(new Date(current));
|
||||
current.setDate(current.getDate() + 1);
|
||||
}
|
||||
return dates;
|
||||
}
|
||||
|
||||
async function fetchNamerecord(dateStr) {
|
||||
const url = 'https://bidv.com.vn/ServicesBIDV/ExchangeDetailSearchTimeServlet';
|
||||
const payload = `date=${encodeURIComponent(dateStr)}`;
|
||||
try {
|
||||
const res = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body: payload
|
||||
});
|
||||
if (!res.ok) throw new Error('API 1 error');
|
||||
const data = await res.json();
|
||||
if (data.status !== 1 || !data.data || !data.data.length) return null;
|
||||
// Get the latest (max time) record
|
||||
let latest = data.data[0];
|
||||
for (const rec of data.data) {
|
||||
if (rec.time > latest.time) latest = rec;
|
||||
}
|
||||
return latest.namerecord;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchUSD(dateStr, namerecord) {
|
||||
const url = 'https://bidv.com.vn/ServicesBIDV/ExchangeDetailServlet';
|
||||
const payload = `date=${encodeURIComponent(dateStr)}&time=${namerecord}`;
|
||||
try {
|
||||
const res = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body: payload
|
||||
});
|
||||
if (!res.ok) throw new Error('API 2 error');
|
||||
const data = await res.json();
|
||||
if (data.status !== 1 || !data.data) return null;
|
||||
// Find USD (currency === 'USD')
|
||||
const usd = data.data.find(item => item.currency === 'USD');
|
||||
if (!usd) return null;
|
||||
return {
|
||||
nameVI: usd.nameVI,
|
||||
muaTm: usd.muaTm,
|
||||
muaCk: usd.muaCk,
|
||||
currency: usd.currency,
|
||||
nameEN: usd.nameEN,
|
||||
ban: usd.ban
|
||||
};
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function renderTable(data) {
|
||||
tbody.innerHTML = '';
|
||||
for (const row of data) {
|
||||
const tr = document.createElement('tr');
|
||||
tr.innerHTML = `
|
||||
<td>${row.date}</td>
|
||||
<td>${row.nameVI}</td>
|
||||
<td>${row.muaTm}</td>
|
||||
<td>${row.muaCk}</td>
|
||||
<td>${row.currency}</td>
|
||||
<td>${row.nameEN}</td>
|
||||
<td>${row.ban}</td>
|
||||
`;
|
||||
tbody.appendChild(tr);
|
||||
}
|
||||
table.style.display = data.length ? '' : 'none';
|
||||
}
|
||||
|
||||
function exportCSV(data) {
|
||||
const header = ['Date', 'nameVI', 'muaTm', 'muaCk', 'currency', 'nameEN', 'ban'];
|
||||
const rows = [header];
|
||||
for (const row of data) {
|
||||
rows.push([
|
||||
row.date,
|
||||
row.nameVI,
|
||||
row.muaTm,
|
||||
row.muaCk,
|
||||
row.currency,
|
||||
row.nameEN,
|
||||
row.ban
|
||||
]);
|
||||
}
|
||||
const csvContent = rows.map(r => r.map(cell => '"' + String(cell).replace(/"/g, '""') + '"').join(',')).join('\r\n');
|
||||
const blob = new Blob([csvContent], { type: 'text/csv' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = 'usd_exchange_rates.csv';
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
fetchBtn.onclick = async () => {
|
||||
const start = startDateInput.value;
|
||||
const end = endDateInput.value;
|
||||
if (!start || !end) {
|
||||
statusDiv.innerHTML = '<div class="error">Please select both start and end dates.</div>';
|
||||
return;
|
||||
}
|
||||
if (start > end) {
|
||||
statusDiv.innerHTML = '<div class="error">Start date must be before or equal to end date.</div>';
|
||||
return;
|
||||
}
|
||||
statusDiv.innerHTML = '<div class="loading">Fetching data, please wait...</div>';
|
||||
fetchBtn.disabled = true;
|
||||
exportBtn.disabled = true;
|
||||
results = [];
|
||||
renderTable([]);
|
||||
const dates = getDateRange(start, end);
|
||||
for (let i = 0; i < dates.length; i++) {
|
||||
const dateObj = dates[i];
|
||||
const dateStr = formatDate(dateObj);
|
||||
statusDiv.innerHTML = `<div class=\"loading\">Fetching: ${dateStr} (${i+1}/${dates.length})</div>`;
|
||||
const namerecord = await fetchNamerecord(dateStr);
|
||||
if (!namerecord) continue;
|
||||
const usd = await fetchUSD(dateStr, namerecord);
|
||||
if (usd) {
|
||||
results.push({ date: dateStr, ...usd });
|
||||
renderTable(results);
|
||||
}
|
||||
}
|
||||
statusDiv.innerHTML = results.length ? '' : '<div class="error">No data found for the selected range.</div>';
|
||||
fetchBtn.disabled = false;
|
||||
exportBtn.disabled = results.length === 0;
|
||||
};
|
||||
|
||||
exportBtn.onclick = () => {
|
||||
if (results.length) exportCSV(results);
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
-279
@@ -1,279 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Exchange Rate Export</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
margin: 40px;
|
||||
background: #f7f7f7;
|
||||
}
|
||||
.container {
|
||||
background: #fff;
|
||||
padding: 24px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.07);
|
||||
max-width: 700px;
|
||||
margin: auto;
|
||||
}
|
||||
h1 {
|
||||
text-align: center;
|
||||
}
|
||||
label {
|
||||
margin-right: 8px;
|
||||
}
|
||||
input[type="date"] {
|
||||
margin-right: 16px;
|
||||
padding: 4px 8px;
|
||||
}
|
||||
button {
|
||||
padding: 8px 16px;
|
||||
margin-right: 8px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
background: #007bff;
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
}
|
||||
button:disabled {
|
||||
background: #aaa;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-top: 24px;
|
||||
}
|
||||
th, td {
|
||||
border: 1px solid #ddd;
|
||||
padding: 8px;
|
||||
text-align: center;
|
||||
}
|
||||
th {
|
||||
background: #007bff;
|
||||
color: #fff;
|
||||
}
|
||||
.loading {
|
||||
text-align: center;
|
||||
margin-top: 20px;
|
||||
color: #007bff;
|
||||
}
|
||||
.error {
|
||||
color: red;
|
||||
text-align: center;
|
||||
margin-top: 20px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>Exchange Rate Export (USD)</h1>
|
||||
<div style="margin-bottom: 20px;">
|
||||
<label for="start-date">Start Date:</label>
|
||||
<input type="date" id="start-date">
|
||||
<label for="end-date">End Date:</label>
|
||||
<input type="date" id="end-date">
|
||||
<button id="fetch-btn">Fetch</button>
|
||||
<button id="export-btn" disabled>Export CSV</button>
|
||||
<label for="bank-select">Bank:</label>
|
||||
<select id="bank-select">
|
||||
<option value="bidv">BIDV</option>
|
||||
<option value="tcb">Techcombank</option>
|
||||
</select>
|
||||
</div>
|
||||
<div id="status"></div>
|
||||
<table id="result-table" style="display:none;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Date</th>
|
||||
<th>nameVI</th>
|
||||
<th>muaTm</th>
|
||||
<th>muaCk</th>
|
||||
<th>currency</th>
|
||||
<th>nameEN</th>
|
||||
<th>ban</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<script>
|
||||
const fetchBtn = document.getElementById('fetch-btn');
|
||||
const exportBtn = document.getElementById('export-btn');
|
||||
const startDateInput = document.getElementById('start-date');
|
||||
const endDateInput = document.getElementById('end-date');
|
||||
const statusDiv = document.getElementById('status');
|
||||
const table = document.getElementById('result-table');
|
||||
const tbody = table.querySelector('tbody');
|
||||
let results = [];
|
||||
|
||||
function formatDate(date) {
|
||||
// Format as dd/MM/yyyy
|
||||
const d = new Date(date);
|
||||
const day = String(d.getDate()).padStart(2, '0');
|
||||
const month = String(d.getMonth() + 1).padStart(2, '0');
|
||||
const year = d.getFullYear();
|
||||
return `${day}/${month}/${year}`;
|
||||
}
|
||||
|
||||
function getDateRange(start, end) {
|
||||
const dates = [];
|
||||
let current = new Date(start);
|
||||
const last = new Date(end);
|
||||
while (current <= last) {
|
||||
dates.push(new Date(current));
|
||||
current.setDate(current.getDate() + 1);
|
||||
}
|
||||
return dates;
|
||||
}
|
||||
|
||||
async function fetchNamerecord(dateStr) {
|
||||
const url = 'https://bidv.com.vn/ServicesBIDV/ExchangeDetailSearchTimeServlet';
|
||||
const payload = `date=${encodeURIComponent(dateStr)}`;
|
||||
try {
|
||||
const res = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body: payload
|
||||
});
|
||||
if (!res.ok) throw new Error('API 1 error');
|
||||
const data = await res.json();
|
||||
if (data.status !== 1 || !data.data || !data.data.length) return null;
|
||||
// Get the latest (max time) record
|
||||
let latest = data.data[0];
|
||||
for (const rec of data.data) {
|
||||
if (rec.time > latest.time) latest = rec;
|
||||
}
|
||||
return latest.namerecord;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function fetchUSD(dateStr, namerecord) {
|
||||
const url = 'https://bidv.com.vn/ServicesBIDV/ExchangeDetailServlet';
|
||||
const payload = `date=${encodeURIComponent(dateStr)}&time=${namerecord}`;
|
||||
try {
|
||||
const res = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body: payload
|
||||
});
|
||||
if (!res.ok) throw new Error('API 2 error');
|
||||
const data = await res.json();
|
||||
if (data.status !== 1 || !data.data) return null;
|
||||
// Find USD (currency === 'USD')
|
||||
const usd = data.data.find(item => item.currency === 'USD');
|
||||
if (!usd) return null;
|
||||
return {
|
||||
nameVI: usd.nameVI,
|
||||
muaTm: usd.muaTm,
|
||||
muaCk: usd.muaCk,
|
||||
currency: usd.currency,
|
||||
nameEN: usd.nameEN,
|
||||
ban: usd.ban
|
||||
};
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function renderTable(data) {
|
||||
tbody.innerHTML = '';
|
||||
for (const row of data) {
|
||||
const tr = document.createElement('tr');
|
||||
tr.innerHTML = `
|
||||
<td>${row.date}</td>
|
||||
<td>${row.nameVI}</td>
|
||||
<td>${row.muaTm}</td>
|
||||
<td>${row.muaCk}</td>
|
||||
<td>${row.currency}</td>
|
||||
<td>${row.nameEN}</td>
|
||||
<td>${row.ban}</td>
|
||||
`;
|
||||
tbody.appendChild(tr);
|
||||
}
|
||||
table.style.display = data.length ? '' : 'none';
|
||||
}
|
||||
|
||||
function exportCSV(data) {
|
||||
const header = ['Date', 'nameVI', 'muaTm', 'muaCk', 'currency', 'nameEN', 'ban'];
|
||||
const rows = [header];
|
||||
for (const row of data) {
|
||||
rows.push([
|
||||
row.date,
|
||||
row.nameVI,
|
||||
row.muaTm,
|
||||
row.muaCk,
|
||||
row.currency,
|
||||
row.nameEN,
|
||||
row.ban
|
||||
]);
|
||||
}
|
||||
const csvContent = rows.map(r => r.map(cell => '"' + String(cell).replace(/"/g, '""') + '"').join(',')).join('\r\n');
|
||||
const blob = new Blob([csvContent], { type: 'text/csv' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = 'usd_exchange_rates.csv';
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
fetchBtn.onclick = async () => {
|
||||
const start = startDateInput.value;
|
||||
const end = endDateInput.value;
|
||||
if (!start || !end) {
|
||||
statusDiv.innerHTML = '<div class="error">Please select both start and end dates.</div>';
|
||||
return;
|
||||
}
|
||||
if (start > end) {
|
||||
statusDiv.innerHTML = '<div class="error">Start date must be before or equal to end date.</div>';
|
||||
return;
|
||||
}
|
||||
statusDiv.innerHTML = '<div class="loading">Fetching data, please wait...</div>';
|
||||
fetchBtn.disabled = true;
|
||||
exportBtn.disabled = true;
|
||||
results = [];
|
||||
renderTable([]);
|
||||
const dates = getDateRange(start, end);
|
||||
for (let i = 0; i < dates.length; i++) {
|
||||
const dateObj = dates[i];
|
||||
const dateStr = formatDate(dateObj);
|
||||
statusDiv.innerHTML = `<div class=\"loading\">Fetching: ${dateStr} (${i+1}/${dates.length})</div>`;
|
||||
const namerecord = await fetchNamerecord(dateStr);
|
||||
if (!namerecord) continue;
|
||||
const usd = await fetchUSD(dateStr, namerecord);
|
||||
if (usd) {
|
||||
results.push({ date: dateStr, ...usd });
|
||||
renderTable(results);
|
||||
}
|
||||
}
|
||||
statusDiv.innerHTML = results.length ? '' : '<div class="error">No data found for the selected range.</div>';
|
||||
fetchBtn.disabled = false;
|
||||
exportBtn.disabled = results.length === 0;
|
||||
};
|
||||
|
||||
exportBtn.onclick = () => {
|
||||
if (results.length) exportCSV(results);
|
||||
};
|
||||
|
||||
document.getElementById('bank-select').onchange = function() {
|
||||
if (this.value === 'bidv') {
|
||||
window.location.href = 'bidv.html';
|
||||
} else if (this.value === 'tcb') {
|
||||
window.location.href = 'tcb.html';
|
||||
}
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Generated
+41
@@ -7,6 +7,9 @@
|
||||
"": {
|
||||
"name": "exchange-rate-export",
|
||||
"version": "0.0.1",
|
||||
"dependencies": {
|
||||
"date-fns": "^4.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@sveltejs/adapter-auto": "^6.0.0",
|
||||
"@sveltejs/adapter-cloudflare": "^7.0.4",
|
||||
@@ -14,6 +17,7 @@
|
||||
"@sveltejs/vite-plugin-svelte": "^5.0.0",
|
||||
"svelte": "^5.0.0",
|
||||
"vite": "^6.2.6",
|
||||
"vite-plugin-devtools-json": "^0.2.0",
|
||||
"wrangler": "^4.22.0"
|
||||
}
|
||||
},
|
||||
@@ -1596,6 +1600,16 @@
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/date-fns": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz",
|
||||
"integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/kossnocorp"
|
||||
}
|
||||
},
|
||||
"node_modules/debug": {
|
||||
"version": "4.4.1",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz",
|
||||
@@ -2289,6 +2303,20 @@
|
||||
"ufo": "^1.6.1"
|
||||
}
|
||||
},
|
||||
"node_modules/uuid": {
|
||||
"version": "11.1.0",
|
||||
"resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz",
|
||||
"integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
"https://github.com/sponsors/broofa",
|
||||
"https://github.com/sponsors/ctavan"
|
||||
],
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"uuid": "dist/esm/bin/uuid"
|
||||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "6.3.5",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz",
|
||||
@@ -2364,6 +2392,19 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/vite-plugin-devtools-json": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/vite-plugin-devtools-json/-/vite-plugin-devtools-json-0.2.0.tgz",
|
||||
"integrity": "sha512-K7PoaWOEJECZ1n3VbhJXsUAX2PsO0xY7KFMM/Leh7tUev0M5zi+lz+vnVVdCK17IOK9Jp9rdzHXc08cnQirGbg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"uuid": "^11.1.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"vite": "^2.7.0 || ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/vitefu": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.0.7.tgz",
|
||||
|
||||
+5
-1
@@ -17,6 +17,10 @@
|
||||
"@sveltejs/vite-plugin-svelte": "^5.0.0",
|
||||
"svelte": "^5.0.0",
|
||||
"vite": "^6.2.6",
|
||||
"vite-plugin-devtools-json": "^0.2.0",
|
||||
"wrangler": "^4.22.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"date-fns": "^4.1.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,470 @@
|
||||
<script>
|
||||
import { format } from 'date-fns';
|
||||
|
||||
// Props
|
||||
export let defaultStartDate = '';
|
||||
export let defaultEndDate = '';
|
||||
|
||||
// State
|
||||
let startDate = defaultStartDate;
|
||||
let endDate = defaultEndDate;
|
||||
let selectedBank = 'bidv'; // 'bidv' or 'tcb'
|
||||
let isLoading = false;
|
||||
let error = '';
|
||||
let results = [];
|
||||
let showTable = false;
|
||||
|
||||
|
||||
|
||||
// Format date for display
|
||||
const formatDisplayDate = (dateStr) => {
|
||||
const [year, month, day] = dateStr.split('-');
|
||||
return `${day}/${month}/${year}`;
|
||||
};
|
||||
|
||||
// Get date range between start and end dates
|
||||
const getDateRange = (start, end) => {
|
||||
const dates = [];
|
||||
let current = new Date(start);
|
||||
const last = new Date(end);
|
||||
|
||||
while (current <= last) {
|
||||
dates.push(new Date(current));
|
||||
current.setDate(current.getDate() + 1);
|
||||
}
|
||||
|
||||
return dates;
|
||||
};
|
||||
|
||||
// Format date for API requests
|
||||
const formatDateForApi = (date) => {
|
||||
const d = new Date(date);
|
||||
const day = String(d.getDate()).padStart(2, '0');
|
||||
const month = String(d.getMonth() + 1).padStart(2, '0');
|
||||
const year = d.getFullYear();
|
||||
return `${day}/${month}/${year}`;
|
||||
};
|
||||
|
||||
// Fetch BIDV exchange rates
|
||||
const fetchBIDVRates = async (dateObj) => {
|
||||
const dateStr = format(dateObj, 'dd/MM/yyyy');
|
||||
|
||||
// First API call to get namerecord
|
||||
const timeRes = await fetch('https://bidv.com.vn/ServicesBIDV/ExchangeDetailSearchTimeServlet', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body: `date=${encodeURIComponent(dateStr)}`
|
||||
});
|
||||
|
||||
if (!timeRes.ok) throw new Error('Failed to fetch BIDV time data');
|
||||
const timeData = await timeRes.json();
|
||||
|
||||
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]);
|
||||
|
||||
// Second API call to get exchange rates
|
||||
const rateRes = await fetch('https://bidv.com.vn/ServicesBIDV/ExchangeDetailServlet', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
},
|
||||
body: `date=${encodeURIComponent(dateStr)}&time=${latest.namerecord}`
|
||||
});
|
||||
|
||||
if (!rateRes.ok) throw new Error('Failed to fetch BIDV rate data');
|
||||
const rateData = await rateRes.json();
|
||||
|
||||
if (rateData.status !== 1 || !rateData.data) return null;
|
||||
|
||||
// Find USD data
|
||||
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 || ''
|
||||
};
|
||||
};
|
||||
|
||||
// Fetch TCB exchange rates
|
||||
const fetchTCBRates = async (dateObj) => {
|
||||
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`;
|
||||
|
||||
const res = await fetch(url);
|
||||
if (!res.ok) throw new Error('Failed to fetch TCB data');
|
||||
|
||||
const data = await res.json();
|
||||
if (!data.exchangeRate?.data) return null;
|
||||
|
||||
// Find USD data
|
||||
const usd = data.exchangeRate.data.find(item => item.label === 'USD (50,100)');
|
||||
if (!usd) return null;
|
||||
|
||||
return {
|
||||
date: formatDisplayDate(dateStr),
|
||||
label: usd.label || '',
|
||||
askRate: usd.askRate || '',
|
||||
bidRateCK: usd.bidRateCK || '',
|
||||
bidRateTM: usd.bidRateTM || '',
|
||||
askRateTM: usd.askRateTM || '',
|
||||
sourceCurrency: usd.sourceCurrency || 'USD',
|
||||
targetCurrency: usd.targetCurrency || 'VND'
|
||||
};
|
||||
};
|
||||
|
||||
// Handle fetch button click
|
||||
const handleFetch = async () => {
|
||||
if (!startDate || !endDate) {
|
||||
error = 'Please select both start and end dates';
|
||||
return;
|
||||
}
|
||||
|
||||
const start = new Date(startDate);
|
||||
const end = new Date(endDate);
|
||||
|
||||
if (start > end) {
|
||||
error = 'Start date must be before end date';
|
||||
return;
|
||||
}
|
||||
|
||||
const dateRange = getDateRange(start, end);
|
||||
if (dateRange.length > 31) {
|
||||
error = 'Date range cannot exceed 31 days';
|
||||
return;
|
||||
}
|
||||
|
||||
isLoading = true;
|
||||
error = '';
|
||||
results = [];
|
||||
showTable = false;
|
||||
|
||||
try {
|
||||
const fetchPromises = dateRange.map(async (date) => {
|
||||
try {
|
||||
if (selectedBank === 'bidv') {
|
||||
return await fetchBIDVRates(date);
|
||||
} else {
|
||||
return await fetchTCBRates(date);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(`Error fetching data for ${date}:`, e);
|
||||
return null;
|
||||
}
|
||||
});
|
||||
|
||||
const fetchedResults = await Promise.all(fetchPromises);
|
||||
results = fetchedResults.filter(Boolean);
|
||||
showTable = results.length > 0;
|
||||
|
||||
if (results.length === 0) {
|
||||
error = 'No data found for the selected date range';
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error fetching data:', e);
|
||||
error = 'An error occurred while fetching data. Please try again.';
|
||||
} finally {
|
||||
isLoading = false;
|
||||
}
|
||||
};
|
||||
|
||||
// Handle export to CSV
|
||||
const handleExport = () => {
|
||||
if (results.length === 0) return;
|
||||
|
||||
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');
|
||||
} 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');
|
||||
}
|
||||
|
||||
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
|
||||
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`;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class="container">
|
||||
<h1>Exchange Rate Export</h1>
|
||||
|
||||
<div class="controls">
|
||||
<div class="form-group">
|
||||
<label for="bank">Bank:</label>
|
||||
<select id="bank" bind:value={selectedBank}>
|
||||
<option value="bidv">BIDV</option>
|
||||
<option value="tcb">Techcombank</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="start-date">Start Date:</label>
|
||||
<input
|
||||
type="date"
|
||||
id="start-date"
|
||||
bind:value={startDate}
|
||||
max={endDate}
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="end-date">End Date:</label>
|
||||
<input
|
||||
type="date"
|
||||
id="end-date"
|
||||
bind:value={endDate}
|
||||
min={startDate}
|
||||
max={new Date().toISOString().split('T')[0]}
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="button-group">
|
||||
<button class="fetch-btn" on:click={handleFetch} disabled={isLoading}>
|
||||
{isLoading ? 'Fetching...' : 'Fetch Rates'}
|
||||
</button>
|
||||
<button
|
||||
class="export-btn"
|
||||
on:click={handleExport}
|
||||
disabled={results.length === 0 || isLoading}
|
||||
>
|
||||
Export CSV
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if error}
|
||||
<div class="error">{error}</div>
|
||||
{/if}
|
||||
|
||||
{#if isLoading}
|
||||
<div class="loading">Loading data, please wait...</div>
|
||||
{/if}
|
||||
|
||||
{#if showTable}
|
||||
<div class="table-container">
|
||||
{#if selectedBank === 'bidv'}
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Date</th>
|
||||
<th>Currency</th>
|
||||
<th>Buy Cash</th>
|
||||
<th>Buy Transfer</th>
|
||||
<th>Sell</th>
|
||||
<th>Name (VI)</th>
|
||||
<th>Name (EN)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each results as result}
|
||||
<tr>
|
||||
<td>{result.date}</td>
|
||||
<td>{result.currency}</td>
|
||||
<td>{result.muaTm}</td>
|
||||
<td>{result.muaCk}</td>
|
||||
<td>{result.ban}</td>
|
||||
<td>{result.nameVI}</td>
|
||||
<td>{result.nameEN}</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
{:else}
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Date</th>
|
||||
<th>Label</th>
|
||||
<th>Ask Rate</th>
|
||||
<th>Bid Rate CK</th>
|
||||
<th>Bid Rate TM</th>
|
||||
<th>Ask Rate TM</th>
|
||||
<th>From</th>
|
||||
<th>To</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{#each results as result}
|
||||
<tr>
|
||||
<td>{result.date}</td>
|
||||
<td>{result.label}</td>
|
||||
<td>{result.askRate}</td>
|
||||
<td>{result.bidRateCK}</td>
|
||||
<td>{result.bidRateTM}</td>
|
||||
<td>{result.askRateTM}</td>
|
||||
<td>{result.sourceCurrency}</td>
|
||||
<td>{result.targetCurrency}</td>
|
||||
</tr>
|
||||
{/each}
|
||||
</tbody>
|
||||
</table>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
font-family: Arial, sans-serif;
|
||||
}
|
||||
|
||||
h1 {
|
||||
text-align: center;
|
||||
margin-bottom: 2rem;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
.controls {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 1rem;
|
||||
margin-bottom: 2rem;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
label {
|
||||
font-weight: 600;
|
||||
color: #2c3e50;
|
||||
}
|
||||
|
||||
input[type="date"],
|
||||
select {
|
||||
padding: 0.5rem;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.button-group {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 0.5rem 1rem;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
font-size: 1rem;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
|
||||
.fetch-btn {
|
||||
background-color: #3498db;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.fetch-btn:hover:not(:disabled) {
|
||||
background-color: #2980b9;
|
||||
}
|
||||
|
||||
.export-btn {
|
||||
background-color: #2ecc71;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.export-btn:hover:not(:disabled) {
|
||||
background-color: #27ae60;
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
background-color: #bdc3c7;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: #e74c3c;
|
||||
background-color: #fadbd8;
|
||||
padding: 1rem;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 1rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.loading {
|
||||
text-align: center;
|
||||
color: #3498db;
|
||||
margin: 2rem 0;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.table-container {
|
||||
overflow-x: auto;
|
||||
margin-top: 2rem;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
background: white;
|
||||
}
|
||||
|
||||
th, td {
|
||||
padding: 0.75rem 1rem;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid #ecf0f1;
|
||||
}
|
||||
|
||||
th {
|
||||
background-color: #3498db;
|
||||
color: white;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
tr:nth-child(even) {
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
tr:hover {
|
||||
background-color: #f1f8ff;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.controls {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.button-group {
|
||||
margin-left: 0;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
+25
-2
@@ -1,2 +1,25 @@
|
||||
<h1>Welcome to SvelteKit</h1>
|
||||
<p>Visit <a href="https://svelte.dev/docs/kit">svelte.dev/docs/kit</a> to read the documentation</p>
|
||||
<script>
|
||||
import { onMount } from 'svelte';
|
||||
import { format, subDays } from 'date-fns';
|
||||
import ExchangeRateViewer from '$lib/ExchangeRateViewer.svelte';
|
||||
|
||||
let defaultStartDate = '';
|
||||
let defaultEndDate = '';
|
||||
|
||||
onMount(() => {
|
||||
const today = new Date();
|
||||
const weekAgo = subDays(today, 7);
|
||||
|
||||
defaultStartDate = format(weekAgo, 'yyyy-MM-dd');
|
||||
defaultEndDate = format(today, 'yyyy-MM-dd');
|
||||
});
|
||||
</script>
|
||||
|
||||
<main>
|
||||
{#if defaultStartDate && defaultEndDate}
|
||||
<ExchangeRateViewer
|
||||
defaultStartDate={defaultStartDate}
|
||||
defaultEndDate={defaultEndDate}
|
||||
/>
|
||||
{/if}
|
||||
</main>
|
||||
@@ -1,242 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Exchange Rate Export - Techcombank</title>
|
||||
<style>
|
||||
body {
|
||||
font-family: Arial, sans-serif;
|
||||
margin: 40px;
|
||||
background: #f7f7f7;
|
||||
}
|
||||
.container {
|
||||
background: #fff;
|
||||
padding: 24px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.07);
|
||||
max-width: 700px;
|
||||
margin: auto;
|
||||
}
|
||||
h1 {
|
||||
text-align: center;
|
||||
}
|
||||
label {
|
||||
margin-right: 8px;
|
||||
}
|
||||
input[type="date"] {
|
||||
margin-right: 16px;
|
||||
padding: 4px 8px;
|
||||
}
|
||||
button {
|
||||
padding: 8px 16px;
|
||||
margin-right: 8px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
background: #007bff;
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
}
|
||||
button:disabled {
|
||||
background: #aaa;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin-top: 24px;
|
||||
}
|
||||
th, td {
|
||||
border: 1px solid #ddd;
|
||||
padding: 8px;
|
||||
text-align: center;
|
||||
}
|
||||
th {
|
||||
background: #007bff;
|
||||
color: #fff;
|
||||
}
|
||||
.loading {
|
||||
text-align: center;
|
||||
margin-top: 20px;
|
||||
color: #007bff;
|
||||
}
|
||||
.error {
|
||||
color: red;
|
||||
text-align: center;
|
||||
margin-top: 20px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>Exchange Rate Export (USD, Techcombank)</h1>
|
||||
<div style="margin-bottom: 20px;">
|
||||
<label for="start-date">Start Date:</label>
|
||||
<input type="date" id="start-date">
|
||||
<label for="end-date">End Date:</label>
|
||||
<input type="date" id="end-date">
|
||||
<button id="fetch-btn">Fetch</button>
|
||||
<button id="export-btn" disabled>Export CSV</button>
|
||||
</div>
|
||||
<div id="status"></div>
|
||||
<table id="result-table" style="display:none;">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Date</th>
|
||||
<th>label</th>
|
||||
<th>askRate</th>
|
||||
<th>bidRateCK</th>
|
||||
<th>bidRateTM</th>
|
||||
<th>askRateTM</th>
|
||||
<th>sourceCurrency</th>
|
||||
<th>targetCurrency</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<script>
|
||||
const fetchBtn = document.getElementById('fetch-btn');
|
||||
const exportBtn = document.getElementById('export-btn');
|
||||
const startDateInput = document.getElementById('start-date');
|
||||
const endDateInput = document.getElementById('end-date');
|
||||
const statusDiv = document.getElementById('status');
|
||||
const table = document.getElementById('result-table');
|
||||
const tbody = table.querySelector('tbody');
|
||||
let results = [];
|
||||
|
||||
function formatDate(date) {
|
||||
// Format as dd/MM/yyyy
|
||||
const d = new Date(date);
|
||||
const day = String(d.getDate()).padStart(2, '0');
|
||||
const month = String(d.getMonth() + 1).padStart(2, '0');
|
||||
const year = d.getFullYear();
|
||||
return `${day}/${month}/${year}`;
|
||||
}
|
||||
|
||||
function getDateRange(start, end) {
|
||||
const dates = [];
|
||||
let current = new Date(start);
|
||||
const last = new Date(end);
|
||||
while (current <= last) {
|
||||
dates.push(new Date(current));
|
||||
current.setDate(current.getDate() + 1);
|
||||
}
|
||||
return dates;
|
||||
}
|
||||
|
||||
async function fetchUSD(dateObj) {
|
||||
// Techcombank API expects yyyy-mm-dd in URL
|
||||
const yyyy = dateObj.getFullYear();
|
||||
const mm = String(dateObj.getMonth() + 1).padStart(2, '0');
|
||||
const dd = String(dateObj.getDate()).padStart(2, '0');
|
||||
const dateStr = `${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 {
|
||||
const res = await fetch(url);
|
||||
if (!res.ok) throw new Error('API error');
|
||||
const data = await res.json();
|
||||
if (!data.exchangeRate || !data.exchangeRate.data) return null;
|
||||
// Find USD (50,100)
|
||||
const usd = data.exchangeRate.data.find(item => item.label === 'USD (50,100)');
|
||||
if (!usd) return null;
|
||||
return {
|
||||
date: formatDate(dateObj),
|
||||
label: usd.label,
|
||||
askRate: usd.askRate,
|
||||
bidRateCK: usd.bidRateCK,
|
||||
bidRateTM: usd.bidRateTM,
|
||||
askRateTM: usd.askRateTM,
|
||||
sourceCurrency: usd.sourceCurrency,
|
||||
targetCurrency: usd.targetCurrency
|
||||
};
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function renderTable(data) {
|
||||
tbody.innerHTML = '';
|
||||
for (const row of data) {
|
||||
const tr = document.createElement('tr');
|
||||
tr.innerHTML = `
|
||||
<td>${row.date}</td>
|
||||
<td>${row.label}</td>
|
||||
<td>${row.askRate || ''}</td>
|
||||
<td>${row.bidRateCK || ''}</td>
|
||||
<td>${row.bidRateTM || ''}</td>
|
||||
<td>${row.askRateTM || ''}</td>
|
||||
<td>${row.sourceCurrency}</td>
|
||||
<td>${row.targetCurrency}</td>
|
||||
`;
|
||||
tbody.appendChild(tr);
|
||||
}
|
||||
table.style.display = data.length ? '' : 'none';
|
||||
}
|
||||
|
||||
function exportCSV(data) {
|
||||
const header = ['Date', 'label', 'askRate', 'bidRateCK', 'bidRateTM', 'askRateTM', 'sourceCurrency', 'targetCurrency'];
|
||||
const rows = [header];
|
||||
for (const row of data) {
|
||||
rows.push([
|
||||
row.date,
|
||||
row.label,
|
||||
row.askRate,
|
||||
row.bidRateCK,
|
||||
row.bidRateTM,
|
||||
row.askRateTM,
|
||||
row.sourceCurrency,
|
||||
row.targetCurrency
|
||||
]);
|
||||
}
|
||||
const csvContent = rows.map(r => r.map(cell => '"' + String(cell || '').replace(/"/g, '""') + '"').join(',')).join('\r\n');
|
||||
const blob = new Blob([csvContent], { type: 'text/csv' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = 'usd_exchange_rates_tcb.csv';
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
|
||||
fetchBtn.onclick = async () => {
|
||||
const start = startDateInput.value;
|
||||
const end = endDateInput.value;
|
||||
if (!start || !end) {
|
||||
statusDiv.innerHTML = '<div class="error">Please select both start and end dates.</div>';
|
||||
return;
|
||||
}
|
||||
if (start > end) {
|
||||
statusDiv.innerHTML = '<div class="error">Start date must be before or equal to end date.</div>';
|
||||
return;
|
||||
}
|
||||
statusDiv.innerHTML = '<div class="loading">Fetching data, please wait...</div>';
|
||||
fetchBtn.disabled = true;
|
||||
exportBtn.disabled = true;
|
||||
results = [];
|
||||
renderTable([]);
|
||||
const dates = getDateRange(start, end);
|
||||
for (let i = 0; i < dates.length; i++) {
|
||||
const dateObj = dates[i];
|
||||
const dateStr = formatDate(dateObj);
|
||||
statusDiv.innerHTML = `<div class=\"loading\">Fetching: ${dateStr} (${i+1}/${dates.length})</div>`;
|
||||
const usd = await fetchUSD(dateObj);
|
||||
if (usd) {
|
||||
results.push(usd);
|
||||
renderTable(results);
|
||||
}
|
||||
}
|
||||
statusDiv.innerHTML = results.length ? '' : '<div class="error">No data found for the selected range.</div>';
|
||||
fetchBtn.disabled = false;
|
||||
exportBtn.disabled = results.length === 0;
|
||||
};
|
||||
|
||||
exportBtn.onclick = () => {
|
||||
if (results.length) exportCSV(results);
|
||||
};
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
+5
-1
@@ -1,6 +1,10 @@
|
||||
import { sveltekit } from '@sveltejs/kit/vite';
|
||||
import { defineConfig } from 'vite';
|
||||
import devtoolsJson from 'vite-plugin-devtools-json';
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [sveltekit()]
|
||||
plugins: [
|
||||
devtoolsJson(),
|
||||
sveltekit()
|
||||
]
|
||||
});
|
||||
Reference in new issue
Block a user