ysdsd
All checks were successful
Deploy / deploy-staging (push) Successful in 6s
Deploy / deploy-production (push) Has been skipped

This commit is contained in:
2026-04-29 02:21:22 +02:00
parent a0f19ff6b4
commit 4ead35047a
7 changed files with 180 additions and 4 deletions

View File

@@ -87,6 +87,7 @@
}
.fx-form-grid input,
.fx-form-grid select,
.fx-block input {
width: 100%;
border: 1px solid #d0d7e2;
@@ -127,3 +128,20 @@
margin-top: 0.75rem;
font-size: 0.95rem;
}
.fx-convert-result {
margin-top: 1rem;
min-height: 1.5rem;
font-size: 1rem;
font-weight: 700;
color: #1c2734;
}
.fx-history-block {
margin-top: 1.25rem;
}
.fx-history-block h3 {
margin: 0 0 0.75rem;
font-size: 1rem;
}

View File

@@ -12,9 +12,14 @@
defaultBase: root.querySelector('[data-bind="default-base"]'),
displayBase: root.querySelector('[data-bind="display-base"]'),
ratesBody: root.querySelector('[data-bind="rates-body"]'),
fetchesBody: root.querySelector('[data-bind="fetches-body"]'),
convertResult: root.querySelector('[data-bind="convert-result"]'),
defaultBaseInput: root.querySelector('input[name="default_base_currency"]'),
displayBaseInput: root.querySelector('input[name="display_base_currency"]'),
preferredCurrenciesInput: root.querySelector('input[name="preferred_currencies"]'),
convertFrom: root.querySelector('select[name="convert_from"]'),
convertTo: root.querySelector('select[name="convert_to"]'),
convertAmount: root.querySelector('input[name="convert_amount"]'),
};
const apiBase = '/api/fx-rates/v1';
@@ -57,6 +62,24 @@
}).join('');
};
const renderFetches = (fetches) => {
if (!nodes.fetchesBody) {
return;
}
const entries = Array.isArray(fetches) ? fetches : [];
if (!entries.length) {
nodes.fetchesBody.innerHTML = '<tr><td colspan="3">Noch keine Abrufe vorhanden.</td></tr>';
return;
}
nodes.fetchesBody.innerHTML = entries.map((entry) => `
<tr>
<td>${entry?.fetched_at || ''}</td>
<td>${entry?.base_currency || ''}</td>
<td>${entry?.provider || ''}</td>
</tr>
`).join('');
};
const request = async (path, options = {}) => {
const response = await fetch(`${apiBase}${path}`, {
credentials: 'same-origin',
@@ -96,6 +119,39 @@
return data;
};
const calculateConversion = async () => {
if (!nodes.convertFrom || !nodes.convertTo || !nodes.convertAmount || !nodes.convertResult) {
return;
}
const from = String(nodes.convertFrom.value || '').trim().toUpperCase();
const to = String(nodes.convertTo.value || '').trim().toUpperCase();
const amount = Number(nodes.convertAmount.value || '0');
if (!from || !to || !Number.isFinite(amount)) {
nodes.convertResult.textContent = 'Bitte Quellwaehrung, Zielwaehrung und Betrag angeben.';
return;
}
if (from === to) {
nodes.convertResult.textContent = `${amount.toLocaleString('de-DE', { maximumFractionDigits: 8 })} ${from} = ${amount.toLocaleString('de-DE', { maximumFractionDigits: 8 })} ${to}`;
return;
}
try {
const query = new URLSearchParams({ from, to });
const data = await request(`/rate?${query.toString()}`);
const rate = Number(data?.rate || 0);
if (!Number.isFinite(rate) || rate <= 0) {
throw new Error('Kein Kurs verfuegbar.');
}
const converted = amount * rate;
nodes.convertResult.textContent = `${amount.toLocaleString('de-DE', { maximumFractionDigits: 8 })} ${from} = ${converted.toLocaleString('de-DE', { maximumFractionDigits: 8 })} ${to} | Kurs ${rate.toLocaleString('de-DE', { maximumFractionDigits: 8 })}`;
} catch (error) {
nodes.convertResult.textContent = error.message || 'Umrechnung konnte nicht berechnet werden.';
}
};
root.querySelector('[data-action="refresh-rates"]')?.addEventListener('click', async () => {
try {
setLoading(true);
@@ -108,6 +164,9 @@
const data = await request('/refresh', { method: 'POST', body: JSON.stringify(payload) });
setMessage(`Aktuelle Kurse gespeichert. ${data?.updated_count || 0} Werte aktualisiert.`, 'success');
await loadLatest();
const recentFetches = await request('/recent-fetches?limit=12');
renderFetches(recentFetches);
await calculateConversion();
} catch (error) {
setMessage(error.message || 'Kurse konnten nicht aktualisiert werden.', 'error');
} finally {
@@ -133,6 +192,7 @@
}
setMessage('Waehrungs-Auswahl gespeichert.', 'success');
await loadLatest();
await calculateConversion();
} catch (error) {
setMessage(error.message || 'Waehrungs-Auswahl konnte nicht gespeichert werden.', 'error');
} finally {
@@ -140,7 +200,19 @@
}
});
[nodes.convertFrom, nodes.convertTo, nodes.convertAmount].forEach((node) => {
node?.addEventListener('change', () => {
calculateConversion().catch(() => {});
});
node?.addEventListener('input', () => {
calculateConversion().catch(() => {});
});
});
renderFetches(page.recent_fetches || []);
loadLatest().catch((error) => {
setMessage(error.message || 'Letzter Snapshot konnte nicht geladen werden.', 'error');
});
calculateConversion().catch(() => {});
})();