From 4ead35047adcb84ef4b3f852032609274a851813 Mon Sep 17 00:00:00 2001 From: Lars Gebhardt-Kusche Date: Wed, 29 Apr 2026 02:21:22 +0200 Subject: [PATCH] ysdsd --- modules/fx-rates/assets/fx-rates.css | 18 +++++ modules/fx-rates/assets/fx-rates.js | 72 +++++++++++++++++++ modules/fx-rates/bootstrap.php | 4 ++ modules/fx-rates/pages/index.php | 63 ++++++++++++++-- modules/fx-rates/src/Api/Router.php | 5 ++ .../fx-rates/src/Domain/FxRatesService.php | 5 ++ .../src/Infrastructure/FxRatesRepository.php | 17 +++++ 7 files changed, 180 insertions(+), 4 deletions(-) diff --git a/modules/fx-rates/assets/fx-rates.css b/modules/fx-rates/assets/fx-rates.css index 9449f9e..a2d4f27 100644 --- a/modules/fx-rates/assets/fx-rates.css +++ b/modules/fx-rates/assets/fx-rates.css @@ -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; +} diff --git a/modules/fx-rates/assets/fx-rates.js b/modules/fx-rates/assets/fx-rates.js index 6d478d7..2e36ac9 100644 --- a/modules/fx-rates/assets/fx-rates.js +++ b/modules/fx-rates/assets/fx-rates.js @@ -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 = 'Noch keine Abrufe vorhanden.'; + return; + } + nodes.fetchesBody.innerHTML = entries.map((entry) => ` + + ${entry?.fetched_at || ''} + ${entry?.base_currency || ''} + ${entry?.provider || ''} + + `).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(() => {}); })(); diff --git a/modules/fx-rates/bootstrap.php b/modules/fx-rates/bootstrap.php index e45c7ca..f1a921b 100644 --- a/modules/fx-rates/bootstrap.php +++ b/modules/fx-rates/bootstrap.php @@ -251,6 +251,10 @@ $mm->registerFunction($moduleName, 'snapshot', static function (?string $baseCur return module_fn('fx-rates', 'service')->snapshot($baseCurrency, $at, $symbols, $windowMinutes); }); +$mm->registerFunction($moduleName, 'recent_fetches', static function (int $limit = 20): array { + return module_fn('fx-rates', 'service')->recentFetches($limit); +}); + $mm->registerFunction($moduleName, 'scheduled_refresh', static function (array $context = []): array { $result = module_fn('fx-rates', 'service')->runScheduledRefresh($context); if (function_exists('module_debug_push')) { diff --git a/modules/fx-rates/pages/index.php b/modules/fx-rates/pages/index.php index 729fb1a..5593cac 100644 --- a/modules/fx-rates/pages/index.php +++ b/modules/fx-rates/pages/index.php @@ -12,11 +12,13 @@ if ($assets) { $settings = module_fn('fx-rates', 'settings'); $service = module_fn('fx-rates', 'service'); $latest = $service->latestStatus(); +$recentFetches = $service->recentFetches(12); $preferredCurrencies = is_array($settings['preferred_currencies'] ?? null) ? $settings['preferred_currencies'] : []; $pageData = json_encode([ 'settings' => $settings, 'latest' => $latest, 'preferred_currencies' => $preferredCurrencies, + 'recent_fetches' => $recentFetches, ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); ?> 'Waehrungskurse']) ?> @@ -28,9 +30,6 @@ $pageData = json_encode([

Waehrungskurse

Zentrale Quelle fuer FX-Snapshots, Zeitabfragen und manuelle Aktualisierung.

-
- -
Standard-Basis:
@@ -42,7 +41,7 @@ $pageData = json_encode([
-

Anzeige und Waehrungen

+

Setup

Die Auswahl wird in den Modul-Settings gespeichert und steuert den Standardaufruf der letzten Kurse.

+
+
+

Umrechnung

+

Umrechnung auf Basis des letzten verfuegbaren Kurses zwischen den bevorzugten Waehrungen.

+
+ + + +
+
Noch keine Umrechnung berechnet.
+
+

Letzter Snapshot

@@ -78,6 +106,33 @@ $pageData = json_encode([
+
+

Letzte Abrufe

+
+ + + + + + + + + + + + + + + + + + + + + +
DatumBasisProvider
Noch keine Abrufe vorhanden.
+
+
diff --git a/modules/fx-rates/src/Api/Router.php b/modules/fx-rates/src/Api/Router.php index 4bcf98f..fe2a43e 100644 --- a/modules/fx-rates/src/Api/Router.php +++ b/modules/fx-rates/src/Api/Router.php @@ -26,6 +26,11 @@ final class Router $this->respond(['data' => $this->service->latestStatuses()]); } + if ($path === 'v1/recent-fetches' && $method === 'GET') { + $limit = max(1, min(50, (int) ($_GET['limit'] ?? 12))); + $this->respond(['data' => $this->service->recentFetches($limit)]); + } + if ($path === 'v1/latest' && $method === 'GET') { $symbols = $this->parseCsv($_GET['symbols'] ?? null); $base = $this->stringOrNull($_GET['base'] ?? null); diff --git a/modules/fx-rates/src/Domain/FxRatesService.php b/modules/fx-rates/src/Domain/FxRatesService.php index 0a8f291..c3672cb 100644 --- a/modules/fx-rates/src/Domain/FxRatesService.php +++ b/modules/fx-rates/src/Domain/FxRatesService.php @@ -27,6 +27,11 @@ final class FxRatesService return $this->repository->listLatestFetches(); } + public function recentFetches(int $limit = 20): array + { + return $this->repository->listRecentFetches($limit); + } + public function snapshot(?string $baseCurrency = null, ?string $at = null, ?array $symbols = null, ?int $windowMinutes = null): ?array { $base = $this->normalizeCurrency($baseCurrency ?: $this->defaultBaseCurrency()); diff --git a/modules/fx-rates/src/Infrastructure/FxRatesRepository.php b/modules/fx-rates/src/Infrastructure/FxRatesRepository.php index 715b981..0c8d4fa 100644 --- a/modules/fx-rates/src/Infrastructure/FxRatesRepository.php +++ b/modules/fx-rates/src/Infrastructure/FxRatesRepository.php @@ -117,6 +117,23 @@ final class FxRatesRepository return array_values($latestByBase); } + public function listRecentFetches(int $limit = 20): array + { + $stmt = $this->pdo->prepare( + 'SELECT id, provider, base_currency, rate_date, fetched_at, created_at + FROM ' . $this->table('fetches') . ' + ORDER BY fetched_at DESC, id DESC + LIMIT :limit' + ); + $stmt->bindValue(':limit', max(1, $limit), PDO::PARAM_INT); + $stmt->execute(); + + return array_map( + fn (array $row): array => $this->normalizeFetch($row), + $stmt->fetchAll(PDO::FETCH_ASSOC) ?: [] + ); + } + public function getSnapshotByFetchId(int $fetchId, ?array $symbols = null): ?array { $fetch = $this->getFetchById($fetchId);