1172 lines
47 KiB
PHP
1172 lines
47 KiB
PHP
<?php
|
||
$moduleName = (string)($_GET['module'] ?? '');
|
||
$module = modules()->get($moduleName);
|
||
$error = null;
|
||
$notice = null;
|
||
$testGroup = null;
|
||
$dbTestMessages = [];
|
||
|
||
require_admin();
|
||
|
||
if (!$module) {
|
||
http_response_code(404);
|
||
echo '<div class="card">Modul nicht gefunden.</div>';
|
||
return;
|
||
}
|
||
|
||
$fields = (array)($module['setup']['fields'] ?? []);
|
||
$hasGlobalDebugField = false;
|
||
foreach ($fields as $field) {
|
||
if ((string)($field['name'] ?? '') === 'debug_enabled') {
|
||
$hasGlobalDebugField = true;
|
||
break;
|
||
}
|
||
}
|
||
if (!$hasGlobalDebugField) {
|
||
$fields[] = [
|
||
'name' => 'debug_enabled',
|
||
'label' => 'Modul-Debug aktivieren',
|
||
'type' => 'checkbox',
|
||
'help' => 'Wenn aktiv, darf das Modul Debug-Daten sammeln und den Debug-Bereich anzeigen.',
|
||
];
|
||
}
|
||
$fieldTypes = [];
|
||
$fieldMeta = [];
|
||
foreach ($fields as $field) {
|
||
$fname = (string)($field['name'] ?? '');
|
||
if ($fname === '') {
|
||
continue;
|
||
}
|
||
$fieldTypes[$fname] = (string)($field['type'] ?? 'text');
|
||
$fieldMeta[$fname] = $field;
|
||
}
|
||
$isFxRatesSetup = $moduleName === 'fx-rates';
|
||
$current = modules()->settings($moduleName);
|
||
$intervalTaskStatuses = modules()->intervalTaskStatuses($moduleName);
|
||
$setupActions = modules()->hasFunction($moduleName, 'setup_actions')
|
||
? (array) module_fn($moduleName, 'setup_actions')
|
||
: [];
|
||
$defaults = $module['db_defaults'] ?? [];
|
||
if (empty($current['db']) && is_array($defaults)) {
|
||
$current['db'] = $defaults;
|
||
}
|
||
$metadataDefaults = $module['metadata_db_defaults'] ?? [];
|
||
if (empty($current['metadata_db']) && is_array($metadataDefaults)) {
|
||
$current['metadata_db'] = $metadataDefaults;
|
||
}
|
||
$dbDefaultsByGroup = [
|
||
'db' => is_array($defaults) ? $defaults : [],
|
||
'metadata_db' => is_array($metadataDefaults) ? $metadataDefaults : [],
|
||
];
|
||
|
||
$setNested = function (array &$target, string $path, mixed $value): void {
|
||
$parts = explode('.', $path);
|
||
$last = array_pop($parts);
|
||
$node = &$target;
|
||
foreach ($parts as $part) {
|
||
if (!isset($node[$part]) || !is_array($node[$part])) {
|
||
$node[$part] = [];
|
||
}
|
||
$node = &$node[$part];
|
||
}
|
||
if ($last !== null && $last !== '') {
|
||
$node[$last] = $value;
|
||
}
|
||
};
|
||
|
||
$getNested = function (array $source, string $path): mixed {
|
||
$node = $source;
|
||
foreach (explode('.', $path) as $part) {
|
||
if (!is_array($node) || !array_key_exists($part, $node)) {
|
||
return null;
|
||
}
|
||
$node = $node[$part];
|
||
}
|
||
return $node;
|
||
};
|
||
|
||
$dbGroups = [];
|
||
$fieldsByDbGroup = [];
|
||
$generalFields = [];
|
||
foreach ($fields as $field) {
|
||
$name = (string)($field['name'] ?? '');
|
||
if (!str_contains($name, '.')) {
|
||
continue;
|
||
}
|
||
|
||
[$group, $key] = explode('.', $name, 2);
|
||
if ($key !== 'driver') {
|
||
continue;
|
||
}
|
||
|
||
$label = (string)($field['label'] ?? $group);
|
||
$label = trim(preg_replace('/\s+DB\s+Driver$/i', ' DB', $label) ?? $label);
|
||
$label = $label !== '' ? $label : $group;
|
||
$dbGroups[$group] = $label;
|
||
}
|
||
|
||
foreach ($fields as $field) {
|
||
$name = (string)($field['name'] ?? '');
|
||
if (str_contains($name, '.')) {
|
||
[$group] = explode('.', $name, 2);
|
||
if (array_key_exists($group, $dbGroups)) {
|
||
$fieldsByDbGroup[$group][] = $field;
|
||
continue;
|
||
}
|
||
}
|
||
|
||
$generalFields[] = $field;
|
||
}
|
||
|
||
$driverOptions = [
|
||
'pgsql' => 'PostgreSQL',
|
||
'mysql' => 'MySQL / MariaDB',
|
||
'sqlite' => 'SQLite',
|
||
];
|
||
|
||
$describeDbConfig = static function (array $dbConfig): string {
|
||
$driver = (string)($dbConfig['driver'] ?? '');
|
||
$host = (string)($dbConfig['host'] ?? '');
|
||
$port = (string)($dbConfig['port'] ?? '');
|
||
|
||
return 'Aktive Einstellung: Treiber ' . ($driver !== '' ? $driver : 'nicht gesetzt')
|
||
. ', Host ' . ($host !== '' ? $host : 'nicht gesetzt')
|
||
. ', Port ' . ($port !== '' ? $port : 'nicht gesetzt') . '.';
|
||
};
|
||
|
||
$describeDbDefaults = static function (array $dbDefaults): string {
|
||
$driver = (string)($dbDefaults['driver'] ?? '');
|
||
$host = (string)($dbDefaults['host'] ?? '');
|
||
$port = (string)($dbDefaults['port'] ?? '');
|
||
|
||
if ($driver === '' && $host === '' && $port === '') {
|
||
return '';
|
||
}
|
||
|
||
return 'Standard: Treiber ' . ($driver !== '' ? $driver : 'nicht gesetzt')
|
||
. ', Host ' . ($host !== '' ? $host : 'nicht gesetzt')
|
||
. ', Port ' . ($port !== '' ? $port : 'nicht gesetzt') . '.';
|
||
};
|
||
|
||
$dbConfigWarning = static function (array $dbConfig): ?string {
|
||
$driver = (string)($dbConfig['driver'] ?? '');
|
||
$port = (string)($dbConfig['port'] ?? '');
|
||
|
||
if ($driver === 'pgsql' && $port === '3306') {
|
||
return 'Port 3306 ist typisch fuer MySQL/MariaDB, aber als Treiber ist PostgreSQL ausgewaehlt. Bitte den Treiber auf MySQL / MariaDB stellen.';
|
||
}
|
||
|
||
if ($driver === 'mysql' && $port === '5432') {
|
||
return 'Port 5432 ist typisch fuer PostgreSQL, aber als Treiber ist MySQL / MariaDB ausgewaehlt. Bitte Port oder Treiber pruefen.';
|
||
}
|
||
|
||
return null;
|
||
};
|
||
|
||
$dbConfigHint = static function (string $group, array $dbConfig, array $dbDefaults): ?string {
|
||
if ($group !== 'metadata_db') {
|
||
return null;
|
||
}
|
||
|
||
$driver = (string)($dbConfig['driver'] ?? '');
|
||
$host = (string)($dbConfig['host'] ?? '');
|
||
$port = (string)($dbConfig['port'] ?? '');
|
||
$defaultDriver = (string)($dbDefaults['driver'] ?? '');
|
||
$defaultHost = (string)($dbDefaults['host'] ?? '');
|
||
$defaultPort = (string)($dbDefaults['port'] ?? '');
|
||
|
||
if ($driver !== $defaultDriver || $host !== $defaultHost || $port !== $defaultPort) {
|
||
return 'Diese Verbindung ist fuer Nexus-eigene DHCP-Zusatzinfos gedacht, nicht fuer die Nexus-App/Base-DB. Erwartet wird normalerweise: '
|
||
. ($defaultDriver !== '' ? $defaultDriver : 'Treiber offen')
|
||
. ' auf '
|
||
. ($defaultHost !== '' ? $defaultHost : 'Host offen')
|
||
. ':'
|
||
. ($defaultPort !== '' ? $defaultPort : 'Port offen')
|
||
. '.';
|
||
}
|
||
|
||
return null;
|
||
};
|
||
|
||
$fetchJsonWithApiKey = static function (string $url, string $apiKey, int $timeout = 10): array {
|
||
$headers = [
|
||
'Accept: application/json',
|
||
'x-api-key: ' . $apiKey,
|
||
];
|
||
|
||
$responseBody = null;
|
||
$httpCode = 0;
|
||
$curlError = '';
|
||
|
||
if (function_exists('curl_init')) {
|
||
$ch = curl_init($url);
|
||
if ($ch !== false) {
|
||
curl_setopt_array($ch, [
|
||
CURLOPT_RETURNTRANSFER => true,
|
||
CURLOPT_FOLLOWLOCATION => true,
|
||
CURLOPT_TIMEOUT => $timeout,
|
||
CURLOPT_CONNECTTIMEOUT => min(5, $timeout),
|
||
CURLOPT_HTTPHEADER => $headers,
|
||
]);
|
||
$responseBody = curl_exec($ch);
|
||
$curlError = curl_error($ch);
|
||
$httpCode = (int) curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
|
||
curl_close($ch);
|
||
}
|
||
}
|
||
|
||
if (!is_string($responseBody) || $responseBody === '') {
|
||
$context = stream_context_create([
|
||
'http' => [
|
||
'method' => 'GET',
|
||
'timeout' => $timeout,
|
||
'header' => implode("\r\n", $headers) . "\r\n",
|
||
],
|
||
]);
|
||
$responseBody = @file_get_contents($url, false, $context);
|
||
}
|
||
|
||
if (!is_string($responseBody) || $responseBody === '') {
|
||
return [
|
||
'ok' => false,
|
||
'message' => 'Abruf fehlgeschlagen.'
|
||
. ($curlError !== '' ? ' ' . $curlError : '')
|
||
. ($httpCode > 0 ? ' HTTP ' . $httpCode : ''),
|
||
];
|
||
}
|
||
|
||
$decoded = json_decode($responseBody, true);
|
||
if (!is_array($decoded)) {
|
||
return [
|
||
'ok' => false,
|
||
'message' => 'Antwort ist kein gueltiges JSON.',
|
||
];
|
||
}
|
||
|
||
foreach (['error', 'message', 'detail'] as $errorKey) {
|
||
if (isset($decoded[$errorKey]) && is_string($decoded[$errorKey]) && trim($decoded[$errorKey]) !== '') {
|
||
return [
|
||
'ok' => false,
|
||
'message' => trim((string) $decoded[$errorKey]),
|
||
];
|
||
}
|
||
}
|
||
|
||
return [
|
||
'ok' => true,
|
||
'data' => $decoded,
|
||
];
|
||
};
|
||
|
||
$normalizeDriver = static function (mixed $value): mixed {
|
||
if (!is_string($value)) {
|
||
return $value;
|
||
}
|
||
|
||
$normalized = strtolower(trim($value));
|
||
return match ($normalized) {
|
||
'postgres', 'postgresql' => 'pgsql',
|
||
'mariadb', 'mysql/mariadb', 'mysql / mariadb' => 'mysql',
|
||
default => $normalized,
|
||
};
|
||
};
|
||
|
||
$formatRunTimestamp = static function (?string $value): string {
|
||
$value = trim((string) $value);
|
||
if ($value === '') {
|
||
return '-';
|
||
}
|
||
|
||
$ts = strtotime($value);
|
||
if ($ts === false) {
|
||
return $value;
|
||
}
|
||
|
||
return date('Y-m-d H:i:s', $ts);
|
||
};
|
||
|
||
$renderField = function (array $field) use (&$current, $getNested, $driverOptions): void {
|
||
$name = (string)($field['name'] ?? '');
|
||
if ($name === '') {
|
||
return;
|
||
}
|
||
$label = (string)($field['label'] ?? $name);
|
||
$type = (string)($field['type'] ?? 'text');
|
||
$required = !empty($field['required']);
|
||
$help = (string)($field['help'] ?? $field['description'] ?? '');
|
||
$postKey = str_replace('.', '_', $name);
|
||
|
||
$value = '';
|
||
if ($name === 'kea_auto_init') {
|
||
$value = !empty($current[$name]) ? '1' : '0';
|
||
} elseif (str_contains($name, '.')) {
|
||
$value = (string)($getNested($current, $name) ?? '');
|
||
} else {
|
||
$value = (string)($current[$name] ?? '');
|
||
}
|
||
?>
|
||
<label class="setup-field muted">
|
||
<span><?= e($label) ?></span>
|
||
<?php if (str_ends_with($name, '.driver')): ?>
|
||
<select name="<?= e($postKey) ?>" <?= $required ? 'required' : '' ?>>
|
||
<option value="">Bitte waehlen</option>
|
||
<?php foreach ($driverOptions as $driver => $driverLabel): ?>
|
||
<option value="<?= e($driver) ?>" <?= $value === $driver ? 'selected' : '' ?>><?= e($driverLabel) ?></option>
|
||
<?php endforeach; ?>
|
||
</select>
|
||
<?php elseif ($type === 'textarea'): ?>
|
||
<textarea name="<?= e($postKey) ?>" rows="3" <?= $required ? 'required' : '' ?>><?= e($value) ?></textarea>
|
||
<?php elseif ($type === 'checkbox'): ?>
|
||
<input type="checkbox" name="<?= e($postKey) ?>" value="1" <?= $value === '1' ? 'checked' : '' ?>>
|
||
<?php else: ?>
|
||
<input type="<?= e($type) ?>" name="<?= e($postKey) ?>" value="<?= e($value) ?>" <?= $required ? 'required' : '' ?>>
|
||
<?php endif; ?>
|
||
<?php if ($help !== ''): ?>
|
||
<small class="muted"><?= e($help) ?></small>
|
||
<?php endif; ?>
|
||
</label>
|
||
<?php
|
||
};
|
||
|
||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||
$payload = [];
|
||
|
||
foreach ($fields as $field) {
|
||
$name = (string)($field['name'] ?? '');
|
||
if ($name === '') {
|
||
continue;
|
||
}
|
||
$type = (string)($field['type'] ?? 'text');
|
||
$postKey = str_replace('.', '_', $name);
|
||
$value = $_POST[$postKey] ?? null;
|
||
|
||
if ($type === 'checkbox') {
|
||
$value = isset($_POST[$postKey]) ? '1' : '0';
|
||
}
|
||
if (is_array($value) && $type === 'multiselect') {
|
||
$value = array_values(array_filter(array_map(
|
||
static fn (mixed $item): string => trim((string) $item),
|
||
$value
|
||
), static fn (string $item): bool => $item !== ''));
|
||
} elseif (is_array($value)) {
|
||
continue;
|
||
}
|
||
$value = is_string($value) ? trim($value) : $value;
|
||
if (str_ends_with($name, '.driver')) {
|
||
$value = $normalizeDriver($value);
|
||
}
|
||
|
||
if ($name === 'kea_auto_init') {
|
||
$payload[$name] = $value === '1';
|
||
continue;
|
||
}
|
||
|
||
if (str_contains($name, '.')) {
|
||
$setNested($payload, $name, $value);
|
||
continue;
|
||
}
|
||
|
||
$payload[$name] = $value;
|
||
}
|
||
|
||
$current = array_replace_recursive($current, $payload);
|
||
|
||
$postedTestGroup = (string)($_POST['test_db'] ?? '');
|
||
$postedResetGroup = (string)($_POST['reset_db'] ?? '');
|
||
$postedSetupAction = trim((string)($_POST['module_setup_action'] ?? ''));
|
||
if ($postedSetupAction !== '') {
|
||
if (!modules()->hasFunction($moduleName, 'run_setup_action')) {
|
||
$error = 'Diese Setup-Aktion wird vom Modul nicht unterstuetzt.';
|
||
} else {
|
||
try {
|
||
$actionResult = module_fn($moduleName, 'run_setup_action', $postedSetupAction);
|
||
$current = modules()->settings($moduleName);
|
||
$notice = 'Setup-Aktion ausgefuehrt.';
|
||
if (is_array($actionResult)) {
|
||
if (isset($actionResult['message']) && is_string($actionResult['message']) && trim($actionResult['message']) !== '') {
|
||
$notice = trim((string) $actionResult['message']);
|
||
} elseif (isset($actionResult['synced_count'])) {
|
||
$notice = 'Waehrungskatalog synchronisiert. ' . (int) $actionResult['synced_count'] . ' Waehrungen verarbeitet.';
|
||
}
|
||
}
|
||
} catch (\Throwable $e) {
|
||
$error = $e->getMessage();
|
||
}
|
||
}
|
||
} elseif ($postedResetGroup !== '') {
|
||
$testGroup = $postedResetGroup;
|
||
if (!array_key_exists($postedResetGroup, $dbGroups)) {
|
||
$error = 'Unbekannte Datenbank-Konfiguration.';
|
||
} elseif (($dbDefaultsByGroup[$postedResetGroup] ?? []) === []) {
|
||
$dbTestMessages[$postedResetGroup] = [
|
||
'type' => 'error',
|
||
'text' => 'Fuer diese Datenbank sind keine Standardwerte hinterlegt.',
|
||
];
|
||
} else {
|
||
$current[$postedResetGroup] = $dbDefaultsByGroup[$postedResetGroup];
|
||
$dbTestMessages[$postedResetGroup] = [
|
||
'type' => 'success',
|
||
'text' => 'Standardwerte geladen. Bitte pruefen und speichern.',
|
||
];
|
||
}
|
||
} elseif ($postedTestGroup !== '') {
|
||
$testGroup = $postedTestGroup;
|
||
if (!array_key_exists($postedTestGroup, $dbGroups)) {
|
||
$error = 'Unbekannte Datenbank-Konfiguration.';
|
||
} else {
|
||
$dbConfig = $getNested($current, $postedTestGroup);
|
||
if (!is_array($dbConfig)) {
|
||
$dbTestMessages[$postedTestGroup] = [
|
||
'type' => 'error',
|
||
'text' => 'Datenbank-Konfiguration ist unvollstaendig.',
|
||
];
|
||
} else {
|
||
$warning = $dbConfigWarning($dbConfig);
|
||
if ($warning !== null) {
|
||
$dbTestMessages[$postedTestGroup] = [
|
||
'type' => 'error',
|
||
'text' => $warning . ' ' . $describeDbConfig($dbConfig),
|
||
];
|
||
} else {
|
||
try {
|
||
$dbConfig['options'] = array_replace([
|
||
\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION,
|
||
\PDO::ATTR_DEFAULT_FETCH_MODE => \PDO::FETCH_ASSOC,
|
||
], (array)($dbConfig['options'] ?? []));
|
||
|
||
$testPdo = \App\Database::createFromArray($dbConfig);
|
||
$testPdo->query('SELECT 1')->fetchColumn();
|
||
$dbTestMessages[$postedTestGroup] = [
|
||
'type' => 'success',
|
||
'text' => 'Verbindung erfolgreich. ' . $describeDbConfig($dbConfig),
|
||
];
|
||
} catch (\Throwable $e) {
|
||
$dbTestMessages[$postedTestGroup] = [
|
||
'type' => 'error',
|
||
'text' => 'Verbindung fehlgeschlagen. ' . $describeDbConfig($dbConfig) . ' ' . $e->getMessage(),
|
||
];
|
||
}
|
||
}
|
||
}
|
||
}
|
||
} else {
|
||
modules()->saveSettings($moduleName, $current);
|
||
if ($isFxRatesSetup && modules()->hasFunction($moduleName, 'save_runtime_settings')) {
|
||
module_fn($moduleName, 'save_runtime_settings', $payload);
|
||
$current = modules()->settings($moduleName);
|
||
}
|
||
if (empty($payload['debug_enabled'])) {
|
||
module_debug_clear($moduleName);
|
||
}
|
||
$notice = 'Setup gespeichert.';
|
||
$module = modules()->get($moduleName) ?: $module;
|
||
}
|
||
}
|
||
|
||
$moduleStatusPanel = null;
|
||
|
||
$activeDbGroup = $testGroup !== null && array_key_exists($testGroup, $dbGroups)
|
||
? $testGroup
|
||
: (array_key_first($dbGroups) ?? '');
|
||
?>
|
||
<div class="setup-shell">
|
||
<div class="pill">Setup</div>
|
||
<h1 class="setup-title"><?= e($module['title']) ?> – Einrichtung</h1>
|
||
<p class="muted">Trage die benötigten Informationen für das Modul ein.</p>
|
||
|
||
<?php if ($error): ?>
|
||
<div class="bg-red-900 border-l-4 border-red-500 text-red-100 p-4 mb-6" role="alert">
|
||
<?= e($error) ?>
|
||
</div>
|
||
<?php elseif ($notice): ?>
|
||
<div class="setup-notice">
|
||
<?= e($notice) ?>
|
||
</div>
|
||
<?php endif; ?>
|
||
|
||
<form method="post" class="setup-form">
|
||
<?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);
|
||
?>
|
||
<section class="setup-panel">
|
||
<div class="setup-panel__head">
|
||
<div>
|
||
<span class="pill">API</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>Standard-Basiswaehrung</span>
|
||
<input type="text" name="default_base_currency" value="<?= e((string) ($current['default_base_currency'] ?? 'EUR')) ?>">
|
||
</label>
|
||
</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>
|
||
<span class="pill">Scheduler</span>
|
||
<h2>Taeglicher Abruf</h2>
|
||
</div>
|
||
</div>
|
||
<div class="setup-grid">
|
||
<label class="setup-field muted">
|
||
<span>Taeglichen Abruf aktivieren</span>
|
||
<input type="checkbox" name="daily_refresh_enabled" value="1" <?= !empty($current['daily_refresh_enabled']) ? 'checked' : '' ?>>
|
||
</label>
|
||
<label class="setup-field muted">
|
||
<span>Taegliche Abrufstunde</span>
|
||
<input type="number" name="daily_refresh_hour" min="0" max="23" value="<?= e((string) ($current['daily_refresh_hour'] ?? 18)) ?>">
|
||
</label>
|
||
<label class="setup-field muted">
|
||
<span>Scheduler-Zeitzone</span>
|
||
<input type="text" name="schedule_timezone" value="<?= e((string) ($current['schedule_timezone'] ?? 'Europe/Berlin')) ?>">
|
||
</label>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="setup-panel">
|
||
<div class="setup-panel__head">
|
||
<div>
|
||
<span class="pill">DB</span>
|
||
<h2>Datenbank</h2>
|
||
</div>
|
||
</div>
|
||
<div class="setup-grid">
|
||
<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>
|
||
|
||
<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>
|
||
<label class="setup-field muted">
|
||
<span>Modul-Debug aktivieren</span>
|
||
<input type="checkbox" name="debug_enabled" value="1" <?= !empty($current['debug_enabled']) ? 'checked' : '' ?>>
|
||
<small class="muted">Wenn aktiv, darf das Modul Debug-Daten sammeln und den Debug-Bereich anzeigen.</small>
|
||
</label>
|
||
</div>
|
||
</section>
|
||
|
||
<?php if ($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; ?>
|
||
|
||
<div class="setup-actions">
|
||
<button class="cta-button" type="submit">Speichern</button>
|
||
<a class="nav-link" href="/modules/access/<?= e($moduleName) ?>">Zugriff verwalten</a>
|
||
<a class="nav-link" href="/modules">Zurück</a>
|
||
</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 ($generalFields !== []): ?>
|
||
<section class="setup-panel">
|
||
<div class="setup-panel__head">
|
||
<div>
|
||
<span class="pill">Allgemein</span>
|
||
<h2>Moduleinstellungen</h2>
|
||
</div>
|
||
</div>
|
||
<div class="setup-grid">
|
||
<?php foreach ($generalFields as $field): ?>
|
||
<?php $renderField($field); ?>
|
||
<?php endforeach; ?>
|
||
</div>
|
||
</section>
|
||
<?php endif; ?>
|
||
|
||
<?php if ($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>
|
||
<small class="muted">Status: <?= e((string) (($state['last_status'] ?? '') !== '' ? $state['last_status'] : '-')) ?></small>
|
||
<?php if (trim((string) ($state['last_message'] ?? '')) !== ''): ?>
|
||
<small class="muted">Meldung: <?= e((string) $state['last_message']) ?></small>
|
||
<?php endif; ?>
|
||
</div>
|
||
<?php endforeach; ?>
|
||
</div>
|
||
</section>
|
||
<?php endif; ?>
|
||
|
||
<?php if ($setupActions !== []): ?>
|
||
<section class="setup-panel">
|
||
<div class="setup-panel__head">
|
||
<div>
|
||
<span class="pill">Aktionen</span>
|
||
<h2>Modulaktionen</h2>
|
||
<p class="muted">Seltene Wartungsaktionen koennen direkt hier aus dem Setup ausgefuehrt werden.</p>
|
||
</div>
|
||
</div>
|
||
<div class="setup-grid">
|
||
<?php foreach ($setupActions as $action): ?>
|
||
<?php
|
||
$actionName = trim((string)($action['name'] ?? ''));
|
||
$actionLabel = trim((string)($action['label'] ?? $actionName));
|
||
$actionHelp = trim((string)($action['help'] ?? ''));
|
||
if ($actionName === '' || $actionLabel === '') {
|
||
continue;
|
||
}
|
||
?>
|
||
<div class="setup-field muted">
|
||
<span><?= e($actionLabel) ?></span>
|
||
<?php if ($actionHelp !== ''): ?>
|
||
<small class="muted"><?= e($actionHelp) ?></small>
|
||
<?php endif; ?>
|
||
<div class="setup-actions" style="justify-content:flex-start; margin-top:12px;">
|
||
<button class="nav-link" type="submit" name="module_setup_action" value="<?= e($actionName) ?>" formnovalidate>
|
||
<?= e($actionLabel) ?>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<?php endforeach; ?>
|
||
</div>
|
||
</section>
|
||
<?php endif; ?>
|
||
|
||
<?php if (is_array($moduleStatusPanel)): ?>
|
||
<section class="setup-panel setup-panel--flat">
|
||
<div class="setup-panel__head">
|
||
<div>
|
||
<span class="pill">Status</span>
|
||
<h2><?= e((string) $moduleStatusPanel['title']) ?></h2>
|
||
<p class="muted">Der Status wird beim Aufruf der Setup-Seite automatisch geprueft.</p>
|
||
</div>
|
||
</div>
|
||
<div class="setup-db-message setup-db-message--<?= e((string) $moduleStatusPanel['type']) ?>">
|
||
<?= e((string) $moduleStatusPanel['text']) ?>
|
||
</div>
|
||
<?php if (!empty($moduleStatusPanel['stats']) && is_array($moduleStatusPanel['stats'])): ?>
|
||
<div class="setup-grid" style="margin-top:16px;">
|
||
<?php foreach ($moduleStatusPanel['stats'] as $stat): ?>
|
||
<label class="setup-field muted">
|
||
<span><?= e((string) ($stat['label'] ?? '')) ?></span>
|
||
<input type="text" value="<?= e((string) ($stat['value'] ?? '')) ?>" readonly>
|
||
</label>
|
||
<?php endforeach; ?>
|
||
</div>
|
||
<?php endif; ?>
|
||
</section>
|
||
<?php endif; ?>
|
||
|
||
<?php if ($dbGroups !== []): ?>
|
||
<section class="setup-panel">
|
||
<div class="setup-panel__head">
|
||
<div>
|
||
<span class="pill">Datenbanken</span>
|
||
<h2>Verbindungen</h2>
|
||
<p class="muted">Jede Verbindung kann getrennt konfiguriert und getestet werden.</p>
|
||
</div>
|
||
</div>
|
||
<div class="setup-tabs" aria-label="Datenbankbereiche">
|
||
<?php foreach ($dbGroups as $group => $label): ?>
|
||
<button
|
||
class="nav-link setup-tab<?= $group === $activeDbGroup ? ' is-active' : '' ?>"
|
||
type="button"
|
||
data-setup-tab-target="setup-db-<?= e($group) ?>"
|
||
aria-controls="setup-db-<?= e($group) ?>"
|
||
aria-selected="<?= $group === $activeDbGroup ? 'true' : 'false' ?>"
|
||
>
|
||
<?= e($label) ?>
|
||
</button>
|
||
<?php endforeach; ?>
|
||
</div>
|
||
<div class="setup-db-panels">
|
||
<?php foreach ($dbGroups as $group => $label): ?>
|
||
<?php
|
||
$panelConfig = $getNested($current, $group);
|
||
$panelConfig = is_array($panelConfig) ? $panelConfig : [];
|
||
$panelDefaults = $dbDefaultsByGroup[$group] ?? [];
|
||
$panelDefaults = is_array($panelDefaults) ? $panelDefaults : [];
|
||
$panelHint = $dbConfigHint($group, $panelConfig, $panelDefaults);
|
||
$defaultDescription = $describeDbDefaults($panelDefaults);
|
||
?>
|
||
<section class="setup-db-panel" id="setup-db-<?= e($group) ?>" <?= $group === $activeDbGroup ? '' : 'hidden' ?>>
|
||
<div class="setup-panel__head">
|
||
<div>
|
||
<span class="pill"><?= e($label) ?></span>
|
||
<h3><?= e($label) ?> konfigurieren</h3>
|
||
<?php if ($defaultDescription !== ''): ?>
|
||
<p class="muted"><?= e($defaultDescription) ?></p>
|
||
<?php endif; ?>
|
||
</div>
|
||
<div class="setup-db-actions">
|
||
<?php if ($panelDefaults !== []): ?>
|
||
<button class="nav-link" type="submit" name="reset_db" value="<?= e($group) ?>" formnovalidate>
|
||
Standardwerte laden
|
||
</button>
|
||
<?php endif; ?>
|
||
<button class="nav-link" type="submit" name="test_db" value="<?= e($group) ?>" formnovalidate>
|
||
Verbindung testen
|
||
</button>
|
||
</div>
|
||
</div>
|
||
<?php if (isset($dbTestMessages[$group])): ?>
|
||
<div class="setup-db-message setup-db-message--<?= e($dbTestMessages[$group]['type']) ?>">
|
||
<?= e($dbTestMessages[$group]['text']) ?>
|
||
</div>
|
||
<?php elseif ($panelHint !== null): ?>
|
||
<div class="setup-db-message setup-db-message--hint">
|
||
<?= e($panelHint) ?>
|
||
</div>
|
||
<?php endif; ?>
|
||
<div class="setup-grid">
|
||
<?php foreach (($fieldsByDbGroup[$group] ?? []) as $field): ?>
|
||
<?php $renderField($field); ?>
|
||
<?php endforeach; ?>
|
||
</div>
|
||
</section>
|
||
<?php endforeach; ?>
|
||
</div>
|
||
</section>
|
||
<?php endif; ?>
|
||
|
||
<div class="setup-actions">
|
||
<button class="cta-button" type="submit">Speichern</button>
|
||
<a class="nav-link" href="/modules/access/<?= e($moduleName) ?>">Zugriff verwalten</a>
|
||
<a class="nav-link" href="/modules">Zurück</a>
|
||
</div>
|
||
<?php endif; ?>
|
||
</form>
|
||
</div>
|