ysads
This commit is contained in:
@@ -9,7 +9,7 @@
|
||||
"database": true
|
||||
},
|
||||
"fields": [
|
||||
{ "name": "use_separate_db", "label": "Datenbank-Modus", "type": "select", "required": false, "help": "Standard nutzt die Nexus-Datenbank. Custom nutzt eine eigene Datenbankverbindung.", "options": { "0": "Standard", "1": "Custom Datenbank" } },
|
||||
{ "name": "use_separate_db", "label": "Eigene Modul-DB nutzen", "type": "checkbox", "required": false, "help": "Wenn aktiv, werden die DB-Daten unten verwendet. Sonst wird die Nexus-Base-DB genutzt." },
|
||||
{ "name": "db.driver", "label": "DB Driver", "type": "text", "required": false, "help": "z.B. pgsql oder mysql" },
|
||||
{ "name": "db.host", "label": "DB Host", "type": "text", "required": false },
|
||||
{ "name": "db.port", "label": "DB Port", "type": "number", "required": false },
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user