asdasd
This commit is contained in:
158
modules/fx-rates/assets/fx-rates-currencies.js
Normal file
158
modules/fx-rates/assets/fx-rates-currencies.js
Normal file
@@ -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) => `<input type="hidden" name="preferred_currencies[]" value="${escapeHtml(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) => `<option value="${escapeHtml(code)}" ${code === displayBase ? 'selected' : ''}>${escapeHtml(code)}</option>`).join('')
|
||||
: '<option value="">Keine Waehrungen ausgewaehlt</option>';
|
||||
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 = '<div class="fx-text">Noch keine bevorzugten Waehrungen ausgewaehlt.</div>';
|
||||
return;
|
||||
}
|
||||
nodes.tokenList.innerHTML = selectedCodes.map((code) => {
|
||||
const currency = currencyByCode.get(code) || { code, name: code };
|
||||
return `
|
||||
<button type="button" class="fx-token" data-remove-code="${escapeHtml(code)}" title="${escapeHtml(code)} entfernen">
|
||||
<span>${escapeHtml(`${code} (${currency.name || code})`)}</span>
|
||||
<span class="fx-token-close">x</span>
|
||||
</button>
|
||||
`;
|
||||
}).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) => `
|
||||
<button type="button" class="fx-suggestion" data-add-code="${escapeHtml(String(currency.code || '').toUpperCase())}">
|
||||
<strong>${escapeHtml(String(currency.code || '').toUpperCase())}</strong>
|
||||
<span>${escapeHtml(String(currency.name || ''))}</span>
|
||||
</button>
|
||||
`).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();
|
||||
})();
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"] }
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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" }
|
||||
]
|
||||
},
|
||||
|
||||
213
modules/fx-rates/pages/currencies.php
Normal file
213
modules/fx-rates/pages/currencies.php
Normal file
@@ -0,0 +1,213 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
require_once dirname(__DIR__) . '/bootstrap.php';
|
||||
|
||||
$assets = app()->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],
|
||||
];
|
||||
?>
|
||||
<?= module_shell_header('fx-rates', [
|
||||
'title' => 'Waehrungskurse',
|
||||
'tabs' => $tabs,
|
||||
'actions' => [
|
||||
['label' => 'Setup', 'href' => '/modules/setup/fx-rates', 'variant' => 'secondary', 'size' => 'sm'],
|
||||
],
|
||||
]) ?>
|
||||
<div id="fx-rates-currencies" data-page='<?= e(is_string($currencyPageData) ? $currencyPageData : '{}') ?>'>
|
||||
<div class="fx-stack">
|
||||
<div class="fx-card">
|
||||
<?php if ($notice !== ''): ?>
|
||||
<div class="fx-message is-success"><?= e($notice) ?></div>
|
||||
<?php elseif ($error !== ''): ?>
|
||||
<div class="fx-message is-error"><?= e($error) ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<h2>Waehrungs-Update</h2>
|
||||
<p>Auswahl wird in den FX-Rates-Einstellungen gespeichert und steht damit auf Handy und Desktop gleich zur Verfuegung.</p>
|
||||
|
||||
<div class="fx-action-row">
|
||||
<form method="post">
|
||||
<input type="hidden" name="fx_action" value="save_selection">
|
||||
<input type="hidden" name="display_base_currency" value="<?= e($displayBaseCurrency) ?>" data-fx-display-base-hidden>
|
||||
<div data-fx-hidden-preferred></div>
|
||||
<button type="submit" class="fx-button fx-button--ghost">Auswahl speichern</button>
|
||||
</form>
|
||||
<form method="post">
|
||||
<input type="hidden" name="fx_action" value="refresh_rates">
|
||||
<button type="submit" class="fx-button fx-button--accent">Alle Wechselkurse aktualisieren</button>
|
||||
</form>
|
||||
<form method="post">
|
||||
<input type="hidden" name="fx_action" value="sync_catalog">
|
||||
<button type="submit" class="fx-button fx-button--ghost">Waehrungskatalog sync</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="fx-mini-grid">
|
||||
<div class="fx-mini-card">
|
||||
<div class="fx-mini-label">Fiat</div>
|
||||
<div><?= e((string) $fiatCount) ?> Waehrungen</div>
|
||||
</div>
|
||||
<div class="fx-mini-card">
|
||||
<div class="fx-mini-label">Krypto</div>
|
||||
<div><?= e((string) $cryptoCount) ?> Waehrungen</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="fx-field-label">Bevorzugte Waehrungen fuer Anzeige</div>
|
||||
<div class="fx-currency-selection-row">
|
||||
<div class="fx-token-list fx-token-list--inline" data-fx-token-list></div>
|
||||
<div class="fx-currency-search">
|
||||
<input type="text" class="fx-input" value="" placeholder="Waehrung hinzufuegen: EUR, USD, DOGE oder Euro" autocomplete="off" data-fx-search-input>
|
||||
</div>
|
||||
</div>
|
||||
<div class="fx-suggestion-list" data-fx-suggestions></div>
|
||||
|
||||
<div class="fx-field fx-currency-search">
|
||||
<label class="fx-field-label" for="fx-display-base-select">Darstellung auf Basis von</label>
|
||||
<select id="fx-display-base-select" class="fx-select" data-fx-display-base-select></select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="fx-card">
|
||||
<div class="fx-card-head">
|
||||
<div>
|
||||
<h2>Letzte 15 Kurs-Uploads</h2>
|
||||
<p>Zeigt die zuletzt gespeicherten Wechselkurse aus der Datenbank.</p>
|
||||
</div>
|
||||
<div class="fx-card-meta">
|
||||
<div><strong>Anzeige-Basis:</strong> <?= e($displayBaseCurrency) ?></div>
|
||||
<div><strong>Letzter Abruf:</strong> <?= e((string) ($latest['fetched_at_display'] ?? $latest['fetched_at'] ?? 'noch keiner')) ?></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="fx-table-wrap">
|
||||
<table class="fx-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Zeit</th>
|
||||
<th>Stichtag</th>
|
||||
<th>Fetch-Basis</th>
|
||||
<?php foreach ($preferredCurrencies as $currency): ?>
|
||||
<th><?= e((string) $currency) ?></th>
|
||||
<?php endforeach; ?>
|
||||
<th>Provider</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<?php if ($recentFetches === []): ?>
|
||||
<tr><td colspan="<?= 4 + count($preferredCurrencies) ?>">Noch keine Abrufe vorhanden.</td></tr>
|
||||
<?php else: ?>
|
||||
<?php foreach ($recentFetches as $fetch): ?>
|
||||
<?php
|
||||
$snapshot = $service->snapshotByFetchId((int) ($fetch['id'] ?? 0), $displayBaseCurrency, $preferredCurrencies);
|
||||
$rates = is_array($snapshot['rates'] ?? null) ? $snapshot['rates'] : [];
|
||||
?>
|
||||
<tr>
|
||||
<td><?= e((string) ($fetch['fetched_at_display'] ?? $fetch['fetched_at'] ?? '')) ?></td>
|
||||
<td><?= e((string) ($fetch['rate_date'] ?? '')) ?></td>
|
||||
<td><?= e((string) ($fetch['base_currency'] ?? '')) ?></td>
|
||||
<?php foreach ($preferredCurrencies as $currency): ?>
|
||||
<?php $value = $rates[(string) $currency] ?? null; ?>
|
||||
<td><?= is_numeric($value) ? e(number_format((float) $value, 8, ',', '')) : '–' ?></td>
|
||||
<?php endforeach; ?>
|
||||
<td><?= e((string) ($fetch['provider'] ?? '')) ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
<?php endif; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?= module_shell_footer() ?>
|
||||
@@ -687,57 +687,6 @@ $activeDbGroup = $testGroup !== null && array_key_exists($testGroup, $dbGroups)
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="setup-panel">
|
||||
<div class="setup-panel__head">
|
||||
<div>
|
||||
<span class="pill">Waehrungen</span>
|
||||
<h2>Waehrungseinstellungen</h2>
|
||||
<p class="muted">Die Auswahl wird erst verfuegbar, nachdem der Waehrungskatalog synchronisiert wurde.</p>
|
||||
</div>
|
||||
</div>
|
||||
<?php if (!$fxCatalogAvailable): ?>
|
||||
<div class="setup-db-message setup-db-message--hint">
|
||||
Noch kein Waehrungskatalog vorhanden. Fuehre zuerst unten die Modulaktion <strong>Waehrungskatalog synchronisieren</strong> aus.
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div class="setup-grid">
|
||||
<label class="setup-field muted">
|
||||
<span>Anzeige-Basiswaehrung</span>
|
||||
<input type="hidden" name="display_base_currency" value="<?= e((string) ($current['display_base_currency'] ?? '')) ?>" data-fx-base-hidden>
|
||||
<div class="fx-setup-picker" data-fx-base-picker data-currencies='<?= e(is_string($fxCatalogJson) ? $fxCatalogJson : '[]') ?>'>
|
||||
<input
|
||||
type="text"
|
||||
value="<?= e((string) (($current['display_base_currency'] ?? '') !== '' && isset($fxCatalogOptions[(string) $current['display_base_currency']]) ? ((string) $current['display_base_currency'] . ' - ' . $fxCatalogOptions[(string) $current['display_base_currency']]) : '')) ?>"
|
||||
placeholder="Waehrung suchen"
|
||||
autocomplete="off"
|
||||
data-fx-base-input
|
||||
>
|
||||
<div class="fx-setup-suggestions" data-fx-base-suggestions hidden></div>
|
||||
</div>
|
||||
<small class="muted">Suche nach Code oder Bezeichnung und waehle einen Treffer aus.</small>
|
||||
</label>
|
||||
<label class="setup-field muted">
|
||||
<span>Bevorzugte Waehrungen</span>
|
||||
<div class="fx-setup-picker" data-fx-multi-picker data-currencies='<?= e(is_string($fxCatalogJson) ? $fxCatalogJson : '[]') ?>'>
|
||||
<input type="text" value="" placeholder="Waehrung suchen und hinzufuegen" autocomplete="off" data-fx-multi-input>
|
||||
<div class="fx-setup-suggestions" data-fx-multi-suggestions hidden></div>
|
||||
<div class="fx-setup-tags fx-setup-tags--stacked" data-fx-tags>
|
||||
<?php foreach ($fxPreferred as $code): ?>
|
||||
<?php if (!isset($fxCatalogOptions[$code])) { continue; } ?>
|
||||
<span class="fx-setup-tag" data-code="<?= e($code) ?>">
|
||||
<?= e($code) ?>
|
||||
<button type="button" data-remove-code="<?= e($code) ?>" aria-label="Entfernen">x</button>
|
||||
</span>
|
||||
<input type="hidden" name="preferred_currencies[]" value="<?= e($code) ?>" data-code-hidden="<?= e($code) ?>">
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</div>
|
||||
<small class="muted">Tippen, Treffer anklicken und zur Liste hinzufuegen.</small>
|
||||
</label>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</section>
|
||||
|
||||
<section class="setup-panel">
|
||||
<div class="setup-panel__head">
|
||||
<div>
|
||||
|
||||
Reference in New Issue
Block a user