ysads
All checks were successful
Deploy / deploy-production (push) Has been skipped
Deploy / deploy-staging (push) Successful in 5s

This commit is contained in:
2026-05-11 00:34:07 +02:00
parent df2f217e22
commit 3379e7f465
2 changed files with 24 additions and 606 deletions

View File

@@ -42,7 +42,6 @@ foreach ($fields as $field) {
$fieldTypes[$fname] = (string)($field['type'] ?? 'text');
$fieldMeta[$fname] = $field;
}
$isFxRatesSetup = $moduleName === 'fx-rates';
$current = modules()->settings($moduleName);
$runtimeSettingsEnabled = modules()->hasFunction($moduleName, 'runtime_settings');
if ($runtimeSettingsEnabled) {
@@ -754,7 +753,7 @@ $manualGroups = array_values(array_filter($allowedGroups, fn (string $value): bo
$hasDatabaseSection = array_key_exists('database', $setupSectionConfig)
? !empty($setupSectionConfig['database'])
: $dbGroups !== [];
$hasCustomSection = $customSetupFields !== [] || $customSectionActions !== [] || $isFxRatesSetup;
$hasCustomSection = $customSetupFields !== [] || $customSectionActions !== [];
$showCustomDbConfig = !empty($current['use_separate_db']) && !in_array(strtolower(trim((string) ($current['use_separate_db'] ?? ''))), ['0', 'false', 'off', 'standard'], true);
$allowedSetupSections = ['general', 'access', 'cron'];
if ($hasDatabaseSection) {
@@ -827,605 +826,12 @@ $GLOBALS['layout_header_context'] = 'Setup / ' . ($sectionTitles[$currentSection
<option value="<?= e((string) $timezoneOption['value']) ?>"><?= e((string) $timezoneOption['label']) ?></option>
<?php endforeach; ?>
</datalist>
<?php if ($isFxRatesSetup): ?>
<?php
$fxCatalog = is_array($current['currency_catalog'] ?? null) ? $current['currency_catalog'] : [];
$fxCatalogOptions = [];
foreach ($fxCatalog as $currency) {
if (!is_array($currency)) {
continue;
}
$code = strtoupper(trim((string) ($currency['code'] ?? '')));
$name = trim((string) ($currency['name'] ?? ''));
if ($code === '' || $name === '') {
continue;
}
$fxCatalogOptions[$code] = $name;
}
ksort($fxCatalogOptions);
$fxCatalogAvailable = $fxCatalogOptions !== [];
$fxPreferred = is_array($current['preferred_currencies'] ?? null) ? $current['preferred_currencies'] : [];
$fxUseSeparateDb = !empty($current['use_separate_db']);
$fxCatalogJson = json_encode(array_map(
static fn (string $name, string $code): array => ['code' => $code, 'name' => $name],
array_values($fxCatalogOptions),
array_keys($fxCatalogOptions)
), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
?>
<?php if ($currentSection === 'custom'): ?>
<section class="setup-panel" id="setup-custom">
<div class="setup-panel__head">
<div>
<span class="pill">Custom Settings</span>
<h2>Provider und Abruf</h2>
</div>
</div>
<div class="setup-grid">
<label class="setup-field muted">
<span>FX Provider</span>
<input type="text" name="provider" value="<?= e((string) ($current['provider'] ?? 'currencyapi')) ?>">
<small class="muted">Unterstuetzt legacy currencyapi.net und currencyapi.com v3.</small>
</label>
<label class="setup-field muted">
<span>FX API Version</span>
<select name="api_version">
<option value="v2" <?= (string) ($current['api_version'] ?? 'v2') === 'v2' ? 'selected' : '' ?>>v2</option>
<option value="v3" <?= (string) ($current['api_version'] ?? 'v2') === 'v3' ? 'selected' : '' ?>>v3</option>
</select>
<small class="muted">Steuert die Endpoint-Version unabhaengig von der Domain.</small>
</label>
<label class="setup-field muted">
<span>FX API URL</span>
<input type="text" name="api_url" value="<?= e((string) ($current['api_url'] ?? 'https://currencyapi.net')) ?>">
<small class="muted">Nur die Basis-URL eintragen, z.B. https://currencyapi.net oder https://api.currencyapi.com.</small>
</label>
<label class="setup-field muted">
<span>FX API Key</span>
<input type="password" name="api_key" value="<?= e((string) ($current['api_key'] ?? '')) ?>">
</label>
<label class="setup-field muted">
<span>Timeout (Sek.)</span>
<input type="number" name="timeout_sec" value="<?= e((string) ($current['timeout_sec'] ?? 10)) ?>">
</label>
<label class="setup-field muted">
<span>Max. Alter fuer API-Refresh (Min.)</span>
<input type="number" name="refresh_max_age_minutes" min="1" value="<?= e((string) ($current['refresh_max_age_minutes'] ?? 60)) ?>">
<small class="muted">API-Refresh aktualisiert nur, wenn der letzte gespeicherte Abruf aelter ist.</small>
</label>
<label class="setup-field muted">
<span>Standard-Basiswaehrung</span>
<input type="text" name="default_base_currency" value="<?= e((string) ($current['default_base_currency'] ?? 'EUR')) ?>">
</label>
</div>
</section>
<?php endif; ?>
<?php if ($currentSection === 'general'): ?>
<section class="setup-panel" id="setup-general">
<div class="setup-panel__head">
<div>
<span class="pill">Allgemein</span>
<h2>Datenbank und Debug</h2>
</div>
</div>
<div class="setup-grid">
<?php foreach ($generalSetupFields as $field): ?>
<?php $renderField($field); ?>
<?php endforeach; ?>
<label class="setup-field muted">
<span>Eigene Modul-DB nutzen</span>
<input type="checkbox" name="use_separate_db" value="1" <?= $fxUseSeparateDb ? 'checked' : '' ?> data-fx-db-toggle>
<small class="muted">Wenn aktiv, werden die folgenden DB-Daten verwendet. Sonst wird die Nexus-Base-DB genutzt.</small>
</label>
</div>
<div data-fx-db-section <?= $fxUseSeparateDb ? '' : 'hidden' ?>>
<div class="setup-grid" style="margin-top:16px;">
<label class="setup-field muted">
<span>DB Driver</span>
<select name="db_driver">
<option value="">Bitte waehlen</option>
<?php foreach ($driverOptions as $driver => $driverLabel): ?>
<option value="<?= e($driver) ?>" <?= (string) ($current['db']['driver'] ?? '') === $driver ? 'selected' : '' ?>><?= e($driverLabel) ?></option>
<?php endforeach; ?>
</select>
</label>
<label class="setup-field muted">
<span>DB Host</span>
<input type="text" name="db_host" value="<?= e((string) ($current['db']['host'] ?? '')) ?>">
</label>
<label class="setup-field muted">
<span>DB Port</span>
<input type="number" name="db_port" value="<?= e((string) ($current['db']['port'] ?? '')) ?>">
</label>
<label class="setup-field muted">
<span>DB Name</span>
<input type="text" name="db_dbname" value="<?= e((string) ($current['db']['dbname'] ?? '')) ?>">
</label>
<label class="setup-field muted">
<span>DB Schema</span>
<input type="text" name="db_schema" value="<?= e((string) ($current['db']['schema'] ?? '')) ?>">
</label>
<label class="setup-field muted">
<span>DB User</span>
<input type="text" name="db_user" value="<?= e((string) ($current['db']['user'] ?? '')) ?>">
</label>
<label class="setup-field muted">
<span>DB Passwort</span>
<input type="password" name="db_password" value="<?= e((string) ($current['db']['password'] ?? '')) ?>">
</label>
</div>
</div>
</section>
<?php endif; ?>
<?php if ($currentSection === 'custom'): ?>
<section class="setup-panel">
<div class="setup-panel__head">
<div>
<span class="pill">Aktionen</span>
<h2>Modulaktionen</h2>
</div>
</div>
<div class="setup-grid">
<div class="setup-field muted">
<span>Waehrungssynch</span>
<small class="muted">Laedt die verfuegbaren Waehrungen einmalig aus dem konfigurierten FX-Provider.</small>
<div class="setup-actions" style="justify-content:flex-start; margin-top:12px;">
<button class="nav-link" type="submit" name="module_setup_action" value="sync_currency_catalog" formnovalidate>
Waehrungskatalog synchronisieren
</button>
</div>
<?php if (trim((string) ($current['currency_catalog_synced_at'] ?? '')) !== ''): ?>
<small class="muted">Letzter Sync: <?= e((string) $current['currency_catalog_synced_at']) ?></small>
<?php endif; ?>
</div>
</div>
</section>
<?php endif; ?>
<?php if ($currentSection === 'access'): ?>
<section class="setup-panel" id="setup-access">
<div class="setup-panel__head">
<div>
<span class="pill">Zugriffsrechte</span>
<h2>Zugriff verwalten</h2>
<p class="muted">Steuert, ob Login erforderlich ist und welche Benutzer oder Gruppen das Modul oeffnen duerfen.</p>
</div>
</div>
<div class="setup-grid">
<div class="setup-field muted">
<span>Login erforderlich</span>
<label style="display:flex; align-items:center; gap:10px;">
<input type="checkbox" name="auth_required" value="1" <?= !empty($authConfig['required']) ? 'checked' : '' ?>>
<span>Nur eingeloggte Nutzer duerfen dieses Modul oeffnen.</span>
</label>
</div>
<div class="setup-field muted">
<span>Erlaubte Benutzer</span>
<?php if ($knownUsers === []): ?>
<small class="muted">Noch keine bekannten Benutzer vorhanden. Nutzer erscheinen hier, sobald sie sich einmal angemeldet haben.</small>
<?php else: ?>
<div class="setup-auth-list">
<?php foreach ($knownUsers as $knownUser): ?>
<?php
$sub = (string) ($knownUser['sub'] ?? '');
$label = trim((string) ($knownUser['name'] ?? ''));
if ($label === '') {
$label = trim((string) ($knownUser['username'] ?? ''));
}
$email = trim((string) ($knownUser['email'] ?? ''));
$suffix = $email !== '' && $email !== $label ? ' (' . $email . ')' : '';
?>
<label style="display:flex; align-items:center; gap:10px;">
<input type="checkbox" name="auth_user_values[]" value="<?= e($sub) ?>" <?= in_array($sub, $allowedUsers, true) ? 'checked' : '' ?>>
<span><?= e(($label !== '' ? $label : $sub) . $suffix) ?></span>
</label>
<?php endforeach; ?>
</div>
<?php endif; ?>
<textarea name="auth_users" rows="3" placeholder="Weitere Keycloak-Sub, Benutzername oder E-Mail, je Zeile oder Komma"><?= e(implode("\n", $manualUsers)) ?></textarea>
</div>
<div class="setup-field muted">
<span>Erlaubte Gruppen</span>
<?php if ($knownGroups === []): ?>
<small class="muted">Noch keine bekannten Gruppen vorhanden.</small>
<?php else: ?>
<div class="setup-auth-list">
<?php foreach ($knownGroups as $knownGroup): ?>
<label style="display:flex; align-items:center; gap:10px;">
<input type="checkbox" name="auth_group_values[]" value="<?= e($knownGroup) ?>" <?= in_array($knownGroup, $allowedGroups, true) ? 'checked' : '' ?>>
<span><?= e($knownGroup) ?></span>
</label>
<?php endforeach; ?>
</div>
<?php endif; ?>
<textarea name="auth_groups" rows="3" placeholder="Weitere Gruppen, je Zeile oder Komma"><?= e(implode("\n", $manualGroups)) ?></textarea>
<small class="muted">Wenn Login aktiv ist und Benutzer sowie Gruppen leer bleiben, darf jeder eingeloggte Benutzer das Modul oeffnen.</small>
</div>
</div>
</section>
<?php endif; ?>
<?php if ($currentSection === 'cron'): ?>
<section class="setup-panel" id="setup-cron">
<div class="setup-panel__head">
<div>
<span class="pill">Cron Einstellungen</span>
<h2>Scheduler und Zeitsteuerung</h2>
<p class="muted">Hier liegen die zeitbezogenen Modul-Einstellungen, Intervall-Tasks und Cron-Jobs.</p>
</div>
</div>
<div class="setup-grid">
<label class="setup-field muted">
<span>Scheduler-Zeitzone</span>
<input type="text" name="schedule_timezone" value="<?= e((string) ($current['schedule_timezone'] ?? 'Europe/Berlin')) ?>" list="timezone-options" autocomplete="off">
<small class="muted">Diese Zeitzone wird fuer Cron-Jobs und lokale Zeitangaben genutzt.</small>
</label>
</div>
</section>
<?php endif; ?>
<?php if ($currentSection === 'cron' && $intervalTaskStatuses !== []): ?>
<section class="setup-panel">
<div class="setup-panel__head">
<div>
<span class="pill">Automationen</span>
<h2>Intervall-Aufgaben</h2>
<p class="muted">Diese Aufgaben werden beim ersten gueltigen Modulaufruf nach Ablauf des Intervalls automatisch ausgefuehrt.</p>
</div>
</div>
<div class="setup-grid">
<?php foreach ($intervalTaskStatuses as $task): ?>
<?php $state = is_array($task['state'] ?? null) ? $task['state'] : []; ?>
<div class="setup-field muted">
<span><?= e((string) ($task['label'] ?? $task['name'] ?? 'Intervall-Aufgabe')) ?></span>
<input type="text" readonly value="<?= e(!empty($task['enabled']) ? 'Aktiv' : 'Deaktiviert') ?>">
<small class="muted">Intervall: <?= e(number_format((float) ($task['interval_hours'] ?? 0), 2, ',', '')) ?> Stunden</small>
<small class="muted">Letzter Start: <?= e($formatRunTimestamp((string) ($state['last_started_at'] ?? ''))) ?></small>
<small class="muted">Letzter Erfolg: <?= e($formatRunTimestamp((string) ($state['last_success_at'] ?? ''))) ?></small>
<small class="muted">Naechster Lauf: <?= e($formatRunTimestamp((string) ($task['next_due_at'] ?? ''))) ?></small>
</div>
<?php endforeach; ?>
</div>
</section>
<?php endif; ?>
<?php if ($currentSection === 'cron' && $cronTaskDefinitions !== []): ?>
<section class="setup-panel">
<div class="setup-panel__head">
<div>
<span class="pill">Automationen</span>
<h2>Cron-Jobs</h2>
<p class="muted">Diese Jobs werden ueber den zentralen Nexus-Scheduler ausgefuehrt. Der System-Cron sollte den CLI-Runner jede Minute starten.</p>
</div>
</div>
<?php foreach ($cronTaskDefinitions as $cronDefinition): ?>
<?php
$cronName = trim((string) ($cronDefinition['name'] ?? ''));
if ($cronName === '') { continue; }
$cronMode = (string) ($cronDefinition['mode'] ?? 'single');
$cronEntries = $cronTaskStatusGroups[$cronName] ?? [];
?>
<div class="setup-field muted" data-scheduler-job data-job-name="<?= e($cronName) ?>" data-job-mode="<?= e($cronMode) ?>" data-job-label="<?= e((string) ($cronDefinition['label'] ?? $cronName)) ?>" data-job-callback="<?= e((string) ($cronDefinition['callback'] ?? '')) ?>">
<span><?= e((string) ($cronDefinition['label'] ?? $cronName)) ?></span>
<?php if (trim((string) ($cronDefinition['help'] ?? '')) !== ''): ?>
<small class="muted"><?= e((string) $cronDefinition['help']) ?></small>
<?php endif; ?>
<div class="scheduler-entries" data-scheduler-entries>
<?php foreach (array_values($cronEntries) as $entryIndex => $task): ?>
<?php
$cronConfig = is_array($task['config'] ?? null) ? $task['config'] : [];
$builder = is_array($cronConfig['builder'] ?? null) ? $cronConfig['builder'] : [];
?>
<div class="scheduler-entry" data-scheduler-entry>
<label><input type="checkbox" name="scheduler_jobs[<?= e($cronName) ?>][entries][<?= e((string) $entryIndex) ?>][enabled]" value="1" data-enabled <?= !empty($cronConfig['enabled']) ? 'checked' : '' ?>> Aktiv</label>
<input type="text" name="scheduler_jobs[<?= e($cronName) ?>][entries][<?= e((string) $entryIndex) ?>][cron_expression]" value="<?= e((string) ($cronConfig['cron_expression'] ?? '')) ?>" data-cron-expression>
<small class="muted">Cron-Syntax: Minute Stunde Tag Monat Wochentag</small>
<input type="text" name="scheduler_jobs[<?= e($cronName) ?>][entries][<?= e((string) $entryIndex) ?>][timezone]" value="<?= e((string) ($cronConfig['timezone'] ?? 'UTC')) ?>" data-cron-timezone>
<select name="scheduler_jobs[<?= e($cronName) ?>][entries][<?= e((string) $entryIndex) ?>][builder][mode]" data-cron-builder-mode>
<option value="builder" <?= (string) ($builder['mode'] ?? 'builder') === 'builder' ? 'selected' : '' ?>>Builder</option>
<option value="manual" <?= (string) ($builder['mode'] ?? 'builder') === 'manual' ? 'selected' : '' ?>>Cron-Syntax</option>
</select>
<div data-cron-builder-fields>
<select name="scheduler_jobs[<?= e($cronName) ?>][entries][<?= e((string) $entryIndex) ?>][builder][kind]" data-cron-builder-kind>
<option value="daily" <?= (string) ($builder['kind'] ?? 'daily') === 'daily' ? 'selected' : '' ?>>Taeglich</option>
<option value="every_x_days" <?= (string) ($builder['kind'] ?? '') === 'every_x_days' ? 'selected' : '' ?>>Alle x Tage</option>
<option value="weekly" <?= (string) ($builder['kind'] ?? '') === 'weekly' ? 'selected' : '' ?>>Woechentlich</option>
<option value="monthly_day" <?= (string) ($builder['kind'] ?? '') === 'monthly_day' ? 'selected' : '' ?>>X-Tag im Monat</option>
<option value="every_x_hours" <?= (string) ($builder['kind'] ?? '') === 'every_x_hours' ? 'selected' : '' ?>>Alle x Stunden</option>
</select>
<input type="time" name="scheduler_jobs[<?= e($cronName) ?>][entries][<?= e((string) $entryIndex) ?>][builder][time]" value="<?= e((string) ($builder['time'] ?? '18:00')) ?>" data-cron-builder-time>
<input type="number" min="1" name="scheduler_jobs[<?= e($cronName) ?>][entries][<?= e((string) $entryIndex) ?>][builder][interval_days]" value="<?= e((string) ($builder['interval_days'] ?? 2)) ?>" data-cron-builder-interval-days>
<select name="scheduler_jobs[<?= e($cronName) ?>][entries][<?= e((string) $entryIndex) ?>][builder][weekday]" data-cron-builder-weekday>
<?php foreach ($cronWeekdays as $weekdayValue => $weekdayLabel): ?>
<option value="<?= e($weekdayValue) ?>" <?= (string) ($builder['weekday'] ?? '1') === $weekdayValue ? 'selected' : '' ?>><?= e($weekdayLabel) ?></option>
<?php endforeach; ?>
</select>
<input type="number" min="1" max="31" name="scheduler_jobs[<?= e($cronName) ?>][entries][<?= e((string) $entryIndex) ?>][builder][month_day]" value="<?= e((string) ($builder['month_day'] ?? 1)) ?>" data-cron-builder-month-day>
<input type="number" min="1" max="23" name="scheduler_jobs[<?= e($cronName) ?>][entries][<?= e((string) $entryIndex) ?>][builder][interval_hours]" value="<?= e((string) ($builder['interval_hours'] ?? 6)) ?>" data-cron-builder-interval-hours>
</div>
<small class="muted">Letzter Start: <?= e($formatRunTimestamp((string) ($task['state']['last_started_at'] ?? ''), (string) ($task['timezone'] ?? 'UTC'))) ?></small>
<small class="muted">Letzter Erfolg: <?= e($formatRunTimestamp((string) ($task['state']['last_success_at'] ?? ''), (string) ($task['timezone'] ?? 'UTC'))) ?></small>
<small class="muted">Naechster Lauf lokal: <?= e(!empty($task['enabled']) ? (string) (($task['next_due_at_local'] ?? '') !== '' ? $task['next_due_at_local'] : '-') : '-') ?></small>
<small class="muted">Aktion: <?= e((string) (($cronDefinition['label'] ?? $cronName) !== '' ? ($cronDefinition['label'] ?? $cronName) : $cronName)) ?><?php if (trim((string) ($cronDefinition['callback'] ?? '')) !== ''): ?> (<?= e((string) $cronDefinition['callback']) ?>)<?php endif; ?></small>
<small class="muted">Status: <?= e((string) (($task['state']['last_status'] ?? '') !== '' ? $task['state']['last_status'] : '-')) ?></small>
<small class="muted">Meldung: <?= e((string) (($task['state']['last_message'] ?? '') !== '' ? $task['state']['last_message'] : '-')) ?></small>
<?php if (trim((string) ($task['parse_error'] ?? '')) !== ''): ?>
<small class="muted" style="color:#b42318;">Cron-Fehler: <?= e((string) $task['parse_error']) ?></small>
<?php endif; ?>
<?php if ($cronMode === 'multi'): ?>
<button class="nav-link" type="button" data-remove-scheduler-entry>Eintrag entfernen</button>
<?php endif; ?>
</div>
<?php endforeach; ?>
</div>
<?php if ($cronMode === 'multi'): ?>
<div class="setup-actions" style="justify-content:flex-start; margin-top:12px;">
<button class="nav-link" type="button" data-add-scheduler-entry>Weiteren Cron hinzufügen</button>
</div>
<?php endif; ?>
</div>
<?php endforeach; ?>
</section>
<?php endif; ?>
<div class="setup-actions setup-actions--footer">
<button class="cta-button" type="submit">Setup speichern</button>
</div>
<script>
(() => {
const toggle = document.querySelector('[data-fx-db-toggle]');
const section = document.querySelector('[data-fx-db-section]');
if (!toggle || !section) return;
const sync = () => {
section.hidden = !toggle.checked;
};
toggle.addEventListener('change', sync);
sync();
})();
(() => {
const baseRoot = document.querySelector('[data-fx-base-picker]');
const baseHidden = document.querySelector('[data-fx-base-hidden]');
const baseInput = document.querySelector('[data-fx-base-input]');
const baseSuggestions = document.querySelector('[data-fx-base-suggestions]');
const multiRoot = document.querySelector('[data-fx-multi-picker]');
const multiInput = document.querySelector('[data-fx-multi-input]');
const multiSuggestions = document.querySelector('[data-fx-multi-suggestions]');
const tagsRoot = document.querySelector('[data-fx-tags]');
const parseCurrencies = (root) => {
if (!root) return [];
try {
const parsed = JSON.parse(root.dataset.currencies || '[]');
return Array.isArray(parsed) ? parsed : [];
} catch (error) {
return [];
}
};
const currencies = parseCurrencies(baseRoot || multiRoot);
const formatLabel = (item) => `${item.code} - ${item.name}`;
const searchCurrencies = (query, excluded = []) => {
const needle = String(query || '').trim().toLowerCase();
const excludedSet = new Set(excluded);
const filtered = currencies.filter((item) => {
if (!item || !item.code || excludedSet.has(item.code)) {
return false;
}
if (needle === '') {
return true;
}
return item.code.toLowerCase().includes(needle) || String(item.name || '').toLowerCase().includes(needle);
});
return filtered.slice(0, 12);
};
const closeSuggestions = (node) => {
if (!node) return;
node.hidden = true;
node.innerHTML = '';
};
const renderSuggestions = (node, items, onSelect) => {
if (!node || items.length === 0) {
closeSuggestions(node);
return;
}
node.innerHTML = '';
items.forEach((item) => {
const button = document.createElement('button');
button.type = 'button';
button.className = 'fx-setup-suggestion';
button.textContent = formatLabel(item);
button.addEventListener('click', () => onSelect(item));
node.appendChild(button);
});
node.hidden = false;
};
if (baseRoot && baseHidden && baseInput && baseSuggestions) {
const applyBase = (item) => {
baseHidden.value = item.code;
baseInput.value = formatLabel(item);
closeSuggestions(baseSuggestions);
};
baseInput.addEventListener('input', () => {
baseHidden.value = '';
renderSuggestions(baseSuggestions, searchCurrencies(baseInput.value), applyBase);
});
baseInput.addEventListener('focus', () => {
renderSuggestions(baseSuggestions, searchCurrencies(baseInput.value), applyBase);
});
baseInput.form?.addEventListener('submit', () => {
if (baseHidden.value !== '') {
return;
}
const first = searchCurrencies(baseInput.value)[0] || null;
if (first) {
applyBase(first);
}
});
}
if (multiRoot && multiInput && multiSuggestions && tagsRoot) {
const selectedCodes = () => Array.from(tagsRoot.querySelectorAll('[data-code]')).map((node) => node.getAttribute('data-code') || '');
const addTag = (item) => {
if (!item || selectedCodes().includes(item.code)) {
return;
}
const tag = document.createElement('span');
tag.className = 'fx-setup-tag';
tag.setAttribute('data-code', item.code);
tag.append(document.createTextNode(item.code));
const remove = document.createElement('button');
remove.type = 'button';
remove.setAttribute('data-remove-code', item.code);
remove.setAttribute('aria-label', 'Entfernen');
remove.textContent = 'x';
remove.addEventListener('click', () => {
tag.remove();
hidden.remove();
renderSuggestions(multiSuggestions, searchCurrencies(multiInput.value, selectedCodes()), addTag);
});
tag.appendChild(remove);
tagsRoot.appendChild(tag);
const hidden = document.createElement('input');
hidden.type = 'hidden';
hidden.name = 'preferred_currencies[]';
hidden.value = item.code;
hidden.setAttribute('data-code-hidden', item.code);
multiRoot.appendChild(hidden);
multiInput.value = '';
renderSuggestions(multiSuggestions, searchCurrencies('', selectedCodes()), addTag);
};
tagsRoot.querySelectorAll('[data-remove-code]').forEach((button) => {
button.addEventListener('click', () => {
const code = button.getAttribute('data-remove-code') || '';
tagsRoot.querySelector(`[data-code="${code}"]`)?.remove();
multiRoot.querySelector(`[data-code-hidden="${code}"]`)?.remove();
renderSuggestions(multiSuggestions, searchCurrencies(multiInput.value, selectedCodes()), addTag);
});
});
multiInput.addEventListener('input', () => {
renderSuggestions(multiSuggestions, searchCurrencies(multiInput.value, selectedCodes()), addTag);
});
multiInput.addEventListener('focus', () => {
renderSuggestions(multiSuggestions, searchCurrencies(multiInput.value, selectedCodes()), addTag);
});
}
document.addEventListener('click', (event) => {
if (baseRoot && !baseRoot.contains(event.target)) {
closeSuggestions(baseSuggestions);
}
if (multiRoot && !multiRoot.contains(event.target)) {
closeSuggestions(multiSuggestions);
}
});
})();
</script>
<style>
.fx-setup-picker {
position: relative;
display: grid;
gap: 10px;
}
.fx-setup-tags {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.fx-setup-tags--stacked {
min-height: 44px;
padding: 10px 12px;
border: 1px solid var(--line);
border-radius: 12px;
background: var(--surface-strong);
}
.fx-setup-tag {
display: inline-flex;
align-items: center;
gap: 8px;
min-height: 30px;
padding: 6px 10px;
border: 1px solid var(--line);
border-radius: 10px;
background: #fff;
font-size: 0.86rem;
font-weight: 700;
}
.fx-setup-tag button {
border: 0;
background: transparent;
color: var(--muted);
cursor: pointer;
font: inherit;
line-height: 1;
padding: 0;
}
.fx-setup-suggestions {
position: absolute;
top: 100%;
left: 0;
right: 0;
z-index: 20;
display: grid;
gap: 6px;
margin-top: 4px;
padding: 8px;
border: 1px solid var(--line);
border-radius: 12px;
background: var(--surface-strong);
box-shadow: 0 16px 35px rgba(1, 22, 32, 0.14);
max-height: 280px;
overflow: auto;
}
.fx-setup-suggestion {
appearance: none;
text-align: left;
border: 1px solid transparent;
border-radius: 10px;
background: transparent;
color: var(--text);
padding: 8px 10px;
cursor: pointer;
font: inherit;
}
.fx-setup-suggestion:hover,
.fx-setup-suggestion:focus-visible {
border-color: color-mix(in srgb, var(--brand-accent) 36%, transparent);
background: color-mix(in srgb, var(--brand-accent) 8%, var(--surface-strong));
outline: none;
}
</style>
<?php else: ?>
<?php if ($currentSection === 'general' && $generalSetupFields !== []): ?>
<section class="setup-panel" id="setup-general">
<div class="setup-panel__head">
<div>
<span class="pill">Allgemein</span>
<h2>Datenbank und Debug</h2>
<h2>Moduleinstellungen</h2>
</div>
</div>
<div class="setup-grid">
@@ -1728,7 +1134,7 @@ $GLOBALS['layout_header_context'] = 'Setup / ' . ($sectionTitles[$currentSection
<section class="setup-panel"<?= $databaseSetupFields === [] ? ' id="setup-general"' : '' ?>>
<div class="setup-panel__head">
<div>
<span class="pill">Datenbanken</span>
<span class="pill">Datenbank</span>
<h2>Verbindungen</h2>
<p class="muted">Standard nutzt die Nexus-Datenbank. Custom blendet eigene Verbindungsdaten und den DB-Test ein.</p>
</div>
@@ -1824,7 +1230,6 @@ $GLOBALS['layout_header_context'] = 'Setup / ' . ($sectionTitles[$currentSection
<div class="setup-actions setup-actions--footer">
<button class="cta-button" type="submit">Setup speichern</button>
</div>
<?php endif; ?>
</form>
</section>
<div class="scheduler-modal" data-scheduler-modal hidden>
@@ -1934,15 +1339,26 @@ $GLOBALS['layout_header_context'] = 'Setup / ' . ($sectionTitles[$currentSection
<script>
(() => {
const modeField = document.querySelector('[name="use_separate_db"]');
const tabsRoot = document.querySelector('[data-setup-db-root]');
const panelsRoot = document.querySelector('[data-setup-db-panels]');
const customBlock = document.querySelector('[data-setup-db-custom-block]');
const tabsRoot = document.querySelector('[data-setup-db-root]');
const dbPanels = Array.from(document.querySelectorAll('.setup-db-panel'));
if (!modeField || !tabsRoot || !panelsRoot || !customBlock) return;
const sync = () => {
if (!modeField || !panelsRoot || !customBlock) return;
const isCustomMode = () => {
if (modeField instanceof HTMLInputElement && modeField.type === 'checkbox') {
return modeField.checked;
}
const value = String(modeField.value || '').trim().toLowerCase();
const isCustom = !['', '0', 'false', 'off', 'standard'].includes(value);
tabsRoot.hidden = !isCustom;
return !['', '0', 'false', 'off', 'standard'].includes(value);
};
const sync = () => {
const isCustom = isCustomMode();
if (tabsRoot) {
tabsRoot.hidden = !isCustom;
}
panelsRoot.hidden = !isCustom;
customBlock.hidden = !isCustom;
if (!isCustom) {
@@ -1951,10 +1367,12 @@ $GLOBALS['layout_header_context'] = 'Setup / ' . ($sectionTitles[$currentSection
});
return;
}
const activeTab = document.querySelector('[data-setup-tab-target].is-active') || document.querySelector('[data-setup-tab-target]');
const activeTab = tabsRoot
? (tabsRoot.querySelector('[data-setup-tab-target].is-active') || tabsRoot.querySelector('[data-setup-tab-target]'))
: null;
const activeTargetId = activeTab ? activeTab.dataset.setupTabTarget : '';
dbPanels.forEach((panel) => {
if (tabsRoot.hidden || tabsRoot.childElementCount <= 1) {
if (!tabsRoot || tabsRoot.hidden || tabsRoot.childElementCount <= 1) {
panel.hidden = false;
return;
}