(() => {
const root = document.getElementById('fx-rates-app');
if (!root) {
return;
}
const page = JSON.parse(root.dataset.page || '{}');
const settings = page.settings || {};
const nodes = {
ratesBody: root.querySelector('[data-bind="rates-body"]'),
fetchesBody: root.querySelector('[data-bind="fetches-body"]'),
convertResult: root.querySelector('[data-bind="convert-result"]'),
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';
const preferredCurrencies = Array.isArray(page.preferred_currencies)
? page.preferred_currencies
.map((item) => String(item || '').trim().toUpperCase())
.filter(Boolean)
: [];
const renderSnapshot = (snapshot) => {
const rates = snapshot && snapshot.rates ? snapshot.rates : null;
const entries = rates ? Object.entries(rates) : [];
if (!nodes.ratesBody) {
return;
}
if (!entries.length) {
nodes.ratesBody.innerHTML = '
| Noch keine Wechselkurse fuer die ausgewaehlten Waehrungen gespeichert. |
';
return;
}
nodes.ratesBody.innerHTML = entries.map(([code, rate]) => {
const formatted = typeof rate === 'number'
? rate.toLocaleString('de-DE', { maximumFractionDigits: 8 })
: 'n/a';
return `| ${code} | ${formatted} |
`;
}).join('');
};
const renderFetches = (fetches) => {
if (!nodes.fetchesBody) {
return;
}
const entries = Array.isArray(fetches) ? fetches : [];
if (!entries.length) {
nodes.fetchesBody.innerHTML = '| Noch keine Abrufe vorhanden. |
';
return;
}
nodes.fetchesBody.innerHTML = entries.map((entry) => `
| ${entry?.fetched_at_display || entry?.fetched_at || ''} |
${entry?.base_currency || ''} |
${entry?.provider || ''} |
`).join('');
};
const request = async (path, options = {}) => {
const response = await fetch(`${apiBase}${path}`, {
credentials: 'same-origin',
headers: {
Accept: 'application/json',
...(options.body ? { 'Content-Type': 'application/json' } : {}),
...(options.headers || {}),
},
...options,
});
const payload = await response.json().catch(() => ({}));
if (!response.ok) {
throw new Error(payload?.context?.message || payload?.error || `HTTP ${response.status}`);
}
return payload.data;
};
const loadLatest = async () => {
const base = String(
settings.display_base_currency || settings.default_base_currency || 'EUR'
).trim().toUpperCase();
const query = new URLSearchParams();
query.set('base', base);
if (preferredCurrencies.length) {
query.set('symbols', preferredCurrencies.join(','));
}
const data = await request(`/latest?${query.toString()}`);
renderSnapshot(data);
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.';
}
};
[nodes.convertFrom, nodes.convertTo, nodes.convertAmount].forEach((node) => {
node?.addEventListener('change', () => {
calculateConversion().catch(() => {});
});
node?.addEventListener('input', () => {
calculateConversion().catch(() => {});
});
});
renderFetches(page.recent_fetches || []);
loadLatest().catch(() => {});
calculateConversion().catch(() => {});
})();