From b9f248aae02c75db4d87963510a274687a4daf38 Mon Sep 17 00:00:00 2001 From: Lars Gebhardt-Kusche Date: Sat, 2 May 2026 02:56:00 +0200 Subject: [PATCH] asdasd --- .../fx-rates/assets/fx-rates-currencies.js | 158 +++++++++++++ modules/fx-rates/assets/fx-rates.css | 141 ++++++++++++ modules/fx-rates/design.json | 3 +- modules/fx-rates/module.json | 2 - modules/fx-rates/pages/currencies.php | 213 ++++++++++++++++++ partials/landingpages/modules/setup.php | 51 ----- 6 files changed, 514 insertions(+), 54 deletions(-) create mode 100644 modules/fx-rates/assets/fx-rates-currencies.js create mode 100644 modules/fx-rates/pages/currencies.php diff --git a/modules/fx-rates/assets/fx-rates-currencies.js b/modules/fx-rates/assets/fx-rates-currencies.js new file mode 100644 index 0000000..57a5693 --- /dev/null +++ b/modules/fx-rates/assets/fx-rates-currencies.js @@ -0,0 +1,158 @@ +(() => { + const root = document.getElementById('fx-rates-currencies'); + if (!root) { + return; + } + + const page = JSON.parse(root.dataset.page || '{}'); + const currencies = Array.isArray(page.currencies) ? page.currencies : []; + const selected = new Set( + (Array.isArray(page.preferred_currencies) ? page.preferred_currencies : []) + .map((code) => String(code || '').trim().toUpperCase()) + .filter(Boolean) + ); + let displayBase = String(page.display_base_currency || '').trim().toUpperCase(); + + const nodes = { + tokenList: root.querySelector('[data-fx-token-list]'), + searchInput: root.querySelector('[data-fx-search-input]'), + suggestions: root.querySelector('[data-fx-suggestions]'), + displayBaseSelect: root.querySelector('[data-fx-display-base-select]'), + displayBaseHidden: root.querySelector('[data-fx-display-base-hidden]'), + hiddenPreferred: root.querySelector('[data-fx-hidden-preferred]'), + }; + + const escapeHtml = (value) => String(value || '') + .replaceAll('&', '&') + .replaceAll('<', '<') + .replaceAll('>', '>') + .replaceAll('"', '"') + .replaceAll("'", '''); + + const currencyByCode = new Map( + currencies.map((currency) => [String(currency.code || '').toUpperCase(), currency]) + ); + + const sortedSelectedCodes = () => Array.from(selected).sort((left, right) => left.localeCompare(right)); + + const ensureDisplayBase = () => { + const available = sortedSelectedCodes(); + if (available.length === 0) { + displayBase = ''; + return; + } + if (!displayBase || !selected.has(displayBase)) { + displayBase = available[0]; + } + }; + + const renderHiddenInputs = () => { + if (!nodes.hiddenPreferred) { + return; + } + nodes.hiddenPreferred.innerHTML = sortedSelectedCodes() + .map((code) => ``) + .join(''); + if (nodes.displayBaseHidden) { + nodes.displayBaseHidden.value = displayBase; + } + }; + + const renderDisplayBase = () => { + if (!nodes.displayBaseSelect) { + return; + } + ensureDisplayBase(); + const available = sortedSelectedCodes(); + nodes.displayBaseSelect.innerHTML = available.length + ? available.map((code) => ``).join('') + : ''; + nodes.displayBaseSelect.disabled = available.length === 0; + }; + + const removeCode = (code) => { + selected.delete(code); + renderAll(); + }; + + const renderTokens = () => { + if (!nodes.tokenList) { + return; + } + const selectedCodes = sortedSelectedCodes(); + if (selectedCodes.length === 0) { + nodes.tokenList.innerHTML = '
Noch keine bevorzugten Waehrungen ausgewaehlt.
'; + return; + } + nodes.tokenList.innerHTML = selectedCodes.map((code) => { + const currency = currencyByCode.get(code) || { code, name: code }; + return ` + + `; + }).join(''); + + nodes.tokenList.querySelectorAll('[data-remove-code]').forEach((button) => { + button.addEventListener('click', () => { + removeCode(String(button.getAttribute('data-remove-code') || '').toUpperCase()); + }); + }); + }; + + const renderSuggestions = () => { + if (!nodes.suggestions || !nodes.searchInput) { + return; + } + const needle = String(nodes.searchInput.value || '').trim().toLowerCase(); + if (!needle) { + nodes.suggestions.innerHTML = ''; + return; + } + const matches = currencies + .filter((currency) => !selected.has(String(currency.code || '').toUpperCase())) + .filter((currency) => { + const code = String(currency.code || '').toLowerCase(); + const name = String(currency.name || '').toLowerCase(); + return code.includes(needle) || name.includes(needle); + }) + .slice(0, 12); + + nodes.suggestions.innerHTML = matches.map((currency) => ` + + `).join(''); + + nodes.suggestions.querySelectorAll('[data-add-code]').forEach((button) => { + button.addEventListener('click', () => { + const code = String(button.getAttribute('data-add-code') || '').toUpperCase(); + if (code) { + selected.add(code); + if (nodes.searchInput) { + nodes.searchInput.value = ''; + } + renderAll(); + } + }); + }); + }; + + const renderAll = () => { + ensureDisplayBase(); + renderTokens(); + renderDisplayBase(); + renderHiddenInputs(); + renderSuggestions(); + }; + + nodes.searchInput?.addEventListener('input', renderSuggestions); + nodes.displayBaseSelect?.addEventListener('change', () => { + displayBase = String(nodes.displayBaseSelect?.value || '').trim().toUpperCase(); + renderHiddenInputs(); + }); + + renderAll(); +})(); diff --git a/modules/fx-rates/assets/fx-rates.css b/modules/fx-rates/assets/fx-rates.css index 60e8e0d..f6e30b8 100644 --- a/modules/fx-rates/assets/fx-rates.css +++ b/modules/fx-rates/assets/fx-rates.css @@ -54,6 +54,19 @@ border-color: #1c2734; } +.fx-button--ghost { + background: #fff4fb; + border-color: #f5b7d7; + color: #ff8a00; +} + +.fx-button--accent { + background: linear-gradient(90deg, #ff006a 0%, #ff9e00 100%); + color: #111827; + border-color: transparent; + font-weight: 700; +} + .fx-button[disabled] { opacity: 0.6; cursor: wait; @@ -139,6 +152,122 @@ text-align: right; } +.fx-action-row { + display: flex; + gap: 0.75rem; + flex-wrap: wrap; + margin-bottom: 1rem; +} + +.fx-action-row form { + margin: 0; +} + +.fx-mini-grid { + display: grid; + gap: 1rem; + grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); + margin: 1rem 0 1.25rem; +} + +.fx-mini-card { + display: grid; + gap: 0.2rem; +} + +.fx-mini-label, +.fx-field-label { + font-size: 0.9rem; + color: #6a7383; + letter-spacing: 0.18em; + text-transform: uppercase; +} + +.fx-currency-selection-row { + display: flex; + flex-wrap: wrap; + gap: 1rem; + align-items: flex-start; +} + +.fx-token-list { + display: flex; + flex-wrap: wrap; + gap: 0.75rem; +} + +.fx-token-list--inline { + flex: 1 1 auto; +} + +.fx-currency-search { + flex: 0 1 360px; + min-width: 260px; +} + +.fx-input, +.fx-select { + width: 100%; + border: 1px solid #d0d7e2; + border-radius: 18px; + padding: 0.8rem 1rem; + background: #fff; + color: #1c2734; +} + +.fx-field { + display: grid; + gap: 0.45rem; + margin-top: 1rem; +} + +.fx-token, +.fx-suggestion { + display: inline-flex; + align-items: center; + gap: 0.6rem; + border: 1px solid #d0d7e2; + border-radius: 999px; + padding: 0.7rem 1rem; + background: #fff; + color: #1c2734; +} + +.fx-token { + cursor: pointer; +} + +.fx-token:hover, +.fx-suggestion:hover { + border-color: rgba(255, 158, 0, 0.45); + background: rgba(255, 244, 251, 0.9); +} + +.fx-token-close { + color: #ff9e00; + font-weight: 700; + text-transform: uppercase; +} + +.fx-suggestion-list { + display: flex; + flex-wrap: wrap; + gap: 0.75rem; + margin-top: 0.9rem; +} + +.fx-suggestion { + cursor: pointer; +} + +.fx-suggestion strong { + color: #111827; +} + +.fx-text { + color: #5b6573; +} + .fx-history-date { display: inline-flex; align-items: center; @@ -161,3 +290,15 @@ justify-content: center; padding: 0; } + +@media (max-width: 860px) { + .fx-currency-selection-row { + flex-direction: column; + } + + .fx-currency-search { + flex: 1 1 auto; + width: 100%; + min-width: 0; + } +} diff --git a/modules/fx-rates/design.json b/modules/fx-rates/design.json index aea9622..23c3963 100644 --- a/modules/fx-rates/design.json +++ b/modules/fx-rates/design.json @@ -6,6 +6,7 @@ { "label": "Setup", "href": "/modules/setup/fx-rates", "variant": "secondary" } ], "tabs": [ - { "label": "Ueberblick", "href": "/module/fx-rates", "match_prefixes": ["/module/fx-rates"] } + { "label": "Ueberblick", "href": "/module/fx-rates" }, + { "label": "Waehrungen", "href": "/module/fx-rates/currencies", "match_prefixes": ["/module/fx-rates/currencies"] } ] } diff --git a/modules/fx-rates/module.json b/modules/fx-rates/module.json index e57ce47..557a2f5 100644 --- a/modules/fx-rates/module.json +++ b/modules/fx-rates/module.json @@ -20,8 +20,6 @@ { "name": "timeout_sec", "label": "Timeout (Sek.)", "type": "number", "required": false }, { "name": "refresh_max_age_minutes", "label": "Max. Alter fuer API-Refresh (Min.)", "type": "number", "required": false, "help": "Blockiert neue API-Refresh-Aufrufe, solange der letzte gespeicherte Abruf juenger ist. Manuelle Abrufe koennen nach Hinweis trotzdem erzwungen werden; Cron ignoriert diesen Wert." }, { "name": "default_base_currency", "label": "Standard-Basiswaehrung", "type": "text", "required": false, "help": "Wird fuer taegliche Abrufe und Snapshot-Abfragen verwendet." }, - { "name": "display_base_currency", "label": "Anzeige-Basiswaehrung", "type": "select", "required": false, "help": "Basis fuer die Anzeige der zuletzt gespeicherten Kurse im Modul." }, - { "name": "preferred_currencies", "label": "Bevorzugte Waehrungen", "type": "multiselect", "required": false, "help": "Auswahl aus dem synchronisierten Waehrungskatalog." }, { "name": "schedule_timezone", "label": "Scheduler-Zeitzone", "type": "text", "required": false, "help": "z.B. Europe/Berlin" } ] }, diff --git a/modules/fx-rates/pages/currencies.php b/modules/fx-rates/pages/currencies.php new file mode 100644 index 0000000..1c9c62d --- /dev/null +++ b/modules/fx-rates/pages/currencies.php @@ -0,0 +1,213 @@ +assets(); +if ($assets) { + $assets->addStyle('/module/fx-rates/asset?file=fx-rates.css'); + $assets->addScript('/module/fx-rates/asset?file=fx-rates-currencies.js', 'footer', true); +} + +$settings = module_fn('fx-rates', 'settings'); +$service = module_fn('fx-rates', 'service'); +$notice = trim((string) ($_GET['notice'] ?? '')); +$error = trim((string) ($_GET['error'] ?? '')); + +if (strtoupper((string) ($_SERVER['REQUEST_METHOD'] ?? 'GET')) === 'POST') { + try { + $action = trim((string) ($_POST['fx_action'] ?? '')); + if ($action === 'save_selection') { + $payload = [ + 'display_base_currency' => (string) ($_POST['display_base_currency'] ?? ''), + 'preferred_currencies' => $_POST['preferred_currencies'] ?? [], + ]; + $saved = module_fn('fx-rates', 'save_runtime_settings', $payload); + $params = ['notice' => 'Waehrungs-Auswahl gespeichert.']; + if (is_array($saved) && !empty($saved['display_base_currency'])) { + $params['base'] = (string) $saved['display_base_currency']; + } + redirect('/module/fx-rates/currencies?' . http_build_query($params)); + } + + if ($action === 'sync_catalog') { + $result = module_fn('fx-rates', 'run_setup_action', 'sync_currency_catalog'); + redirect('/module/fx-rates/currencies?' . http_build_query([ + 'notice' => sprintf('Waehrungskatalog synchronisiert. %d Waehrungen verarbeitet.', (int) ($result['synced_count'] ?? 0)), + ])); + } + + if ($action === 'refresh_rates') { + $result = $service->refreshLatestRates(null, (string) ($settings['default_base_currency'] ?? ''), 'manual'); + redirect('/module/fx-rates/currencies?' . http_build_query([ + 'notice' => sprintf('Alle Wechselkurse aktualisiert. %d Werte gespeichert.', (int) ($result['updated_count'] ?? 0)), + ])); + } + } catch (\Throwable $exception) { + redirect('/module/fx-rates/currencies?' . http_build_query([ + 'error' => $exception->getMessage() !== '' ? $exception->getMessage() : 'Aktion konnte nicht ausgefuehrt werden.', + ])); + } +} + +$catalog = is_array($settings['currency_catalog'] ?? null) ? $settings['currency_catalog'] : []; +$preferredCurrencies = is_array($settings['preferred_currencies'] ?? null) ? $settings['preferred_currencies'] : []; +$displayBaseCurrency = strtoupper(trim((string) ($settings['display_base_currency'] ?? $settings['default_base_currency'] ?? 'EUR'))); +$latest = $service->latestStatus(); +$recentFetches = $service->recentFetches(15); + +$currencies = []; +foreach ($catalog as $item) { + if (!is_array($item)) { + continue; + } + $code = strtoupper(trim((string) ($item['code'] ?? ''))); + $name = trim((string) ($item['name'] ?? '')); + if ($code === '' || $name === '') { + continue; + } + $currencies[] = [ + 'code' => $code, + 'name' => $name, + ]; +} + +$cryptoCodes = array_fill_keys([ + 'ADA', 'ARB', 'BNB', 'BTC', 'DAI', 'DOGE', 'DOT', 'ETH', 'LINK', 'LTC', + 'SOL', 'USDC', 'USDT', 'XAG', 'XAU', 'XRP', +], true); +$fiatCount = 0; +$cryptoCount = 0; +foreach ($currencies as $currency) { + if (isset($cryptoCodes[$currency['code']])) { + $cryptoCount++; + } else { + $fiatCount++; + } +} + +$currencyPageData = json_encode([ + 'currencies' => $currencies, + 'preferred_currencies' => array_values(array_unique(array_map(static fn (mixed $code): string => strtoupper(trim((string) $code)), $preferredCurrencies))), + 'display_base_currency' => $displayBaseCurrency, +], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); + +$tabs = [ + ['label' => 'Ueberblick', 'href' => '/module/fx-rates'], + ['label' => 'Waehrungen', 'href' => '/module/fx-rates/currencies', 'active' => true], +]; +?> + 'Waehrungskurse', + 'tabs' => $tabs, + 'actions' => [ + ['label' => 'Setup', 'href' => '/modules/setup/fx-rates', 'variant' => 'secondary', 'size' => 'sm'], + ], +]) ?> +
'> +
+
+ +
+ +
+ + +

Waehrungs-Update

+

Auswahl wird in den FX-Rates-Einstellungen gespeichert und steht damit auf Handy und Desktop gleich zur Verfuegung.

+ +
+
+ + +
+ +
+
+ + +
+
+ + +
+
+ +
+
+
Fiat
+
Waehrungen
+
+
+
Krypto
+
Waehrungen
+
+
+ +
Bevorzugte Waehrungen fuer Anzeige
+
+
+ +
+
+ + +
+ +
+
+
+

Letzte 15 Kurs-Uploads

+

Zeigt die zuletzt gespeicherten Wechselkurse aus der Datenbank.

+
+
+
Anzeige-Basis:
+
Letzter Abruf:
+
+
+
+ + + + + + + + + + + + + + + + + + snapshotByFetchId((int) ($fetch['id'] ?? 0), $displayBaseCurrency, $preferredCurrencies); + $rates = is_array($snapshot['rates'] ?? null) ? $snapshot['rates'] : []; + ?> + + + + + + + + + + + + + +
ZeitStichtagFetch-BasisProvider
Noch keine Abrufe vorhanden.
+
+
+
+
+ diff --git a/partials/landingpages/modules/setup.php b/partials/landingpages/modules/setup.php index c4b5053..90df052 100644 --- a/partials/landingpages/modules/setup.php +++ b/partials/landingpages/modules/setup.php @@ -687,57 +687,6 @@ $activeDbGroup = $testGroup !== null && array_key_exists($testGroup, $dbGroups) -
-
-
- Waehrungen -

Waehrungseinstellungen

-

Die Auswahl wird erst verfuegbar, nachdem der Waehrungskatalog synchronisiert wurde.

-
-
- -
- Noch kein Waehrungskatalog vorhanden. Fuehre zuerst unten die Modulaktion Waehrungskatalog synchronisieren aus. -
- -
- - -
- -
-