Für Empfang, Lobby oder Tagungsraum lässt sich je Site eine öffentliche Seite anlegen (kiosk.php), die auf einem Bildschirm oder Tablet läuft: ein großer Knopf, ein Klick, ein Zugangscode mit QR-Code. Nach einer einstellbaren Anzeigedauer springt der Bildschirm zurück, damit der nächste Gast nicht den Code seines Vorgängers sieht. Verwaltung unter Administration → Display-Seiten: - Site und optionales Voucher-Profil (bestimmt Laufzeit, Geräte, QoS) - eigene Überschrift und Text für den Bildschirm - Codes pro Tag, Wartezeit zwischen zwei Codes, Anzeigedauer - geheimer Link zum Kopieren, als QR-Code anzeigbar und jederzeit erneuerbar (der alte Link gilt dann sofort nicht mehr) Absicherung: der Link ist der Zugang, deshalb Tageslimit und Wartezeit je Display, CSRF-Token am Formular, `noindex` im Kopf und ein Eintrag im Audit-Log für jeden ausgegebenen Code. Webhooks werden für Kiosk-Codes bewusst nicht ausgelöst – ein Empfangsdisplay würde den Kanal fluten. Technik: - neue Tabelle `kiosks`, `vouchers.kiosk_id` hält die Herkunft fest (Migration 0005, database.sql nachgezogen) - includes/Kiosk.php kapselt Token, Limits und Profil-Auflösung - includes/VoucherService.php bündelt die Voucher-Erstellung, die vorher in index.php lag und für den Kiosk ein zweites Mal nötig gewesen wäre - Startbildschirm zeigt zusätzlich einen QR auf sich selbst, damit Gäste die Seite am eigenen Handy öffnen können Tests: 9 neue Fälle für Token-Prüfung, Wartezeit, Tageslimit und Profil-Auflösung (38 Tests gesamt), PHPStan deckt Kiosk.php mit ab. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
125 lines
5.7 KiB
PHP
125 lines
5.7 KiB
PHP
<?php
|
||
/**
|
||
* Shared admin navigation/header snippet.
|
||
* Expects $currentPage (string), $appTitle (string), $auth, $db to be set before include.
|
||
* Expects I18n to be initialized.
|
||
*/
|
||
require_once __DIR__ . '/Ui.php';
|
||
|
||
$currentPage = $currentPage ?? '';
|
||
$currentUser = isset($auth) ? $auth->getCurrentUser() : null;
|
||
$lang = I18n::getLanguage();
|
||
$base = $adminBase ?? ''; // Prefix bis zum admin/-Ordner
|
||
$rootBase = $base === '' ? '../' : ''; // Prefix bis zum Projekt-Root
|
||
|
||
/** Navigation als Datenstruktur – Reihenfolge = Darstellung. */
|
||
$navGroups = [
|
||
'nav_group_overview' => [
|
||
['dashboard', 'index.php', 'fa-chart-pie', 'nav_dashboard'],
|
||
['reports', 'reports.php', 'fa-chart-line', 'nav_reports'],
|
||
['audit_log', 'audit_log.php', 'fa-clock-rotate-left','nav_audit_log'],
|
||
],
|
||
'nav_group_manage' => [
|
||
['vouchers', 'vouchers.php', 'fa-ticket', 'nav_vouchers'],
|
||
['templates', 'templates.php', 'fa-layer-group', 'nav_templates'],
|
||
['import', 'import.php', 'fa-file-arrow-up', 'nav_import'],
|
||
['kiosks', 'kiosks.php', 'fa-display', 'nav_kiosks'],
|
||
['sites', 'sites.php', 'fa-location-dot', 'nav_sites'],
|
||
['users', 'users.php', 'fa-users', 'nav_users'],
|
||
],
|
||
'nav_group_system' => [
|
||
['settings', 'settings.php', 'fa-sliders', 'nav_settings'],
|
||
['integrations', 'integrations.php', 'fa-plug', 'nav_integrations'],
|
||
['api_keys', 'api_keys.php', 'fa-key', 'nav_api_keys'],
|
||
['security', 'security.php', 'fa-shield-halved','nav_security'],
|
||
['backup', 'backup.php', 'fa-database', 'nav_backup'],
|
||
['update', 'update.php', 'fa-rotate', 'nav_update'],
|
||
],
|
||
];
|
||
|
||
/** Aktuellen Seitentitel für die Breadcrumb finden. */
|
||
$currentLabel = __('nav_dashboard');
|
||
foreach ($navGroups as $items) {
|
||
foreach ($items as $item) {
|
||
if ($item[0] === $currentPage) { $currentLabel = __($item[3]); }
|
||
}
|
||
}
|
||
?>
|
||
<?= Ui::head($db ?? null, $rootBase) ?>
|
||
</head>
|
||
<body>
|
||
|
||
<a class="skip-link" href="#main-content"><?= __('a11y_skip') ?></a>
|
||
<div class="sidebar-overlay" onclick="closeMobileSidebar()"></div>
|
||
|
||
<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" 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>
|
||
</span>
|
||
</a>
|
||
|
||
<nav>
|
||
<?php foreach ($navGroups as $groupKey => $items): ?>
|
||
<div class="sidebar-section"><?= __($groupKey) ?></div>
|
||
<ul class="sidebar-nav">
|
||
<?php foreach ($items as [$key, $href, $icon, $labelKey]): ?>
|
||
<li>
|
||
<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; ?>
|
||
</ul>
|
||
<?php endforeach; ?>
|
||
</nav>
|
||
|
||
<div class="sidebar-foot">
|
||
<?php if ($currentUser): ?>
|
||
<div class="user-menu" style="width:100%;justify-content:flex-start;">
|
||
<div class="user-avatar"><?= strtoupper(mb_substr($currentUser['name'], 0, 1)) ?></div>
|
||
<div style="min-width:0;flex:1;">
|
||
<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') ?>" 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>
|
||
<?php endif; ?>
|
||
<?= Ui::credit(true) ?>
|
||
</div>
|
||
</aside>
|
||
|
||
<header class="topbar">
|
||
<div class="header-left">
|
||
<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>
|
||
<span class="sep">/</span>
|
||
<span class="current"><?= htmlspecialchars($currentLabel) ?></span>
|
||
</div>
|
||
</div>
|
||
<div class="header-right">
|
||
<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()" 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" aria-hidden="true"></i>
|
||
<span class="hide-mobile"><?= __('nav_back') ?></span>
|
||
</a>
|
||
</div>
|
||
</header>
|
||
|
||
<main class="main-content" id="main-content">
|