From 79872f33373b14d715f1b2f27ec5703361f56ede Mon Sep 17 00:00:00 2001 From: Lars Gebhardt-Kusche Date: Wed, 29 Apr 2026 02:32:42 +0200 Subject: [PATCH] asdasd --- modules/fx-rates/assets/fx-rates.css | 21 +-- modules/fx-rates/assets/fx-rates.js | 98 ++----------- modules/fx-rates/pages/index.php | 136 +++++++++--------- modules/fx-rates/src/Api/Router.php | 9 +- .../fx-rates/src/Domain/FxRatesService.php | 86 +++++++++-- 5 files changed, 159 insertions(+), 191 deletions(-) diff --git a/modules/fx-rates/assets/fx-rates.css b/modules/fx-rates/assets/fx-rates.css index a2d4f27..c16282a 100644 --- a/modules/fx-rates/assets/fx-rates.css +++ b/modules/fx-rates/assets/fx-rates.css @@ -59,17 +59,11 @@ cursor: wait; } -.fx-meta-grid, .fx-form-grid { display: grid; gap: 0.75rem; } -.fx-meta-grid { - grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); - margin-top: 0.75rem; -} - .fx-form-grid { grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); } @@ -96,7 +90,7 @@ } .fx-message { - min-height: 1.5rem; + margin-bottom: 0.9rem; color: #1c2734; } @@ -137,11 +131,10 @@ color: #1c2734; } -.fx-history-block { - margin-top: 1.25rem; -} - -.fx-history-block h3 { - margin: 0 0 0.75rem; - font-size: 1rem; +.fx-card-meta { + display: grid; + gap: 0.35rem; + color: #5b6573; + font-size: 0.95rem; + text-align: right; } diff --git a/modules/fx-rates/assets/fx-rates.js b/modules/fx-rates/assets/fx-rates.js index 2e36ac9..ad61718 100644 --- a/modules/fx-rates/assets/fx-rates.js +++ b/modules/fx-rates/assets/fx-rates.js @@ -7,40 +7,19 @@ const page = JSON.parse(root.dataset.page || '{}'); const settings = page.settings || {}; const nodes = { - message: root.querySelector('[data-bind="message"]'), - lastFetch: root.querySelector('[data-bind="last-fetch"]'), - 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'; - - const setMessage = (text, type = '') => { - if (!nodes.message) { - return; - } - nodes.message.textContent = text || ''; - nodes.message.className = `fx-message${type ? ` is-${type}` : ''}`; - }; - - const setLoading = (state) => { - root.querySelectorAll('button[data-action]').forEach((button) => { - button.disabled = state; - }); - }; - - const parsePreferredCurrencies = () => String(nodes.preferredCurrenciesInput?.value || '') - .split(/[\s,;]+/) - .map((item) => item.trim().toUpperCase()) - .filter(Boolean); + 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; @@ -73,7 +52,7 @@ } nodes.fetchesBody.innerHTML = entries.map((entry) => ` - ${entry?.fetched_at || ''} + ${entry?.fetched_at_display || entry?.fetched_at || ''} ${entry?.base_currency || ''} ${entry?.provider || ''} @@ -99,23 +78,16 @@ const loadLatest = async () => { const base = String( - nodes.displayBaseInput?.value || settings.display_base_currency || settings.default_base_currency || 'EUR' + settings.display_base_currency || settings.default_base_currency || 'EUR' ).trim().toUpperCase(); - const preferred = parsePreferredCurrencies(); const query = new URLSearchParams(); query.set('base', base); - if (preferred.length) { - query.set('symbols', preferred.join(',')); + if (preferredCurrencies.length) { + query.set('symbols', preferredCurrencies.join(',')); } const data = await request(`/latest?${query.toString()}`); renderSnapshot(data); - if (nodes.lastFetch) { - nodes.lastFetch.textContent = data?.fetched_at || 'noch keiner'; - } - if (nodes.displayBase) { - nodes.displayBase.textContent = base; - } return data; }; @@ -152,54 +124,6 @@ } }; - root.querySelector('[data-action="refresh-rates"]')?.addEventListener('click', async () => { - try { - setLoading(true); - setMessage('Ich rufe jetzt die aktuellen Kurse ab.'); - const payload = { - force: true, - base: String(nodes.defaultBaseInput?.value || settings.default_base_currency || 'EUR').trim().toUpperCase(), - currencies: parsePreferredCurrencies(), - }; - 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 { - setLoading(false); - } - }); - - root.querySelector('[data-action="save-settings"]')?.addEventListener('click', async () => { - try { - setLoading(true); - setMessage('Ich speichere jetzt die Waehrungs-Auswahl.'); - const payload = { - default_base_currency: String(nodes.defaultBaseInput?.value || '').trim().toUpperCase(), - display_base_currency: String(nodes.displayBaseInput?.value || '').trim().toUpperCase(), - preferred_currencies: parsePreferredCurrencies(), - }; - const data = await request('/settings', { method: 'PUT', body: JSON.stringify(payload) }); - if (nodes.defaultBase) { - nodes.defaultBase.textContent = data?.default_base_currency || ''; - } - if (nodes.displayBase) { - nodes.displayBase.textContent = data?.display_base_currency || ''; - } - setMessage('Waehrungs-Auswahl gespeichert.', 'success'); - await loadLatest(); - await calculateConversion(); - } catch (error) { - setMessage(error.message || 'Waehrungs-Auswahl konnte nicht gespeichert werden.', 'error'); - } finally { - setLoading(false); - } - }); - [nodes.convertFrom, nodes.convertTo, nodes.convertAmount].forEach((node) => { node?.addEventListener('change', () => { calculateConversion().catch(() => {}); @@ -211,8 +135,6 @@ renderFetches(page.recent_fetches || []); - loadLatest().catch((error) => { - setMessage(error.message || 'Letzter Snapshot konnte nicht geladen werden.', 'error'); - }); + loadLatest().catch(() => {}); calculateConversion().catch(() => {}); })(); diff --git a/modules/fx-rates/pages/index.php b/modules/fx-rates/pages/index.php index 5593cac..fbf7138 100644 --- a/modules/fx-rates/pages/index.php +++ b/modules/fx-rates/pages/index.php @@ -11,9 +11,28 @@ if ($assets) { $settings = module_fn('fx-rates', 'settings'); $service = module_fn('fx-rates', 'service'); +$preferredCurrencies = is_array($settings['preferred_currencies'] ?? null) ? $settings['preferred_currencies'] : []; +$notice = trim((string) ($_GET['notice'] ?? '')); +$error = trim((string) ($_GET['error'] ?? '')); + +if ((string) ($_GET['refresh'] ?? '') === '1') { + try { + $result = $service->refreshLatestRates(null, (string) ($settings['default_base_currency'] ?? '')); + $params = [ + 'notice' => sprintf( + 'Aktuelle Kurse gespeichert. %d Werte aktualisiert.', + (int) ($result['updated_count'] ?? 0) + ), + ]; + } catch (\Throwable $exception) { + $params = ['error' => $exception->getMessage() !== '' ? $exception->getMessage() : 'Kurse konnten nicht aktualisiert werden.']; + } + + redirect('/module/fx-rates?' . http_build_query($params)); +} + $latest = $service->latestStatus(); $recentFetches = $service->recentFetches(12); -$preferredCurrencies = is_array($settings['preferred_currencies'] ?? null) ? $settings['preferred_currencies'] : []; $pageData = json_encode([ 'settings' => $settings, 'latest' => $latest, @@ -21,49 +40,22 @@ $pageData = json_encode([ 'recent_fetches' => $recentFetches, ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); ?> - 'Waehrungskurse']) ?> + 'Waehrungskurse', + 'actions' => [ + ['label' => 'Setup', 'href' => '/modules/setup/fx-rates', 'variant' => 'secondary', 'size' => 'sm'], + ['label' => 'Aktuelle Kurse abrufen', 'href' => '/module/fx-rates?refresh=1', 'variant' => 'secondary', 'size' => 'sm'], + ], +]) ?>
'>
-
-
-

Waehrungskurse

-

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

-
-
-
-
Standard-Basis:
-
Anzeige-Basis:
-
Scheduler: taeglich um :00 ()
-
Letzter Abruf:
-
-
-
+ +
+ +
+ -
-

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.

@@ -92,7 +84,16 @@ $pageData = json_encode([
-

Letzter Snapshot

+
+
+

Letzte Kurse

+

Letzter gespeicherter Snapshot fuer .

+
+
+
Letzter Abruf:
+
Basis:
+
+
@@ -106,32 +107,33 @@ $pageData = json_encode([
-
-

Letzte Abrufe

-
- - - - - - - - - - - - - - - - - - - - - -
DatumBasisProvider
Noch keine Abrufe vorhanden.
-
+
+ +
+

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 fe2a43e..b1ee7f0 100644 --- a/modules/fx-rates/src/Api/Router.php +++ b/modules/fx-rates/src/Api/Router.php @@ -73,17 +73,12 @@ final class Router if ($path === 'v1/refresh' && $method === 'POST') { $input = $this->input(); $base = $this->stringOrNull($input['base'] ?? null); - $currencies = $this->parseCsv($input['currencies'] ?? null); - if ($currencies === null) { - $settings = module_fn('fx-rates', 'settings'); - $currencies = is_array($settings['preferred_currencies'] ?? null) ? $settings['preferred_currencies'] : null; - } $force = !empty($input['force']); $maxAgeHours = is_numeric($input['max_age_hours'] ?? null) ? (float) $input['max_age_hours'] : 24.0; $result = $force - ? $this->service->refreshLatestRates($currencies, $base) - : $this->service->ensureFreshLatestRates($maxAgeHours, $base, $currencies); + ? $this->service->refreshLatestRates(null, $base) + : $this->service->ensureFreshLatestRates($maxAgeHours, $base, null); $this->respond(['data' => $result], 201); } diff --git a/modules/fx-rates/src/Domain/FxRatesService.php b/modules/fx-rates/src/Domain/FxRatesService.php index c3672cb..476e23a 100644 --- a/modules/fx-rates/src/Domain/FxRatesService.php +++ b/modules/fx-rates/src/Domain/FxRatesService.php @@ -19,17 +19,17 @@ final class FxRatesService public function latestStatus(): ?array { - return $this->repository->getLatestFetch($this->defaultBaseCurrency()); + return $this->localizeFetch($this->repository->getLatestFetch($this->defaultBaseCurrency())); } public function latestStatuses(): array { - return $this->repository->listLatestFetches(); + return array_map(fn (array $fetch): array => $this->localizeFetch($fetch), $this->repository->listLatestFetches()); } public function recentFetches(int $limit = 20): array { - return $this->repository->listRecentFetches($limit); + return array_map(fn (array $fetch): array => $this->localizeFetch($fetch), $this->repository->listRecentFetches($limit)); } public function snapshot(?string $baseCurrency = null, ?string $at = null, ?array $symbols = null, ?int $windowMinutes = null): ?array @@ -53,7 +53,7 @@ final class FxRatesService return null; } - return $this->rebaseSnapshot($snapshot, $base, $symbols); + return $this->localizeSnapshot($this->rebaseSnapshot($snapshot, $base, $symbols)); } $atUtc = $this->normalizeTimestamp($at); @@ -76,10 +76,10 @@ final class FxRatesService return null; } - return $rebased + [ + return $this->localizeSnapshot($rebased + [ 'requested_at' => $atUtc, 'distance_seconds' => $nearest['distance_seconds'] ?? null, - ]; + ]); } public function findRate(?string $fromCurrency, ?string $toCurrency, ?string $at = null, ?int $windowMinutes = null): ?array @@ -91,7 +91,7 @@ final class FxRatesService } if ($from === $to) { - return [ + return $this->localizeRateResult([ 'base_currency' => $from, 'target_currency' => $to, 'rate' => 1.0, @@ -99,7 +99,7 @@ final class FxRatesService 'fetched_at' => $at ? $this->normalizeTimestamp($at) : null, 'rate_date' => $at ? substr((string) $this->normalizeTimestamp($at), 0, 10) : gmdate('Y-m-d'), 'is_exact_pair' => true, - ]; + ]); } $cacheKey = implode(':', [$from, $to, $at ?? '', (string) ($windowMinutes ?? 0)]); @@ -124,7 +124,7 @@ final class FxRatesService $rates = is_array($snapshot['rates'] ?? null) ? $snapshot['rates'] : []; $resolved = $this->resolveRateFromSnapshot($snapshot, $rates, $from, $to); if ($resolved !== null) { - return $this->memoryCache[$cacheKey] = $resolved; + return $this->memoryCache[$cacheKey] = $this->localizeRateResult($resolved); } } @@ -148,7 +148,7 @@ final class FxRatesService public function refreshLatestRates(?array $currencies = null, ?string $baseCurrency = null): array { $requestedBase = $this->normalizeCurrency($baseCurrency ?: $this->defaultBaseCurrency()); - $payload = $this->fetchLatestPayload($requestedBase, $currencies); + $payload = $this->fetchLatestPayload($requestedBase, null); $base = $this->normalizeCurrency((string) ($payload['base'] ?? $requestedBase)); if ($base === '') { $base = $requestedBase !== '' ? $requestedBase : 'USD'; @@ -169,7 +169,7 @@ final class FxRatesService 'rate_date' => $rateDate, 'updated_count' => count($saved['rates'] ?? []), 'rates' => $saved['rates'] ?? [], - 'fetch' => $saved['fetch'] ?? null, + 'fetch' => $this->localizeFetch(is_array($saved['fetch'] ?? null) ? $saved['fetch'] : null), ]; } @@ -186,7 +186,7 @@ final class FxRatesService 'rate_date' => $latest['rate_date'] ?? null, 'updated_count' => 0, 'rates' => [], - 'fetch' => $latest, + 'fetch' => $this->localizeFetch($latest), 'reused' => true, ]; } @@ -210,7 +210,7 @@ final class FxRatesService $direct = $this->repository->listDirectHistory($fromCurrency, $toCurrency, $this->normalizeTimestamp($from), $this->normalizeTimestamp($to), $limit); if ($direct !== []) { - return $direct; + return array_map(fn (array $row): array => $this->localizeRateResult($row), $direct); } $inverse = $this->repository->listDirectHistory($toCurrency, $fromCurrency, $this->normalizeTimestamp($from), $this->normalizeTimestamp($to), $limit); @@ -237,7 +237,7 @@ final class FxRatesService ]; } - return $result; + return array_map(fn (array $row): array => $this->localizeRateResult($row), $result); } public function runScheduledRefresh(array $context = []): array @@ -268,7 +268,7 @@ final class FxRatesService 'ok' => true, 'message' => 'Kein FX-Abruf: fuer heute existiert bereits ein Snapshot nach ' . str_pad((string) $targetHour, 2, '0', STR_PAD_LEFT) . ':00.', 'skipped' => true, - 'fetch' => $latest, + 'fetch' => $this->localizeFetch($latest), 'context' => $context, ]; } @@ -647,6 +647,57 @@ final class FxRatesService return $filtered; } + private function localizeFetch(?array $fetch): ?array + { + if (!is_array($fetch)) { + return null; + } + + $fetch['fetched_at_display'] = $this->formatDisplayTimestamp($fetch['fetched_at'] ?? null); + $fetch['created_at_display'] = $this->formatDisplayTimestamp($fetch['created_at'] ?? null); + return $fetch; + } + + private function localizeSnapshot(?array $snapshot): ?array + { + if (!is_array($snapshot)) { + return null; + } + + $snapshot['fetched_at_display'] = $this->formatDisplayTimestamp($snapshot['fetched_at'] ?? null); + if (array_key_exists('requested_at', $snapshot)) { + $snapshot['requested_at_display'] = $this->formatDisplayTimestamp($snapshot['requested_at']); + } + return $snapshot; + } + + private function localizeRateResult(array $rate): array + { + $rate['fetched_at_display'] = $this->formatDisplayTimestamp($rate['fetched_at'] ?? null); + if (array_key_exists('requested_at', $rate)) { + $rate['requested_at_display'] = $this->formatDisplayTimestamp($rate['requested_at']); + } + return $rate; + } + + private function formatDisplayTimestamp(mixed $value): string + { + $raw = trim((string) $value); + if ($raw === '') { + return ''; + } + + try { + $date = DateTimeImmutable::createFromFormat('Y-m-d H:i:s', $raw, new DateTimeZone('UTC')); + if (!$date instanceof DateTimeImmutable) { + $date = new DateTimeImmutable($raw, new DateTimeZone('UTC')); + } + return $date->setTimezone($this->displayTimezone())->format('d.m.Y H:i:s'); + } catch (\Throwable) { + return $raw; + } + } + private function normalizeCurrencyApiComCurrenciesPayload(array $payload): array { $rawCurrencies = is_array($payload['data'] ?? null) ? $payload['data'] : null; @@ -815,4 +866,9 @@ final class FxRatesService return new DateTimeZone('Europe/Berlin'); } } + + private function displayTimezone(): DateTimeZone + { + return $this->scheduleTimezone(); + } }