Barrierefreiheit und mobile Darstellung
- Kontrast: gedämpfter Text war mit 3,1:1 unter WCAG AA, jetzt 4,9:1 (hell) bzw. 6,4:1 (dunkel) - Sprungmarke „Zum Inhalt springen" in Admin-Shell und Voucher-Seite - aria-label für alle reinen Icon-Schaltflächen (Theme, Menü, Abmelden, Zeilenaktionen), aria-current auf dem aktiven Navigationspunkt, role="group" für den Sprachumschalter - 194 dekorative Icons mit aria-hidden versehen, damit Screenreader sie nicht vorlesen - Toast-Container als aria-live-Bereich ausgezeichnet - prefers-reduced-motion schaltet Animationen und Übergänge ab Tabellen (Benutzer, Vouchers, Profile, Audit-Log, Dashboard, API-Keys) werden unter 720 px zu Karten: die Spaltenüberschrift steht per data-label vor dem Wert, statt horizontal zu scrollen. Datums- und Zeitformat in der Voucher-Liste folgen jetzt der gewählten Sprache statt fest de-DE. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
850a04d628
commit
7f1d93debd
21 changed files with 716 additions and 632 deletions
|
|
@ -115,17 +115,17 @@ $adminBase = '';
|
|||
<p class="muted"><?= __('api_none') ?></p>
|
||||
<?php else: ?>
|
||||
<div class="table-container">
|
||||
<table>
|
||||
<table class="table-stack">
|
||||
<tr><th><?= __('label_name') ?></th><th><?= __('api_col_prefix') ?></th><th><?= __('api_col_scope') ?></th><th><?= __('api_col_limit') ?></th><th><?= __('label_status') ?></th><th><?= __('api_col_last_used') ?></th><th><?= __('api_col_created_by') ?></th><th></th></tr>
|
||||
<?php foreach ($keys as $k): ?>
|
||||
<tr>
|
||||
<td><?= htmlspecialchars($k['name']) ?></td>
|
||||
<td><code>uvt_<?= htmlspecialchars($k['key_prefix']) ?>…</code></td>
|
||||
<td><?= ($k['scope'] ?? 'write') === 'read' ? __('api_scope_read_short') : __('api_scope_write_short') ?></td>
|
||||
<td><?= (int)($k['rate_limit'] ?? 0) === 0 ? '∞' : (int)$k['rate_limit'] . '/min' ?></td>
|
||||
<td><span class="badge <?= $k['is_active'] ? 'b-on' : 'b-off' ?>"><?= $k['is_active'] ? __('api_state_active') : __('api_state_blocked') ?></span></td>
|
||||
<td class="muted"><?= $k['last_used_at'] ? date('d.m.Y H:i', strtotime($k['last_used_at'])) : '–' ?></td>
|
||||
<td class="muted"><?= htmlspecialchars($k['creator'] ?? '–') ?></td>
|
||||
<td data-label="<?= __('label_name') ?>"><?= htmlspecialchars($k['name']) ?></td>
|
||||
<td data-label="<?= __('api_col_prefix') ?>"><code>uvt_<?= htmlspecialchars($k['key_prefix']) ?>…</code></td>
|
||||
<td data-label="<?= __('api_col_scope') ?>"><?= ($k['scope'] ?? 'write') === 'read' ? __('api_scope_read_short') : __('api_scope_write_short') ?></td>
|
||||
<td data-label="<?= __('api_col_limit') ?>"><?= (int)($k['rate_limit'] ?? 0) === 0 ? '∞' : (int)$k['rate_limit'] . '/min' ?></td>
|
||||
<td data-label="<?= __('label_status') ?>"><span class="badge <?= $k['is_active'] ? 'b-on' : 'b-off' ?>"><?= $k['is_active'] ? __('api_state_active') : __('api_state_blocked') ?></span></td>
|
||||
<td class="muted" data-label="<?= __('api_col_last_used') ?>"><?= $k['last_used_at'] ? date('d.m.Y H:i', strtotime($k['last_used_at'])) : '–' ?></td>
|
||||
<td class="muted" data-label="<?= __('api_col_created_by') ?>"><?= htmlspecialchars($k['creator'] ?? '–') ?></td>
|
||||
<td style="text-align:right;white-space:nowrap;">
|
||||
<a class="a-link" href="?toggle=<?= (int)$k['id'] ?>&token=<?= urlencode($csrf) ?>"><?= $k['is_active'] ? __('api_action_block') : __('api_action_unblock') ?></a>
|
||||
<a class="a-link" style="color:var(--danger);" href="?delete=<?= (int)$k['id'] ?>&token=<?= urlencode($csrf) ?>" onclick="return confirm('<?= __('api_delete_confirm') ?>');"><?= __('btn_delete') ?></a>
|
||||
|
|
|
|||
|
|
@ -71,7 +71,7 @@ foreach (['voucher_created', 'voucher_bulk', 'user_login', 'user_logout', 'user_
|
|||
|
||||
<!-- Filter -->
|
||||
<div class="card" style="margin-bottom: 20px;">
|
||||
<div class="card-header"><span class="card-title"><i class="fas fa-filter"></i> <?= __('audit_filter') ?></span></div>
|
||||
<div class="card-header"><span class="card-title"><i class="fas fa-filter" aria-hidden="true"></i> <?= __('audit_filter') ?></span></div>
|
||||
<div style="padding: 20px 25px;">
|
||||
<form method="get" class="filter-bar">
|
||||
<div>
|
||||
|
|
@ -96,8 +96,8 @@ foreach (['voucher_created', 'voucher_bulk', 'user_login', 'user_logout', 'user_
|
|||
<?php endforeach; ?>
|
||||
</select>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary btn-small"><i class="fas fa-search"></i> Filtern</button>
|
||||
<a href="audit_log.php" class="btn btn-secondary btn-small"><i class="fas fa-times"></i> Zurücksetzen</a>
|
||||
<button type="submit" class="btn btn-primary btn-small"><i class="fas fa-search" aria-hidden="true"></i> Filtern</button>
|
||||
<a href="audit_log.php" class="btn btn-secondary btn-small"><i class="fas fa-times" aria-hidden="true"></i> Zurücksetzen</a>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -105,15 +105,15 @@ foreach (['voucher_created', 'voucher_bulk', 'user_login', 'user_logout', 'user_
|
|||
<!-- Log-Tabelle -->
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<span class="card-title"><i class="fas fa-history"></i> <?= __('audit_title') ?></span>
|
||||
<span class="card-title"><i class="fas fa-history" aria-hidden="true"></i> <?= __('audit_title') ?></span>
|
||||
<span style="font-size:13px;color:var(--text-muted);"><?= number_format($total) ?> Einträge</span>
|
||||
</div>
|
||||
<?php if (empty($logs)): ?>
|
||||
<div class="empty-state"><i class="fas fa-history"></i><p><?= __('audit_none') ?></p></div>
|
||||
<div class="empty-state"><i class="fas fa-history" aria-hidden="true"></i><p><?= __('audit_none') ?></p></div>
|
||||
<?php else: ?>
|
||||
<div style="overflow-x:auto;">
|
||||
<div class="table-container">
|
||||
<table class="table">
|
||||
<table class="table table-stack">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><?= __('audit_time') ?></th>
|
||||
|
|
@ -127,34 +127,34 @@ foreach (['voucher_created', 'voucher_bulk', 'user_login', 'user_logout', 'user_
|
|||
<tbody>
|
||||
<?php foreach ($logs as $log): ?>
|
||||
<tr>
|
||||
<td style="white-space:nowrap;color:var(--text-muted);">
|
||||
<td data-label="<?= __('audit_time') ?>" style="white-space:nowrap;color:var(--text-muted);">
|
||||
<?= date('d.m.Y', strtotime($log['created_at'])) ?><br>
|
||||
<small><?= date('H:i:s', strtotime($log['created_at'])) ?></small>
|
||||
</td>
|
||||
<td>
|
||||
<td data-label="<?= __('audit_action') ?>">
|
||||
<span class="action-chip">
|
||||
<?= htmlspecialchars($actionLabels[$log['action']] ?? $log['action']) ?>
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
<td data-label="<?= __('audit_user') ?>">
|
||||
<?php if ($log['user_name']): ?>
|
||||
<strong style="font-size:13px;"><?= htmlspecialchars($log['user_name']) ?></strong><br>
|
||||
<small style="color:var(--text-muted);"><?= htmlspecialchars($log['user_email'] ?? '') ?></small>
|
||||
<?php else: ?>
|
||||
<em style="color:var(--text-muted);">System/Anonym</em>
|
||||
<em style="color:var(--text-muted);"><?= __('audit_system_anon') ?></em>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td style="color:var(--text-secondary);">
|
||||
<td data-label="<?= __('audit_entity') ?>" style="color:var(--text-secondary);">
|
||||
<?php if ($log['entity_type']): ?>
|
||||
<code style="font-size:11px;"><?= htmlspecialchars($log['entity_type']) ?>:<?= htmlspecialchars($log['entity_id'] ?? '') ?></code>
|
||||
<?php else: ?>
|
||||
<span style="color:var(--text-muted);">-</span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td class="details-cell" title="<?= htmlspecialchars($log['details'] ?? '') ?>">
|
||||
<td class="details-cell" data-label="<?= __('audit_details') ?>" title="<?= htmlspecialchars($log['details'] ?? '') ?>">
|
||||
<?= htmlspecialchars(mb_strimwidth($log['details'] ?? '-', 0, 80, '…')) ?>
|
||||
</td>
|
||||
<td class="ip-cell"><?= htmlspecialchars($log['ip_address'] ?? '-') ?></td>
|
||||
<td class="ip-cell" data-label="<?= __('audit_ip') ?>"><?= htmlspecialchars($log['ip_address'] ?? '-') ?></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
|
|
@ -169,13 +169,13 @@ foreach (['voucher_created', 'voucher_bulk', 'user_login', 'user_logout', 'user_
|
|||
<?php
|
||||
$baseUrl = '?' . http_build_query(array_filter(['action' => $filterAction, 'user_id' => $filterUser]));
|
||||
if ($page > 1): ?>
|
||||
<a href="<?= $baseUrl ?>&page=<?= $page - 1 ?>" class="page-btn"><i class="fas fa-chevron-left"></i></a>
|
||||
<a href="<?= $baseUrl ?>&page=<?= $page - 1 ?>" class="page-btn"><i class="fas fa-chevron-left" aria-hidden="true"></i></a>
|
||||
<?php endif;
|
||||
for ($p = max(1, $page - 2); $p <= min($pages, $page + 2); $p++): ?>
|
||||
<a href="<?= $baseUrl ?>&page=<?= $p ?>" class="page-btn <?= $p === $page ? 'active' : '' ?>"><?= $p ?></a>
|
||||
<?php endfor;
|
||||
if ($page < $pages): ?>
|
||||
<a href="<?= $baseUrl ?>&page=<?= $page + 1 ?>" class="page-btn"><i class="fas fa-chevron-right"></i></a>
|
||||
<a href="<?= $baseUrl ?>&page=<?= $page + 1 ?>" class="page-btn"><i class="fas fa-chevron-right" aria-hidden="true"></i></a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -105,7 +105,7 @@ $currentPage = 'dashboard';
|
|||
<span id="liveStatus">DB</span>
|
||||
</span>
|
||||
<button onclick="refreshData('live')" class="btn btn-secondary btn-sm" id="refreshBtn">
|
||||
<i class="fas fa-sync-alt"></i> <?= __('dashboard_live_refresh') ?>
|
||||
<i class="fas fa-sync-alt" aria-hidden="true"></i> <?= __('dashboard_live_refresh') ?>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -114,21 +114,21 @@ $currentPage = 'dashboard';
|
|||
<div class="stat-card">
|
||||
<div class="stat-card-header">
|
||||
<div class="stat-card-title"><?= __('dashboard_active_sites') ?></div>
|
||||
<div class="stat-card-icon info"><i class="fas fa-location-dot"></i></div>
|
||||
<div class="stat-card-icon info"><i class="fas fa-location-dot" aria-hidden="true"></i></div>
|
||||
</div>
|
||||
<div class="stat-card-value"><?= $stats['total_sites'] ?></div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-card-header">
|
||||
<div class="stat-card-title"><?= __('dashboard_users') ?></div>
|
||||
<div class="stat-card-icon accent"><i class="fas fa-users"></i></div>
|
||||
<div class="stat-card-icon accent"><i class="fas fa-users" aria-hidden="true"></i></div>
|
||||
</div>
|
||||
<div class="stat-card-value"><?= $stats['total_users'] ?></div>
|
||||
</div>
|
||||
<div class="stat-card live">
|
||||
<div class="stat-card-header">
|
||||
<div class="stat-card-title"><?= __('dashboard_valid') ?></div>
|
||||
<div class="stat-card-icon success"><i class="fas fa-circle-check"></i></div>
|
||||
<div class="stat-card-icon success"><i class="fas fa-circle-check" aria-hidden="true"></i></div>
|
||||
</div>
|
||||
<div class="stat-card-value valid" id="liveValid"><?= (int)($voucherStats['valid']??0) ?></div>
|
||||
<div class="stat-card-sub" id="subValid"><?= $lastCronSync ? date('H:i', strtotime($lastCronSync)) : __('never') ?></div>
|
||||
|
|
@ -136,21 +136,21 @@ $currentPage = 'dashboard';
|
|||
<div class="stat-card live">
|
||||
<div class="stat-card-header">
|
||||
<div class="stat-card-title"><?= __('dashboard_used') ?></div>
|
||||
<div class="stat-card-icon warning"><i class="fas fa-user-check"></i></div>
|
||||
<div class="stat-card-icon warning"><i class="fas fa-user-check" aria-hidden="true"></i></div>
|
||||
</div>
|
||||
<div class="stat-card-value used" id="liveUsed"><?= (int)($voucherStats['used']??0) ?></div>
|
||||
</div>
|
||||
<div class="stat-card live">
|
||||
<div class="stat-card-header">
|
||||
<div class="stat-card-title"><?= __('dashboard_expired') ?></div>
|
||||
<div class="stat-card-icon danger"><i class="fas fa-circle-xmark"></i></div>
|
||||
<div class="stat-card-icon danger"><i class="fas fa-circle-xmark" aria-hidden="true"></i></div>
|
||||
</div>
|
||||
<div class="stat-card-value expired" id="liveExpired"><?= (int)($voucherStats['expired']??0) ?></div>
|
||||
</div>
|
||||
<div class="stat-card live">
|
||||
<div class="stat-card-header">
|
||||
<div class="stat-card-title"><?= __('dashboard_total') ?></div>
|
||||
<div class="stat-card-icon"><i class="fas fa-ticket"></i></div>
|
||||
<div class="stat-card-icon"><i class="fas fa-ticket" aria-hidden="true"></i></div>
|
||||
</div>
|
||||
<div class="stat-card-value" id="liveTotal"><?= (int)($voucherStats['total']??0) ?></div>
|
||||
</div>
|
||||
|
|
@ -183,9 +183,9 @@ $currentPage = 'dashboard';
|
|||
<?php endforeach; ?>
|
||||
<?php if (empty($sites)): ?>
|
||||
<div class="empty-state" style="grid-column:1/-1;">
|
||||
<div class="empty-icon"><i class="fas fa-location-dot"></i></div>
|
||||
<div class="empty-icon"><i class="fas fa-location-dot" aria-hidden="true"></i></div>
|
||||
<p><?= __('dashboard_no_data') ?></p>
|
||||
<a href="sites.php" class="btn btn-primary" style="margin-top:15px;"><i class="fas fa-plus"></i> <?= __('sites_add') ?></a>
|
||||
<a href="sites.php" class="btn btn-primary" style="margin-top:15px;"><i class="fas fa-plus" aria-hidden="true"></i> <?= __('sites_add') ?></a>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
|
@ -204,7 +204,7 @@ $currentPage = 'dashboard';
|
|||
<div class="card-header"><h2 class="card-title"><?= __('dashboard_top_users') ?></h2></div>
|
||||
<div class="card-body">
|
||||
<?php if (empty($topUsers)): ?>
|
||||
<div class="empty-state"><i class="fas fa-users"></i><p><?= __('dashboard_no_data') ?></p></div>
|
||||
<div class="empty-state"><i class="fas fa-users" aria-hidden="true"></i><p><?= __('dashboard_no_data') ?></p></div>
|
||||
<?php else: ?>
|
||||
<ul class="top-users-list">
|
||||
<?php foreach ($topUsers as $u): ?>
|
||||
|
|
@ -225,21 +225,21 @@ $currentPage = 'dashboard';
|
|||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header"><h2 class="card-title"><?= __('dashboard_recent') ?></h2><a href="vouchers.php" class="btn btn-secondary btn-sm"><i class="fas fa-arrow-up-right-from-square"></i> Live</a></div>
|
||||
<div class="card-header"><h2 class="card-title"><?= __('dashboard_recent') ?></h2><a href="vouchers.php" class="btn btn-secondary btn-sm"><i class="fas fa-arrow-up-right-from-square" aria-hidden="true"></i> Live</a></div>
|
||||
<div class="card-body" style="padding:0;">
|
||||
<?php if (empty($recentVouchers)): ?>
|
||||
<div class="empty-state"><i class="fas fa-ticket-alt"></i><p><?= __('dashboard_no_vouchers') ?></p></div>
|
||||
<div class="empty-state"><i class="fas fa-ticket-alt" aria-hidden="true"></i><p><?= __('dashboard_no_vouchers') ?></p></div>
|
||||
<?php else: ?>
|
||||
<div class="table-container">
|
||||
<table class="table">
|
||||
<table class="table table-stack">
|
||||
<thead><tr><th><?= __('label_created') ?></th><th><?= __('label_code') ?></th><th><?= __('label_site') ?></th><th><?= __('label_status') ?></th></tr></thead>
|
||||
<tbody>
|
||||
<?php foreach ($recentVouchers as $v): ?>
|
||||
<tr>
|
||||
<td style="font-size:12px;"><?= date('d.m H:i', strtotime($v['created_at'])) ?></td>
|
||||
<td><code><?= htmlspecialchars($v['voucher_code']) ?></code></td>
|
||||
<td><span class="badge badge-info"><?= htmlspecialchars($v['site_name']??'') ?></span></td>
|
||||
<td>
|
||||
<td data-label="<?= __('label_created') ?>" style="font-size:12px;"><?= date('d.m H:i', strtotime($v['created_at'])) ?></td>
|
||||
<td data-label="<?= __('label_code') ?>"><code><?= htmlspecialchars($v['voucher_code']) ?></code></td>
|
||||
<td data-label="<?= __('label_site') ?>"><span class="badge badge-info"><?= htmlspecialchars($v['site_name']??'') ?></span></td>
|
||||
<td data-label="<?= __('label_status') ?>">
|
||||
<?php $st=$v['status']??'valid'; ?>
|
||||
<span class="badge badge-<?= $st==='valid'?'success':($st==='used'?'warning':'danger') ?>"><?= __('status_'.$st) ?></span>
|
||||
</td>
|
||||
|
|
@ -255,7 +255,7 @@ $currentPage = 'dashboard';
|
|||
|
||||
</main>
|
||||
|
||||
<div id="toast-container"></div>
|
||||
<div id="toast-container" role="status" aria-live="polite"></div>
|
||||
<script src="../assets/global.js"></script>
|
||||
<script>
|
||||
const rootStyles = getComputedStyle(document.documentElement);
|
||||
|
|
@ -302,7 +302,7 @@ async function refreshData(mode='db') {
|
|||
liveBadge.className = 'live-badge loading';
|
||||
liveStatus.textContent = '...';
|
||||
refreshBtn.disabled = true;
|
||||
refreshBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i>';
|
||||
refreshBtn.innerHTML = '<i class="fas fa-spinner fa-spin" aria-hidden="true"></i>';
|
||||
try {
|
||||
const url = `index.php?ajax_stats=1${mode==='live'?'&sync=1':''}`;
|
||||
const result = await fetch(url).then(r=>r.json());
|
||||
|
|
@ -340,7 +340,7 @@ async function refreshData(mode='db') {
|
|||
}
|
||||
isLoading = false;
|
||||
refreshBtn.disabled = false;
|
||||
refreshBtn.innerHTML = '<i class="fas fa-sync-alt"></i> <?= __('dashboard_live_refresh') ?>';
|
||||
refreshBtn.innerHTML = '<i class="fas fa-sync-alt" aria-hidden="true"></i> <?= __('dashboard_live_refresh') ?>';
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
|
|
|
|||
|
|
@ -115,10 +115,10 @@ $adminBase = '';
|
|||
<?php endforeach; ?>
|
||||
</select>
|
||||
</form>
|
||||
<a class="btn btn-secondary" href="?export=daily&days=<?= $days ?>"><i class="fas fa-download"></i> <?= __('rep_csv_daily') ?></a>
|
||||
<a class="btn btn-secondary" href="?export=per_site"><i class="fas fa-download"></i> <?= __('rep_csv_site') ?></a>
|
||||
<a class="btn btn-secondary" href="?export=per_user"><i class="fas fa-download"></i> <?= __('rep_csv_user') ?></a>
|
||||
<button class="btn btn-secondary" onclick="window.print()"><i class="fas fa-print"></i> <?= __('rep_print') ?></button>
|
||||
<a class="btn btn-secondary" href="?export=daily&days=<?= $days ?>"><i class="fas fa-download" aria-hidden="true"></i> <?= __('rep_csv_daily') ?></a>
|
||||
<a class="btn btn-secondary" href="?export=per_site"><i class="fas fa-download" aria-hidden="true"></i> <?= __('rep_csv_site') ?></a>
|
||||
<a class="btn btn-secondary" href="?export=per_user"><i class="fas fa-download" aria-hidden="true"></i> <?= __('rep_csv_user') ?></a>
|
||||
<button class="btn btn-secondary" onclick="window.print()"><i class="fas fa-print" aria-hidden="true"></i> <?= __('rep_print') ?></button>
|
||||
</div>
|
||||
|
||||
<div class="grid4">
|
||||
|
|
|
|||
|
|
@ -104,7 +104,7 @@ $activeSessions = $dbSessions ? $auth->activeSessionCount() : 0;
|
|||
<body class="app-body focus-page">
|
||||
<div class="focus-card card">
|
||||
<div class="focus-head">
|
||||
<span class="focus-icon"><i class="fas fa-shield-halved"></i></span>
|
||||
<span class="focus-icon"><i class="fas fa-shield-halved" aria-hidden="true"></i></span>
|
||||
<div>
|
||||
<h1><?= __('sec_title') ?></h1>
|
||||
<p class="sub"><?= __('sec_account') ?> <?= htmlspecialchars($user['email']) ?></p>
|
||||
|
|
@ -119,7 +119,7 @@ $activeSessions = $dbSessions ? $auth->activeSessionCount() : 0;
|
|||
|
||||
<?php if (!empty($backupCodes)): ?>
|
||||
<div class="codes-box">
|
||||
<strong><i class="fas fa-key"></i> <?= __('sec_recovery_codes') ?></strong>
|
||||
<strong><i class="fas fa-key" aria-hidden="true"></i> <?= __('sec_recovery_codes') ?></strong>
|
||||
<p><?= __('sec_recovery_hint') ?></p>
|
||||
<div class="codes">
|
||||
<?php foreach ($backupCodes as $c): ?><span><?= htmlspecialchars($c) ?></span><?php endforeach; ?>
|
||||
|
|
@ -128,10 +128,10 @@ $activeSessions = $dbSessions ? $auth->activeSessionCount() : 0;
|
|||
<?php endif; ?>
|
||||
|
||||
<?php if (!$hasPassword): ?>
|
||||
<div class="status off"><i class="fas fa-circle-minus"></i> <?= __('sec_unavailable') ?></div>
|
||||
<div class="status off"><i class="fas fa-circle-minus" aria-hidden="true"></i> <?= __('sec_unavailable') ?></div>
|
||||
<p class="sub"><?= __('sec_m365_hint') ?></p>
|
||||
<?php elseif ($totpEnabled): ?>
|
||||
<div class="status on"><i class="fas fa-circle-check"></i> <?= __('sec_active') ?></div>
|
||||
<div class="status on"><i class="fas fa-circle-check" aria-hidden="true"></i> <?= __('sec_active') ?></div>
|
||||
<p class="sub"><?= __('sec_active_hint') ?><br>
|
||||
<?= __('sec_codes_left') ?> <strong><?= (int)$auth->backupCodesRemaining($user) ?></strong></p>
|
||||
<form method="post" style="margin-bottom:10px;">
|
||||
|
|
@ -143,7 +143,7 @@ $activeSessions = $dbSessions ? $auth->activeSessionCount() : 0;
|
|||
<button type="submit" name="disable_totp" class="btn btn-danger btn-lg btn-block"><?= __('sec_disable') ?></button>
|
||||
</form>
|
||||
<?php else: ?>
|
||||
<div class="status off"><i class="fas fa-circle-minus"></i> <?= __('sec_inactive') ?></div>
|
||||
<div class="status off"><i class="fas fa-circle-minus" aria-hidden="true"></i> <?= __('sec_inactive') ?></div>
|
||||
<ol>
|
||||
<li><?= __('sec_step_1') ?></li>
|
||||
<li><?= __('sec_step_2') ?></li>
|
||||
|
|
@ -175,7 +175,7 @@ $activeSessions = $dbSessions ? $auth->activeSessionCount() : 0;
|
|||
</form>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="auth-links"><a class="back-link" href="../index.php"><i class="fas fa-arrow-left"></i> <?= __('nav_back') ?></a></div>
|
||||
<div class="auth-links"><a class="back-link" href="../index.php"><i class="fas fa-arrow-left" aria-hidden="true"></i> <?= __('nav_back') ?></a></div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -277,7 +277,7 @@ function imageField(string $name, string $label, string $value, string $hint = '
|
|||
<?php if ($preview !== ''): ?>
|
||||
<img src="<?= htmlspecialchars($preview) ?>" alt="">
|
||||
<?php else: ?>
|
||||
<i class="fas fa-image"></i>
|
||||
<i class="fas fa-image" aria-hidden="true"></i>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<div class="image-field-controls">
|
||||
|
|
@ -318,29 +318,29 @@ $adminBase = '';
|
|||
</div>
|
||||
|
||||
<?php if ($error): ?>
|
||||
<div class="alert alert-error"><i class="fas fa-exclamation-circle"></i><span><?= htmlspecialchars($error) ?></span></div>
|
||||
<div class="alert alert-error"><i class="fas fa-exclamation-circle" aria-hidden="true"></i><span><?= htmlspecialchars($error) ?></span></div>
|
||||
<?php endif; ?>
|
||||
<?php if ($success): ?>
|
||||
<div class="alert alert-success"><i class="fas fa-check-circle"></i><span><?= htmlspecialchars($success) ?></span></div>
|
||||
<div class="alert alert-success"><i class="fas fa-check-circle" aria-hidden="true"></i><span><?= htmlspecialchars($success) ?></span></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="tab-container">
|
||||
<div class="tab-navigation" id="tabNav">
|
||||
<button class="tab-button active" data-tab="general"><i class="fas fa-sliders-h"></i> <?= __('settings_tab_general') ?></button>
|
||||
<button class="tab-button" data-tab="defaults"><i class="fas fa-sliders-h"></i> <?= __('settings_tab_defaults') ?></button>
|
||||
<button class="tab-button" data-tab="branding"><i class="fas fa-palette"></i> <?= __('settings_tab_branding') ?></button>
|
||||
<button class="tab-button" data-tab="login"><i class="fas fa-right-to-bracket"></i> <?= __('settings_tab_login') ?></button>
|
||||
<button class="tab-button" data-tab="cron"><i class="fas fa-clock"></i> <?= __('settings_tab_cron') ?></button>
|
||||
<button class="tab-button" data-tab="m365"><i class="fab fa-microsoft"></i> <?= __('settings_tab_m365') ?></button>
|
||||
<button class="tab-button" data-tab="smtp"><i class="fas fa-envelope"></i> <?= __('settings_tab_smtp') ?></button>
|
||||
<button class="tab-button" data-tab="templates_email"><i class="fas fa-file-alt"></i> <?= __('settings_tab_templates_email') ?></button>
|
||||
<button class="tab-button" data-tab="system"><i class="fas fa-cogs"></i> <?= __('settings_tab_system') ?></button>
|
||||
<button class="tab-button" data-tab="password"><i class="fas fa-key"></i> <?= __('settings_tab_password') ?></button>
|
||||
<button class="tab-button active" data-tab="general"><i class="fas fa-sliders-h" aria-hidden="true"></i> <?= __('settings_tab_general') ?></button>
|
||||
<button class="tab-button" data-tab="defaults"><i class="fas fa-sliders-h" aria-hidden="true"></i> <?= __('settings_tab_defaults') ?></button>
|
||||
<button class="tab-button" data-tab="branding"><i class="fas fa-palette" aria-hidden="true"></i> <?= __('settings_tab_branding') ?></button>
|
||||
<button class="tab-button" data-tab="login"><i class="fas fa-right-to-bracket" aria-hidden="true"></i> <?= __('settings_tab_login') ?></button>
|
||||
<button class="tab-button" data-tab="cron"><i class="fas fa-clock" aria-hidden="true"></i> <?= __('settings_tab_cron') ?></button>
|
||||
<button class="tab-button" data-tab="m365"><i class="fab fa-microsoft" aria-hidden="true"></i> <?= __('settings_tab_m365') ?></button>
|
||||
<button class="tab-button" data-tab="smtp"><i class="fas fa-envelope" aria-hidden="true"></i> <?= __('settings_tab_smtp') ?></button>
|
||||
<button class="tab-button" data-tab="templates_email"><i class="fas fa-file-alt" aria-hidden="true"></i> <?= __('settings_tab_templates_email') ?></button>
|
||||
<button class="tab-button" data-tab="system"><i class="fas fa-cogs" aria-hidden="true"></i> <?= __('settings_tab_system') ?></button>
|
||||
<button class="tab-button" data-tab="password"><i class="fas fa-key" aria-hidden="true"></i> <?= __('settings_tab_password') ?></button>
|
||||
</div>
|
||||
|
||||
<!-- Allgemein -->
|
||||
<div id="tab-general" class="tab-content active">
|
||||
<h2 style="margin-bottom: 20px; color: var(--text-primary);"><i class="fas fa-sliders-h"></i> <?= __('settings_tab_general') ?></h2>
|
||||
<h2 style="margin-bottom: 20px; color: var(--text-primary);"><i class="fas fa-sliders-h" aria-hidden="true"></i> <?= __('settings_tab_general') ?></h2>
|
||||
<form method="post" enctype="multipart/form-data">
|
||||
<input type="hidden" name="csrf_token" value="<?= $auth->getCsrfToken() ?>">
|
||||
<input type="hidden" name="form_type" value="general">
|
||||
|
|
@ -353,13 +353,13 @@ $adminBase = '';
|
|||
<div class="form-group"><label><?= __('settings_instr_header') ?></label><input type="text" name="instruction_header" value="<?= htmlspecialchars($cs['instruction_header']) ?>"></div>
|
||||
<div class="form-group"><label><?= __('settings_instr_text') ?></label><textarea name="instruction_text" class="tinymce-editor"><?= htmlspecialchars($cs['instruction_text']) ?></textarea></div>
|
||||
<div class="checkbox-group" style="margin-bottom: 20px;"><input type="checkbox" name="public_access" id="public_access" <?= $cs['public_access'] == '1' ? 'checked' : '' ?>><label for="public_access" style="margin:0;"><?= __('settings_public_access') ?></label></div>
|
||||
<button type="submit" name="save_settings" class="btn btn-primary"><i class="fas fa-save"></i> <?= __('btn_save') ?></button>
|
||||
<button type="submit" name="save_settings" class="btn btn-primary"><i class="fas fa-save" aria-hidden="true"></i> <?= __('btn_save') ?></button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Voucher-Standards -->
|
||||
<div id="tab-defaults" class="tab-content">
|
||||
<h2 style="margin-bottom: 8px; color: var(--text-primary);"><i class="fas fa-sliders-h"></i> <?= __('settings_tab_defaults') ?></h2>
|
||||
<h2 style="margin-bottom: 8px; color: var(--text-primary);"><i class="fas fa-sliders-h" aria-hidden="true"></i> <?= __('settings_tab_defaults') ?></h2>
|
||||
<p style="color: var(--text-muted); font-size: 14px; margin-bottom: 24px;"><?= __('settings_defaults_hint') ?></p>
|
||||
<form method="post">
|
||||
<input type="hidden" name="csrf_token" value="<?= $auth->getCsrfToken() ?>">
|
||||
|
|
@ -382,20 +382,20 @@ $adminBase = '';
|
|||
</div>
|
||||
</div>
|
||||
<div class="info-box" style="margin-top: 10px;">
|
||||
<h4><i class="fas fa-info-circle"></i> Gültigkeits-Referenz</h4>
|
||||
<h4><i class="fas fa-info-circle" aria-hidden="true"></i> Gültigkeits-Referenz</h4>
|
||||
<p>
|
||||
60 Min = 1 Stunde | 480 Min = 8 Stunden |
|
||||
1440 Min = 1 Tag | 10080 Min = 1 Woche |
|
||||
43200 Min = 30 Tage
|
||||
</p>
|
||||
</div>
|
||||
<button type="submit" name="save_settings" class="btn btn-primary" style="margin-top: 16px;"><i class="fas fa-save"></i> <?= __('btn_save') ?></button>
|
||||
<button type="submit" name="save_settings" class="btn btn-primary" style="margin-top: 16px;"><i class="fas fa-save" aria-hidden="true"></i> <?= __('btn_save') ?></button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Design & Branding -->
|
||||
<div id="tab-branding" class="tab-content">
|
||||
<h2 style="margin-bottom: 8px; color: var(--text-primary);"><i class="fas fa-palette"></i> <?= __('settings_tab_branding') ?></h2>
|
||||
<h2 style="margin-bottom: 8px; color: var(--text-primary);"><i class="fas fa-palette" aria-hidden="true"></i> <?= __('settings_tab_branding') ?></h2>
|
||||
<p style="color: var(--text-muted); font-size: 14px; margin-bottom: 24px;"><?= __('settings_branding_intro') ?></p>
|
||||
<form method="post">
|
||||
<input type="hidden" name="csrf_token" value="<?= $auth->getCsrfToken() ?>">
|
||||
|
|
@ -449,8 +449,8 @@ $adminBase = '';
|
|||
<div class="card" id="brandPreview" style="margin-top: 8px;">
|
||||
<h3><?= __('settings_brand_preview') ?></h3>
|
||||
<div style="display:flex;flex-wrap:wrap;gap:12px;align-items:center;margin-top:14px;">
|
||||
<span class="brand-mark" style="width:38px;height:38px;"><i class="fas fa-wifi"></i></span>
|
||||
<button type="button" class="btn btn-primary"><i class="fas fa-ticket"></i> <?= __('voucher_create_btn') ?></button>
|
||||
<span class="brand-mark" style="width:38px;height:38px;"><i class="fas fa-wifi" aria-hidden="true"></i></span>
|
||||
<button type="button" class="btn btn-primary"><i class="fas fa-ticket" aria-hidden="true"></i> <?= __('voucher_create_btn') ?></button>
|
||||
<button type="button" class="btn btn-secondary"><?= __('btn_cancel') ?></button>
|
||||
<span class="badge badge-success"><?= __('status_valid') ?></span>
|
||||
<span class="chip"><?= __('nav_vouchers') ?></span>
|
||||
|
|
@ -458,13 +458,13 @@ $adminBase = '';
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" name="save_settings" class="btn btn-primary" style="margin-top:16px;"><i class="fas fa-save"></i> <?= __('btn_save') ?></button>
|
||||
<button type="submit" name="save_settings" class="btn btn-primary" style="margin-top:16px;"><i class="fas fa-save" aria-hidden="true"></i> <?= __('btn_save') ?></button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Login-Seite -->
|
||||
<div id="tab-login" class="tab-content">
|
||||
<h2 style="margin-bottom: 8px; color: var(--text-primary);"><i class="fas fa-right-to-bracket"></i> <?= __('settings_tab_login') ?></h2>
|
||||
<h2 style="margin-bottom: 8px; color: var(--text-primary);"><i class="fas fa-right-to-bracket" aria-hidden="true"></i> <?= __('settings_tab_login') ?></h2>
|
||||
<p style="color: var(--text-muted); font-size: 14px; margin-bottom: 24px;"><?= __('settings_login_intro') ?></p>
|
||||
<form method="post" enctype="multipart/form-data">
|
||||
<input type="hidden" name="csrf_token" value="<?= $auth->getCsrfToken() ?>">
|
||||
|
|
@ -532,9 +532,9 @@ $adminBase = '';
|
|||
</div>
|
||||
|
||||
<div style="display:flex;gap:10px;flex-wrap:wrap;margin-top:16px;">
|
||||
<button type="submit" name="save_settings" class="btn btn-primary"><i class="fas fa-save"></i> <?= __('btn_save') ?></button>
|
||||
<button type="submit" name="save_settings" class="btn btn-primary"><i class="fas fa-save" aria-hidden="true"></i> <?= __('btn_save') ?></button>
|
||||
<a href="../login.php?preview=1" target="_blank" rel="noopener" class="btn btn-secondary">
|
||||
<i class="fas fa-arrow-up-right-from-square"></i> <?= __('settings_login_preview') ?>
|
||||
<i class="fas fa-arrow-up-right-from-square" aria-hidden="true"></i> <?= __('settings_login_preview') ?>
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
|
|
@ -542,24 +542,24 @@ $adminBase = '';
|
|||
|
||||
<!-- Cron-Sync -->
|
||||
<div id="tab-cron" class="tab-content">
|
||||
<h2 style="margin-bottom: 20px; color: var(--text-primary);"><i class="fas fa-clock"></i> <?= __('settings_tab_cron') ?></h2>
|
||||
<h2 style="margin-bottom: 20px; color: var(--text-primary);"><i class="fas fa-clock" aria-hidden="true"></i> <?= __('settings_tab_cron') ?></h2>
|
||||
<div class="info-box">
|
||||
<h4><i class="fas fa-info-circle"></i> <?= __('settings_cron_what') ?></h4>
|
||||
<h4><i class="fas fa-info-circle" aria-hidden="true"></i> <?= __('settings_cron_what') ?></h4>
|
||||
<p>Der Cron-Job synchronisiert automatisch alle Voucher von Ihren UniFi Controllern in die lokale Datenbank.</p>
|
||||
</div>
|
||||
<hr class="section-divider">
|
||||
<?php if (empty($cs['cron_token'])): ?>
|
||||
<p style="color: var(--text-muted); margin-bottom: 15px;"><i class="fas fa-exclamation-triangle" style="color: var(--warning);"></i> Kein Token konfiguriert.</p>
|
||||
<p style="color: var(--text-muted); margin-bottom: 15px;"><i class="fas fa-exclamation-triangle" style="color: var(--warning);" aria-hidden="true"></i> Kein Token konfiguriert.</p>
|
||||
<form method="post">
|
||||
<input type="hidden" name="csrf_token" value="<?= $auth->getCsrfToken() ?>">
|
||||
<button type="submit" name="generate_cron_token" class="btn btn-primary"><i class="fas fa-key"></i> Token generieren</button>
|
||||
<button type="submit" name="generate_cron_token" class="btn btn-primary"><i class="fas fa-key" aria-hidden="true"></i> Token generieren</button>
|
||||
</form>
|
||||
<?php else: ?>
|
||||
<div class="token-box" style="margin-bottom: 15px;"><?= htmlspecialchars($cs['cron_token']) ?></div>
|
||||
<div style="display: flex; gap: 10px; flex-wrap: wrap; margin-bottom: 20px;">
|
||||
<button onclick="copyToClipboard('<?= htmlspecialchars($cs['cron_token']) ?>')" class="btn btn-secondary"><i class="fas fa-copy"></i> Kopieren</button>
|
||||
<form method="post" style="display:inline;"><input type="hidden" name="csrf_token" value="<?= $auth->getCsrfToken() ?>"><button type="submit" name="generate_cron_token" class="btn btn-secondary"><i class="fas fa-sync"></i> Neu generieren</button></form>
|
||||
<form method="post" style="display:inline;" onsubmit="return confirm('<?= __('js_confirm_delete_token') ?>');"><input type="hidden" name="csrf_token" value="<?= $auth->getCsrfToken() ?>"><button type="submit" name="delete_cron_token" class="btn btn-secondary" style="color: var(--danger);"><i class="fas fa-trash"></i> Löschen</button></form>
|
||||
<button onclick="copyToClipboard('<?= htmlspecialchars($cs['cron_token']) ?>')" class="btn btn-secondary"><i class="fas fa-copy" aria-hidden="true"></i> Kopieren</button>
|
||||
<form method="post" style="display:inline;"><input type="hidden" name="csrf_token" value="<?= $auth->getCsrfToken() ?>"><button type="submit" name="generate_cron_token" class="btn btn-secondary"><i class="fas fa-sync" aria-hidden="true"></i> Neu generieren</button></form>
|
||||
<form method="post" style="display:inline;" onsubmit="return confirm('<?= __('js_confirm_delete_token') ?>');"><input type="hidden" name="csrf_token" value="<?= $auth->getCsrfToken() ?>"><button type="submit" name="delete_cron_token" class="btn btn-secondary" style="color: var(--danger);"><i class="fas fa-trash" aria-hidden="true"></i> Löschen</button></form>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php
|
||||
|
|
@ -569,7 +569,7 @@ $adminBase = '';
|
|||
<label>Cron-URL</label>
|
||||
<div style="display:flex;gap:10px;">
|
||||
<input type="text" id="cronUrl" value="<?= htmlspecialchars($cronUrl) ?>" readonly>
|
||||
<button onclick="copyToClipboard(document.getElementById('cronUrl').value)" class="btn btn-secondary"><i class="fas fa-copy"></i></button>
|
||||
<button onclick="copyToClipboard(document.getElementById('cronUrl').value)" class="btn btn-secondary"><i class="fas fa-copy" aria-hidden="true"></i></button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="placeholder-info" style="background: var(--bg-hover); border-color: var(--border-color);">
|
||||
|
|
@ -580,7 +580,7 @@ $adminBase = '';
|
|||
</div>
|
||||
<?php if ($cs['cron_token']): ?>
|
||||
<div style="margin-top: 20px;">
|
||||
<button onclick="testCronJob()" class="btn btn-primary" id="testCronBtn"><i class="fas fa-play"></i> Jetzt ausführen</button>
|
||||
<button onclick="testCronJob()" class="btn btn-primary" id="testCronBtn"><i class="fas fa-play" aria-hidden="true"></i> Jetzt ausführen</button>
|
||||
<span id="testCronResult" style="margin-left: 15px;"></span>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
|
@ -589,9 +589,9 @@ $adminBase = '';
|
|||
|
||||
<!-- M365 -->
|
||||
<div id="tab-m365" class="tab-content">
|
||||
<h2 style="margin-bottom: 20px; color: var(--text-primary);"><i class="fab fa-microsoft"></i> Microsoft 365</h2>
|
||||
<h2 style="margin-bottom: 20px; color: var(--text-primary);"><i class="fab fa-microsoft" aria-hidden="true"></i> Microsoft 365</h2>
|
||||
<div class="info-box">
|
||||
<h4><i class="fas fa-info-circle"></i> Azure AD App</h4>
|
||||
<h4><i class="fas fa-info-circle" aria-hidden="true"></i> Azure AD App</h4>
|
||||
<p>Redirect URI: <strong><?= $protocol . '://' . $host . $scriptPath ?>/m365_callback.php</strong></p>
|
||||
</div>
|
||||
<form method="post">
|
||||
|
|
@ -600,13 +600,13 @@ $adminBase = '';
|
|||
<div class="form-group"><label>Client ID</label><input type="text" name="m365_client_id" value="<?= htmlspecialchars($cs['m365_client_id']) ?>"></div>
|
||||
<div class="form-group"><label>Client Secret</label><input type="password" name="m365_client_secret" value="<?= htmlspecialchars($cs['m365_client_secret']) ?>"></div>
|
||||
<div class="form-group"><label>Tenant ID</label><input type="text" name="m365_tenant_id" value="<?= htmlspecialchars($cs['m365_tenant_id']) ?>"></div>
|
||||
<button type="submit" name="save_settings" class="btn btn-primary"><i class="fas fa-save"></i> <?= __('btn_save') ?></button>
|
||||
<button type="submit" name="save_settings" class="btn btn-primary"><i class="fas fa-save" aria-hidden="true"></i> <?= __('btn_save') ?></button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- SMTP -->
|
||||
<div id="tab-smtp" class="tab-content">
|
||||
<h2 style="margin-bottom: 20px; color: var(--text-primary);"><i class="fas fa-envelope"></i> SMTP</h2>
|
||||
<h2 style="margin-bottom: 20px; color: var(--text-primary);"><i class="fas fa-envelope" aria-hidden="true"></i> SMTP</h2>
|
||||
<form method="post">
|
||||
<input type="hidden" name="csrf_token" value="<?= $auth->getCsrfToken() ?>">
|
||||
<input type="hidden" name="form_type" value="smtp">
|
||||
|
|
@ -624,20 +624,20 @@ $adminBase = '';
|
|||
<div class="form-group"><label>Absender E-Mail</label><input type="email" name="smtp_from_email" value="<?= htmlspecialchars($cs['smtp_from_email']) ?>"></div>
|
||||
<div class="form-group"><label><?= __('settings_smtp_from_name') ?></label><input type="text" name="smtp_from_name" value="<?= htmlspecialchars($cs['smtp_from_name']) ?>"></div>
|
||||
</div>
|
||||
<button type="submit" name="save_settings" class="btn btn-primary"><i class="fas fa-save"></i> <?= __('btn_save') ?></button>
|
||||
<button type="submit" name="save_settings" class="btn btn-primary"><i class="fas fa-save" aria-hidden="true"></i> <?= __('btn_save') ?></button>
|
||||
</form>
|
||||
<hr class="section-divider">
|
||||
<h3 style="margin-bottom:15px; color: var(--text-primary);">SMTP testen</h3>
|
||||
<div style="display:flex;gap:10px;align-items:flex-end;">
|
||||
<div style="flex:1;"><label>Test-E-Mail senden an</label><input type="email" id="smtpTestEmail" placeholder="empfaenger@example.com" style="margin-top:6px;"></div>
|
||||
<button onclick="testSmtp()" class="btn btn-secondary" id="smtpTestBtn"><i class="fas fa-paper-plane"></i> <?= __('btn_test') ?></button>
|
||||
<button onclick="testSmtp()" class="btn btn-secondary" id="smtpTestBtn"><i class="fas fa-paper-plane" aria-hidden="true"></i> <?= __('btn_test') ?></button>
|
||||
</div>
|
||||
<span id="smtpTestResult" style="display:block;margin-top:10px;font-size:13px;"></span>
|
||||
</div>
|
||||
|
||||
<!-- E-Mail Templates -->
|
||||
<div id="tab-templates_email" class="tab-content">
|
||||
<h2 style="margin-bottom: 20px; color: var(--text-primary);"><i class="fas fa-file-alt"></i> E-Mail Templates</h2>
|
||||
<h2 style="margin-bottom: 20px; color: var(--text-primary);"><i class="fas fa-file-alt" aria-hidden="true"></i> E-Mail Templates</h2>
|
||||
<form method="post">
|
||||
<input type="hidden" name="csrf_token" value="<?= $auth->getCsrfToken() ?>">
|
||||
<input type="hidden" name="form_type" value="templates">
|
||||
|
|
@ -652,18 +652,18 @@ $adminBase = '';
|
|||
<div class="placeholder-info"><h4>Platzhalter:</h4><div class="placeholder-list"><code>{USER_NAME}</code><code>{CHANGES}</code><code>{APP_TITLE}</code><code>{SYSTEM_URL}</code></div></div>
|
||||
<div class="form-group"><label>Betreff</label><input type="text" name="email_user_notification_subject" value="<?= htmlspecialchars($cs['email_user_notification_subject']) ?>"></div>
|
||||
<div class="form-group"><label>E-Mail Text</label><textarea name="email_user_notification_body" class="tinymce-editor"><?= htmlspecialchars($cs['email_user_notification_body']) ?></textarea></div>
|
||||
<button type="submit" name="save_settings" class="btn btn-primary"><i class="fas fa-save"></i> <?= __('btn_save') ?></button>
|
||||
<button type="submit" name="save_settings" class="btn btn-primary"><i class="fas fa-save" aria-hidden="true"></i> <?= __('btn_save') ?></button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- System -->
|
||||
<div id="tab-system" class="tab-content">
|
||||
<h2 style="margin-bottom: 20px; color: var(--text-primary);"><i class="fas fa-cogs"></i> System & Erweitert</h2>
|
||||
<h2 style="margin-bottom: 20px; color: var(--text-primary);"><i class="fas fa-cogs" aria-hidden="true"></i> System & Erweitert</h2>
|
||||
<form method="post">
|
||||
<input type="hidden" name="csrf_token" value="<?= $auth->getCsrfToken() ?>">
|
||||
<input type="hidden" name="form_type" value="system">
|
||||
<div class="info-box">
|
||||
<h4><i class="fas fa-info-circle"></i> WYSIWYG-Editor</h4>
|
||||
<h4><i class="fas fa-info-circle" aria-hidden="true"></i> WYSIWYG-Editor</h4>
|
||||
<p><?= __('settings_editor_hint') ?> <code>assets/vendor/</code> <?= __('settings_editor_hint2') ?></p>
|
||||
</div>
|
||||
|
||||
|
|
@ -671,7 +671,7 @@ $adminBase = '';
|
|||
<h3 style="margin-bottom:15px; color: var(--text-primary);">Druck-Template</h3>
|
||||
<div class="placeholder-info"><h4>Platzhalter:</h4><div class="placeholder-list"><code>{VOUCHER_CODE}</code><code>{EXPIRY_DATE}</code><code>{EXPIRY_TIME}</code><code>{SITE_NAME}</code><code>{MAX_USES}</code><code>{APP_TITLE}</code><code>{INSTRUCTIONS}</code></div></div>
|
||||
<div class="form-group"><label><?= __('settings_print_template') ?></label><textarea name="print_template" class="tinymce-editor" style="min-height:250px;"><?= htmlspecialchars($cs['print_template']) ?></textarea></div>
|
||||
<button type="submit" name="save_settings" class="btn btn-primary"><i class="fas fa-save"></i> <?= __('btn_save') ?></button>
|
||||
<button type="submit" name="save_settings" class="btn btn-primary"><i class="fas fa-save" aria-hidden="true"></i> <?= __('btn_save') ?></button>
|
||||
</form>
|
||||
<hr class="section-divider">
|
||||
<h3 style="margin-bottom:15px; color: var(--text-primary);">System-Information</h3>
|
||||
|
|
@ -684,13 +684,13 @@ $adminBase = '';
|
|||
|
||||
<!-- Passwort -->
|
||||
<div id="tab-password" class="tab-content">
|
||||
<h2 style="margin-bottom: 20px; color: var(--text-primary);"><i class="fas fa-key"></i> <?= __('settings_tab_password') ?></h2>
|
||||
<h2 style="margin-bottom: 20px; color: var(--text-primary);"><i class="fas fa-key" aria-hidden="true"></i> <?= __('settings_tab_password') ?></h2>
|
||||
<form method="post" style="max-width:500px;">
|
||||
<input type="hidden" name="csrf_token" value="<?= $auth->getCsrfToken() ?>">
|
||||
<div class="form-group"><label><?= __('settings_pw_current') ?></label><input type="password" name="current_password" required></div>
|
||||
<div class="form-group"><label><?= __('settings_pw_new') ?></label><input type="password" name="new_password" required minlength="8"><div class="help-text"><?= __('settings_pw_minlength') ?></div></div>
|
||||
<div class="form-group"><label><?= __('settings_pw_confirm') ?></label><input type="password" name="confirm_password" required></div>
|
||||
<button type="submit" name="change_password" class="btn btn-primary"><i class="fas fa-lock"></i> <?= __('settings_tab_password') ?></button>
|
||||
<button type="submit" name="change_password" class="btn btn-primary"><i class="fas fa-lock" aria-hidden="true"></i> <?= __('settings_tab_password') ?></button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -763,7 +763,7 @@ async function testSmtp() {
|
|||
const result = document.getElementById('smtpTestResult');
|
||||
if (!email) { result.textContent = '<?= __('js_enter_email') ?>'; return; }
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i>';
|
||||
btn.innerHTML = '<i class="fas fa-spinner fa-spin" aria-hidden="true"></i>';
|
||||
const fd = new FormData();
|
||||
fd.append('ajax_smtp_test', '1');
|
||||
fd.append('csrf_token', '<?= $auth->getCsrfToken() ?>');
|
||||
|
|
@ -773,26 +773,26 @@ async function testSmtp() {
|
|||
result.textContent = data.message;
|
||||
result.style.color = data.success ? 'var(--success)' : 'var(--danger)';
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = '<i class="fas fa-paper-plane"></i> Testen';
|
||||
btn.innerHTML = '<i class="fas fa-paper-plane" aria-hidden="true"></i> Testen';
|
||||
}
|
||||
|
||||
async function testCronJob() {
|
||||
const btn = document.getElementById('testCronBtn');
|
||||
const result = document.getElementById('testCronResult');
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> <?= __('js_running') ?>';
|
||||
btn.innerHTML = '<i class="fas fa-spinner fa-spin" aria-hidden="true"></i> <?= __('js_running') ?>';
|
||||
try {
|
||||
const res = await fetch('../cron_sync.php?token=<?= htmlspecialchars($cs['cron_token']) ?>');
|
||||
const data = await res.json();
|
||||
result.innerHTML = data.success
|
||||
? `<span style="color:var(--success)"><i class="fas fa-check-circle"></i> ${data.message}</span>`
|
||||
: `<span style="color:var(--danger)"><i class="fas fa-times-circle"></i> ${data.message}</span>`;
|
||||
? `<span style="color:var(--success)"><i class="fas fa-check-circle" aria-hidden="true"></i> ${data.message}</span>`
|
||||
: `<span style="color:var(--danger)"><i class="fas fa-times-circle" aria-hidden="true"></i> ${data.message}</span>`;
|
||||
if (data.success) showToast('success', 'Cron ausgeführt', data.message);
|
||||
} catch (e) {
|
||||
result.innerHTML = `<span style="color:var(--danger)"><?= __('js_error') ?>: ${e.message}</span>`;
|
||||
}
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = '<i class="fas fa-play"></i> <?= __('js_run_now') ?>';
|
||||
btn.innerHTML = '<i class="fas fa-play" aria-hidden="true"></i> <?= __('js_run_now') ?>';
|
||||
}
|
||||
|
||||
function initTinyMCE() {
|
||||
|
|
|
|||
|
|
@ -105,20 +105,20 @@ $currentPage = 'sites';
|
|||
<div class="page-header">
|
||||
<h1 class="page-title"><?= __('sites_title') ?></h1>
|
||||
<button onclick="openModal()" class="btn btn-primary">
|
||||
<i class="fas fa-plus"></i> <?= __('sites_add') ?>
|
||||
<i class="fas fa-plus" aria-hidden="true"></i> <?= __('sites_add') ?>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<?php if ($error): ?>
|
||||
<div class="alert alert-error"><i class="fas fa-exclamation-circle"></i> <?= htmlspecialchars($error) ?></div>
|
||||
<div class="alert alert-error"><i class="fas fa-exclamation-circle" aria-hidden="true"></i> <?= htmlspecialchars($error) ?></div>
|
||||
<?php endif; ?>
|
||||
<?php if ($success): ?>
|
||||
<div class="alert alert-success"><i class="fas fa-check-circle"></i> <?= htmlspecialchars($success) ?></div>
|
||||
<div class="alert alert-success"><i class="fas fa-check-circle" aria-hidden="true"></i> <?= htmlspecialchars($success) ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if (empty($sites)): ?>
|
||||
<div class="empty-card">
|
||||
<i class="fas fa-map-marker-alt" style="font-size:48px;margin-bottom:20px;opacity:.3;display:block;"></i>
|
||||
<i class="fas fa-map-marker-alt" style="font-size:48px;margin-bottom:20px;opacity:.3;display:block;" aria-hidden="true"></i>
|
||||
<p><?= __('sites_none') ?></p>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
|
|
@ -132,43 +132,43 @@ $currentPage = 'sites';
|
|||
</div>
|
||||
<div>
|
||||
<?php if ($site['is_active']): ?>
|
||||
<span class="badge badge-success"><i class="fas fa-check"></i> <?= __('status_active') ?></span>
|
||||
<span class="badge badge-success"><i class="fas fa-check" aria-hidden="true"></i> <?= __('status_active') ?></span>
|
||||
<?php else: ?>
|
||||
<span class="badge badge-warning"><i class="fas fa-pause"></i> <?= __('status_inactive') ?></span>
|
||||
<span class="badge badge-warning"><i class="fas fa-pause" aria-hidden="true"></i> <?= __('status_inactive') ?></span>
|
||||
<?php endif; ?>
|
||||
<?php if ($site['public_access']): ?>
|
||||
<span class="badge badge-info"><i class="fas fa-globe"></i> <?= __('status_public') ?></span>
|
||||
<span class="badge badge-info"><i class="fas fa-globe" aria-hidden="true"></i> <?= __('status_public') ?></span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<div class="site-info">
|
||||
<div class="site-info-item">
|
||||
<i class="fas fa-server" style="color:var(--accent);width:16px;"></i>
|
||||
<i class="fas fa-server" style="color:var(--accent);width:16px;" aria-hidden="true"></i>
|
||||
<span style="word-break:break-all;"><?= htmlspecialchars($site['unifi_controller_url']) ?></span>
|
||||
</div>
|
||||
<div class="site-info-item">
|
||||
<i class="fas fa-user" style="color:var(--accent);width:16px;"></i>
|
||||
<i class="fas fa-user" style="color:var(--accent);width:16px;" aria-hidden="true"></i>
|
||||
<span><?= htmlspecialchars($site['unifi_username']) ?></span>
|
||||
</div>
|
||||
<div class="site-info-item">
|
||||
<i class="fas fa-clock" style="color:var(--text-muted);width:16px;"></i>
|
||||
<i class="fas fa-clock" style="color:var(--text-muted);width:16px;" aria-hidden="true"></i>
|
||||
<span style="color:var(--text-muted);"><?= date('d.m.Y', strtotime($site['created_at'])) ?></span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="site-actions">
|
||||
<button onclick="openEditModal(<?= $site['id'] ?>, '<?= htmlspecialchars($site['name'], ENT_QUOTES) ?>', '<?= htmlspecialchars($site['site_id'], ENT_QUOTES) ?>', '<?= htmlspecialchars($site['unifi_controller_url'], ENT_QUOTES) ?>', '<?= htmlspecialchars($site['unifi_username'], ENT_QUOTES) ?>', <?= $site['public_access'] ?>)"
|
||||
class="btn btn-secondary btn-sm">
|
||||
<i class="fas fa-edit"></i> <?= __('btn_edit') ?>
|
||||
<i class="fas fa-edit" aria-hidden="true"></i> <?= __('btn_edit') ?>
|
||||
</button>
|
||||
<a href="?toggle=<?= $site['id'] ?>&token=<?= $auth->getCsrfToken() ?>"
|
||||
class="btn btn-secondary btn-sm">
|
||||
<i class="fas fa-<?= $site['is_active'] ? 'pause' : 'play' ?>"></i>
|
||||
<i class="fas fa-<?= $site['is_active'] ? 'pause' : 'play' ?>" aria-hidden="true"></i>
|
||||
<?= $site['is_active'] ? __('sites_deactivate') : __('sites_activate') ?>
|
||||
</a>
|
||||
<a href="?delete=<?= $site['id'] ?>&token=<?= $auth->getCsrfToken() ?>"
|
||||
class="btn btn-danger-soft btn-sm"
|
||||
onclick="return confirm('<?= __('js_confirm_delete_site') ?>')">
|
||||
<i class="fas fa-trash"></i>
|
||||
<i class="fas fa-trash" aria-hidden="true"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -219,7 +219,7 @@ $currentPage = 'sites';
|
|||
</div>
|
||||
<div style="display:flex;gap:10px;margin-top:20px;">
|
||||
<button type="submit" class="btn btn-primary" style="flex:1;" id="addSiteSubmitBtn">
|
||||
<i class="fas fa-save"></i> <?= __('sites_add') ?>
|
||||
<i class="fas fa-save" aria-hidden="true"></i> <?= __('sites_add') ?>
|
||||
</button>
|
||||
<button type="button" onclick="closeModal('addSiteModal')" class="btn btn-secondary"><?= __('btn_cancel') ?></button>
|
||||
</div>
|
||||
|
|
@ -269,7 +269,7 @@ $currentPage = 'sites';
|
|||
</div>
|
||||
<div style="display:flex;gap:10px;margin-top:20px;">
|
||||
<button type="submit" class="btn btn-primary" style="flex:1;" id="editSiteSubmitBtn">
|
||||
<i class="fas fa-save"></i> <?= __('btn_save') ?>
|
||||
<i class="fas fa-save" aria-hidden="true"></i> <?= __('btn_save') ?>
|
||||
</button>
|
||||
<button type="button" onclick="closeModal('editSiteModal')" class="btn btn-secondary"><?= __('btn_cancel') ?></button>
|
||||
</div>
|
||||
|
|
@ -278,7 +278,7 @@ $currentPage = 'sites';
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div id="toast-container"></div>
|
||||
<div id="toast-container" role="status" aria-live="polite"></div>
|
||||
<script src="../assets/global.js"></script>
|
||||
<script>
|
||||
function openModal() { document.getElementById('addSiteModal').classList.add('active'); }
|
||||
|
|
@ -298,12 +298,12 @@ function openEditModal(id, name, siteIdStr, controllerUrl, username, publicAcces
|
|||
document.getElementById('addSiteForm').addEventListener('submit', function() {
|
||||
const btn = document.getElementById('addSiteSubmitBtn');
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> <?= addslashes(__('sites_testing')) ?>';
|
||||
btn.innerHTML = '<i class="fas fa-spinner fa-spin" aria-hidden="true"></i> <?= addslashes(__('sites_testing')) ?>';
|
||||
});
|
||||
document.getElementById('editSiteForm').addEventListener('submit', function() {
|
||||
const btn = document.getElementById('editSiteSubmitBtn');
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> <?= addslashes(__('sites_testing')) ?>';
|
||||
btn.innerHTML = '<i class="fas fa-spinner fa-spin" aria-hidden="true"></i> <?= addslashes(__('sites_testing')) ?>';
|
||||
});
|
||||
|
||||
['addSiteModal','editSiteModal'].forEach(id => {
|
||||
|
|
|
|||
|
|
@ -107,15 +107,15 @@ $adminBase = '';
|
|||
<p class="page-subtitle"><?= __('templates_subtitle') ?></p>
|
||||
</div>
|
||||
<button onclick="openAddModal()" class="btn btn-primary">
|
||||
<i class="fas fa-plus"></i> <?= __('templates_add') ?>
|
||||
<i class="fas fa-plus" aria-hidden="true"></i> <?= __('templates_add') ?>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<?php if ($error): ?>
|
||||
<div class="alert alert-error"><i class="fas fa-exclamation-circle"></i> <?= htmlspecialchars($error) ?></div>
|
||||
<div class="alert alert-error"><i class="fas fa-exclamation-circle" aria-hidden="true"></i> <?= htmlspecialchars($error) ?></div>
|
||||
<?php endif; ?>
|
||||
<?php if ($success): ?>
|
||||
<div class="alert alert-success"><i class="fas fa-check-circle"></i> <?= htmlspecialchars($success) ?></div>
|
||||
<div class="alert alert-success"><i class="fas fa-check-circle" aria-hidden="true"></i> <?= htmlspecialchars($success) ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="card">
|
||||
|
|
@ -125,15 +125,15 @@ $adminBase = '';
|
|||
</div>
|
||||
<?php if (empty($templates)): ?>
|
||||
<div class="empty-state">
|
||||
<i class="fas fa-layer-group"></i>
|
||||
<i class="fas fa-layer-group" aria-hidden="true"></i>
|
||||
<p style="font-size: 15px; margin-bottom: 8px;"><?= __('templates_none') ?></p>
|
||||
<p style="font-size: 13px;"><?= __('templates_add_hint') ?></p>
|
||||
<button onclick="openAddModal()" class="btn btn-primary" style="margin-top: 20px;"><i class="fas fa-plus"></i> <?= __('templates_add') ?></button>
|
||||
<button onclick="openAddModal()" class="btn btn-primary" style="margin-top: 20px;"><i class="fas fa-plus" aria-hidden="true"></i> <?= __('templates_add') ?></button>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
<div style="overflow-x: auto;">
|
||||
<div class="table-container">
|
||||
<table class="table">
|
||||
<table class="table table-stack">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><?= __('templates_name') ?></th>
|
||||
|
|
@ -147,11 +147,11 @@ $adminBase = '';
|
|||
<tbody>
|
||||
<?php foreach ($templates as $t): ?>
|
||||
<tr>
|
||||
<td><strong><?= htmlspecialchars($t['name']) ?></strong></td>
|
||||
<td>
|
||||
<span class="duration-badge"><i class="fas fa-mobile-alt"></i> <?= (int)$t['max_uses'] ?></span>
|
||||
<td data-label="<?= __('templates_name') ?>"><strong><?= htmlspecialchars($t['name']) ?></strong></td>
|
||||
<td data-label="<?= __('templates_devices') ?>">
|
||||
<span class="duration-badge"><i class="fas fa-mobile-alt" aria-hidden="true"></i> <?= (int)$t['max_uses'] ?></span>
|
||||
</td>
|
||||
<td>
|
||||
<td data-label="<?= __('templates_duration') ?>">
|
||||
<?php
|
||||
$m = (int)$t['expire_minutes'];
|
||||
if ($m >= 1440 && $m % 1440 === 0) {
|
||||
|
|
@ -162,22 +162,22 @@ $adminBase = '';
|
|||
$durLabel = $m . ' Min.';
|
||||
}
|
||||
?>
|
||||
<span class="duration-badge"><i class="fas fa-clock"></i> <?= $durLabel ?></span>
|
||||
<span class="duration-badge"><i class="fas fa-clock" aria-hidden="true"></i> <?= $durLabel ?></span>
|
||||
</td>
|
||||
<td style="color: var(--text-secondary);"><?= htmlspecialchars($t['description'] ?? '-') ?></td>
|
||||
<td>
|
||||
<td data-label="<?= __('templates_desc') ?>" style="color: var(--text-secondary);"><?= htmlspecialchars($t['description'] ?? '-') ?></td>
|
||||
<td data-label="<?= __('label_status') ?>">
|
||||
<?php if ($t['is_active']): ?>
|
||||
<span class="badge badge-success"><?= __('status_active') ?></span>
|
||||
<?php else: ?>
|
||||
<span class="badge badge-secondary"><?= __('status_inactive') ?></span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td>
|
||||
<td data-label="<?= __('label_actions') ?>">
|
||||
<button onclick="openEditModal(<?= $t['id'] ?>, '<?= htmlspecialchars($t['name'], ENT_QUOTES) ?>', <?= (int)$t['max_uses'] ?>, <?= (int)$t['expire_minutes'] ?>, '<?= htmlspecialchars($t['description'] ?? '', ENT_QUOTES) ?>', <?= (int)$t['is_active'] ?>, <?= (int)($t['qos_rate_max_down'] ?? 0) ?>, <?= (int)($t['qos_rate_max_up'] ?? 0) ?>, <?= (int)($t['qos_usage_quota'] ?? 0) ?>)"
|
||||
class="btn btn-secondary btn-small"><i class="fas fa-edit"></i></button>
|
||||
class="btn btn-secondary btn-small"><i class="fas fa-edit" aria-hidden="true"></i></button>
|
||||
<a href="?delete=<?= $t['id'] ?>&token=<?= $auth->getCsrfToken() ?>"
|
||||
onclick="return confirm('<?= __('js_confirm_delete_template') ?>')"
|
||||
class="btn btn-danger-soft btn-small"><i class="fas fa-trash"></i></a>
|
||||
class="btn btn-danger-soft btn-small"><i class="fas fa-trash" aria-hidden="true"></i></a>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
|
|
@ -194,7 +194,7 @@ $adminBase = '';
|
|||
<div id="addModal" class="modal">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title"><i class="fas fa-plus-circle" style="color: var(--accent);"></i> <?= __('templates_add') ?></h2>
|
||||
<h2 class="modal-title"><i class="fas fa-plus-circle" style="color: var(--accent);" aria-hidden="true"></i> <?= __('templates_add') ?></h2>
|
||||
<button class="modal-close" onclick="closeModal('addModal')">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
|
|
@ -220,7 +220,7 @@ $adminBase = '';
|
|||
<div class="form-group"><label>Datenlimit (MB)</label><input type="number" name="qos_usage_quota" min="0" placeholder="0 = unbegrenzt"></div>
|
||||
</div>
|
||||
<div style="display:flex;gap:10px;margin-top:20px;">
|
||||
<button type="submit" name="add_template" class="btn btn-primary" style="flex:1;"><i class="fas fa-save"></i> <?= __('btn_save') ?></button>
|
||||
<button type="submit" name="add_template" class="btn btn-primary" style="flex:1;"><i class="fas fa-save" aria-hidden="true"></i> <?= __('btn_save') ?></button>
|
||||
<button type="button" onclick="closeModal('addModal')" class="btn btn-secondary"><?= __('btn_cancel') ?></button>
|
||||
</div>
|
||||
</form>
|
||||
|
|
@ -232,7 +232,7 @@ $adminBase = '';
|
|||
<div id="editModal" class="modal">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-title"><i class="fas fa-edit" style="color: var(--accent);"></i> <?= __('templates_edit') ?></h2>
|
||||
<h2 class="modal-title"><i class="fas fa-edit" style="color: var(--accent);" aria-hidden="true"></i> <?= __('templates_edit') ?></h2>
|
||||
<button class="modal-close" onclick="closeModal('editModal')">×</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
|
|
@ -261,7 +261,7 @@ $adminBase = '';
|
|||
<label for="editActive" style="margin:0;"><?= __('status_active') ?></label>
|
||||
</div>
|
||||
<div style="display:flex;gap:10px;">
|
||||
<button type="submit" name="edit_template" class="btn btn-primary" style="flex:1;"><i class="fas fa-save"></i> <?= __('btn_save') ?></button>
|
||||
<button type="submit" name="edit_template" class="btn btn-primary" style="flex:1;"><i class="fas fa-save" aria-hidden="true"></i> <?= __('btn_save') ?></button>
|
||||
<button type="button" onclick="closeModal('editModal')" class="btn btn-secondary"><?= __('btn_cancel') ?></button>
|
||||
</div>
|
||||
</form>
|
||||
|
|
|
|||
|
|
@ -175,26 +175,26 @@ $currentPage = 'users';
|
|||
<div class="page-header">
|
||||
<h1 class="page-title"><?= __('users_title') ?></h1>
|
||||
<button onclick="openModal()" class="btn btn-primary">
|
||||
<i class="fas fa-plus"></i> <?= __('users_add') ?>
|
||||
<i class="fas fa-plus" aria-hidden="true"></i> <?= __('users_add') ?>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<?php if ($error): ?>
|
||||
<div class="alert alert-error"><i class="fas fa-exclamation-circle"></i> <?= htmlspecialchars($error) ?></div>
|
||||
<div class="alert alert-error"><i class="fas fa-exclamation-circle" aria-hidden="true"></i> <?= htmlspecialchars($error) ?></div>
|
||||
<?php endif; ?>
|
||||
<?php if ($success): ?>
|
||||
<div class="alert alert-success"><i class="fas fa-check-circle"></i> <?= htmlspecialchars($success) ?></div>
|
||||
<div class="alert alert-success"><i class="fas fa-check-circle" aria-hidden="true"></i> <?= htmlspecialchars($success) ?></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-header"><h2 class="card-title"><?= __('users_all') ?></h2></div>
|
||||
<div class="card-body" style="padding:0;">
|
||||
<?php if (empty($users)): ?>
|
||||
<div class="empty-state"><i class="fas fa-users"></i><p><?= __('users_none_found') ?></p></div>
|
||||
<div class="empty-state"><i class="fas fa-users" aria-hidden="true"></i><p><?= __('users_none_found') ?></p></div>
|
||||
<?php else: ?>
|
||||
<div style="overflow-x:auto;">
|
||||
<div class="table-container">
|
||||
<table class="table">
|
||||
<table class="table table-stack">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><?= __('label_name') ?></th>
|
||||
|
|
@ -210,28 +210,28 @@ $currentPage = 'users';
|
|||
<?php foreach ($users as $user): ?>
|
||||
<?php $userSiteIds = array_column($userSiteAccess[$user['id']]??[], 'id'); ?>
|
||||
<tr>
|
||||
<td>
|
||||
<td data-label="<?= __('label_name') ?>">
|
||||
<strong><?= htmlspecialchars($user['name']) ?></strong>
|
||||
<?php if ($user['id'] == $_SESSION['user_id']): ?>
|
||||
<span class="badge badge-info"><?= __('users_you') ?></span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td><?= htmlspecialchars($user['email']) ?></td>
|
||||
<td>
|
||||
<td data-label="<?= __('label_email') ?>"><?= htmlspecialchars($user['email']) ?></td>
|
||||
<td data-label="<?= __('label_role') ?>">
|
||||
<?php if ($user['is_admin']): ?>
|
||||
<span class="badge badge-danger"><i class="fas fa-crown"></i> <?= __('status_admin') ?></span>
|
||||
<span class="badge badge-danger"><i class="fas fa-crown" aria-hidden="true"></i> <?= __('status_admin') ?></span>
|
||||
<?php else: ?>
|
||||
<span class="badge badge-info"><?= __('status_user') ?></span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td>
|
||||
<td data-label="<?= __('label_status') ?>">
|
||||
<?php if ($user['is_active']): ?>
|
||||
<span class="badge badge-success"><i class="fas fa-check"></i> <?= __('status_active') ?></span>
|
||||
<span class="badge badge-success"><i class="fas fa-check" aria-hidden="true"></i> <?= __('status_active') ?></span>
|
||||
<?php else: ?>
|
||||
<span class="badge badge-warning"><i class="fas fa-pause"></i> <?= __('status_inactive') ?></span>
|
||||
<span class="badge badge-warning"><i class="fas fa-pause" aria-hidden="true"></i> <?= __('status_inactive') ?></span>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td>
|
||||
<td data-label="<?= __('users_site_access') ?>">
|
||||
<?php if ($user['is_admin']): ?>
|
||||
<em style="color:var(--text-muted);"><?= __('users_all_sites') ?></em>
|
||||
<?php elseif (!empty($userSiteAccess[$user['id']])): ?>
|
||||
|
|
@ -242,7 +242,7 @@ $currentPage = 'users';
|
|||
<em style="color:var(--text-muted);"><?= __('users_none') ?></em>
|
||||
<?php endif; ?>
|
||||
</td>
|
||||
<td style="font-size:13px;">
|
||||
<td data-label="<?= __('users_last_login') ?>" style="font-size:13px;">
|
||||
<?php if ($user['last_login']): ?>
|
||||
<?= date('d.m.Y H:i', strtotime($user['last_login'])) ?>
|
||||
<?php else: ?>
|
||||
|
|
@ -253,31 +253,31 @@ $currentPage = 'users';
|
|||
<div class="action-btns">
|
||||
<button onclick="openEditModal(<?= $user['id'] ?>, '<?= htmlspecialchars($user['name'], ENT_QUOTES) ?>', <?= $user['is_admin'] ?>, [<?= implode(',', array_map('intval', $userSiteIds)) ?>])"
|
||||
class="btn btn-secondary btn-sm" title="<?= __('btn_edit') ?>">
|
||||
<i class="fas fa-edit"></i>
|
||||
<i class="fas fa-edit" aria-hidden="true"></i>
|
||||
</button>
|
||||
<?php if ($user['id'] != $_SESSION['user_id']): ?>
|
||||
<a href="?toggle=<?= $user['id'] ?>&token=<?= $auth->getCsrfToken() ?>"
|
||||
class="btn btn-secondary btn-sm" title="<?= $user['is_active'] ? __('sites_deactivate') : __('sites_activate') ?>">
|
||||
<i class="fas fa-<?= $user['is_active'] ? 'pause' : 'play' ?>"></i>
|
||||
<i class="fas fa-<?= $user['is_active'] ? 'pause' : 'play' ?>" aria-hidden="true"></i>
|
||||
</a>
|
||||
<?php if ($smtpEnabled && !empty($user['password_hash'])): ?>
|
||||
<a href="?send_reset=<?= $user['id'] ?>&token=<?= $auth->getCsrfToken() ?>"
|
||||
class="btn btn-warning btn-sm" title="<?= __('users_reset_pw') ?>"
|
||||
onclick="return confirm('Passwort-Reset-Link senden an <?= htmlspecialchars($user['email'], ENT_QUOTES) ?>?')">
|
||||
<i class="fas fa-key"></i>
|
||||
<i class="fas fa-key" aria-hidden="true"></i>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
<?php if (!empty($user['totp_enabled'])): ?>
|
||||
<a href="?reset_2fa=<?= $user['id'] ?>&token=<?= $auth->getCsrfToken() ?>"
|
||||
class="btn btn-secondary btn-sm" title="2FA zurücksetzen"
|
||||
onclick="return confirm('2FA für <?= htmlspecialchars($user['email'], ENT_QUOTES) ?> zurücksetzen?')">
|
||||
<i class="fas fa-user-shield"></i>
|
||||
<i class="fas fa-user-shield" aria-hidden="true"></i>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
<a href="?delete=<?= $user['id'] ?>&token=<?= $auth->getCsrfToken() ?>"
|
||||
class="btn btn-danger-soft btn-sm" title="<?= __('btn_delete') ?>"
|
||||
onclick="return confirm('<?= __('js_confirm_delete_user') ?>')">
|
||||
<i class="fas fa-trash"></i>
|
||||
<i class="fas fa-trash" aria-hidden="true"></i>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
|
@ -342,7 +342,7 @@ $currentPage = 'users';
|
|||
</div>
|
||||
<div style="display:flex;gap:10px;margin-top:20px;">
|
||||
<button type="submit" name="add_user" class="btn btn-primary" style="flex:1;">
|
||||
<i class="fas fa-save"></i> <?= __('users_save') ?>
|
||||
<i class="fas fa-save" aria-hidden="true"></i> <?= __('users_save') ?>
|
||||
</button>
|
||||
<button type="button" onclick="closeModal('addUserModal')" class="btn btn-secondary"><?= __('btn_cancel') ?></button>
|
||||
</div>
|
||||
|
|
@ -386,7 +386,7 @@ $currentPage = 'users';
|
|||
</div>
|
||||
<div style="display:flex;gap:10px;margin-top:20px;">
|
||||
<button type="submit" name="edit_user" class="btn btn-primary" style="flex:1;">
|
||||
<i class="fas fa-save"></i> <?= __('users_save_edit') ?>
|
||||
<i class="fas fa-save" aria-hidden="true"></i> <?= __('users_save_edit') ?>
|
||||
</button>
|
||||
<button type="button" onclick="closeModal('editUserModal')" class="btn btn-secondary"><?= __('btn_cancel') ?></button>
|
||||
</div>
|
||||
|
|
@ -395,7 +395,7 @@ $currentPage = 'users';
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div id="toast-container"></div>
|
||||
<div id="toast-container" role="status" aria-live="polite"></div>
|
||||
<script src="../assets/global.js"></script>
|
||||
<script>
|
||||
function openModal() { document.getElementById('addUserModal').classList.add('active'); }
|
||||
|
|
|
|||
|
|
@ -143,9 +143,9 @@ $currentPage = 'vouchers';
|
|||
|
||||
<?php if (empty($sites)): ?>
|
||||
<div class="no-sites-warning">
|
||||
<i class="fas fa-triangle-exclamation" style="font-size:26px;margin-bottom:12px;display:block;"></i>
|
||||
<i class="fas fa-triangle-exclamation" style="font-size:26px;margin-bottom:12px;display:block;" aria-hidden="true"></i>
|
||||
<h3><?= __('vouchers_no_sites') ?></h3>
|
||||
<a href="sites.php" class="btn btn-primary" style="margin-top:20px;"><i class="fas fa-plus"></i> <?= __('nav_sites') ?></a>
|
||||
<a href="sites.php" class="btn btn-primary" style="margin-top:20px;"><i class="fas fa-plus" aria-hidden="true"></i> <?= __('nav_sites') ?></a>
|
||||
</div>
|
||||
<?php else: ?>
|
||||
|
||||
|
|
@ -168,7 +168,7 @@ $currentPage = 'vouchers';
|
|||
</select>
|
||||
<input type="search" id="searchInput" class="search-bar" placeholder="<?= __('vouchers_search') ?>" oninput="filterAndRender()" style="display:none;">
|
||||
<button id="refreshBtn" class="btn btn-secondary" onclick="loadVouchers(true)" disabled>
|
||||
<i class="fas fa-sync-alt"></i> <?= __('btn_refresh') ?>
|
||||
<i class="fas fa-sync-alt" aria-hidden="true"></i> <?= __('btn_refresh') ?>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -187,12 +187,12 @@ $currentPage = 'vouchers';
|
|||
<div class="card-header">
|
||||
<h2 class="card-title" id="voucherListTitle">Vouchers</h2>
|
||||
<a id="csvExportBtn" style="display:none;" class="btn btn-secondary btn-sm" href="#">
|
||||
<i class="fas fa-download"></i> <?= __('btn_export_csv') ?>
|
||||
<i class="fas fa-download" aria-hidden="true"></i> <?= __('btn_export_csv') ?>
|
||||
</a>
|
||||
</div>
|
||||
<div class="card-body" style="padding:0;">
|
||||
<div id="voucherContent">
|
||||
<div class="empty-state"><i class="fas fa-ticket-alt"></i><p><?= __('vouchers_select_hint') ?></p></div>
|
||||
<div class="empty-state"><i class="fas fa-ticket-alt" aria-hidden="true"></i><p><?= __('vouchers_select_hint') ?></p></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -200,10 +200,12 @@ $currentPage = 'vouchers';
|
|||
|
||||
</main>
|
||||
|
||||
<div id="toast-container"></div>
|
||||
<div id="toast-container" role="status" aria-live="polite"></div>
|
||||
<script src="../assets/global.js"></script>
|
||||
<script>
|
||||
const csrfToken = '<?= $auth->getCsrfToken() ?>';
|
||||
// Datums- und Zeitformat folgen der gewaehlten Sprache
|
||||
const LOCALE = '<?= I18n::getLanguage() === 'en' ? 'en-GB' : 'de-DE' ?>';
|
||||
let currentSiteId = null;
|
||||
let allVouchers = [];
|
||||
let currentFilter = 'all';
|
||||
|
|
@ -218,7 +220,7 @@ async function loadVouchers(syncFirst=false) {
|
|||
const searchInput = document.getElementById('searchInput');
|
||||
|
||||
if (!siteId) {
|
||||
document.getElementById('voucherContent').innerHTML = `<div class="empty-state"><i class="fas fa-ticket-alt"></i><p><?= addslashes(__('vouchers_select_hint')) ?></p></div>`;
|
||||
document.getElementById('voucherContent').innerHTML = `<div class="empty-state"><i class="fas fa-ticket-alt" aria-hidden="true"></i><p><?= addslashes(__('vouchers_select_hint')) ?></p></div>`;
|
||||
document.getElementById('statsContainer').style.display = 'none';
|
||||
searchInput.style.display = 'none';
|
||||
refreshBtn.disabled = true;
|
||||
|
|
@ -227,9 +229,9 @@ async function loadVouchers(syncFirst=false) {
|
|||
|
||||
currentSiteId = siteId;
|
||||
refreshBtn.disabled = false;
|
||||
refreshBtn.innerHTML = `<i class="fas fa-spinner fa-spin"></i> ${syncFirst ? '<?= addslashes(__('btn_refresh')) ?>' : '<?= addslashes(__('btn_refresh')) ?>'}`;
|
||||
refreshBtn.innerHTML = `<i class="fas fa-spinner fa-spin" aria-hidden="true"></i> ${syncFirst ? '<?= addslashes(__('btn_refresh')) ?>' : '<?= addslashes(__('btn_refresh')) ?>'}`;
|
||||
|
||||
document.getElementById('voucherContent').innerHTML = `<div class="loading"><i class="fas fa-spinner"></i><span>${syncFirst ? 'Synchronisiere...' : 'Lade...'}</span></div>`;
|
||||
document.getElementById('voucherContent').innerHTML = `<div class="loading"><i class="fas fa-spinner" aria-hidden="true"></i><span>${syncFirst ? 'Synchronisiere...' : 'Lade...'}</span></div>`;
|
||||
|
||||
try {
|
||||
const result = await fetch(`vouchers.php?ajax_get_vouchers=1&site_id=${siteId}${syncFirst?'&sync=1':''}`).then(r=>r.json());
|
||||
|
|
@ -247,15 +249,15 @@ async function loadVouchers(syncFirst=false) {
|
|||
csvBtn.href = `vouchers.php?export_csv=1&site_id=${siteId}&token=${csrfToken}`;
|
||||
if (syncFirst) showToast('success', '<?= addslashes(__('btn_refresh')) ?>', `${result.count} Vouchers geladen`);
|
||||
} else {
|
||||
document.getElementById('voucherContent').innerHTML = `<div class="empty-state"><i class="fas fa-exclamation-circle" style="color:var(--danger)"></i><p>${result.message}</p></div>`;
|
||||
document.getElementById('voucherContent').innerHTML = `<div class="empty-state"><i class="fas fa-exclamation-circle" style="color:var(--danger)" aria-hidden="true"></i><p>${result.message}</p></div>`;
|
||||
document.getElementById('statsContainer').style.display = 'none';
|
||||
}
|
||||
} catch(e) {
|
||||
document.getElementById('voucherContent').innerHTML = `<div class="empty-state"><i class="fas fa-exclamation-circle" style="color:var(--danger)"></i><p>Verbindungsfehler: ${e.message}</p></div>`;
|
||||
document.getElementById('voucherContent').innerHTML = `<div class="empty-state"><i class="fas fa-exclamation-circle" style="color:var(--danger)" aria-hidden="true"></i><p>Verbindungsfehler: ${e.message}</p></div>`;
|
||||
document.getElementById('statsContainer').style.display = 'none';
|
||||
}
|
||||
refreshBtn.disabled = false;
|
||||
refreshBtn.innerHTML = '<i class="fas fa-sync-alt"></i> <?= addslashes(__('btn_refresh')) ?>';
|
||||
refreshBtn.innerHTML = '<i class="fas fa-sync-alt" aria-hidden="true"></i> <?= addslashes(__('btn_refresh')) ?>';
|
||||
}
|
||||
|
||||
function updateStats() {
|
||||
|
|
@ -293,7 +295,7 @@ function renderVouchers() {
|
|||
const pageVouchers = vouchers.slice((currentPage-1)*PAGE_SIZE, currentPage*PAGE_SIZE);
|
||||
|
||||
if (vouchers.length === 0) {
|
||||
document.getElementById('voucherContent').innerHTML = `<div class="empty-state"><i class="fas fa-ticket-alt"></i><p><?= addslashes(__('vouchers_none')) ?></p></div>`;
|
||||
document.getElementById('voucherContent').innerHTML = `<div class="empty-state"><i class="fas fa-ticket-alt" aria-hidden="true"></i><p><?= addslashes(__('vouchers_none')) ?></p></div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -309,7 +311,7 @@ function renderVouchers() {
|
|||
<button class="filter-btn ${currentFilter==='expired'?'active':''}" data-filter="expired" onclick="setFilter('expired')"><?= __('vouchers_filter_expired') ?> (${expiredCnt})</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="table-container"><table class="table">
|
||||
<div class="table-container"><table class="table table-stack">
|
||||
<thead><tr>
|
||||
<th><?= __('label_created') ?></th>
|
||||
<th><?= __('label_code') ?></th>
|
||||
|
|
@ -330,21 +332,21 @@ function renderVouchers() {
|
|||
})() : '';
|
||||
const usagePct = v.quota > 0 ? Math.min(100, (v.used/v.quota)*100) : 0;
|
||||
const statusBadge = v.status==='valid'
|
||||
? `<span class="badge badge-success"><i class="fas fa-check"></i> <?= __('status_valid') ?></span>`
|
||||
? `<span class="badge badge-success"><i class="fas fa-check" aria-hidden="true"></i> <?= __('status_valid') ?></span>`
|
||||
: v.status==='used'
|
||||
? `<span class="badge badge-warning"><i class="fas fa-user-check"></i> <?= __('status_used') ?></span>`
|
||||
: `<span class="badge badge-danger"><i class="fas fa-times"></i> <?= __('status_expired') ?></span>`;
|
||||
? `<span class="badge badge-warning"><i class="fas fa-user-check" aria-hidden="true"></i> <?= __('status_used') ?></span>`
|
||||
: `<span class="badge badge-danger"><i class="fas fa-times" aria-hidden="true"></i> <?= __('status_expired') ?></span>`;
|
||||
|
||||
html += `<tr id="voucher-${v._id}">
|
||||
<td><strong>${createDate.toLocaleDateString('de-DE')}</strong><br><small style="color:var(--text-muted)">${createDate.toLocaleTimeString('de-DE',{hour:'2-digit',minute:'2-digit'})}</small></td>
|
||||
<td><code onclick="copyToClipboard('${escapeHtml(v.formatted_code||'')}','Kopiert!')" title="Kopieren" style="cursor:pointer">${escapeHtml(v.formatted_code||'')}</code></td>
|
||||
<td class="voucher-note" title="${escapeHtml(v.note||'-')}">${escapeHtml(v.note||'-')}</td>
|
||||
<td>${statusBadge}</td>
|
||||
<td><div class="usage-info"><span>${v.used}/${v.quota>0?v.quota:'∞'}</span>${v.quota>0?`<div class="usage-bar"><div class="usage-bar-fill" style="width:${usagePct}%"></div></div>`:''}</div></td>
|
||||
<td>${remaining?`<span style="color:var(--success)"><i class="fas fa-clock"></i> ${remaining}</span><br>`:''}<small style="color:var(--text-muted)">${v.duration} Min.</small></td>
|
||||
<td style="white-space:nowrap;">
|
||||
<button onclick="resendVoucher('${v._id}','${escapeHtml(v.formatted_code||'')}')" class="btn btn-secondary btn-sm" title="Per E-Mail senden"><i class="fas fa-envelope"></i></button>
|
||||
<button onclick="deleteVoucher('${v._id}')" class="btn btn-danger-soft btn-sm" title="<?= __('btn_delete') ?>"><i class="fas fa-trash"></i></button>
|
||||
<td data-label="<?= __('label_created') ?>"><strong>${createDate.toLocaleDateString(LOCALE)}</strong><br><small style="color:var(--text-muted)">${createDate.toLocaleTimeString(LOCALE,{hour:'2-digit',minute:'2-digit'})}</small></td>
|
||||
<td data-label="<?= __('label_code') ?>"><code onclick="copyToClipboard('${escapeHtml(v.formatted_code||'')}','<?= __('js_copied') ?>')" title="<?= __('js_copy') ?>" style="cursor:pointer">${escapeHtml(v.formatted_code||'')}</code></td>
|
||||
<td class="voucher-note" data-label="<?= __('label_note') ?>" title="${escapeHtml(v.note||'-')}">${escapeHtml(v.note||'-')}</td>
|
||||
<td data-label="<?= __('label_status') ?>">${statusBadge}</td>
|
||||
<td data-label="<?= __('label_usage') ?>"><div class="usage-info"><span>${v.used}/${v.quota>0?v.quota:'∞'}</span>${v.quota>0?`<div class="usage-bar"><div class="usage-bar-fill" style="width:${usagePct}%"></div></div>`:''}</div></td>
|
||||
<td data-label="<?= __('label_expires') ?>">${remaining?`<span style="color:var(--success)"><i class="fas fa-clock" aria-hidden="true"></i> ${remaining}</span><br>`:''}<small style="color:var(--text-muted)">${v.duration} <?= __('label_minutes_short') ?></small></td>
|
||||
<td data-label="<?= __('label_actions') ?>" style="white-space:nowrap;">
|
||||
<button onclick="resendVoucher('${v._id}','${escapeHtml(v.formatted_code||'')}')" class="btn btn-secondary btn-sm" title="<?= __('vouchers_resend') ?>" aria-label="<?= __('vouchers_resend') ?>"><i class="fas fa-envelope" aria-hidden="true"></i></button>
|
||||
<button onclick="deleteVoucher('${v._id}')" class="btn btn-danger-soft btn-sm" title="<?= __('btn_delete') ?>" aria-label="<?= __('btn_delete') ?>"><i class="fas fa-trash" aria-hidden="true"></i></button>
|
||||
</td>
|
||||
</tr>`;
|
||||
});
|
||||
|
|
@ -356,11 +358,11 @@ function renderVouchers() {
|
|||
<span style="font-size:13px;color:var(--text-muted);">${<?= json_encode(__('vouchers_page_of')) ?>
|
||||
.replace('{current}',currentPage).replace('{total}',totalPages)} (${vouchers.length})</span>
|
||||
<div style="display:flex;gap:5px;">
|
||||
<button class="btn btn-secondary btn-sm" onclick="setPage(${currentPage-1})" ${currentPage<=1?'disabled':''}><i class="fas fa-chevron-left"></i></button>`;
|
||||
<button class="btn btn-secondary btn-sm" onclick="setPage(${currentPage-1})" ${currentPage<=1?'disabled':''}><i class="fas fa-chevron-left" aria-hidden="true"></i></button>`;
|
||||
for (let p=Math.max(1,currentPage-2); p<=Math.min(totalPages,currentPage+2); p++) {
|
||||
html += `<button class="btn btn-sm ${p===currentPage?'btn-primary':'btn-secondary'}" onclick="setPage(${p})">${p}</button>`;
|
||||
}
|
||||
html += `<button class="btn btn-secondary btn-sm" onclick="setPage(${currentPage+1})" ${currentPage>=totalPages?'disabled':''}><i class="fas fa-chevron-right"></i></button>
|
||||
html += `<button class="btn btn-secondary btn-sm" onclick="setPage(${currentPage+1})" ${currentPage>=totalPages?'disabled':''}><i class="fas fa-chevron-right" aria-hidden="true"></i></button>
|
||||
</div></div>`;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@
|
|||
/* Text */
|
||||
--text-primary: #101625;
|
||||
--text-secondary: #525c6e;
|
||||
--text-muted: #8a93a3;
|
||||
--text-muted: #6b7280;
|
||||
--text-inverse: #ffffff;
|
||||
|
||||
/* Linien */
|
||||
|
|
@ -107,7 +107,7 @@
|
|||
|
||||
--text-primary: #e9ecf3;
|
||||
--text-secondary: #a2abbd;
|
||||
--text-muted: #6f7889;
|
||||
--text-muted: #8a93a6;
|
||||
--text-inverse: #0a0c11;
|
||||
|
||||
--border-color: #232838;
|
||||
|
|
@ -1531,6 +1531,67 @@ input.search-bar, .site-selector .search-bar { min-width: 240px; width: auto; fl
|
|||
.row3 { display: grid; grid-template-columns: repeat(3, 1fr); gap: 14px; }
|
||||
@media (max-width: 720px) { .row, .row3 { grid-template-columns: 1fr; } }
|
||||
|
||||
/* Sprungmarke zum Inhalt (nur bei Tastaturfokus sichtbar) */
|
||||
.skip-link {
|
||||
position: absolute;
|
||||
left: 12px;
|
||||
top: -60px;
|
||||
z-index: 999;
|
||||
padding: 10px 16px;
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--accent);
|
||||
border-radius: var(--r-md);
|
||||
box-shadow: var(--shadow-md);
|
||||
color: var(--accent);
|
||||
font-size: 13.5px;
|
||||
font-weight: 600;
|
||||
transition: top .15s;
|
||||
}
|
||||
.skip-link:focus { top: 12px; }
|
||||
|
||||
/* Tabellen, die auf schmalen Geräten zu Karten werden.
|
||||
Die Spaltenüberschrift steht dann als data-label vor dem Wert. */
|
||||
@media (max-width: 720px) {
|
||||
.table-stack thead { display: none; }
|
||||
.table-stack, .table-stack tbody, .table-stack tr, .table-stack td { display: block; width: 100%; }
|
||||
.table-stack tr {
|
||||
margin-bottom: 10px;
|
||||
padding: 6px 14px;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--r-md);
|
||||
background: var(--bg-card);
|
||||
}
|
||||
.table-stack td {
|
||||
display: flex; align-items: center; justify-content: space-between; gap: 14px;
|
||||
padding: 8px 0;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
text-align: right;
|
||||
}
|
||||
.table-stack tr td:last-child { border-bottom: none; }
|
||||
.table-stack td::before {
|
||||
content: attr(data-label);
|
||||
flex-shrink: 0;
|
||||
color: var(--text-muted);
|
||||
font-size: 11.5px;
|
||||
font-weight: 620;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: .04em;
|
||||
text-align: left;
|
||||
}
|
||||
.table-stack td:not([data-label])::before { content: ''; }
|
||||
.table-stack .details-cell, .table-stack .voucher-note { max-width: none; white-space: normal; }
|
||||
}
|
||||
|
||||
/* Nutzer, die Bewegung reduzieren möchten, bekommen keine Animationen. */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*, *::before, *::after {
|
||||
animation-duration: .001ms !important;
|
||||
animation-iteration-count: 1 !important;
|
||||
transition-duration: .001ms !important;
|
||||
scroll-behavior: auto !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* =========================================================================
|
||||
13. RESPONSIVE
|
||||
========================================================================= */
|
||||
|
|
|
|||
|
|
@ -114,7 +114,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||
</form>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="auth-links"><a href="login.php" class="back-link"><i class="fas fa-arrow-left"></i> <?= __('reset_back_login') ?></a></div>
|
||||
<div class="auth-links"><a href="login.php" class="back-link"><i class="fas fa-arrow-left" aria-hidden="true"></i> <?= __('reset_back_login') ?></a></div>
|
||||
</div>
|
||||
<script src="assets/global.js"></script>
|
||||
</body>
|
||||
|
|
|
|||
|
|
@ -48,11 +48,12 @@ foreach ($navGroups as $items) {
|
|||
</head>
|
||||
<body>
|
||||
|
||||
<a class="skip-link" href="#main-content"><?= __('a11y_skip') ?></a>
|
||||
<div class="sidebar-overlay" onclick="closeMobileSidebar()"></div>
|
||||
|
||||
<aside class="sidebar" id="adminSidebar">
|
||||
<aside class="sidebar" id="adminSidebar" aria-label="<?= __('nav_administration') ?>">
|
||||
<a href="<?= $rootBase ?>index.php" class="sidebar-brand">
|
||||
<span class="brand-mark"><i class="fas fa-wifi"></i></span>
|
||||
<span class="brand-mark"><i class="fas fa-wifi" aria-hidden="true"></i></span>
|
||||
<span class="brand-text">
|
||||
<span class="brand-name"><?= htmlspecialchars($appTitle ?? 'Voucher Tool') ?></span>
|
||||
<span class="brand-sub"><?= __('nav_administration') ?></span>
|
||||
|
|
@ -65,8 +66,9 @@ foreach ($navGroups as $items) {
|
|||
<ul class="sidebar-nav">
|
||||
<?php foreach ($items as [$key, $href, $icon, $labelKey]): ?>
|
||||
<li>
|
||||
<a href="<?= $base . $href ?>" class="<?= $currentPage === $key ? 'active' : '' ?>">
|
||||
<i class="fas <?= $icon ?>"></i> <?= __($labelKey) ?>
|
||||
<a href="<?= $base . $href ?>" class="<?= $currentPage === $key ? 'active' : '' ?>"
|
||||
<?= $currentPage === $key ? 'aria-current="page"' : '' ?>>
|
||||
<i class="fas <?= $icon ?>" aria-hidden="true"></i> <?= __($labelKey) ?>
|
||||
</a>
|
||||
</li>
|
||||
<?php endforeach; ?>
|
||||
|
|
@ -82,8 +84,8 @@ foreach ($navGroups as $items) {
|
|||
<div class="user-name" style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap;"><?= htmlspecialchars($currentUser['name']) ?></div>
|
||||
<div class="user-role"><?= htmlspecialchars($currentUser['role'] ?? '') ?></div>
|
||||
</div>
|
||||
<a href="<?= $rootBase ?>logout.php" class="icon-btn" title="<?= __('btn_logout') ?>" style="width:30px;height:30px;font-size:12px;">
|
||||
<i class="fas fa-arrow-right-from-bracket"></i>
|
||||
<a href="<?= $rootBase ?>logout.php" class="icon-btn" title="<?= __('btn_logout') ?>" aria-label="<?= __('btn_logout') ?>" style="width:30px;height:30px;font-size:12px;">
|
||||
<i class="fas fa-arrow-right-from-bracket" aria-hidden="true"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -92,8 +94,8 @@ foreach ($navGroups as $items) {
|
|||
|
||||
<header class="topbar">
|
||||
<div class="header-left">
|
||||
<button class="mobile-menu-btn" onclick="toggleMobileSidebar()" title="Menu">
|
||||
<i class="fas fa-bars"></i>
|
||||
<button class="mobile-menu-btn" onclick="toggleMobileSidebar()" aria-label="<?= __('a11y_menu') ?>" title="<?= __('a11y_menu') ?>">
|
||||
<i class="fas fa-bars" aria-hidden="true"></i>
|
||||
</button>
|
||||
<div class="breadcrumb">
|
||||
<span><?= __('nav_administration') ?></span>
|
||||
|
|
@ -102,20 +104,20 @@ foreach ($navGroups as $items) {
|
|||
</div>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<div class="lang-switcher">
|
||||
<div class="lang-switcher" role="group" aria-label="<?= __('a11y_language') ?>">
|
||||
<?php foreach (I18n::getAvailable() as $code => $label): ?>
|
||||
<button class="lang-btn <?= $lang === $code ? 'active' : '' ?>"
|
||||
onclick="switchLanguage('<?= $code ?>')"><?= strtoupper($code) ?></button>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<button id="darkModeBtn" class="dark-mode-toggle" onclick="toggleDarkMode()" title="Dark Mode">
|
||||
<i class="fas fa-moon"></i>
|
||||
<button id="darkModeBtn" class="dark-mode-toggle" onclick="toggleDarkMode()" aria-label="<?= __('a11y_theme') ?>" title="<?= __('a11y_theme') ?>">
|
||||
<i class="fas fa-moon" aria-hidden="true"></i>
|
||||
</button>
|
||||
<a href="<?= $rootBase ?>index.php" class="btn btn-secondary">
|
||||
<i class="fas fa-arrow-left"></i>
|
||||
<i class="fas fa-arrow-left" aria-hidden="true"></i>
|
||||
<span class="hide-mobile"><?= __('nav_back') ?></span>
|
||||
</a>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="main-content">
|
||||
<main class="main-content" id="main-content">
|
||||
|
|
|
|||
53
index.php
53
index.php
|
|
@ -308,43 +308,44 @@ function buildPrintCard($template, $data, $instructionHeader, $instructionText,
|
|||
</head>
|
||||
<body class="app-body">
|
||||
|
||||
<a class="skip-link no-print" href="#main-content"><?= __('a11y_skip') ?></a>
|
||||
<header class="app-topbar no-print">
|
||||
<a href="index.php" class="brand">
|
||||
<span class="brand-mark"><i class="fas fa-wifi"></i></span>
|
||||
<span class="brand-mark"><i class="fas fa-wifi" aria-hidden="true"></i></span>
|
||||
<span class="brand-name"><?= htmlspecialchars($appTitle) ?></span>
|
||||
</a>
|
||||
<div class="app-actions">
|
||||
<div class="lang-switcher">
|
||||
<div class="lang-switcher" role="group" aria-label="<?= __('a11y_language') ?>">
|
||||
<?php foreach (I18n::getAvailable() as $code => $label): ?>
|
||||
<button class="lang-btn <?= I18n::getLanguage() === $code ? 'active' : '' ?>"
|
||||
onclick="switchLanguage('<?= $code ?>')"><?= strtoupper($code) ?></button>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<button id="darkModeBtn" class="dark-mode-toggle" onclick="toggleDarkMode()" title="Dark Mode">
|
||||
<i class="fas fa-moon"></i>
|
||||
<button id="darkModeBtn" class="dark-mode-toggle" onclick="toggleDarkMode()" aria-label="<?= __('a11y_theme') ?>" title="<?= __('a11y_theme') ?>">
|
||||
<i class="fas fa-moon" aria-hidden="true"></i>
|
||||
</button>
|
||||
<?php if ($currentUser): ?>
|
||||
<?php if ($auth->isAdmin()): ?>
|
||||
<a href="admin/" class="btn btn-secondary">
|
||||
<i class="fas fa-sliders"></i> <span class="hide-mobile"><?= __('nav_administration') ?></span>
|
||||
<i class="fas fa-sliders" aria-hidden="true"></i> <span class="hide-mobile"><?= __('nav_administration') ?></span>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
<div class="user-menu">
|
||||
<div class="user-avatar"><?= strtoupper(mb_substr($currentUser['name'], 0, 1)) ?></div>
|
||||
<div class="user-name hide-mobile"><?= htmlspecialchars($currentUser['name']) ?></div>
|
||||
<a href="logout.php" class="icon-btn" title="<?= __('btn_logout') ?>" style="width:28px;height:28px;font-size:11px;">
|
||||
<i class="fas fa-arrow-right-from-bracket"></i>
|
||||
<a href="logout.php" class="icon-btn" title="<?= __('btn_logout') ?>" aria-label="<?= __('btn_logout') ?>" style="width:28px;height:28px;font-size:11px;">
|
||||
<i class="fas fa-arrow-right-from-bracket" aria-hidden="true"></i>
|
||||
</a>
|
||||
</div>
|
||||
<?php elseif ($publicAccess): ?>
|
||||
<a href="login.php" class="btn btn-secondary">
|
||||
<i class="fas fa-right-to-bracket"></i> <?= __('btn_login') ?>
|
||||
<i class="fas fa-right-to-bracket" aria-hidden="true"></i> <?= __('btn_login') ?>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="container">
|
||||
<main class="container" id="main-content">
|
||||
<?php if ($logoUrl && !$voucherCreated && !$bulkCreated): ?>
|
||||
<img src="<?= htmlspecialchars(Ui::mediaUrl($logoUrl)) ?>" alt="Logo" class="logo">
|
||||
<?php endif; ?>
|
||||
|
|
@ -366,17 +367,17 @@ function buildPrintCard($template, $data, $instructionHeader, $instructionText,
|
|||
</div>
|
||||
|
||||
<div class="voucher-result no-print">
|
||||
<div class="result-label"><i class="fas fa-circle-check"></i> <?= __('voucher_success_title') ?></div>
|
||||
<div class="result-label"><i class="fas fa-circle-check" aria-hidden="true"></i> <?= __('voucher_success_title') ?></div>
|
||||
<div class="voucher-code" id="voucherCode" onclick="copyCode()" title="<?= __('js_click_to_copy') ?>">
|
||||
<?= htmlspecialchars($voucherCode) ?>
|
||||
</div>
|
||||
<div class="voucher-info">
|
||||
<i class="fas fa-copy"></i> <?= __('voucher_copy_hint') ?>
|
||||
<i class="fas fa-copy" aria-hidden="true"></i> <?= __('voucher_copy_hint') ?>
|
||||
</div>
|
||||
<div class="voucher-meta">
|
||||
<span><i class="fas fa-clock"></i> <?= str_replace('{minutes}', $voucherData['expire_min'], __('voucher_validity')) ?></span>
|
||||
<span><i class="fas fa-location-dot"></i> <?= htmlspecialchars($voucherData['site_name']) ?></span>
|
||||
<span><i class="fas fa-mobile-screen"></i> <?= (int)$voucherData['max_uses'] ?> <?= __('label_devices') ?></span>
|
||||
<span><i class="fas fa-clock" aria-hidden="true"></i> <?= str_replace('{minutes}', $voucherData['expire_min'], __('voucher_validity')) ?></span>
|
||||
<span><i class="fas fa-location-dot" aria-hidden="true"></i> <?= htmlspecialchars($voucherData['site_name']) ?></span>
|
||||
<span><i class="fas fa-mobile-screen" aria-hidden="true"></i> <?= (int)$voucherData['max_uses'] ?> <?= __('label_devices') ?></span>
|
||||
</div>
|
||||
<div class="ticket-divider"></div>
|
||||
<div class="qr-wrapper no-print">
|
||||
|
|
@ -395,11 +396,11 @@ function buildPrintCard($template, $data, $instructionHeader, $instructionText,
|
|||
<div class="no-print" style="display:flex;gap:10px;margin-top:18px;">
|
||||
<form method="get" style="flex:1;">
|
||||
<button type="submit" class="btn btn-primary btn-lg btn-block">
|
||||
<i class="fas fa-plus"></i> <?= __('btn_new_code') ?>
|
||||
<i class="fas fa-plus" aria-hidden="true"></i> <?= __('btn_new_code') ?>
|
||||
</button>
|
||||
</form>
|
||||
<button onclick="window.print()" class="btn btn-secondary btn-lg">
|
||||
<i class="fas fa-print"></i> <?= __('voucher_print_btn') ?>
|
||||
<i class="fas fa-print" aria-hidden="true"></i> <?= __('voucher_print_btn') ?>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
|
@ -441,12 +442,12 @@ function buildPrintCard($template, $data, $instructionHeader, $instructionText,
|
|||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
<p class="copy-hint"><i class="fas fa-copy"></i> <?= __('voucher_copy_hint') ?></p>
|
||||
<p class="copy-hint"><i class="fas fa-copy" aria-hidden="true"></i> <?= __('voucher_copy_hint') ?></p>
|
||||
</div>
|
||||
|
||||
<div class="no-print" style="display:flex;gap:10px;margin-top:20px;">
|
||||
<button onclick="window.print()" class="btn btn-primary btn-lg" style="flex:1;">
|
||||
<i class="fas fa-print"></i> <?= __('bulk_print_all') ?>
|
||||
<i class="fas fa-print" aria-hidden="true"></i> <?= __('bulk_print_all') ?>
|
||||
</button>
|
||||
<form method="get" style="flex:1;">
|
||||
<button type="submit" class="btn btn-secondary btn-lg btn-block"><?= __('btn_new_code') ?></button>
|
||||
|
|
@ -455,7 +456,7 @@ function buildPrintCard($template, $data, $instructionHeader, $instructionText,
|
|||
|
||||
<?php elseif (empty($sites)): ?>
|
||||
<div class="empty-state">
|
||||
<div class="empty-icon"><i class="fas fa-wifi"></i></div>
|
||||
<div class="empty-icon"><i class="fas fa-wifi" aria-hidden="true"></i></div>
|
||||
<p><?= __('voucher_no_sites') ?><br>
|
||||
<?php if ($auth->isAdmin()): ?>
|
||||
<a href="admin/" style="color:var(--accent);"><?= __('voucher_no_sites_admin') ?></a>
|
||||
|
|
@ -539,7 +540,7 @@ function buildPrintCard($template, $data, $instructionHeader, $instructionText,
|
|||
<div class="email-checkbox-wrapper">
|
||||
<input type="checkbox" id="send_email" name="send_email" onchange="toggleEmailField()">
|
||||
<label for="send_email">
|
||||
<i class="fas fa-envelope" style="color:var(--accent);"></i> <?= __('voucher_email_send') ?>
|
||||
<i class="fas fa-envelope" style="color:var(--accent);" aria-hidden="true"></i> <?= __('voucher_email_send') ?>
|
||||
</label>
|
||||
</div>
|
||||
<div class="email-input-wrapper" id="email_field">
|
||||
|
|
@ -556,7 +557,7 @@ function buildPrintCard($template, $data, $instructionHeader, $instructionText,
|
|||
<div class="email-option">
|
||||
<div class="email-checkbox-wrapper">
|
||||
<input type="checkbox" id="send_sms" name="send_sms" onchange="document.getElementById('sms_field').style.display=this.checked?'block':'none'">
|
||||
<label for="send_sms"><i class="fas fa-comment-sms" style="color:var(--accent);"></i> Code per SMS versenden</label>
|
||||
<label for="send_sms"><i class="fas fa-comment-sms" style="color:var(--accent);" aria-hidden="true"></i> Code per SMS versenden</label>
|
||||
</div>
|
||||
<div id="sms_field" style="display:none;margin-top:12px;">
|
||||
<label for="recipient_phone">Telefonnummer (international, z.B. +49170…)</label>
|
||||
|
|
@ -566,7 +567,7 @@ function buildPrintCard($template, $data, $instructionHeader, $instructionText,
|
|||
<?php endif; ?>
|
||||
|
||||
<button type="submit" class="btn btn-primary btn-lg btn-block" id="submitBtn">
|
||||
<i class="fas fa-ticket"></i> <?= __('voucher_create_btn') ?>
|
||||
<i class="fas fa-ticket" aria-hidden="true"></i> <?= __('voucher_create_btn') ?>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
|
@ -629,9 +630,9 @@ function buildPrintCard($template, $data, $instructionHeader, $instructionText,
|
|||
<?php endif; ?>
|
||||
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<div id="toast-container"></div>
|
||||
<div id="toast-container" role="status" aria-live="polite"></div>
|
||||
<script src="assets/global.js"></script>
|
||||
<script>
|
||||
<?php if ($voucherCreated): ?>
|
||||
|
|
@ -711,7 +712,7 @@ function buildPrintCard($template, $data, $instructionHeader, $instructionText,
|
|||
const btn = document.getElementById('submitBtn');
|
||||
if (!btn || btn.disabled) { e.preventDefault(); return; }
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<i class="fas fa-circle-notch fa-spin"></i> <?= __('voucher_creating') ?>';
|
||||
btn.innerHTML = '<i class="fas fa-circle-notch fa-spin" aria-hidden="true"></i> <?= __('voucher_creating') ?>';
|
||||
});
|
||||
|
||||
document.getElementById('bulkForm')?.addEventListener('submit', function(e) {
|
||||
|
|
@ -719,7 +720,7 @@ function buildPrintCard($template, $data, $instructionHeader, $instructionText,
|
|||
if (!btn || btn.disabled) { e.preventDefault(); return; }
|
||||
const count = document.getElementById('bulk_count').value;
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<i class="fas fa-circle-notch fa-spin"></i> <?= addslashes(str_replace('{count}', "' + count + '", __('bulk_creating'))) ?>';
|
||||
btn.innerHTML = '<i class="fas fa-circle-notch fa-spin" aria-hidden="true"></i> <?= addslashes(str_replace('{count}', "' + count + '", __('bulk_creating'))) ?>';
|
||||
});
|
||||
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
|
|
|
|||
758
install.php
758
install.php
|
|
@ -1,380 +1,380 @@
|
|||
<?php
|
||||
session_start();
|
||||
|
||||
// Prüfen ob bereits installiert.
|
||||
// Wenn eine config.php existiert, darf der Installer NICHT mehr ohne Weiteres
|
||||
// erreichbar sein – sonst koennte jeder die Konfiguration ueberschreiben und
|
||||
// einen neuen Admin anlegen. Reinstall ist nur fuer angemeldete Admins erlaubt.
|
||||
if (file_exists(__DIR__ . '/config.php')) {
|
||||
if (!isset($_GET['reinstall'])) {
|
||||
die('System bereits installiert. Eine Neuinstallation ist nur fuer angemeldete Administratoren ueber install.php?reinstall=1 moeglich.');
|
||||
}
|
||||
|
||||
// Reinstall angefordert -> Admin-Authentifizierung erzwingen
|
||||
require_once __DIR__ . '/config.php';
|
||||
require_once __DIR__ . '/includes/Database.php';
|
||||
require_once __DIR__ . '/includes/Auth.php';
|
||||
try {
|
||||
$reinstallAuth = new Auth();
|
||||
if (!$reinstallAuth->isAdmin()) {
|
||||
die('Neuinstallation nicht erlaubt: Bitte zuerst als Administrator <a href="login.php">anmelden</a>.');
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
die('Neuinstallation nicht moeglich (Konfigurationsfehler).');
|
||||
}
|
||||
}
|
||||
|
||||
$step = isset($_POST['step']) ? (int)$_POST['step'] : 1;
|
||||
$errors = [];
|
||||
$success = false;
|
||||
|
||||
// Step 1: Datenbankverbindung testen
|
||||
if ($step === 2 && $_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$db_host = $_POST['db_host'] ?? '';
|
||||
$db_name = $_POST['db_name'] ?? '';
|
||||
$db_user = $_POST['db_user'] ?? '';
|
||||
$db_pass = $_POST['db_pass'] ?? '';
|
||||
|
||||
try {
|
||||
$pdo = new PDO("mysql:host=$db_host;charset=utf8mb4", $db_user, $db_pass);
|
||||
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
||||
|
||||
// Datenbank erstellen falls nicht vorhanden
|
||||
$pdo->exec("CREATE DATABASE IF NOT EXISTS `$db_name` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci");
|
||||
$pdo->exec("USE `$db_name`");
|
||||
|
||||
// Tabellen erstellen
|
||||
$sql = file_get_contents(__DIR__ . '/database.sql');
|
||||
$pdo->exec($sql);
|
||||
|
||||
$_SESSION['install_db'] = [
|
||||
'host' => $db_host,
|
||||
'name' => $db_name,
|
||||
'user' => $db_user,
|
||||
'pass' => $db_pass
|
||||
];
|
||||
|
||||
} catch (PDOException $e) {
|
||||
$errors[] = "Datenbankfehler: " . $e->getMessage();
|
||||
$step = 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Step 2: Admin-Account erstellen
|
||||
if ($step === 3 && $_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$admin_email = filter_var($_POST['admin_email'] ?? '', FILTER_VALIDATE_EMAIL);
|
||||
$admin_name = $_POST['admin_name'] ?? '';
|
||||
$admin_password = $_POST['admin_password'] ?? '';
|
||||
$admin_password_confirm = $_POST['admin_password_confirm'] ?? '';
|
||||
|
||||
if (!$admin_email) {
|
||||
$errors[] = "Ungültige E-Mail-Adresse";
|
||||
$step = 2;
|
||||
} elseif (strlen($admin_password) < 8) {
|
||||
$errors[] = "Passwort muss mindestens 8 Zeichen lang sein";
|
||||
$step = 2;
|
||||
} elseif ($admin_password !== $admin_password_confirm) {
|
||||
$errors[] = "Passwörter stimmen nicht überein";
|
||||
$step = 2;
|
||||
} else {
|
||||
$_SESSION['install_admin'] = [
|
||||
'email' => $admin_email,
|
||||
'name' => $admin_name,
|
||||
'password' => password_hash($admin_password, PASSWORD_DEFAULT)
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// Step 3: Allgemeine Einstellungen
|
||||
if ($step === 4 && $_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$app_title = $_POST['app_title'] ?? 'UniFi Voucher System';
|
||||
$logo_url = $_POST['logo_url'] ?? '';
|
||||
$instruction_header = $_POST['instruction_header'] ?? '';
|
||||
$instruction_text = $_POST['instruction_text'] ?? '';
|
||||
$public_access = isset($_POST['public_access']) ? 1 : 0;
|
||||
|
||||
// Microsoft 365 OAuth (optional)
|
||||
$m365_client_id = $_POST['m365_client_id'] ?? '';
|
||||
$m365_client_secret = $_POST['m365_client_secret'] ?? '';
|
||||
$m365_tenant_id = $_POST['m365_tenant_id'] ?? '';
|
||||
|
||||
$_SESSION['install_settings'] = [
|
||||
'app_title' => $app_title,
|
||||
'logo_url' => $logo_url,
|
||||
'instruction_header' => $instruction_header,
|
||||
'instruction_text' => $instruction_text,
|
||||
'public_access' => $public_access,
|
||||
'm365_client_id' => $m365_client_id,
|
||||
'm365_client_secret' => $m365_client_secret,
|
||||
'm365_tenant_id' => $m365_tenant_id
|
||||
];
|
||||
}
|
||||
|
||||
// Step 4: Installation abschließen
|
||||
if ($step === 5 && $_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
try {
|
||||
$db = $_SESSION['install_db'];
|
||||
$admin = $_SESSION['install_admin'];
|
||||
$settings = $_SESSION['install_settings'];
|
||||
|
||||
$pdo = new PDO("mysql:host={$db['host']};dbname={$db['name']};charset=utf8mb4", $db['user'], $db['pass']);
|
||||
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
||||
|
||||
// Admin-User erstellen
|
||||
$stmt = $pdo->prepare("INSERT INTO users (email, name, password_hash, is_admin, is_active) VALUES (?, ?, ?, 1, 1)");
|
||||
$stmt->execute([$admin['email'], $admin['name'], $admin['password']]);
|
||||
|
||||
// Settings speichern
|
||||
$settingsData = [
|
||||
'app_title' => $settings['app_title'],
|
||||
'logo_url' => $settings['logo_url'],
|
||||
'instruction_header' => $settings['instruction_header'],
|
||||
'instruction_text' => $settings['instruction_text'],
|
||||
'public_access' => $settings['public_access'],
|
||||
'm365_client_id' => $settings['m365_client_id'],
|
||||
'm365_client_secret' => $settings['m365_client_secret'],
|
||||
'm365_tenant_id' => $settings['m365_tenant_id']
|
||||
];
|
||||
|
||||
$stmt = $pdo->prepare("INSERT INTO settings (setting_key, setting_value) VALUES (?, ?) ON DUPLICATE KEY UPDATE setting_value = VALUES(setting_value)");
|
||||
foreach ($settingsData as $key => $value) {
|
||||
$stmt->execute([$key, $value]);
|
||||
}
|
||||
|
||||
// config.php erstellen
|
||||
$configContent = "<?php\n";
|
||||
$configContent .= "// UniFi Voucher Management System - Configuration\n\n";
|
||||
$configContent .= "define('DB_HOST', '{$db['host']}');\n";
|
||||
$configContent .= "define('DB_NAME', '{$db['name']}');\n";
|
||||
$configContent .= "define('DB_USER', '{$db['user']}');\n";
|
||||
$configContent .= "define('DB_PASS', '" . addslashes($db['pass']) . "');\n\n";
|
||||
$configContent .= "// Anwendungs-Schluessel fuer Verschluesselung-at-rest (z.B. UniFi-Passwoerter)\n";
|
||||
$configContent .= "// NICHT aendern, sonst koennen bestehende verschluesselte Werte nicht mehr gelesen werden.\n";
|
||||
$configContent .= "define('APP_KEY', '" . base64_encode(random_bytes(32)) . "');\n\n";
|
||||
$configContent .= "// Sitzungs-Einstellungen\n";
|
||||
$configContent .= "define('SESSION_LIFETIME', 3600); // 1 Stunde\n\n";
|
||||
$configContent .= "// Zeitzone\n";
|
||||
$configContent .= "date_default_timezone_set('Europe/Berlin');\n";
|
||||
|
||||
file_put_contents(__DIR__ . '/config.php', $configContent);
|
||||
|
||||
// .htaccess erstellen (ohne Rewrite Rules die Probleme machen)
|
||||
$htaccess = "# UniFi Voucher System\n\n";
|
||||
$htaccess .= "# Security\n";
|
||||
$htaccess .= "<FilesMatch \"(config\\.php|database\\.sql|install\\.php|test\\.php|m365_debug\\.php|\\.md)$\">\n";
|
||||
$htaccess .= " Order Allow,Deny\n";
|
||||
$htaccess .= " Deny from all\n";
|
||||
$htaccess .= "</FilesMatch>\n\n";
|
||||
$htaccess .= "DirectoryIndex index.php\n";
|
||||
file_put_contents(__DIR__ . '/.htaccess', $htaccess);
|
||||
|
||||
$success = true;
|
||||
|
||||
// Session-Daten löschen
|
||||
unset($_SESSION['install_db'], $_SESSION['install_admin'], $_SESSION['install_settings']);
|
||||
|
||||
} catch (Exception $e) {
|
||||
$errors[] = "Fehler bei der Installation: " . $e->getMessage();
|
||||
$step = 4;
|
||||
}
|
||||
}
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>UniFi Voucher System - Installation</title>
|
||||
<?php require_once __DIR__ . '/includes/Ui.php'; ?>
|
||||
<?= Ui::head(null) ?>
|
||||
<style>
|
||||
/* Installer-spezifisch: Fortschrittsanzeige und Abschnitte */
|
||||
.install-head { display: flex; align-items: center; gap: 14px; margin-bottom: 4px; }
|
||||
.progress { position: relative; display: flex; justify-content: space-between; margin: 26px 0 28px; }
|
||||
.progress::before {
|
||||
content: ''; position: absolute; top: 14px; left: 14px; right: 14px;
|
||||
height: 2px; background: var(--border-color); z-index: 0;
|
||||
}
|
||||
.progress-step {
|
||||
position: relative; z-index: 1;
|
||||
width: 28px; height: 28px; border-radius: 50%;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
background: var(--bg-card); border: 1px solid var(--border-color);
|
||||
color: var(--text-muted); font-size: 12px; font-weight: 650;
|
||||
}
|
||||
.progress-step.active { background: var(--accent); border-color: var(--accent); color: #fff; box-shadow: var(--ring); }
|
||||
.progress-step.completed { background: var(--success); border-color: var(--success); color: #fff; }
|
||||
.section {
|
||||
padding: 18px;
|
||||
margin-bottom: 20px;
|
||||
background: var(--bg-subtle);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--r-md);
|
||||
}
|
||||
.section h3 { font-size: 14px; margin-bottom: 14px; }
|
||||
.error, .success {
|
||||
padding: 12px 15px; margin-bottom: 20px;
|
||||
border-radius: var(--r-md); font-size: 13.5px;
|
||||
border: 1px solid var(--danger-border); background: var(--danger-soft); color: var(--danger);
|
||||
}
|
||||
.success { border-color: var(--success-border); background: var(--success-soft); color: var(--success); }
|
||||
form > h3 { font-size: 15px; margin-bottom: 16px; }
|
||||
</style>
|
||||
</head>
|
||||
<body class="app-body focus-page">
|
||||
<div class="focus-card card">
|
||||
<div class="install-head">
|
||||
<span class="focus-icon"><i class="fas fa-rocket"></i></span>
|
||||
<div>
|
||||
<h1 style="font-size:20px;">UniFi Voucher System</h1>
|
||||
<p class="sub">Installation in fünf Schritten</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="progress">
|
||||
<div class="progress-step <?= $step >= 1 ? 'completed' : '' ?>">1</div>
|
||||
<div class="progress-step <?= $step >= 2 ? 'completed' : ($step === 1 ? 'active' : '') ?>">2</div>
|
||||
<div class="progress-step <?= $step >= 3 ? 'completed' : ($step === 2 ? 'active' : '') ?>">3</div>
|
||||
<div class="progress-step <?= $step >= 4 ? 'completed' : ($step === 3 ? 'active' : '') ?>">4</div>
|
||||
<div class="progress-step <?= $step >= 5 ? 'active' : '' ?>">5</div>
|
||||
</div>
|
||||
|
||||
<?php if (!empty($errors)): ?>
|
||||
<div class="error">
|
||||
<?php foreach ($errors as $error): ?>
|
||||
<div><?= htmlspecialchars($error) ?></div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($success): ?>
|
||||
<div class="success">
|
||||
<strong>✓ Installation erfolgreich abgeschlossen!</strong><br>
|
||||
Sie können sich jetzt mit Ihren Admin-Zugangsdaten anmelden.
|
||||
</div>
|
||||
<a href="index.php" class="btn btn-primary btn-lg btn-block">Zum Login</a>
|
||||
<?php elseif ($step === 1): ?>
|
||||
<form method="post">
|
||||
<input type="hidden" name="step" value="2">
|
||||
<h3>Schritt 1: Datenbank-Konfiguration</h3>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Datenbank-Host</label>
|
||||
<input type="text" name="db_host" value="localhost" required>
|
||||
<div class="help-text">Meist "localhost"</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Datenbankname</label>
|
||||
<input type="text" name="db_name" value="unifi_voucher" required>
|
||||
<div class="help-text">Name der Datenbank (wird erstellt falls nicht vorhanden)</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Datenbank-Benutzer</label>
|
||||
<input type="text" name="db_user" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Datenbank-Passwort</label>
|
||||
<input type="password" name="db_pass">
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary btn-lg btn-block">Weiter <i class="fas fa-arrow-right"></i></button>
|
||||
</form>
|
||||
|
||||
<?php elseif ($step === 2): ?>
|
||||
<form method="post">
|
||||
<input type="hidden" name="step" value="3">
|
||||
<h3>Schritt 2: Administrator-Account</h3>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Name</label>
|
||||
<input type="text" name="admin_name" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>E-Mail</label>
|
||||
<input type="email" name="admin_email" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Passwort</label>
|
||||
<input type="password" name="admin_password" required minlength="8">
|
||||
<div class="help-text">Mindestens 8 Zeichen</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Passwort bestätigen</label>
|
||||
<input type="password" name="admin_password_confirm" required>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary btn-lg btn-block">Weiter <i class="fas fa-arrow-right"></i></button>
|
||||
</form>
|
||||
|
||||
<?php elseif ($step === 3): ?>
|
||||
<form method="post">
|
||||
<input type="hidden" name="step" value="4">
|
||||
<h3>Schritt 3: Allgemeine Einstellungen</h3>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Anwendungs-Titel</label>
|
||||
<input type="text" name="app_title" value="UniFi Voucher System" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Logo-URL (optional)</label>
|
||||
<input type="text" name="logo_url" placeholder="https://example.com/logo.png">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Anleitung Überschrift</label>
|
||||
<input type="text" name="instruction_header" value="So verwenden Sie Ihren Code">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Anleitung Text</label>
|
||||
<textarea name="instruction_text">Verbinden Sie sich mit dem WLAN und geben Sie den Code auf der Anmeldeseite ein.</textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-group checkbox-group">
|
||||
<input type="checkbox" name="public_access" id="public_access">
|
||||
<label for="public_access" style="margin: 0;">Öffentlicher Zugriff auf Code-Erstellung</label>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h3>Microsoft 365 Login (Optional)</h3>
|
||||
<div class="form-group">
|
||||
<label>Client ID</label>
|
||||
<input type="text" name="m365_client_id">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Client Secret</label>
|
||||
<input type="password" name="m365_client_secret">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Tenant ID</label>
|
||||
<input type="text" name="m365_tenant_id">
|
||||
</div>
|
||||
<div class="help-text">Leer lassen, wenn M365-Login nicht verwendet werden soll</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary btn-lg btn-block">Weiter <i class="fas fa-arrow-right"></i></button>
|
||||
</form>
|
||||
|
||||
<?php elseif ($step === 4): ?>
|
||||
<form method="post">
|
||||
<input type="hidden" name="step" value="5">
|
||||
<h3>Schritt 4: Installation abschließen</h3>
|
||||
|
||||
<p style="margin-bottom: 20px; color: #666;">
|
||||
Klicken Sie auf "Installation abschließen", um die Einrichtung zu beenden.
|
||||
Die Datenbank und alle notwendigen Dateien werden erstellt.
|
||||
</p>
|
||||
|
||||
<button type="submit" class="btn btn-primary btn-lg btn-block">Installation abschließen</button>
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</body>
|
||||
<?php
|
||||
session_start();
|
||||
|
||||
// Prüfen ob bereits installiert.
|
||||
// Wenn eine config.php existiert, darf der Installer NICHT mehr ohne Weiteres
|
||||
// erreichbar sein – sonst koennte jeder die Konfiguration ueberschreiben und
|
||||
// einen neuen Admin anlegen. Reinstall ist nur fuer angemeldete Admins erlaubt.
|
||||
if (file_exists(__DIR__ . '/config.php')) {
|
||||
if (!isset($_GET['reinstall'])) {
|
||||
die('System bereits installiert. Eine Neuinstallation ist nur fuer angemeldete Administratoren ueber install.php?reinstall=1 moeglich.');
|
||||
}
|
||||
|
||||
// Reinstall angefordert -> Admin-Authentifizierung erzwingen
|
||||
require_once __DIR__ . '/config.php';
|
||||
require_once __DIR__ . '/includes/Database.php';
|
||||
require_once __DIR__ . '/includes/Auth.php';
|
||||
try {
|
||||
$reinstallAuth = new Auth();
|
||||
if (!$reinstallAuth->isAdmin()) {
|
||||
die('Neuinstallation nicht erlaubt: Bitte zuerst als Administrator <a href="login.php">anmelden</a>.');
|
||||
}
|
||||
} catch (Exception $e) {
|
||||
die('Neuinstallation nicht moeglich (Konfigurationsfehler).');
|
||||
}
|
||||
}
|
||||
|
||||
$step = isset($_POST['step']) ? (int)$_POST['step'] : 1;
|
||||
$errors = [];
|
||||
$success = false;
|
||||
|
||||
// Step 1: Datenbankverbindung testen
|
||||
if ($step === 2 && $_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$db_host = $_POST['db_host'] ?? '';
|
||||
$db_name = $_POST['db_name'] ?? '';
|
||||
$db_user = $_POST['db_user'] ?? '';
|
||||
$db_pass = $_POST['db_pass'] ?? '';
|
||||
|
||||
try {
|
||||
$pdo = new PDO("mysql:host=$db_host;charset=utf8mb4", $db_user, $db_pass);
|
||||
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
||||
|
||||
// Datenbank erstellen falls nicht vorhanden
|
||||
$pdo->exec("CREATE DATABASE IF NOT EXISTS `$db_name` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci");
|
||||
$pdo->exec("USE `$db_name`");
|
||||
|
||||
// Tabellen erstellen
|
||||
$sql = file_get_contents(__DIR__ . '/database.sql');
|
||||
$pdo->exec($sql);
|
||||
|
||||
$_SESSION['install_db'] = [
|
||||
'host' => $db_host,
|
||||
'name' => $db_name,
|
||||
'user' => $db_user,
|
||||
'pass' => $db_pass
|
||||
];
|
||||
|
||||
} catch (PDOException $e) {
|
||||
$errors[] = "Datenbankfehler: " . $e->getMessage();
|
||||
$step = 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Step 2: Admin-Account erstellen
|
||||
if ($step === 3 && $_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$admin_email = filter_var($_POST['admin_email'] ?? '', FILTER_VALIDATE_EMAIL);
|
||||
$admin_name = $_POST['admin_name'] ?? '';
|
||||
$admin_password = $_POST['admin_password'] ?? '';
|
||||
$admin_password_confirm = $_POST['admin_password_confirm'] ?? '';
|
||||
|
||||
if (!$admin_email) {
|
||||
$errors[] = "Ungültige E-Mail-Adresse";
|
||||
$step = 2;
|
||||
} elseif (strlen($admin_password) < 8) {
|
||||
$errors[] = "Passwort muss mindestens 8 Zeichen lang sein";
|
||||
$step = 2;
|
||||
} elseif ($admin_password !== $admin_password_confirm) {
|
||||
$errors[] = "Passwörter stimmen nicht überein";
|
||||
$step = 2;
|
||||
} else {
|
||||
$_SESSION['install_admin'] = [
|
||||
'email' => $admin_email,
|
||||
'name' => $admin_name,
|
||||
'password' => password_hash($admin_password, PASSWORD_DEFAULT)
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// Step 3: Allgemeine Einstellungen
|
||||
if ($step === 4 && $_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
$app_title = $_POST['app_title'] ?? 'UniFi Voucher System';
|
||||
$logo_url = $_POST['logo_url'] ?? '';
|
||||
$instruction_header = $_POST['instruction_header'] ?? '';
|
||||
$instruction_text = $_POST['instruction_text'] ?? '';
|
||||
$public_access = isset($_POST['public_access']) ? 1 : 0;
|
||||
|
||||
// Microsoft 365 OAuth (optional)
|
||||
$m365_client_id = $_POST['m365_client_id'] ?? '';
|
||||
$m365_client_secret = $_POST['m365_client_secret'] ?? '';
|
||||
$m365_tenant_id = $_POST['m365_tenant_id'] ?? '';
|
||||
|
||||
$_SESSION['install_settings'] = [
|
||||
'app_title' => $app_title,
|
||||
'logo_url' => $logo_url,
|
||||
'instruction_header' => $instruction_header,
|
||||
'instruction_text' => $instruction_text,
|
||||
'public_access' => $public_access,
|
||||
'm365_client_id' => $m365_client_id,
|
||||
'm365_client_secret' => $m365_client_secret,
|
||||
'm365_tenant_id' => $m365_tenant_id
|
||||
];
|
||||
}
|
||||
|
||||
// Step 4: Installation abschließen
|
||||
if ($step === 5 && $_SERVER['REQUEST_METHOD'] === 'POST') {
|
||||
try {
|
||||
$db = $_SESSION['install_db'];
|
||||
$admin = $_SESSION['install_admin'];
|
||||
$settings = $_SESSION['install_settings'];
|
||||
|
||||
$pdo = new PDO("mysql:host={$db['host']};dbname={$db['name']};charset=utf8mb4", $db['user'], $db['pass']);
|
||||
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
||||
|
||||
// Admin-User erstellen
|
||||
$stmt = $pdo->prepare("INSERT INTO users (email, name, password_hash, is_admin, is_active) VALUES (?, ?, ?, 1, 1)");
|
||||
$stmt->execute([$admin['email'], $admin['name'], $admin['password']]);
|
||||
|
||||
// Settings speichern
|
||||
$settingsData = [
|
||||
'app_title' => $settings['app_title'],
|
||||
'logo_url' => $settings['logo_url'],
|
||||
'instruction_header' => $settings['instruction_header'],
|
||||
'instruction_text' => $settings['instruction_text'],
|
||||
'public_access' => $settings['public_access'],
|
||||
'm365_client_id' => $settings['m365_client_id'],
|
||||
'm365_client_secret' => $settings['m365_client_secret'],
|
||||
'm365_tenant_id' => $settings['m365_tenant_id']
|
||||
];
|
||||
|
||||
$stmt = $pdo->prepare("INSERT INTO settings (setting_key, setting_value) VALUES (?, ?) ON DUPLICATE KEY UPDATE setting_value = VALUES(setting_value)");
|
||||
foreach ($settingsData as $key => $value) {
|
||||
$stmt->execute([$key, $value]);
|
||||
}
|
||||
|
||||
// config.php erstellen
|
||||
$configContent = "<?php\n";
|
||||
$configContent .= "// UniFi Voucher Management System - Configuration\n\n";
|
||||
$configContent .= "define('DB_HOST', '{$db['host']}');\n";
|
||||
$configContent .= "define('DB_NAME', '{$db['name']}');\n";
|
||||
$configContent .= "define('DB_USER', '{$db['user']}');\n";
|
||||
$configContent .= "define('DB_PASS', '" . addslashes($db['pass']) . "');\n\n";
|
||||
$configContent .= "// Anwendungs-Schluessel fuer Verschluesselung-at-rest (z.B. UniFi-Passwoerter)\n";
|
||||
$configContent .= "// NICHT aendern, sonst koennen bestehende verschluesselte Werte nicht mehr gelesen werden.\n";
|
||||
$configContent .= "define('APP_KEY', '" . base64_encode(random_bytes(32)) . "');\n\n";
|
||||
$configContent .= "// Sitzungs-Einstellungen\n";
|
||||
$configContent .= "define('SESSION_LIFETIME', 3600); // 1 Stunde\n\n";
|
||||
$configContent .= "// Zeitzone\n";
|
||||
$configContent .= "date_default_timezone_set('Europe/Berlin');\n";
|
||||
|
||||
file_put_contents(__DIR__ . '/config.php', $configContent);
|
||||
|
||||
// .htaccess erstellen (ohne Rewrite Rules die Probleme machen)
|
||||
$htaccess = "# UniFi Voucher System\n\n";
|
||||
$htaccess .= "# Security\n";
|
||||
$htaccess .= "<FilesMatch \"(config\\.php|database\\.sql|install\\.php|test\\.php|m365_debug\\.php|\\.md)$\">\n";
|
||||
$htaccess .= " Order Allow,Deny\n";
|
||||
$htaccess .= " Deny from all\n";
|
||||
$htaccess .= "</FilesMatch>\n\n";
|
||||
$htaccess .= "DirectoryIndex index.php\n";
|
||||
file_put_contents(__DIR__ . '/.htaccess', $htaccess);
|
||||
|
||||
$success = true;
|
||||
|
||||
// Session-Daten löschen
|
||||
unset($_SESSION['install_db'], $_SESSION['install_admin'], $_SESSION['install_settings']);
|
||||
|
||||
} catch (Exception $e) {
|
||||
$errors[] = "Fehler bei der Installation: " . $e->getMessage();
|
||||
$step = 4;
|
||||
}
|
||||
}
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>UniFi Voucher System - Installation</title>
|
||||
<?php require_once __DIR__ . '/includes/Ui.php'; ?>
|
||||
<?= Ui::head(null) ?>
|
||||
<style>
|
||||
/* Installer-spezifisch: Fortschrittsanzeige und Abschnitte */
|
||||
.install-head { display: flex; align-items: center; gap: 14px; margin-bottom: 4px; }
|
||||
.progress { position: relative; display: flex; justify-content: space-between; margin: 26px 0 28px; }
|
||||
.progress::before {
|
||||
content: ''; position: absolute; top: 14px; left: 14px; right: 14px;
|
||||
height: 2px; background: var(--border-color); z-index: 0;
|
||||
}
|
||||
.progress-step {
|
||||
position: relative; z-index: 1;
|
||||
width: 28px; height: 28px; border-radius: 50%;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
background: var(--bg-card); border: 1px solid var(--border-color);
|
||||
color: var(--text-muted); font-size: 12px; font-weight: 650;
|
||||
}
|
||||
.progress-step.active { background: var(--accent); border-color: var(--accent); color: #fff; box-shadow: var(--ring); }
|
||||
.progress-step.completed { background: var(--success); border-color: var(--success); color: #fff; }
|
||||
.section {
|
||||
padding: 18px;
|
||||
margin-bottom: 20px;
|
||||
background: var(--bg-subtle);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--r-md);
|
||||
}
|
||||
.section h3 { font-size: 14px; margin-bottom: 14px; }
|
||||
.error, .success {
|
||||
padding: 12px 15px; margin-bottom: 20px;
|
||||
border-radius: var(--r-md); font-size: 13.5px;
|
||||
border: 1px solid var(--danger-border); background: var(--danger-soft); color: var(--danger);
|
||||
}
|
||||
.success { border-color: var(--success-border); background: var(--success-soft); color: var(--success); }
|
||||
form > h3 { font-size: 15px; margin-bottom: 16px; }
|
||||
</style>
|
||||
</head>
|
||||
<body class="app-body focus-page">
|
||||
<div class="focus-card card">
|
||||
<div class="install-head">
|
||||
<span class="focus-icon"><i class="fas fa-rocket" aria-hidden="true"></i></span>
|
||||
<div>
|
||||
<h1 style="font-size:20px;">UniFi Voucher System</h1>
|
||||
<p class="sub">Installation in fünf Schritten</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="progress">
|
||||
<div class="progress-step <?= $step >= 1 ? 'completed' : '' ?>">1</div>
|
||||
<div class="progress-step <?= $step >= 2 ? 'completed' : ($step === 1 ? 'active' : '') ?>">2</div>
|
||||
<div class="progress-step <?= $step >= 3 ? 'completed' : ($step === 2 ? 'active' : '') ?>">3</div>
|
||||
<div class="progress-step <?= $step >= 4 ? 'completed' : ($step === 3 ? 'active' : '') ?>">4</div>
|
||||
<div class="progress-step <?= $step >= 5 ? 'active' : '' ?>">5</div>
|
||||
</div>
|
||||
|
||||
<?php if (!empty($errors)): ?>
|
||||
<div class="error">
|
||||
<?php foreach ($errors as $error): ?>
|
||||
<div><?= htmlspecialchars($error) ?></div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($success): ?>
|
||||
<div class="success">
|
||||
<strong>✓ Installation erfolgreich abgeschlossen!</strong><br>
|
||||
Sie können sich jetzt mit Ihren Admin-Zugangsdaten anmelden.
|
||||
</div>
|
||||
<a href="index.php" class="btn btn-primary btn-lg btn-block">Zum Login</a>
|
||||
<?php elseif ($step === 1): ?>
|
||||
<form method="post">
|
||||
<input type="hidden" name="step" value="2">
|
||||
<h3>Schritt 1: Datenbank-Konfiguration</h3>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Datenbank-Host</label>
|
||||
<input type="text" name="db_host" value="localhost" required>
|
||||
<div class="help-text">Meist "localhost"</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Datenbankname</label>
|
||||
<input type="text" name="db_name" value="unifi_voucher" required>
|
||||
<div class="help-text">Name der Datenbank (wird erstellt falls nicht vorhanden)</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Datenbank-Benutzer</label>
|
||||
<input type="text" name="db_user" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Datenbank-Passwort</label>
|
||||
<input type="password" name="db_pass">
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary btn-lg btn-block">Weiter <i class="fas fa-arrow-right" aria-hidden="true"></i></button>
|
||||
</form>
|
||||
|
||||
<?php elseif ($step === 2): ?>
|
||||
<form method="post">
|
||||
<input type="hidden" name="step" value="3">
|
||||
<h3>Schritt 2: Administrator-Account</h3>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Name</label>
|
||||
<input type="text" name="admin_name" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>E-Mail</label>
|
||||
<input type="email" name="admin_email" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Passwort</label>
|
||||
<input type="password" name="admin_password" required minlength="8">
|
||||
<div class="help-text">Mindestens 8 Zeichen</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Passwort bestätigen</label>
|
||||
<input type="password" name="admin_password_confirm" required>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary btn-lg btn-block">Weiter <i class="fas fa-arrow-right" aria-hidden="true"></i></button>
|
||||
</form>
|
||||
|
||||
<?php elseif ($step === 3): ?>
|
||||
<form method="post">
|
||||
<input type="hidden" name="step" value="4">
|
||||
<h3>Schritt 3: Allgemeine Einstellungen</h3>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Anwendungs-Titel</label>
|
||||
<input type="text" name="app_title" value="UniFi Voucher System" required>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Logo-URL (optional)</label>
|
||||
<input type="text" name="logo_url" placeholder="https://example.com/logo.png">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Anleitung Überschrift</label>
|
||||
<input type="text" name="instruction_header" value="So verwenden Sie Ihren Code">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>Anleitung Text</label>
|
||||
<textarea name="instruction_text">Verbinden Sie sich mit dem WLAN und geben Sie den Code auf der Anmeldeseite ein.</textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-group checkbox-group">
|
||||
<input type="checkbox" name="public_access" id="public_access">
|
||||
<label for="public_access" style="margin: 0;">Öffentlicher Zugriff auf Code-Erstellung</label>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<h3>Microsoft 365 Login (Optional)</h3>
|
||||
<div class="form-group">
|
||||
<label>Client ID</label>
|
||||
<input type="text" name="m365_client_id">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Client Secret</label>
|
||||
<input type="password" name="m365_client_secret">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Tenant ID</label>
|
||||
<input type="text" name="m365_tenant_id">
|
||||
</div>
|
||||
<div class="help-text">Leer lassen, wenn M365-Login nicht verwendet werden soll</div>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary btn-lg btn-block">Weiter <i class="fas fa-arrow-right" aria-hidden="true"></i></button>
|
||||
</form>
|
||||
|
||||
<?php elseif ($step === 4): ?>
|
||||
<form method="post">
|
||||
<input type="hidden" name="step" value="5">
|
||||
<h3>Schritt 4: Installation abschließen</h3>
|
||||
|
||||
<p style="margin-bottom: 20px; color: #666;">
|
||||
Klicken Sie auf "Installation abschließen", um die Einrichtung zu beenden.
|
||||
Die Datenbank und alle notwendigen Dateien werden erstellt.
|
||||
</p>
|
||||
|
||||
<button type="submit" class="btn btn-primary btn-lg btn-block">Installation abschließen</button>
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -455,6 +455,15 @@ return [
|
|||
'audit_action_template_updated' => 'Profil geändert',
|
||||
'audit_action_template_deleted' => 'Profil gelöscht',
|
||||
'audit_page_info' => 'Seite {page} von {pages} ({total} Einträge)',
|
||||
'a11y_skip' => 'Zum Inhalt springen',
|
||||
'a11y_theme' => 'Darstellung umschalten (hell/dunkel)',
|
||||
'a11y_menu' => 'Navigation öffnen',
|
||||
'a11y_language' => 'Sprache',
|
||||
'a11y_notifications' => 'Benachrichtigungen',
|
||||
'audit_system_anon' => 'System/Anonym',
|
||||
'vouchers_resend' => 'Per E-Mail senden',
|
||||
'label_minutes_short' => 'Min.',
|
||||
'js_copy' => 'Kopieren',
|
||||
'settings_tab_general' => 'Allgemein',
|
||||
'settings_tab_defaults' => 'Voucher-Standards',
|
||||
'settings_tab_cron' => 'Cron-Sync',
|
||||
|
|
|
|||
|
|
@ -455,6 +455,15 @@ return [
|
|||
'audit_action_template_updated' => 'Profile updated',
|
||||
'audit_action_template_deleted' => 'Profile deleted',
|
||||
'audit_page_info' => 'Page {page} of {pages} ({total} entries)',
|
||||
'a11y_skip' => 'Skip to content',
|
||||
'a11y_theme' => 'Toggle appearance (light/dark)',
|
||||
'a11y_menu' => 'Open navigation',
|
||||
'a11y_language' => 'Language',
|
||||
'a11y_notifications' => 'Notifications',
|
||||
'audit_system_anon' => 'System/anonymous',
|
||||
'vouchers_resend' => 'Send by email',
|
||||
'label_minutes_short' => 'min',
|
||||
'js_copy' => 'Copy',
|
||||
'settings_tab_general' => 'General',
|
||||
'settings_tab_defaults' => 'Voucher Defaults',
|
||||
'settings_tab_cron' => 'Cron Sync',
|
||||
|
|
|
|||
16
login.php
16
login.php
|
|
@ -169,7 +169,7 @@ try {
|
|||
<?php if ($loginLogo): ?>
|
||||
<img src="<?= htmlspecialchars(Ui::mediaUrl($loginLogo)) ?>" alt="<?= htmlspecialchars($loginBrand) ?>" class="auth-brand-logo">
|
||||
<?php else: ?>
|
||||
<span class="brand-mark"><i class="fas fa-wifi"></i></span>
|
||||
<span class="brand-mark"><i class="fas fa-wifi" aria-hidden="true"></i></span>
|
||||
<span><?= htmlspecialchars($loginBrand) ?></span>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
|
@ -179,7 +179,7 @@ try {
|
|||
<?php if (!empty($loginFeatures)): ?>
|
||||
<ul class="auth-features">
|
||||
<?php foreach ($loginFeatures as $feature): ?>
|
||||
<li><span class="tick"><i class="fas fa-check"></i></span> <?= htmlspecialchars($feature) ?></li>
|
||||
<li><span class="tick"><i class="fas fa-check" aria-hidden="true"></i></span> <?= htmlspecialchars($feature) ?></li>
|
||||
<?php endforeach; ?>
|
||||
</ul>
|
||||
<?php endif; ?>
|
||||
|
|
@ -190,14 +190,14 @@ try {
|
|||
|
||||
<section class="auth-panel">
|
||||
<div class="auth-tools">
|
||||
<div class="lang-switcher">
|
||||
<div class="lang-switcher" role="group" aria-label="<?= __('a11y_language') ?>">
|
||||
<?php foreach (I18n::getAvailable() as $code => $label): ?>
|
||||
<button class="lang-btn <?= I18n::getLanguage() === $code ? 'active' : '' ?>"
|
||||
onclick="switchLanguage('<?= $code ?>')"><?= strtoupper($code) ?></button>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<button id="darkModeBtn" class="dark-mode-toggle" onclick="toggleDarkMode()" title="Dark Mode">
|
||||
<i class="fas fa-moon"></i>
|
||||
<button id="darkModeBtn" class="dark-mode-toggle" onclick="toggleDarkMode()" aria-label="<?= __('a11y_theme') ?>" title="<?= __('a11y_theme') ?>">
|
||||
<i class="fas fa-moon" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
|
|
@ -207,7 +207,7 @@ try {
|
|||
<img src="<?= htmlspecialchars(Ui::mediaUrl($loginLogo)) ?>" alt="<?= htmlspecialchars($loginBrand) ?>" class="logo">
|
||||
<?php else: ?>
|
||||
<div class="auth-mark">
|
||||
<span class="brand-mark"><i class="fas fa-wifi"></i></span>
|
||||
<span class="brand-mark"><i class="fas fa-wifi" aria-hidden="true"></i></span>
|
||||
<span><?= htmlspecialchars($loginBrand) ?></span>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
|
@ -281,12 +281,12 @@ try {
|
|||
<?php if (!$show2fa && $oidcEnabled): ?>
|
||||
<div class="divider"><span><?= __('or') ?></span></div>
|
||||
<a href="<?= htmlspecialchars($oidcLoginUrl) ?>" class="btn btn-secondary btn-lg">
|
||||
<i class="fas fa-key"></i> <?= htmlspecialchars($oidcName) ?>
|
||||
<i class="fas fa-key" aria-hidden="true"></i> <?= htmlspecialchars($oidcName) ?>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($publicAccess): ?>
|
||||
<div class="auth-links"><a href="index.php" class="back-link"><i class="fas fa-arrow-left"></i> <?= __('login_back') ?></a></div>
|
||||
<div class="auth-links"><a href="index.php" class="back-link"><i class="fas fa-arrow-left" aria-hidden="true"></i> <?= __('login_back') ?></a></div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</section>
|
||||
|
|
|
|||
|
|
@ -166,12 +166,12 @@ try {
|
|||
<?php if ($m365Enabled): ?>
|
||||
<div class="divider"><span>oder</span></div>
|
||||
<a href="<?= htmlspecialchars($m365LoginUrl) ?>" class="btn btn-microsoft">
|
||||
<i class="fab fa-microsoft"></i> Mit Microsoft 365 anmelden
|
||||
<i class="fab fa-microsoft" aria-hidden="true"></i> Mit Microsoft 365 anmelden
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php if ($publicAccess): ?>
|
||||
<div class="auth-links"><a href="index.php" class="back-link"><i class="fas fa-arrow-left"></i> Zurück zur Code-Erstellung</a></div>
|
||||
<div class="auth-links"><a href="index.php" class="back-link"><i class="fas fa-arrow-left" aria-hidden="true"></i> Zurück zur Code-Erstellung</a></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<!-- Debug Info (kann nach erfolgreicher Einrichtung entfernt werden) -->
|
||||
|
|
|
|||
|
|
@ -83,7 +83,7 @@ if ($valid && $_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||
<?php if ($logoUrl): ?>
|
||||
<img src="<?= htmlspecialchars(Ui::mediaUrl($logoUrl)) ?>" alt="Logo" class="logo">
|
||||
<?php else: ?>
|
||||
<div class="focus-icon" style="margin-bottom:14px;"><i class="fas fa-key"></i></div>
|
||||
<div class="focus-icon" style="margin-bottom:14px;"><i class="fas fa-key" aria-hidden="true"></i></div>
|
||||
<?php endif; ?>
|
||||
|
||||
<h1><?= __('reset_new_pw') ?></h1>
|
||||
|
|
|
|||
|
|
@ -61,13 +61,13 @@ $channels = \Updater\UpdateManager::CHANNELS;
|
|||
<div class="wrap">
|
||||
<div class="update-head">
|
||||
<div class="title">
|
||||
<span class="focus-icon"><i class="fas fa-rotate"></i></span>
|
||||
<span class="focus-icon"><i class="fas fa-rotate" aria-hidden="true"></i></span>
|
||||
<div>
|
||||
<h1 style="font-size:20px;">System-Update</h1>
|
||||
<p class="sub">Aktualisierung aus dem gewählten Release-Channel</p>
|
||||
</div>
|
||||
</div>
|
||||
<a href="/admin/" class="btn btn-secondary"><i class="fas fa-arrow-left"></i> Administration</a>
|
||||
<a href="/admin/" class="btn btn-secondary"><i class="fas fa-arrow-left" aria-hidden="true"></i> Administration</a>
|
||||
</div>
|
||||
|
||||
<div class="tabs">
|
||||
|
|
@ -93,8 +93,8 @@ $channels = \Updater\UpdateManager::CHANNELS;
|
|||
|
||||
<div class="card">
|
||||
<h2>Updates</h2>
|
||||
<button class="btn btn-primary" id="btnCheck"><i class="fas fa-magnifying-glass"></i> Auf Updates prüfen</button>
|
||||
<button class="btn btn-success" id="btnInstall" style="display:none;margin-left:8px;"><i class="fas fa-download"></i> Update installieren</button>
|
||||
<button class="btn btn-primary" id="btnCheck"><i class="fas fa-magnifying-glass" aria-hidden="true"></i> Auf Updates prüfen</button>
|
||||
<button class="btn btn-success" id="btnInstall" style="display:none;margin-left:8px;"><i class="fas fa-download" aria-hidden="true"></i> Update installieren</button>
|
||||
|
||||
<div class="changelog" id="changelog"></div>
|
||||
|
||||
|
|
@ -150,7 +150,7 @@ $('channel').addEventListener('change', async (e) => {
|
|||
// Auf Updates pruefen
|
||||
$('btnCheck').addEventListener('click', async () => {
|
||||
clearAlerts();
|
||||
$('btnCheck').disabled = true; $('btnCheck').innerHTML = '<i class="fas fa-circle-notch fa-spin"></i> Prüfe …';
|
||||
$('btnCheck').disabled = true; $('btnCheck').innerHTML = '<i class="fas fa-circle-notch fa-spin" aria-hidden="true"></i> Prüfe …';
|
||||
try {
|
||||
const r = await fetch('update.php?action=check');
|
||||
const d = await r.json();
|
||||
|
|
@ -172,7 +172,7 @@ $('btnCheck').addEventListener('click', async () => {
|
|||
} catch (e) {
|
||||
showAlert('error', 'Prüfung fehlgeschlagen: ' + e.message);
|
||||
} finally {
|
||||
$('btnCheck').disabled = false; $('btnCheck').innerHTML = '<i class="fas fa-magnifying-glass"></i> Auf Updates prüfen';
|
||||
$('btnCheck').disabled = false; $('btnCheck').innerHTML = '<i class="fas fa-magnifying-glass" aria-hidden="true"></i> Auf Updates prüfen';
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue