fx-rates
This commit is contained in:
@@ -35,6 +35,15 @@ $mm->registerFunction($moduleName, 'settings', static function (): array {
|
|||||||
$timeout = max(2, (int) ($saved['timeout_sec'] ?? (getenv('FX_RATES_TIMEOUT') ?: getenv('MINING_CHECKER_FX_TIMEOUT') ?: 10)));
|
$timeout = max(2, (int) ($saved['timeout_sec'] ?? (getenv('FX_RATES_TIMEOUT') ?: getenv('MINING_CHECKER_FX_TIMEOUT') ?: 10)));
|
||||||
$cacheTtl = max(60, (int) ($saved['cache_ttl_sec'] ?? (getenv('FX_RATES_CACHE_TTL') ?: getenv('MINING_CHECKER_FX_CACHE_TTL') ?: 21600)));
|
$cacheTtl = max(60, (int) ($saved['cache_ttl_sec'] ?? (getenv('FX_RATES_CACHE_TTL') ?: getenv('MINING_CHECKER_FX_CACHE_TTL') ?: 21600)));
|
||||||
|
|
||||||
|
$preferredCurrencies = $saved['preferred_currencies'] ?? ['EUR', 'USD', 'DOGE'];
|
||||||
|
if (is_string($preferredCurrencies)) {
|
||||||
|
$preferredCurrencies = preg_split('/[\s,;]+/', $preferredCurrencies) ?: [];
|
||||||
|
}
|
||||||
|
$preferredCurrencies = array_values(array_unique(array_filter(array_map(
|
||||||
|
static fn (mixed $code): string => strtoupper(trim((string) $code)),
|
||||||
|
is_array($preferredCurrencies) ? $preferredCurrencies : []
|
||||||
|
), static fn (string $code): bool => $code !== '')));
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'provider' => $provider !== '' ? $provider : 'currencyapi',
|
'provider' => $provider !== '' ? $provider : 'currencyapi',
|
||||||
'api_url' => $apiUrl,
|
'api_url' => $apiUrl,
|
||||||
@@ -43,12 +52,46 @@ $mm->registerFunction($moduleName, 'settings', static function (): array {
|
|||||||
'timeout_sec' => $timeout,
|
'timeout_sec' => $timeout,
|
||||||
'cache_ttl_sec' => $cacheTtl,
|
'cache_ttl_sec' => $cacheTtl,
|
||||||
'default_base_currency' => strtoupper(trim((string) ($saved['default_base_currency'] ?? 'EUR'))) ?: 'EUR',
|
'default_base_currency' => strtoupper(trim((string) ($saved['default_base_currency'] ?? 'EUR'))) ?: 'EUR',
|
||||||
|
'display_base_currency' => strtoupper(trim((string) ($saved['display_base_currency'] ?? ($saved['default_base_currency'] ?? 'EUR')))) ?: 'EUR',
|
||||||
|
'preferred_currencies' => $preferredCurrencies,
|
||||||
'daily_refresh_enabled' => array_key_exists('daily_refresh_enabled', $saved) ? (bool) $saved['daily_refresh_enabled'] : true,
|
'daily_refresh_enabled' => array_key_exists('daily_refresh_enabled', $saved) ? (bool) $saved['daily_refresh_enabled'] : true,
|
||||||
'daily_refresh_hour' => max(0, min(23, (int) ($saved['daily_refresh_hour'] ?? 18))),
|
'daily_refresh_hour' => max(0, min(23, (int) ($saved['daily_refresh_hour'] ?? 18))),
|
||||||
'schedule_timezone' => trim((string) ($saved['schedule_timezone'] ?? 'Europe/Berlin')) ?: 'Europe/Berlin',
|
'schedule_timezone' => trim((string) ($saved['schedule_timezone'] ?? 'Europe/Berlin')) ?: 'Europe/Berlin',
|
||||||
];
|
];
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$mm->registerFunction($moduleName, 'save_runtime_settings', static function (array $payload): array {
|
||||||
|
$current = modules()->settings('fx-rates');
|
||||||
|
$normalized = module_fn('fx-rates', 'settings');
|
||||||
|
|
||||||
|
if (array_key_exists('default_base_currency', $payload)) {
|
||||||
|
$normalized['default_base_currency'] = strtoupper(trim((string) $payload['default_base_currency'])) ?: $normalized['default_base_currency'];
|
||||||
|
}
|
||||||
|
if (array_key_exists('display_base_currency', $payload)) {
|
||||||
|
$normalized['display_base_currency'] = strtoupper(trim((string) $payload['display_base_currency'])) ?: $normalized['display_base_currency'];
|
||||||
|
}
|
||||||
|
if (array_key_exists('preferred_currencies', $payload)) {
|
||||||
|
$preferredCurrencies = $payload['preferred_currencies'];
|
||||||
|
if (is_string($preferredCurrencies)) {
|
||||||
|
$preferredCurrencies = preg_split('/[\s,;]+/', $preferredCurrencies) ?: [];
|
||||||
|
}
|
||||||
|
$normalized['preferred_currencies'] = array_values(array_unique(array_filter(array_map(
|
||||||
|
static fn (mixed $code): string => strtoupper(trim((string) $code)),
|
||||||
|
is_array($preferredCurrencies) ? $preferredCurrencies : []
|
||||||
|
), static fn (string $code): bool => $code !== '')));
|
||||||
|
}
|
||||||
|
|
||||||
|
$toSave = array_merge($current, [
|
||||||
|
'default_base_currency' => $normalized['default_base_currency'],
|
||||||
|
'display_base_currency' => $normalized['display_base_currency'],
|
||||||
|
'preferred_currencies' => $normalized['preferred_currencies'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
modules()->saveSettings('fx-rates', $toSave);
|
||||||
|
|
||||||
|
return module_fn('fx-rates', 'settings');
|
||||||
|
});
|
||||||
|
|
||||||
$mm->registerFunction($moduleName, 'pdo', function () use ($moduleName): PDO {
|
$mm->registerFunction($moduleName, 'pdo', function () use ($moduleName): PDO {
|
||||||
$settings = modules()->settings($moduleName);
|
$settings = modules()->settings($moduleName);
|
||||||
$useSeparate = !empty($settings['use_separate_db']);
|
$useSeparate = !empty($settings['use_separate_db']);
|
||||||
|
|||||||
@@ -20,6 +20,8 @@
|
|||||||
{ "name": "timeout_sec", "label": "Timeout (Sek.)", "type": "number", "required": false },
|
{ "name": "timeout_sec", "label": "Timeout (Sek.)", "type": "number", "required": false },
|
||||||
{ "name": "cache_ttl_sec", "label": "Datei-Cache TTL (Sek.)", "type": "number", "required": false },
|
{ "name": "cache_ttl_sec", "label": "Datei-Cache TTL (Sek.)", "type": "number", "required": false },
|
||||||
{ "name": "default_base_currency", "label": "Standard-Basiswaehrung", "type": "text", "required": false, "help": "Wird fuer taegliche Abrufe und Snapshot-Abfragen verwendet." },
|
{ "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": "text", "required": false, "help": "Basis fuer die Anzeige der zuletzt gespeicherten Kurse im Modul." },
|
||||||
|
{ "name": "preferred_currencies", "label": "Bevorzugte Waehrungen", "type": "text", "required": false, "help": "Kommagetrennte Liste, z.B. EUR,USD,DOGE,BTC." },
|
||||||
{ "name": "daily_refresh_enabled", "label": "Taeglichen Abruf aktivieren", "type": "checkbox", "required": false },
|
{ "name": "daily_refresh_enabled", "label": "Taeglichen Abruf aktivieren", "type": "checkbox", "required": false },
|
||||||
{ "name": "daily_refresh_hour", "label": "Taegliche Abrufstunde", "type": "number", "required": false, "help": "Lokale Stunde fuer den Scheduler, standardmaessig 18." },
|
{ "name": "daily_refresh_hour", "label": "Taegliche Abrufstunde", "type": "number", "required": false, "help": "Lokale Stunde fuer den Scheduler, standardmaessig 18." },
|
||||||
{ "name": "schedule_timezone", "label": "Scheduler-Zeitzone", "type": "text", "required": false, "help": "z.B. Europe/Berlin" }
|
{ "name": "schedule_timezone", "label": "Scheduler-Zeitzone", "type": "text", "required": false, "help": "z.B. Europe/Berlin" }
|
||||||
|
|||||||
@@ -6,14 +6,243 @@ require_once dirname(__DIR__) . '/bootstrap.php';
|
|||||||
$settings = module_fn('fx-rates', 'settings');
|
$settings = module_fn('fx-rates', 'settings');
|
||||||
$service = module_fn('fx-rates', 'service');
|
$service = module_fn('fx-rates', 'service');
|
||||||
$latest = $service->latestStatus();
|
$latest = $service->latestStatus();
|
||||||
|
$preferredCurrencies = is_array($settings['preferred_currencies'] ?? null) ? $settings['preferred_currencies'] : [];
|
||||||
|
$pageData = json_encode([
|
||||||
|
'settings' => $settings,
|
||||||
|
'latest' => $latest,
|
||||||
|
'preferred_currencies' => $preferredCurrencies,
|
||||||
|
], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||||
?>
|
?>
|
||||||
<?= module_shell_header('fx-rates', ['title' => 'Waehrungskurse']) ?>
|
<?= module_shell_header('fx-rates', ['title' => 'Waehrungskurse']) ?>
|
||||||
<div class="module-copy">
|
<div id="fx-rates-app" data-page='<?= e(is_string($pageData) ? $pageData : '{}') ?>'>
|
||||||
|
<div class="fx-stack">
|
||||||
|
<div class="fx-card">
|
||||||
|
<div class="fx-card-head">
|
||||||
|
<div>
|
||||||
<h1>Waehrungskurse</h1>
|
<h1>Waehrungskurse</h1>
|
||||||
<p>Zentrale Quelle fuer FX-Snapshots, Zeitabfragen und manuelle Aktualisierung.</p>
|
<p>Zentrale Quelle fuer FX-Snapshots, Zeitabfragen und manuelle Aktualisierung.</p>
|
||||||
<p><strong>Standard-Basis:</strong> <?= e((string) ($settings['default_base_currency'] ?? 'EUR')) ?></p>
|
</div>
|
||||||
<p><strong>Scheduler:</strong> taeglich um <?= e(str_pad((string) ($settings['daily_refresh_hour'] ?? 18), 2, '0', STR_PAD_LEFT)) ?>:00 (<?= e((string) ($settings['schedule_timezone'] ?? 'Europe/Berlin')) ?>)</p>
|
<div class="fx-actions">
|
||||||
<p><strong>Letzter Abruf:</strong> <?= e((string) ($latest['fetched_at'] ?? 'noch keiner')) ?></p>
|
<button type="button" class="fx-button fx-button--primary" data-action="refresh-rates">Aktuelle Kurse abrufen</button>
|
||||||
<p><strong>API:</strong> <code>/api/fx-rates/v1/latest</code>, <code>/api/fx-rates/v1/rate</code>, <code>/api/fx-rates/v1/history</code>, <code>/api/fx-rates/v1/refresh</code></p>
|
<button type="button" class="fx-button" data-action="refresh-catalog">Waehrungskatalog sync</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="fx-meta-grid">
|
||||||
|
<div><strong>Standard-Basis:</strong> <span data-bind="default-base"><?= e((string) ($settings['default_base_currency'] ?? 'EUR')) ?></span></div>
|
||||||
|
<div><strong>Anzeige-Basis:</strong> <span data-bind="display-base"><?= e((string) ($settings['display_base_currency'] ?? 'EUR')) ?></span></div>
|
||||||
|
<div><strong>Scheduler:</strong> taeglich um <?= e(str_pad((string) ($settings['daily_refresh_hour'] ?? 18), 2, '0', STR_PAD_LEFT)) ?>:00 (<?= e((string) ($settings['schedule_timezone'] ?? 'Europe/Berlin')) ?>)</div>
|
||||||
|
<div><strong>Letzter Abruf:</strong> <span data-bind="last-fetch"><?= e((string) ($latest['fetched_at'] ?? 'noch keiner')) ?></span></div>
|
||||||
|
</div>
|
||||||
|
<div class="fx-message" data-bind="message"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="fx-card">
|
||||||
|
<h2>Anzeige und Waehrungen</h2>
|
||||||
|
<p>Die Auswahl wird in den Modul-Settings gespeichert und steuert den Standardaufruf der letzten Kurse.</p>
|
||||||
|
<div class="fx-form-grid">
|
||||||
|
<label>
|
||||||
|
<span>Standard-Basiswaehrung</span>
|
||||||
|
<input type="text" name="default_base_currency" value="<?= e((string) ($settings['default_base_currency'] ?? 'EUR')) ?>">
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
<span>Anzeige-Basiswaehrung</span>
|
||||||
|
<input type="text" name="display_base_currency" value="<?= e((string) ($settings['display_base_currency'] ?? 'EUR')) ?>">
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<label class="fx-block">
|
||||||
|
<span>Bevorzugte Waehrungen</span>
|
||||||
|
<input type="text" name="preferred_currencies" value="<?= e(implode(', ', $preferredCurrencies)) ?>" placeholder="EUR, USD, DOGE, BTC">
|
||||||
|
</label>
|
||||||
|
<div class="fx-actions">
|
||||||
|
<button type="button" class="fx-button" data-action="save-settings">Auswahl speichern</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="fx-card">
|
||||||
|
<h2>Letzter Snapshot</h2>
|
||||||
|
<div class="fx-table-wrap">
|
||||||
|
<table class="fx-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Waehrung</th>
|
||||||
|
<th>Kurs</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody data-bind="rates-body">
|
||||||
|
<tr><td colspan="2">Noch keine Daten geladen.</td></tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<p class="fx-api-note">API: <code>/api/fx-rates/v1/latest</code>, <code>/api/fx-rates/v1/rate</code>, <code>/api/fx-rates/v1/history</code>, <code>/api/fx-rates/v1/refresh</code></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<style>
|
||||||
|
#fx-rates-app { padding: 1rem 0 2rem; }
|
||||||
|
.fx-stack { display: grid; gap: 1rem; }
|
||||||
|
.fx-card { background: var(--panel-bg, #fff); border: 1px solid rgba(15,23,42,.12); border-radius: 8px; padding: 1rem; }
|
||||||
|
.fx-card h1, .fx-card h2 { margin: 0 0 .5rem; }
|
||||||
|
.fx-card p { margin: 0 0 .75rem; color: #5b6573; }
|
||||||
|
.fx-card-head { display: flex; justify-content: space-between; gap: 1rem; align-items: flex-start; flex-wrap: wrap; }
|
||||||
|
.fx-actions { display: flex; gap: .75rem; flex-wrap: wrap; }
|
||||||
|
.fx-button { appearance: none; border: 1px solid #d0d7e2; background: #fff; color: #1c2734; border-radius: 8px; padding: .7rem 1rem; cursor: pointer; }
|
||||||
|
.fx-button--primary { background: #1c2734; color: #fff; border-color: #1c2734; }
|
||||||
|
.fx-button[disabled] { opacity: .6; cursor: wait; }
|
||||||
|
.fx-meta-grid, .fx-form-grid { display: grid; gap: .75rem; }
|
||||||
|
.fx-meta-grid { grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); margin-top: .75rem; }
|
||||||
|
.fx-form-grid { grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); }
|
||||||
|
.fx-form-grid label, .fx-block { display: grid; gap: .35rem; }
|
||||||
|
.fx-form-grid span, .fx-block span { font-size: .9rem; color: #5b6573; }
|
||||||
|
.fx-form-grid input, .fx-block input { width: 100%; border: 1px solid #d0d7e2; border-radius: 8px; padding: .7rem .8rem; }
|
||||||
|
.fx-message { min-height: 1.5rem; color: #1c2734; }
|
||||||
|
.fx-message.is-error { color: #b42318; }
|
||||||
|
.fx-message.is-success { color: #027a48; }
|
||||||
|
.fx-table-wrap { overflow-x: auto; }
|
||||||
|
.fx-table { width: 100%; border-collapse: collapse; }
|
||||||
|
.fx-table th, .fx-table td { text-align: left; border-bottom: 1px solid #eef2f6; padding: .65rem .4rem; }
|
||||||
|
.fx-api-note { margin-top: .75rem; font-size: .95rem; }
|
||||||
|
</style>
|
||||||
|
<script>
|
||||||
|
(() => {
|
||||||
|
const root = document.getElementById('fx-rates-app');
|
||||||
|
if (!root) return;
|
||||||
|
|
||||||
|
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"]'),
|
||||||
|
defaultBaseInput: root.querySelector('input[name="default_base_currency"]'),
|
||||||
|
displayBaseInput: root.querySelector('input[name="display_base_currency"]'),
|
||||||
|
preferredCurrenciesInput: root.querySelector('input[name="preferred_currencies"]'),
|
||||||
|
};
|
||||||
|
const apiBase = '/api/fx-rates/v1';
|
||||||
|
let loading = false;
|
||||||
|
|
||||||
|
const setMessage = (text, type = '') => {
|
||||||
|
if (!nodes.message) return;
|
||||||
|
nodes.message.textContent = text || '';
|
||||||
|
nodes.message.className = `fx-message${type ? ` is-${type}` : ''}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const setLoading = (state) => {
|
||||||
|
loading = state;
|
||||||
|
root.querySelectorAll('button[data-action]').forEach((button) => {
|
||||||
|
button.disabled = state;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const parsePreferredCurrencies = () => {
|
||||||
|
return String(nodes.preferredCurrenciesInput?.value || '')
|
||||||
|
.split(/[\s,;]+/)
|
||||||
|
.map((item) => 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 = '<tr><td colspan="2">Noch keine Wechselkurse fuer die ausgewaehlten Waehrungen gespeichert.</td></tr>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
nodes.ratesBody.innerHTML = entries.map(([code, rate]) => {
|
||||||
|
const formatted = typeof rate === 'number' ? rate.toLocaleString('de-DE', { maximumFractionDigits: 8 }) : 'n/a';
|
||||||
|
return `<tr><td>${code}</td><td>${formatted}</td></tr>`;
|
||||||
|
}).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(nodes.displayBaseInput?.value || 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(','));
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
|
||||||
|
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();
|
||||||
|
} catch (error) {
|
||||||
|
setMessage(error.message || 'Kurse konnten nicht aktualisiert werden.', 'error');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
root.querySelector('[data-action="refresh-catalog"]')?.addEventListener('click', async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
setMessage('Ich synchronisiere jetzt den Waehrungskatalog.');
|
||||||
|
const data = await request('/currencies-refresh', { method: 'POST', body: JSON.stringify({}) });
|
||||||
|
setMessage(`Waehrungskatalog synchronisiert. ${data?.synced_count || 0} Waehrungen verarbeitet.`, 'success');
|
||||||
|
} catch (error) {
|
||||||
|
setMessage(error.message || 'Waehrungskatalog konnte nicht synchronisiert 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();
|
||||||
|
} catch (error) {
|
||||||
|
setMessage(error.message || 'Waehrungs-Auswahl konnte nicht gespeichert werden.', 'error');
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
loadLatest().catch((error) => {
|
||||||
|
setMessage(error.message || 'Letzter Snapshot konnte nicht geladen werden.', 'error');
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
<?= module_shell_footer() ?>
|
<?= module_shell_footer() ?>
|
||||||
|
|||||||
@@ -29,6 +29,10 @@ final class Router
|
|||||||
if ($path === 'v1/latest' && $method === 'GET') {
|
if ($path === 'v1/latest' && $method === 'GET') {
|
||||||
$symbols = $this->parseCsv($_GET['symbols'] ?? null);
|
$symbols = $this->parseCsv($_GET['symbols'] ?? null);
|
||||||
$base = $this->stringOrNull($_GET['base'] ?? null);
|
$base = $this->stringOrNull($_GET['base'] ?? null);
|
||||||
|
if ($symbols === null) {
|
||||||
|
$settings = module_fn('fx-rates', 'settings');
|
||||||
|
$symbols = is_array($settings['preferred_currencies'] ?? null) ? $settings['preferred_currencies'] : null;
|
||||||
|
}
|
||||||
$snapshot = $this->service->snapshot($base, null, $symbols, null);
|
$snapshot = $this->service->snapshot($base, null, $symbols, null);
|
||||||
$this->respond(['data' => $snapshot]);
|
$this->respond(['data' => $snapshot]);
|
||||||
}
|
}
|
||||||
@@ -65,6 +69,10 @@ final class Router
|
|||||||
$input = $this->input();
|
$input = $this->input();
|
||||||
$base = $this->stringOrNull($input['base'] ?? null);
|
$base = $this->stringOrNull($input['base'] ?? null);
|
||||||
$currencies = $this->parseCsv($input['currencies'] ?? 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']);
|
$force = !empty($input['force']);
|
||||||
$maxAgeHours = is_numeric($input['max_age_hours'] ?? null) ? (float) $input['max_age_hours'] : 24.0;
|
$maxAgeHours = is_numeric($input['max_age_hours'] ?? null) ? (float) $input['max_age_hours'] : 24.0;
|
||||||
|
|
||||||
@@ -84,6 +92,18 @@ final class Router
|
|||||||
$this->respond(['data' => $this->service->probeCurrencyCatalog()]);
|
$this->respond(['data' => $this->service->probeCurrencyCatalog()]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($path === 'v1/currencies-refresh' && $method === 'POST') {
|
||||||
|
$this->respond(['data' => $this->service->refreshCurrencyCatalog()], 201);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($path === 'v1/settings' && $method === 'GET') {
|
||||||
|
$this->respond(['data' => module_fn('fx-rates', 'settings')]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($path === 'v1/settings' && $method === 'PUT') {
|
||||||
|
$this->respond(['data' => module_fn('fx-rates', 'save_runtime_settings', $this->input())]);
|
||||||
|
}
|
||||||
|
|
||||||
$this->respond(['error' => 'Unbekannter API-Pfad.'], 404);
|
$this->respond(['error' => 'Unbekannter API-Pfad.'], 404);
|
||||||
} catch (\Throwable $exception) {
|
} catch (\Throwable $exception) {
|
||||||
$this->respond([
|
$this->respond([
|
||||||
|
|||||||
Reference in New Issue
Block a user