Redesign: gemeinsames Design-System für Frontend und Backend #1

Merged
friloo merged 2 commits from redesign/design-system into main 2026-09-23 06:16:42 +00:00
45 changed files with 2461 additions and 1744 deletions

View file

@ -8,7 +8,7 @@
![MySQL](https://img.shields.io/badge/MySQL-5.7%2B%20%2F%20MariaDB-4479A1?logo=mysql&logoColor=white) ![MySQL](https://img.shields.io/badge/MySQL-5.7%2B%20%2F%20MariaDB-4479A1?logo=mysql&logoColor=white)
![UniFi OS](https://img.shields.io/badge/UniFi%20OS-7.0%2B-0559C9?logo=ubiquiti&logoColor=white) ![UniFi OS](https://img.shields.io/badge/UniFi%20OS-7.0%2B-0559C9?logo=ubiquiti&logoColor=white)
![License](https://img.shields.io/badge/Lizenz-MIT-green) ![License](https://img.shields.io/badge/Lizenz-MIT-green)
![Version](https://img.shields.io/badge/Version-2.4.0-blueviolet) ![Version](https://img.shields.io/badge/Version-2.5.0-blueviolet)
![CI](https://github.com/friloo/unifi-voucher-tool/actions/workflows/ci.yml/badge.svg) ![CI](https://github.com/friloo/unifi-voucher-tool/actions/workflows/ci.yml/badge.svg)
</div> </div>
@ -45,6 +45,8 @@
- 💾 **Config-Backup & -Restore** (JSON Export/Import) - 💾 **Config-Backup & -Restore** (JSON Export/Import)
- 🐳 **Docker** Dockerfile + docker-compose (MariaDB) - 🐳 **Docker** Dockerfile + docker-compose (MariaDB)
- 🌍 **Öffentlicher Modus** optional ohne Login nutzbar (mit CSRF-Schutz & Throttle) - 🌍 **Öffentlicher Modus** optional ohne Login nutzbar (mit CSRF-Schutz & Throttle)
- 🎨 **Einheitliches Design-System** ein Stylesheet für Frontend, Login und Backend (Tokens, Komponenten, Light/Dark)
- 🏷️ **Login-Seite individualisierbar** Firmenname, Logo, Texte, Hintergrundbild bzw. Farbverlauf
- 🌗 **Dark Mode** umschaltbar, Einstellung wird im Browser gespeichert - 🌗 **Dark Mode** umschaltbar, Einstellung wird im Browser gespeichert
- 🌐 **Mehrsprachig** Deutsch / Englisch per Umschalter (`lang/`) - 🌐 **Mehrsprachig** Deutsch / Englisch per Umschalter (`lang/`)
- 📱 **Responsive Admin-Layout** mit Hamburger-Menü & Sidebar-Overlay - 📱 **Responsive Admin-Layout** mit Hamburger-Menü & Sidebar-Overlay
@ -59,31 +61,35 @@
## 📸 Screenshots ## 📸 Screenshots
> Alle Screenshots stammen aus der Oberfläche in Version 2.5.0 (neues Design-System).
### Anmeldung & Voucher-Erstellung ### Anmeldung & Voucher-Erstellung
<div align="center"> <div align="center">
<img src="docs/screenshots/login.png" alt="Login mit Microsoft 365" width="32%"> <img src="docs/screenshots/login.png" alt="Anmeldung" width="48%">
<img src="docs/screenshots/voucher-form.png" alt="Voucher erstellen" width="32%"> <img src="docs/screenshots/login-branding.png" alt="Anmeldung mit eigenem Branding" width="48%">
<img src="docs/screenshots/voucher-result.png" alt="Voucher-Ergebnis" width="32%">
</div> </div>
### Bulk-Erstellung & Dark Mode
<div align="center"> <div align="center">
<img src="docs/screenshots/bulk-vouchers.png" alt="Bulk-Voucher-Erstellung" width="48%"> <img src="docs/screenshots/voucher-form.png" alt="Voucher erstellen" width="48%">
<img src="docs/screenshots/admin-dashboard-dark.png" alt="Dashboard im Dark Mode" width="48%"> <img src="docs/screenshots/settings-login.png" alt="Einstellungen der Login-Seite" width="48%">
</div> </div>
### Administration & Updater <div align="center">
<img src="docs/screenshots/voucher-result.png" alt="Voucher-Ergebnis mit QR-Code" width="48%">
<img src="docs/screenshots/bulk-vouchers.png" alt="Bulk-Voucher-Erstellung" width="48%">
</div>
### Administration
<div align="center"> <div align="center">
<img src="docs/screenshots/admin-dashboard.png" alt="Dashboard" width="48%"> <img src="docs/screenshots/admin-dashboard.png" alt="Dashboard" width="48%">
<img src="docs/screenshots/updater-available.png" alt="Auto-Updater" width="48%"> <img src="docs/screenshots/admin-dashboard-dark.png" alt="Dashboard im Dark Mode" width="48%">
</div> </div>
<div align="center"> <div align="center">
<img src="docs/screenshots/updater.png" alt="Updater Ausgangszustand" width="48%"> <img src="docs/screenshots/vouchers.png" alt="Live-Voucher-Verwaltung" width="48%">
<img src="docs/screenshots/maintenance.png" alt="Wartungsmodus" width="48%"> <img src="docs/screenshots/settings.png" alt="Einstellungen" width="48%">
</div> </div>
### REST-API, 2FA & Integrationen ### REST-API, 2FA & Integrationen
@ -97,6 +103,17 @@
<img src="docs/screenshots/two-factor.png" alt="Zwei-Faktor-Authentifizierung" width="60%"> <img src="docs/screenshots/two-factor.png" alt="Zwei-Faktor-Authentifizierung" width="60%">
</div> </div>
### Updater & Wartungsmodus
<div align="center">
<img src="docs/screenshots/updater.png" alt="Updater Ausgangszustand" width="48%">
<img src="docs/screenshots/updater-available.png" alt="Auto-Updater mit verfügbarem Update" width="48%">
</div>
<div align="center">
<img src="docs/screenshots/maintenance.png" alt="Wartungsmodus" width="60%">
</div>
--- ---
## 📋 Anforderungen ## 📋 Anforderungen
@ -220,6 +237,57 @@ Den Token finden Sie unter **Administration → Einstellungen → Cron**.
--- ---
## 🎨 Design-System
Frontend, Login-Seiten, Installer, Updater und der gesamte Admin-Bereich nutzen ein
gemeinsames Stylesheet: **`assets/global.css`**.
- **Design-Tokens** (`:root` bzw. `[data-theme="dark"]`) für Flächen, Text, Linien,
Markenfarbe, Statusfarben, Radien, Schatten und Layout-Maße
- **Komponenten** darauf aufgebaut: Buttons, Formularfelder, Cards, Tabellen, Badges,
Alerts, Tabs, Pagination, Modals, Toasts, Statistik-Kacheln, Sidebar/Topbar
- **Dark Mode** ausschließlich über Tokens keine `!important`-Overrides mehr
- **Schriftart** Inter (via Google Fonts) mit System-Font-Fallback
Eigenes Branding lässt sich meist mit wenigen Zeilen umsetzen z. B. in einer
eigenen CSS-Datei oder direkt in `assets/global.css`:
```css
:root {
--accent: #0f766e; /* Primärfarbe (Buttons, aktive Navigation) */
--accent-hover: #0d5f59;
--accent-soft: #e6f4f2; /* Flächen für aktive Zustände */
--brand-gradient: linear-gradient(135deg, #0f766e 0%, #0ea5e9 100%);
--r-lg: 14px; /* Eckenradius für Cards */
}
```
Logo und Favicon werden nicht über CSS, sondern unter
**Administration → Einstellungen → Allgemein** gesetzt.
### Login-Seite individualisieren
Unter **Administration → Einstellungen → Login-Seite** lässt sich die Anmeldeseite
ohne Code-Änderung an das eigene Haus anpassen. Leere Felder verwenden jeweils den
Standardwert eine frische Installation sieht also unverändert aus.
| Einstellung | Wirkung |
|---|---|
| Linke Bildspalte anzeigen | Split-Screen an/aus. Aus = zentrierte Anmeldekarte |
| Firmenname | Name neben dem Logo bzw. in der Fußzeile (leer = Anwendungstitel) |
| Logo (URL) | Eigenes Logo in der Bildspalte (leer = allgemeines Logo) |
| Überschrift / Beschreibungstext | Claim in der Bildspalte |
| Stichpunkte | Liste mit Haken ein Stichpunkt pro Zeile, leer = keine Liste |
| Fußzeile | z. B. `© 2026 Muster GmbH · Datenschutz · Impressum` |
| Hintergrundbild (URL) | Formatfüllendes Bild der Bildspalte |
| Verlauf Start-/Endfarbe | Farbverlauf, wenn kein Bild gesetzt ist |
| Abdunklung (%) | Dunkle Ebene über dem Bild, damit der Text lesbar bleibt |
Über **„Vorschau öffnen"** lässt sich die Login-Seite als angemeldeter Administrator
ansehen (`login.php?preview=1`), ohne sich abzumelden.
---
## 🛡️ Sicherheit ## 🛡️ Sicherheit
Das Tool ist auf einen sicheren Standardbetrieb ausgelegt: Das Tool ist auf einen sicheren Standardbetrieb ausgelegt:
@ -378,11 +446,12 @@ setzen (`DB_*`, `APP_KEY`).
- [x] Docker-Container - [x] Docker-Container
- [x] Erweiterte Reporting-Funktionen (CSV/PDF) + Health-Endpoint - [x] Erweiterte Reporting-Funktionen (CSV/PDF) + Health-Endpoint
- [x] 2FA-Recovery-Codes, API-Scopes/Rate-Limit/OpenAPI, Test-Suite (PHPUnit/PHPStan) - [x] 2FA-Recovery-Codes, API-Scopes/Rate-Limit/OpenAPI, Test-Suite (PHPUnit/PHPStan)
- [x] Gemeinsames Design-System für Frontend, Login und Backend
--- ---
<div align="center"> <div align="center">
**Version 2.4.0** · Autor: **Friederich Loheide** · Lizenz: **MIT** **Version 2.5.0** · Autor: **Friederich Loheide** · Lizenz: **MIT**
</div> </div>

View file

@ -68,28 +68,12 @@ $adminBase = '';
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>API-Schlüssel <?= htmlspecialchars($appTitle) ?></title> <title>API-Schlüssel <?= htmlspecialchars($appTitle) ?></title>
<?php require __DIR__ . '/../includes/admin_nav.php'; ?> <?php require __DIR__ . '/../includes/admin_nav.php'; ?>
<style> <div class="page-header">
.card { background: var(--bg-card); border:1px solid var(--border-color); border-radius:14px; padding:24px; margin-bottom:22px; box-shadow:0 4px 14px var(--shadow); } <div>
.card h2 { font-size:16px; margin-bottom:16px; color:var(--text-primary); } <h1 class="page-title">API-Schlüssel</h1>
table { width:100%; border-collapse:collapse; } <p class="page-subtitle">Zugänge für externe Systeme mit Scope und Rate-Limit.</p>
th,td { text-align:left; padding:11px 8px; font-size:14px; border-bottom:1px solid var(--border-color); color:var(--text-primary); } </div>
th { color:var(--text-muted); font-weight:600; } </div>
code { font-family:monospace; background:var(--bg-hover); padding:2px 6px; border-radius:5px; }
.btn { padding:10px 16px; border:none; border-radius:8px; font-weight:600; cursor:pointer; font-size:14px; text-decoration:none; display:inline-block; }
.btn-primary { background:var(--accent,#667eea); color:#fff; }
.input { width:100%; padding:11px; border:2px solid var(--border-color); border-radius:8px; background:var(--bg-input,#fff); color:var(--text-primary); font-size:14px; }
.alert { padding:12px 14px; border-radius:9px; font-size:14px; margin-bottom:18px; }
.alert-error { background:#fee; border:1px solid #fcc; color:#c33; }
.alert-ok { background:#efe; border:1px solid #cfc; color:#2a7; }
.keybox { background:#0f1117; color:#7CFFB2; font-family:monospace; padding:14px; border-radius:8px; word-break:break-all; font-size:15px; margin-top:10px; }
.badge { padding:3px 9px; border-radius:6px; font-size:12px; font-weight:600; }
.b-on { background:#e3f6ea; color:#2a7; } .b-off { background:#fdeaea; color:#c33; }
.a-link { color:var(--accent,#667eea); text-decoration:none; margin-right:10px; font-size:13px; }
.muted { color:var(--text-muted); font-size:13px; }
</style>
</head>
<body>
<h1 style="font-size:24px;margin-bottom:20px;color:var(--text-primary);">🔑 API-Schlüssel</h1>
<?php if ($error): ?><div class="alert alert-error"><?= htmlspecialchars($error) ?></div><?php endif; ?> <?php if ($error): ?><div class="alert alert-error"><?= htmlspecialchars($error) ?></div><?php endif; ?>
<?php if ($success): ?><div class="alert alert-ok"><?= htmlspecialchars($success) ?></div><?php endif; ?> <?php if ($success): ?><div class="alert alert-ok"><?= htmlspecialchars($success) ?></div><?php endif; ?>
@ -130,6 +114,7 @@ code { font-family:monospace; background:var(--bg-hover); padding:2px 6px; borde
<?php if (empty($keys)): ?> <?php if (empty($keys)): ?>
<p class="muted">Noch keine API-Schlüssel angelegt.</p> <p class="muted">Noch keine API-Schlüssel angelegt.</p>
<?php else: ?> <?php else: ?>
<div class="table-container">
<table> <table>
<tr><th>Name</th><th>Präfix</th><th>Scope</th><th>Limit</th><th>Status</th><th>Zuletzt genutzt</th><th>Erstellt von</th><th></th></tr> <tr><th>Name</th><th>Präfix</th><th>Scope</th><th>Limit</th><th>Status</th><th>Zuletzt genutzt</th><th>Erstellt von</th><th></th></tr>
<?php foreach ($keys as $k): ?> <?php foreach ($keys as $k): ?>
@ -139,15 +124,16 @@ code { font-family:monospace; background:var(--bg-hover); padding:2px 6px; borde
<td><?= ($k['scope'] ?? 'write') === 'read' ? 'nur Lesen' : 'Lesen+Erstellen' ?></td> <td><?= ($k['scope'] ?? 'write') === 'read' ? 'nur Lesen' : 'Lesen+Erstellen' ?></td>
<td><?= (int)($k['rate_limit'] ?? 0) === 0 ? '∞' : (int)$k['rate_limit'] . '/min' ?></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'] ? 'aktiv' : 'gesperrt' ?></span></td> <td><span class="badge <?= $k['is_active'] ? 'b-on' : 'b-off' ?>"><?= $k['is_active'] ? 'aktiv' : 'gesperrt' ?></span></td>
<td class="muted"><?= $k['last_used_at'] ? htmlspecialchars($k['last_used_at']) : '' ?></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 class="muted"><?= htmlspecialchars($k['creator'] ?? '') ?></td>
<td style="text-align:right;white-space:nowrap;"> <td style="text-align:right;white-space:nowrap;">
<a class="a-link" href="?toggle=<?= (int)$k['id'] ?>&token=<?= urlencode($csrf) ?>"><?= $k['is_active'] ? 'Sperren' : 'Aktivieren' ?></a> <a class="a-link" href="?toggle=<?= (int)$k['id'] ?>&token=<?= urlencode($csrf) ?>"><?= $k['is_active'] ? 'Sperren' : 'Aktivieren' ?></a>
<a class="a-link" style="color:#e25555;" href="?delete=<?= (int)$k['id'] ?>&token=<?= urlencode($csrf) ?>" onclick="return confirm('Schlüssel löschen?');">Löschen</a> <a class="a-link" style="color:var(--danger);" href="?delete=<?= (int)$k['id'] ?>&token=<?= urlencode($csrf) ?>" onclick="return confirm('Schlüssel löschen?');">Löschen</a>
</td> </td>
</tr> </tr>
<?php endforeach; ?> <?php endforeach; ?>
</table> </table>
</div>
<?php endif; ?> <?php endif; ?>
</div> </div>
@ -165,7 +151,7 @@ curl https://IHRE-DOMAIN/api/sites.php -H "X-API-Key: uvt_…"</pre>
<p class="muted" style="margin-top:12px;">OpenAPI-Spezifikation (Import in Postman/Swagger): <a href="../api/openapi.php" target="_blank">/api/openapi.php</a></p> <p class="muted" style="margin-top:12px;">OpenAPI-Spezifikation (Import in Postman/Swagger): <a href="../api/openapi.php" target="_blank">/api/openapi.php</a></p>
</div> </div>
</div><!-- /main-content --> </main>
<script src="../assets/global.js"></script> <script src="../assets/global.js"></script>
</body> </body>
</html> </html>

View file

@ -46,21 +46,21 @@ $currentPage = 'audit_log';
$adminBase = ''; $adminBase = '';
$actionLabels = [ $actionLabels = [
'voucher_created' => '🎫 Voucher erstellt', 'voucher_created' => 'Voucher erstellt',
'voucher_bulk' => '🎫 Bulk Voucher', 'voucher_bulk' => 'Bulk Voucher',
'user_login' => '🔐 Login', 'user_login' => 'Login',
'user_logout' => '🚪 Logout', 'user_logout' => 'Logout',
'user_created' => '👤 Benutzer erstellt', 'user_created' => 'Benutzer erstellt',
'user_updated' => '👤 Benutzer geändert', 'user_updated' => 'Benutzer geändert',
'user_deleted' => '👤 Benutzer gelöscht', 'user_deleted' => 'Benutzer gelöscht',
'site_added' => '🌐 Site hinzugefügt', 'site_added' => 'Site hinzugefügt',
'site_updated' => '🌐 Site geändert', 'site_updated' => 'Site geändert',
'site_deleted' => '🌐 Site gelöscht', 'site_deleted' => 'Site gelöscht',
'settings_saved' => '⚙️ Einstellungen gespeichert', 'settings_saved' => 'Einstellungen gespeichert',
'password_reset' => '🔑 Passwort-Reset', 'password_reset' => 'Passwort-Reset',
'template_created' => '📋 Profil erstellt', 'template_created' => 'Profil erstellt',
'template_updated' => '📋 Profil geändert', 'template_updated' => 'Profil geändert',
'template_deleted' => '📋 Profil gelöscht', 'template_deleted' => 'Profil gelöscht',
]; ];
?> ?>
<!DOCTYPE html> <!DOCTYPE html>
@ -70,42 +70,12 @@ $actionLabels = [
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?= __('audit_title') ?> - <?= htmlspecialchars($appTitle) ?></title> <title><?= __('audit_title') ?> - <?= htmlspecialchars($appTitle) ?></title>
<?php include __DIR__ . '/../includes/admin_nav.php'; ?> <?php include __DIR__ . '/../includes/admin_nav.php'; ?>
<style>
.page-header { margin-bottom: 25px; }
.page-title { font-size: 28px; font-weight: 600; color: var(--text-primary); margin-bottom: 6px; }
.card { background: var(--bg-card); border-radius: 15px; box-shadow: 0 2px 10px var(--shadow); border: 1px solid var(--border-color); overflow: hidden; margin-bottom: 20px; }
.card-header { padding: 18px 25px; border-bottom: 1px solid var(--border-color); display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 10px; }
.card-title { font-size: 16px; font-weight: 600; color: var(--text-primary); }
.table { width: 100%; border-collapse: collapse; }
.table th { text-align: left; padding: 11px 15px; background: var(--bg-table-head); color: var(--text-secondary); font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; }
.table td { padding: 12px 15px; border-bottom: 1px solid var(--border-color); font-size: 13px; color: var(--text-primary); }
.table tr:last-child td { border-bottom: none; }
.table tr:hover td { background: var(--bg-hover); }
.badge { display: inline-block; padding: 3px 9px; border-radius: 5px; font-size: 11px; font-weight: 500; }
.filter-bar { display: flex; gap: 12px; flex-wrap: wrap; align-items: flex-end; }
.filter-bar select { padding: 9px 12px; border: 2px solid var(--border-color); border-radius: 8px; font-size: 13px; background: var(--bg-input); color: var(--text-primary); }
.filter-bar select:focus { outline: none; border-color: var(--accent); }
.btn-primary { background: var(--accent); color: white; }
.btn-primary:hover { background: var(--accent-hover); }
.btn-small { padding: 6px 12px; font-size: 12px; }
.ip-cell { font-family: monospace; font-size: 12px; color: var(--text-muted); }
.details-cell { max-width: 250px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--text-secondary); }
.pagination { display: flex; align-items: center; justify-content: space-between; padding: 14px 25px; border-top: 1px solid var(--border-color); flex-wrap: wrap; gap: 10px; }
.page-info { font-size: 13px; color: var(--text-muted); }
.page-btns { display: flex; gap: 5px; flex-wrap: wrap; }
.page-btn { padding: 6px 12px; border-radius: 6px; border: 1px solid var(--border-color); background: var(--bg-card); color: var(--text-secondary); cursor: pointer; font-size: 13px; text-decoration: none; transition: all 0.2s; }
.page-btn:hover { border-color: var(--accent); color: var(--accent); }
.page-btn.active { background: var(--accent); color: white; border-color: var(--accent); }
.page-btn:disabled { opacity: 0.4; cursor: not-allowed; }
.empty-state { text-align: center; padding: 60px 20px; color: var(--text-muted); }
.empty-state i { font-size: 42px; margin-bottom: 15px; display: block; opacity: 0.3; }
.action-chip { display: inline-flex; align-items: center; gap: 5px; padding: 3px 10px; border-radius: 20px; font-size: 12px; font-weight: 500; background: var(--bg-hover); color: var(--text-secondary); }
</style>
</head>
<div class="page-header"> <div class="page-header">
<div>
<h1 class="page-title"><?= __('audit_title') ?></h1> <h1 class="page-title"><?= __('audit_title') ?></h1>
<p style="color: var(--text-muted); font-size: 14px;"><?= __('audit_subtitle') ?></p> <p class="page-subtitle"><?= __('audit_subtitle') ?></p>
</div>
</div> </div>
<!-- Filter --> <!-- Filter -->
@ -151,6 +121,7 @@ $actionLabels = [
<div class="empty-state"><i class="fas fa-history"></i><p><?= __('audit_none') ?></p></div> <div class="empty-state"><i class="fas fa-history"></i><p><?= __('audit_none') ?></p></div>
<?php else: ?> <?php else: ?>
<div style="overflow-x:auto;"> <div style="overflow-x:auto;">
<div class="table-container">
<table class="table"> <table class="table">
<thead> <thead>
<tr> <tr>
@ -198,6 +169,7 @@ $actionLabels = [
</tbody> </tbody>
</table> </table>
</div> </div>
</div>
<?php if ($pages > 1): ?> <?php if ($pages > 1): ?>
<div class="pagination"> <div class="pagination">
@ -221,7 +193,7 @@ $actionLabels = [
<?php endif; ?> <?php endif; ?>
</div> </div>
</div><!-- main-content --> </main>
<script src="../assets/global.js"></script> <script src="../assets/global.js"></script>
</body> </body>
</html> </html>

View file

@ -114,22 +114,12 @@ $adminBase = '';
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Backup & Restore <?= htmlspecialchars($appTitle) ?></title> <title>Backup & Restore <?= htmlspecialchars($appTitle) ?></title>
<?php require __DIR__ . '/../includes/admin_nav.php'; ?> <?php require __DIR__ . '/../includes/admin_nav.php'; ?>
<style> <div class="page-header">
.card { background:var(--bg-card); border:1px solid var(--border-color); border-radius:14px; padding:24px; margin-bottom:22px; box-shadow:0 4px 14px var(--shadow); max-width:680px; } <div>
.card h2 { font-size:16px; margin-bottom:12px; color:var(--text-primary); } <h1 class="page-title">Backup &amp; Restore</h1>
.muted { color:var(--text-muted); font-size:13px; margin-bottom:14px; } <p class="page-subtitle">Konfiguration und Daten sichern oder wiederherstellen.</p>
.btn { padding:11px 18px; border:none; border-radius:8px; font-weight:600; cursor:pointer; font-size:14px; text-decoration:none; display:inline-block; } </div>
.btn-primary { background:var(--accent,#667eea); color:#fff; } </div>
.btn-secondary { background:var(--bg-hover); color:var(--text-primary); border:1px solid var(--border-color); }
.alert { padding:12px 14px; border-radius:9px; font-size:14px; margin-bottom:18px; max-width:680px; }
.alert-error { background:#fee; border:1px solid #fcc; color:#c33; }
.alert-ok { background:#efe; border:1px solid #cfc; color:#2a7; }
label.chk { display:flex; align-items:center; gap:9px; margin:8px 0; color:var(--text-primary); font-size:14px; }
input[type=file] { margin:10px 0; color:var(--text-primary); }
</style>
</head>
<body>
<h1 style="font-size:24px;margin-bottom:20px;color:var(--text-primary);">💾 Backup &amp; Restore</h1>
<?php if ($error): ?><div class="alert alert-error"><?= htmlspecialchars($error) ?></div><?php endif; ?> <?php if ($error): ?><div class="alert alert-error"><?= htmlspecialchars($error) ?></div><?php endif; ?>
<?php if ($success): ?><div class="alert alert-ok"><?= htmlspecialchars($success) ?></div><?php endif; ?> <?php if ($success): ?><div class="alert alert-ok"><?= htmlspecialchars($success) ?></div><?php endif; ?>
@ -153,7 +143,7 @@ input[type=file] { margin:10px 0; color:var(--text-primary); }
</form> </form>
</div> </div>
</div><!-- /main-content --> </main>
<script src="../assets/global.js"></script> <script src="../assets/global.js"></script>
</body> </body>
</html> </html>

View file

@ -93,22 +93,12 @@ $adminBase = '';
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>CSV-Import <?= htmlspecialchars($appTitle) ?></title> <title>CSV-Import <?= htmlspecialchars($appTitle) ?></title>
<?php require __DIR__ . '/../includes/admin_nav.php'; ?> <?php require __DIR__ . '/../includes/admin_nav.php'; ?>
<style> <div class="page-header">
.card { background:var(--bg-card); border:1px solid var(--border-color); border-radius:14px; padding:24px; margin-bottom:20px; box-shadow:0 4px 14px var(--shadow); max-width:760px; } <div>
.card h2 { font-size:15px; margin-bottom:12px; color:var(--text-primary); } <h1 class="page-title">Voucher-Import</h1>
.muted { color:var(--text-muted); font-size:13px; margin-bottom:12px; } <p class="page-subtitle">Mehrere Vouchers auf einmal aus einer CSV-Liste erstellen.</p>
label { display:block; font-size:13px; color:var(--text-secondary); margin:12px 0 6px; } </div>
.input,textarea,select { width:100%; padding:11px; border:2px solid var(--border-color); border-radius:8px; background:var(--bg-input,#fff); color:var(--text-primary); font-size:14px; font-family:inherit; } </div>
textarea { min-height:140px; font-family:monospace; }
.btn { padding:11px 18px; border:none; border-radius:8px; font-weight:600; font-size:14px; cursor:pointer; background:var(--accent,#667eea); color:#fff; }
.alert { padding:12px 14px; border-radius:9px; font-size:14px; margin-bottom:18px; max-width:760px; }
.alert-error { background:#fee; border:1px solid #fcc; color:#c33; } .alert-ok { background:#efe; border:1px solid #cfc; color:#2a7; }
table { width:100%; border-collapse:collapse; } th,td { text-align:left; padding:8px; font-size:13px; border-bottom:1px solid var(--border-color); color:var(--text-primary); }
code { font-family:monospace; background:var(--bg-hover); padding:2px 6px; border-radius:5px; }
</style>
</head>
<body>
<h1 style="font-size:24px;margin-bottom:18px;color:var(--text-primary);">📥 Voucher-Import (CSV)</h1>
<?php if ($error): ?><div class="alert alert-error"><?= htmlspecialchars($error) ?></div><?php endif; ?> <?php if ($error): ?><div class="alert alert-error"><?= htmlspecialchars($error) ?></div><?php endif; ?>
<?php if ($success): ?><div class="alert alert-ok"><?= htmlspecialchars($success) ?></div><?php endif; ?> <?php if ($success): ?><div class="alert alert-ok"><?= htmlspecialchars($success) ?></div><?php endif; ?>
@ -142,7 +132,7 @@ code { font-family:monospace; background:var(--bg-hover); padding:2px 6px; borde
</div> </div>
<?php endif; ?> <?php endif; ?>
</div><!-- /main-content --> </main>
<script src="../assets/global.js"></script> <script src="../assets/global.js"></script>
</body> </body>
</html> </html>

View file

@ -92,74 +92,6 @@ $currentPage = 'dashboard';
<title><?= __('dashboard_title') ?> <?= htmlspecialchars($appTitle) ?></title> <title><?= __('dashboard_title') ?> <?= htmlspecialchars($appTitle) ?></title>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
<?php include __DIR__ . '/../includes/admin_nav.php'; ?> <?php include __DIR__ . '/../includes/admin_nav.php'; ?>
<style>
.page-header { margin-bottom: 30px; display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 15px; }
.page-title { font-size: 26px; font-weight: 700; color: var(--text-primary); }
.page-subtitle { color: var(--text-muted); font-size: 14px; margin-top: 4px; }
.live-badge { display: inline-flex; align-items: center; gap: 8px; background: #d4edda; color: #155724; padding: 8px 16px; border-radius: 20px; font-size: 13px; font-weight: 500; }
.live-badge .dot { width: 8px; height: 8px; background: #28a745; border-radius: 50%; animation: pulse 2s infinite; }
.live-badge.loading { background: #fff3cd; color: #856404; }
.live-badge.loading .dot { background: #ffc107; }
.live-badge.error { background: #f8d7da; color: #721c24; }
.live-badge.error .dot { background: var(--danger); animation: none; }
@keyframes pulse { 0%,100%{opacity:1}50%{opacity:.5} }
.stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); gap: 20px; margin-bottom: 30px; }
.stat-card { background: var(--bg-card); padding: 22px; border-radius: 14px; box-shadow: 0 2px 10px var(--shadow); border: 1px solid var(--border-color); }
.stat-card.live { border-color: var(--success); border-width: 2px; }
.stat-card-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; }
.stat-card-title { color: var(--text-muted); font-size: 13px; font-weight: 500; }
.stat-card-icon { width: 38px; height: 38px; border-radius: 10px; display: flex; align-items: center; justify-content: center; font-size: 17px; }
.stat-card-value { font-size: 30px; font-weight: 700; color: var(--text-primary); }
.stat-card-value.valid { color: var(--success); }
.stat-card-value.used { color: var(--warning); }
.stat-card-value.expired { color: var(--danger); }
.stat-card-sub { font-size: 11px; color: var(--text-muted); margin-top: 4px; }
.card { background: var(--bg-card); border-radius: 14px; box-shadow: 0 2px 10px var(--shadow); border: 1px solid var(--border-color); margin-bottom: 28px; overflow: hidden; }
.card-header { padding: 18px 22px; border-bottom: 1px solid var(--border-color); display: flex; justify-content: space-between; align-items: center; }
.card-title { font-size: 17px; font-weight: 600; color: var(--text-primary); }
.card-body { padding: 22px; }
.table { width: 100%; border-collapse: collapse; }
.table th { text-align: left; padding: 11px 14px; background: var(--bg-table-head); color: var(--text-muted); font-weight: 600; font-size: 12px; text-transform: uppercase; letter-spacing: .5px; }
.table td { padding: 13px 14px; border-bottom: 1px solid var(--border-color); color: var(--text-primary); font-size: 14px; }
.table tr:last-child td { border-bottom: none; }
.badge { display: inline-block; padding: 3px 9px; border-radius: 5px; font-size: 11px; font-weight: 500; }
.badge-success { background: #d4edda; color: #155724; }
.badge-warning { background: #fff3cd; color: #856404; }
.badge-danger { background: #f8d7da; color: #721c24; }
.badge-info { background: var(--bg-badge-info); color: var(--text-badge-info); }
.btn-primary { background: var(--accent); color: white; }
.btn-primary:hover { background: var(--accent-hover); }
.btn-success { background: var(--success); color: white; }
.btn-success:hover { opacity: .9; }
.btn-sm { padding: 6px 12px; font-size: 12px; }
.chart-container { position: relative; height: 280px; }
.site-status { display: grid; grid-template-columns: repeat(auto-fill, minmax(270px, 1fr)); gap: 14px; }
.site-status-card { background: var(--bg-card); border: 2px solid var(--border-color); border-radius: 12px; padding: 18px; transition: border-color .2s; }
.site-status-card:hover { border-color: var(--accent); }
.site-status-card.error { border-color: var(--danger); }
.site-status-name { font-weight: 600; color: var(--text-primary); font-size: 15px; }
.site-voucher-count { background: var(--accent); color: white; padding: 3px 11px; border-radius: 20px; font-size: 13px; font-weight: 600; }
.site-voucher-count.zero { background: var(--text-muted); }
.site-status-stats { display: grid; grid-template-columns: repeat(3,1fr); gap: 8px; margin-top: 12px; }
.site-stat { text-align: center; padding: 9px; background: var(--bg-hover); border-radius: 8px; }
.site-stat-value { font-size: 18px; font-weight: 700; }
.site-stat-value.valid { color: var(--success); }
.site-stat-value.used { color: var(--warning); }
.site-stat-value.expired { color: var(--danger); }
.site-stat-label { font-size: 11px; color: var(--text-muted); margin-top: 2px; }
.site-error { color: var(--danger); font-size: 12px; margin-top: 8px; }
.top-users-list { list-style: none; }
.top-users-list li { display: flex; justify-content: space-between; align-items: center; padding: 11px 0; border-bottom: 1px solid var(--border-color); }
.top-users-list li:last-child { border-bottom: none; }
.user-info-row { display: flex; align-items: center; gap: 10px; }
.user-avatar-sm { width: 30px; height: 30px; border-radius: 50%; background: linear-gradient(135deg,#667eea,#764ba2); display: flex; align-items: center; justify-content: center; color: white; font-weight: 700; font-size: 13px; }
.user-count { background: var(--bg-hover); padding: 3px 10px; border-radius: 12px; font-weight: 600; color: var(--text-primary); font-size: 13px; }
.refresh-indicator { display: flex; align-items: center; gap: 10px; }
.last-update { color: var(--text-muted); font-size: 12px; }
.empty-state { text-align: center; padding: 50px 20px; color: var(--text-muted); }
.empty-state i { font-size: 40px; margin-bottom: 15px; opacity: .3; display: block; }
@media(max-width:768px){ .main-content{ margin-left:0!important; } .stats-grid{ grid-template-columns:1fr 1fr; } }
</style>
<div class="page-header"> <div class="page-header">
<div> <div>
@ -171,7 +103,7 @@ $currentPage = 'dashboard';
<span class="dot"></span> <span class="dot"></span>
<span id="liveStatus">DB</span> <span id="liveStatus">DB</span>
</span> </span>
<button onclick="refreshData('live')" class="btn btn-success btn-sm" id="refreshBtn"> <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"></i> <?= __('dashboard_live_refresh') ?>
</button> </button>
</div> </div>
@ -181,43 +113,43 @@ $currentPage = 'dashboard';
<div class="stat-card"> <div class="stat-card">
<div class="stat-card-header"> <div class="stat-card-header">
<div class="stat-card-title"><?= __('dashboard_active_sites') ?></div> <div class="stat-card-title"><?= __('dashboard_active_sites') ?></div>
<div class="stat-card-icon" style="background:#e3f2fd;color:#1976d2;"><i class="fas fa-map-marker-alt"></i></div> <div class="stat-card-icon info"><i class="fas fa-location-dot"></i></div>
</div> </div>
<div class="stat-card-value"><?= $stats['total_sites'] ?></div> <div class="stat-card-value"><?= $stats['total_sites'] ?></div>
</div> </div>
<div class="stat-card"> <div class="stat-card">
<div class="stat-card-header"> <div class="stat-card-header">
<div class="stat-card-title"><?= __('dashboard_users') ?></div> <div class="stat-card-title"><?= __('dashboard_users') ?></div>
<div class="stat-card-icon" style="background:#f3e5f5;color:#7b1fa2;"><i class="fas fa-users"></i></div> <div class="stat-card-icon accent"><i class="fas fa-users"></i></div>
</div> </div>
<div class="stat-card-value"><?= $stats['total_users'] ?></div> <div class="stat-card-value"><?= $stats['total_users'] ?></div>
</div> </div>
<div class="stat-card live"> <div class="stat-card live">
<div class="stat-card-header"> <div class="stat-card-header">
<div class="stat-card-title">🟢 <?= __('dashboard_valid') ?></div> <div class="stat-card-title"><?= __('dashboard_valid') ?></div>
<div class="stat-card-icon" style="background:#e8f5e9;color:#388e3c;"><i class="fas fa-check-circle"></i></div> <div class="stat-card-icon success"><i class="fas fa-circle-check"></i></div>
</div> </div>
<div class="stat-card-value valid" id="liveValid"><?= (int)($voucherStats['valid']??0) ?></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> <div class="stat-card-sub" id="subValid"><?= $lastCronSync ? date('H:i', strtotime($lastCronSync)) : __('never') ?></div>
</div> </div>
<div class="stat-card live"> <div class="stat-card live">
<div class="stat-card-header"> <div class="stat-card-header">
<div class="stat-card-title">🟡 <?= __('dashboard_used') ?></div> <div class="stat-card-title"><?= __('dashboard_used') ?></div>
<div class="stat-card-icon" style="background:#fff3e0;color:#f57c00;"><i class="fas fa-user-check"></i></div> <div class="stat-card-icon warning"><i class="fas fa-user-check"></i></div>
</div> </div>
<div class="stat-card-value used" id="liveUsed"><?= (int)($voucherStats['used']??0) ?></div> <div class="stat-card-value used" id="liveUsed"><?= (int)($voucherStats['used']??0) ?></div>
</div> </div>
<div class="stat-card live"> <div class="stat-card live">
<div class="stat-card-header"> <div class="stat-card-header">
<div class="stat-card-title">🔴 <?= __('dashboard_expired') ?></div> <div class="stat-card-title"><?= __('dashboard_expired') ?></div>
<div class="stat-card-icon" style="background:#ffebee;color:#d32f2f;"><i class="fas fa-times-circle"></i></div> <div class="stat-card-icon danger"><i class="fas fa-circle-xmark"></i></div>
</div> </div>
<div class="stat-card-value expired" id="liveExpired"><?= (int)($voucherStats['expired']??0) ?></div> <div class="stat-card-value expired" id="liveExpired"><?= (int)($voucherStats['expired']??0) ?></div>
</div> </div>
<div class="stat-card live"> <div class="stat-card live">
<div class="stat-card-header"> <div class="stat-card-header">
<div class="stat-card-title">📊 <?= __('dashboard_total') ?></div> <div class="stat-card-title"><?= __('dashboard_total') ?></div>
<div class="stat-card-icon" style="background:var(--bg-hover);color:var(--text-muted);"><i class="fas fa-ticket-alt"></i></div> <div class="stat-card-icon"><i class="fas fa-ticket"></i></div>
</div> </div>
<div class="stat-card-value" id="liveTotal"><?= (int)($voucherStats['total']??0) ?></div> <div class="stat-card-value" id="liveTotal"><?= (int)($voucherStats['total']??0) ?></div>
</div> </div>
@ -225,16 +157,16 @@ $currentPage = 'dashboard';
<div class="card"> <div class="card">
<div class="card-header"> <div class="card-header">
<h2 class="card-title">🔴 <?= __('dashboard_vouchers_per_site') ?></h2> <h2 class="card-title"><?= __('dashboard_vouchers_per_site') ?></h2>
<span class="last-update" id="lastUpdate"><?= $lastCronSync ? date('d.m.Y H:i', strtotime($lastCronSync)) : __('never') ?></span> <span class="last-update" id="lastUpdate"><?= $lastCronSync ? date('d.m.Y H:i', strtotime($lastCronSync)) : __('never') ?></span>
</div> </div>
<div class="card-body" style="padding:18px;"> <div class="card-body">
<div class="site-status" id="siteStatusContainer"> <div class="site-status" id="siteStatusContainer">
<?php foreach ($sites as $site): <?php foreach ($sites as $site):
$ss = $siteStats[$site['id']] ?? ['total'=>0,'valid'=>0,'used'=>0,'expired'=>0]; $ss = $siteStats[$site['id']] ?? ['total'=>0,'valid'=>0,'used'=>0,'expired'=>0];
?> ?>
<div class="site-status-card" id="site-<?= $site['id'] ?>"> <div class="site-status-card" id="site-<?= $site['id'] ?>">
<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px;"> <div style="display:flex;justify-content:space-between;align-items:center;gap:10px;">
<div class="site-status-name"><?= htmlspecialchars($site['name']) ?></div> <div class="site-status-name"><?= htmlspecialchars($site['name']) ?></div>
<span class="site-voucher-count <?= (int)$ss['total']===0 ? 'zero' : '' ?>" id="site-total-<?= $site['id'] ?>"> <span class="site-voucher-count <?= (int)$ss['total']===0 ? 'zero' : '' ?>" id="site-total-<?= $site['id'] ?>">
<?= (int)$ss['total'] ?> <?= (int)$ss['total'] ?>
@ -249,8 +181,8 @@ $currentPage = 'dashboard';
</div> </div>
<?php endforeach; ?> <?php endforeach; ?>
<?php if (empty($sites)): ?> <?php if (empty($sites)): ?>
<div style="grid-column:1/-1;text-align:center;padding:40px;color:var(--text-muted);"> <div class="empty-state" style="grid-column:1/-1;">
<i class="fas fa-map-marker-alt" style="font-size:32px;margin-bottom:15px;opacity:.3;display:block;"></i> <div class="empty-icon"><i class="fas fa-location-dot"></i></div>
<p><?= __('dashboard_no_data') ?></p> <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"></i> <?= __('sites_add') ?></a>
</div> </div>
@ -260,16 +192,16 @@ $currentPage = 'dashboard';
</div> </div>
<div class="card"> <div class="card">
<div class="card-header"><h2 class="card-title">📊 <?= __('dashboard_trend') ?></h2></div> <div class="card-header"><h2 class="card-title"><?= __('dashboard_trend') ?></h2></div>
<div class="card-body"> <div class="card-body">
<div class="chart-container"><canvas id="voucherChart"></canvas></div> <div class="chart-container"><canvas id="voucherChart"></canvas></div>
</div> </div>
</div> </div>
<div style="display:grid;grid-template-columns:1fr 1fr;gap:28px;margin-bottom:28px;" class="two-col-grid"> <div class="two-col-grid">
<div class="card" style="margin-bottom:0;"> <div class="card">
<div class="card-header"><h2 class="card-title">🏆 <?= __('dashboard_top_users') ?></h2></div> <div class="card-header"><h2 class="card-title"><?= __('dashboard_top_users') ?></h2></div>
<div class="card-body" style="padding:18px 22px;"> <div class="card-body">
<?php if (empty($topUsers)): ?> <?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"></i><p><?= __('dashboard_no_data') ?></p></div>
<?php else: ?> <?php else: ?>
@ -291,19 +223,20 @@ $currentPage = 'dashboard';
</div> </div>
</div> </div>
<div class="card" style="margin-bottom:0;"> <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-external-link-alt"></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"></i> Live</a></div>
<div class="card-body" style="padding:0;"> <div class="card-body" style="padding:0;">
<?php if (empty($recentVouchers)): ?> <?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"></i><p><?= __('dashboard_no_vouchers') ?></p></div>
<?php else: ?> <?php else: ?>
<div class="table-container">
<table class="table"> <table class="table">
<thead><tr><th><?= __('label_created') ?></th><th><?= __('label_code') ?></th><th><?= __('label_site') ?></th><th><?= __('label_status') ?></th></tr></thead> <thead><tr><th><?= __('label_created') ?></th><th><?= __('label_code') ?></th><th><?= __('label_site') ?></th><th><?= __('label_status') ?></th></tr></thead>
<tbody> <tbody>
<?php foreach ($recentVouchers as $v): ?> <?php foreach ($recentVouchers as $v): ?>
<tr> <tr>
<td style="font-size:12px;"><?= date('d.m H:i', strtotime($v['created_at'])) ?></td> <td style="font-size:12px;"><?= date('d.m H:i', strtotime($v['created_at'])) ?></td>
<td><code style="font-size:12px;background:var(--bg-hover);padding:3px 6px;border-radius:4px;"><?= htmlspecialchars($v['voucher_code']) ?></code></td> <td><code><?= htmlspecialchars($v['voucher_code']) ?></code></td>
<td><span class="badge badge-info"><?= htmlspecialchars($v['site_name']??'') ?></span></td> <td><span class="badge badge-info"><?= htmlspecialchars($v['site_name']??'') ?></span></td>
<td> <td>
<?php $st=$v['status']??'valid'; ?> <?php $st=$v['status']??'valid'; ?>
@ -313,17 +246,26 @@ $currentPage = 'dashboard';
<?php endforeach; ?> <?php endforeach; ?>
</tbody> </tbody>
</table> </table>
</div>
<?php endif; ?> <?php endif; ?>
</div> </div>
</div> </div>
</div> </div>
</div><!-- /main-content --> </main>
<div id="toast-container"></div> <div id="toast-container"></div>
<script src="../assets/global.js"></script> <script src="../assets/global.js"></script>
<script> <script>
const rootStyles = getComputedStyle(document.documentElement);
const accentColor = rootStyles.getPropertyValue('--accent').trim();
const surfaceColor = rootStyles.getPropertyValue('--bg-card').trim();
const gridColor = rootStyles.getPropertyValue('--border-color').trim();
const mutedColor = rootStyles.getPropertyValue('--text-muted').trim();
const ctx = document.getElementById('voucherChart').getContext('2d'); const ctx = document.getElementById('voucherChart').getContext('2d');
const accentFill = ctx.createLinearGradient(0, 0, 0, 280);
accentFill.addColorStop(0, accentColor + '33');
accentFill.addColorStop(1, accentColor + '00');
const chartData = <?= json_encode($chartData) ?>; const chartData = <?= json_encode($chartData) ?>;
new Chart(ctx, { new Chart(ctx, {
type: 'line', type: 'line',
@ -332,17 +274,20 @@ new Chart(ctx, {
datasets: [{ datasets: [{
label: 'Vouchers', label: 'Vouchers',
data: chartData.map(d => d.count), data: chartData.map(d => d.count),
borderColor: '#667eea', borderColor: accentColor,
backgroundColor: 'rgba(102,126,234,0.1)', backgroundColor: accentFill,
tension: 0.4, fill: true, tension: 0.4, fill: true,
pointBackgroundColor: '#667eea', pointBorderColor: '#fff', pointBackgroundColor: accentColor, pointBorderColor: surfaceColor,
pointBorderWidth: 2, pointRadius: 5, pointHoverRadius: 7 pointBorderWidth: 2, pointRadius: 4, pointHoverRadius: 6
}] }]
}, },
options: { options: {
responsive: true, maintainAspectRatio: false, responsive: true, maintainAspectRatio: false,
plugins: { legend: { display: false } }, plugins: { legend: { display: false } },
scales: { y: { beginAtZero: true, ticks: { stepSize: 1, color: getComputedStyle(document.documentElement).getPropertyValue('--text-muted').trim() } }, x: { ticks: { color: getComputedStyle(document.documentElement).getPropertyValue('--text-muted').trim() } } } scales: {
y: { beginAtZero: true, border: { display: false }, grid: { color: gridColor, drawTicks: false }, ticks: { stepSize: 1, color: mutedColor, padding: 10, font: { size: 11 } } },
x: { border: { display: false }, grid: { display: false }, ticks: { color: mutedColor, padding: 8, font: { size: 11 } } }
}
} }
}); });
@ -396,11 +341,6 @@ async function refreshData(mode='db') {
refreshBtn.disabled = false; refreshBtn.disabled = false;
refreshBtn.innerHTML = '<i class="fas fa-sync-alt"></i> <?= __('dashboard_live_refresh') ?>'; refreshBtn.innerHTML = '<i class="fas fa-sync-alt"></i> <?= __('dashboard_live_refresh') ?>';
} }
// Responsive two-column grid
const twoCol = document.querySelector('.two-col-grid');
function checkGrid() { if (twoCol) twoCol.style.gridTemplateColumns = window.innerWidth < 900 ? '1fr' : '1fr 1fr'; }
checkGrid(); window.addEventListener('resize', checkGrid);
</script> </script>
</body> </body>
</html> </html>

View file

@ -94,22 +94,12 @@ $adminBase = '';
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Integration & Wartung <?= htmlspecialchars($appTitle) ?></title> <title>Integration & Wartung <?= htmlspecialchars($appTitle) ?></title>
<?php require __DIR__ . '/../includes/admin_nav.php'; ?> <?php require __DIR__ . '/../includes/admin_nav.php'; ?>
<style> <div class="page-header">
.card { background:var(--bg-card); border:1px solid var(--border-color); border-radius:14px; padding:24px; margin-bottom:22px; box-shadow:0 4px 14px var(--shadow); max-width:680px; } <div>
.card h2 { font-size:16px; margin-bottom:6px; color:var(--text-primary); } <h1 class="page-title">Integration &amp; Wartung</h1>
.muted { color:var(--text-muted); font-size:13px; margin-bottom:14px; } <p class="page-subtitle">SSO, Webhooks, SMS-Versand und Aufbewahrungsfristen.</p>
label { display:block; font-size:14px; color:var(--text-secondary); margin:14px 0 6px; } </div>
.input { width:100%; padding:11px; border:2px solid var(--border-color); border-radius:8px; background:var(--bg-input,#fff); color:var(--text-primary); font-size:14px; } </div>
.row { display:grid; grid-template-columns:1fr 1fr 1fr; gap:12px; }
.chk { display:flex; align-items:center; gap:9px; margin-top:14px; color:var(--text-primary); font-size:14px; }
.btn { padding:11px 18px; border:none; border-radius:8px; font-weight:600; cursor:pointer; font-size:14px; text-decoration:none; display:inline-block; }
.btn-primary { background:var(--accent,#667eea); color:#fff; } .btn-secondary { background:var(--bg-hover); color:var(--text-primary); border:1px solid var(--border-color); }
.alert { padding:12px 14px; border-radius:9px; font-size:14px; margin-bottom:18px; max-width:680px; }
.alert-error { background:#fee; border:1px solid #fcc; color:#c33; } .alert-ok { background:#efe; border:1px solid #cfc; color:#2a7; }
</style>
</head>
<body>
<h1 style="font-size:24px;margin-bottom:20px;color:var(--text-primary);">🔧 Integration &amp; Wartung</h1>
<?php if ($error): ?><div class="alert alert-error"><?= htmlspecialchars($error) ?></div><?php endif; ?> <?php if ($error): ?><div class="alert alert-error"><?= htmlspecialchars($error) ?></div><?php endif; ?>
<?php if ($success): ?><div class="alert alert-ok"><?= htmlspecialchars($success) ?></div><?php endif; ?> <?php if ($success): ?><div class="alert alert-ok"><?= htmlspecialchars($success) ?></div><?php endif; ?>
@ -124,12 +114,12 @@ label { display:block; font-size:14px; color:var(--text-secondary); margin:14px
<label>Tageslimit Voucher pro Nicht-Admin-Benutzer (0 = unbegrenzt)</label> <label>Tageslimit Voucher pro Nicht-Admin-Benutzer (0 = unbegrenzt)</label>
<input class="input" type="number" min="0" name="user_daily_voucher_limit" value="<?= $dailyLimit ?>" style="max-width:200px;"> <input class="input" type="number" min="0" name="user_daily_voucher_limit" value="<?= $dailyLimit ?>" style="max-width:200px;">
<label>Session-Speicher</label> <label>Session-Speicher</label>
<select class="input" name="session_driver" style="max-width:240px;"> <select class="input" name="session_driver" style="max-width:340px;">
<option value="php" <?= $sessionDriver==='php'?'selected':'' ?>>PHP-Standard (Dateien)</option> <option value="php" <?= $sessionDriver==='php'?'selected':'' ?>>PHP-Standard (Dateien)</option>
<option value="db" <?= $sessionDriver==='db'?'selected':'' ?>>Datenbank (ermöglicht „überall abmelden")</option> <option value="db" <?= $sessionDriver==='db'?'selected':'' ?>>Datenbank (ermöglicht „überall abmelden")</option>
</select> </select>
<label>Captcha im öffentlichen Modus</label> <label>Captcha im öffentlichen Modus</label>
<select class="input" name="captcha_mode" style="max-width:240px;"> <select class="input" name="captcha_mode" style="max-width:340px;">
<option value="off" <?= $captchaMode==='off'?'selected':'' ?>>Aus</option> <option value="off" <?= $captchaMode==='off'?'selected':'' ?>>Aus</option>
<option value="math" <?= $captchaMode==='math'?'selected':'' ?>>Rechenaufgabe (ohne externen Dienst)</option> <option value="math" <?= $captchaMode==='math'?'selected':'' ?>>Rechenaufgabe (ohne externen Dienst)</option>
<option value="hcaptcha" <?= $captchaMode==='hcaptcha'?'selected':'' ?>>hCaptcha</option> <option value="hcaptcha" <?= $captchaMode==='hcaptcha'?'selected':'' ?>>hCaptcha</option>
@ -198,7 +188,7 @@ label { display:block; font-size:14px; color:var(--text-secondary); margin:14px
<button class="btn btn-primary" type="submit" name="save">Speichern</button> <button class="btn btn-primary" type="submit" name="save">Speichern</button>
</form> </form>
</div><!-- /main-content --> </main>
<script src="../assets/global.js"></script> <script src="../assets/global.js"></script>
</body> </body>
</html> </html>

View file

@ -98,36 +98,26 @@ $adminBase = '';
<title>Reporting <?= htmlspecialchars($appTitle) ?></title> <title>Reporting <?= htmlspecialchars($appTitle) ?></title>
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
<?php require __DIR__ . '/../includes/admin_nav.php'; ?> <?php require __DIR__ . '/../includes/admin_nav.php'; ?>
<style> <div class="page-header">
.card { background:var(--bg-card); border:1px solid var(--border-color); border-radius:14px; padding:22px; margin-bottom:20px; box-shadow:0 4px 14px var(--shadow); } <div>
.card h2 { font-size:15px; margin-bottom:14px; color:var(--text-primary); } <h1 class="page-title">Reporting</h1>
.grid4 { display:grid; grid-template-columns:repeat(4,1fr); gap:16px; margin-bottom:20px; } <p class="page-subtitle">Auswertungen nach Zeitraum, Site und Benutzer.</p>
.stat { background:var(--bg-card); border:1px solid var(--border-color); border-radius:14px; padding:20px; } </div>
.stat .n { font-size:28px; font-weight:700; color:var(--text-primary); } .stat .l { color:var(--text-muted); font-size:12.5px; margin-top:3px; } </div>
table { width:100%; border-collapse:collapse; } th,td { text-align:left; padding:10px 8px; font-size:14px; border-bottom:1px solid var(--border-color); color:var(--text-primary); } th { color:var(--text-muted); }
.btn { padding:9px 15px; border:none; border-radius:8px; font-weight:600; font-size:13px; text-decoration:none; display:inline-block; cursor:pointer; }
.btn-s { background:var(--bg-hover); color:var(--text-primary); border:1px solid var(--border-color); }
.toolbar { display:flex; gap:10px; align-items:center; flex-wrap:wrap; margin-bottom:18px; }
select.input { padding:9px; border:2px solid var(--border-color); border-radius:8px; background:var(--bg-input,#fff); color:var(--text-primary); }
@media print { .sidebar,.header,.toolbar,.no-print { display:none !important; } .main-content { margin:0 !important; } }
</style>
</head>
<body>
<h1 style="font-size:24px;margin-bottom:18px;color:var(--text-primary);">📊 Reporting</h1>
<div class="toolbar no-print"> <div class="toolbar no-print">
<form method="get" style="display:flex;gap:8px;align-items:center;"> <form method="get" style="display:flex;gap:8px;align-items:center;">
<label style="color:var(--text-muted);font-size:13px;">Zeitraum:</label> <label style="margin:0;">Zeitraum</label>
<select class="input" name="days" onchange="this.form.submit()"> <select class="input" name="days" onchange="this.form.submit()">
<?php foreach ([7,30,90,365] as $d): ?> <?php foreach ([7,30,90,365] as $d): ?>
<option value="<?= $d ?>" <?= $days===$d?'selected':'' ?>><?= $d ?> Tage</option> <option value="<?= $d ?>" <?= $days===$d?'selected':'' ?>><?= $d ?> Tage</option>
<?php endforeach; ?> <?php endforeach; ?>
</select> </select>
</form> </form>
<a class="btn btn-s" href="?export=daily&days=<?= $days ?>">⬇️ CSV (täglich)</a> <a class="btn btn-secondary" href="?export=daily&days=<?= $days ?>"><i class="fas fa-download"></i> CSV (täglich)</a>
<a class="btn btn-s" href="?export=per_site">⬇️ CSV (pro Site)</a> <a class="btn btn-secondary" href="?export=per_site"><i class="fas fa-download"></i> CSV (pro Site)</a>
<a class="btn btn-s" href="?export=per_user">⬇️ CSV (pro Nutzer)</a> <a class="btn btn-secondary" href="?export=per_user"><i class="fas fa-download"></i> CSV (pro Nutzer)</a>
<button class="btn btn-s" onclick="window.print()">🖨️ Drucken/PDF</button> <button class="btn btn-secondary" onclick="window.print()"><i class="fas fa-print"></i> Drucken/PDF</button>
</div> </div>
<div class="grid4"> <div class="grid4">
@ -161,13 +151,23 @@ select.input { padding:9px; border:2px solid var(--border-color); border-radius:
</table> </table>
</div> </div>
</div><!-- /main-content --> </main>
<script src="../assets/global.js"></script> <script src="../assets/global.js"></script>
<script> <script>
const styles = getComputedStyle(document.documentElement);
const accent = styles.getPropertyValue('--accent').trim();
const grid = styles.getPropertyValue('--border-color').trim();
const muted = styles.getPropertyValue('--text-muted').trim();
new Chart(document.getElementById('chart'), { new Chart(document.getElementById('chart'), {
type:'line', type:'line',
data:{ labels: <?= json_encode($chartLabels) ?>, datasets:[{ label:'Voucher', data: <?= json_encode($chartData) ?>, borderColor:'#667eea', backgroundColor:'rgba(102,126,234,.15)', fill:true, tension:.3 }] }, data:{ labels: <?= json_encode($chartLabels) ?>, datasets:[{ label:'Voucher', data: <?= json_encode($chartData) ?>, borderColor: accent, backgroundColor: accent + '22', fill:true, tension:.35, pointRadius:0, pointHoverRadius:4, borderWidth:2 }] },
options:{ plugins:{legend:{display:false}}, scales:{y:{beginAtZero:true,ticks:{precision:0}}} } options:{
plugins:{legend:{display:false}},
scales:{
y:{ beginAtZero:true, border:{display:false}, grid:{color:grid, drawTicks:false}, ticks:{precision:0, color:muted, padding:10, font:{size:11}} },
x:{ border:{display:false}, grid:{display:false}, ticks:{color:muted, padding:8, font:{size:11}} }
}
}
}); });
</script> </script>
</body> </body>

View file

@ -93,37 +93,24 @@ $activeSessions = $dbSessions ? $auth->activeSessionCount() : 0;
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Zwei-Faktor-Authentifizierung <?= htmlspecialchars($appTitle) ?></title> <title>Zwei-Faktor-Authentifizierung <?= htmlspecialchars($appTitle) ?></title>
<?php if (!$totpEnabled && $hasPassword): ?> <?php if (!$totpEnabled && $hasPassword): ?>
<script src="https://cdnjs.cloudflare.com/ajax/libs/qrcodejs/1.0.0/qrcode.min.js" integrity="sha512-CNgIRecGo7nphbeZ04Sc13ka07paqdeTu0WR1IM4kNcpmBAUSHSe2keRB6Q5pBUtIxCY7bQMsVB0ANBpd6JDg==" crossorigin="anonymous" referrerpolicy="no-referrer"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/qrcodejs/1.0.0/qrcode.min.js" integrity="sha512-CNgIRecGo7nphbeZ04Sc13ka07paqdeTu0WR1IM4kNcpmBAUSHSQX0FslNhTDadL4O5SAGapGt4FodqL8My0mA==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<?php endif; ?> <?php endif; ?>
<style> <link rel="preconnect" href="https://fonts.googleapis.com">
* { margin:0; padding:0; box-sizing:border-box; font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif; } <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
body { background:linear-gradient(135deg,#667eea 0%,#764ba2 100%); min-height:100vh; display:flex; align-items:center; justify-content:center; padding:20px; } <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap">
.card { background:#fff; border-radius:18px; box-shadow:0 20px 60px rgba(0,0,0,.3); max-width:480px; width:100%; padding:36px; } <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
h1 { font-size:22px; color:#333; margin-bottom:6px; } <link rel="stylesheet" href="../assets/global.css">
.sub { color:#777; font-size:14px; margin-bottom:24px; } <script>(function(){ const t=localStorage.getItem('theme')||'light'; document.documentElement.setAttribute('data-theme',t); })();</script>
.alert { padding:12px 14px; border-radius:9px; font-size:14px; margin-bottom:18px; }
.alert-error { background:#fee; border:1px solid #fcc; color:#c33; }
.alert-ok { background:#efe; border:1px solid #cfc; color:#2a7; }
.status { display:inline-flex; align-items:center; gap:8px; padding:6px 12px; border-radius:8px; font-size:13px; font-weight:600; margin-bottom:20px; }
.on { background:#e3f6ea; color:#2a7; } .off { background:#fdeaea; color:#c33; }
.qr { display:flex; justify-content:center; margin:18px 0; }
.secret { font-family:monospace; background:#f5f6fa; padding:10px; border-radius:8px; text-align:center; letter-spacing:2px; word-break:break-all; font-size:14px; margin-bottom:18px; }
ol { margin:0 0 18px 18px; color:#555; font-size:14px; line-height:1.7; }
label { display:block; font-size:14px; color:#555; margin-bottom:8px; font-weight:500; }
input[type=text] { width:100%; padding:13px; border:2px solid #e0e0e0; border-radius:10px; font-size:18px; letter-spacing:6px; text-align:center; }
.btn { width:100%; padding:14px; border:none; border-radius:10px; font-size:15px; font-weight:600; cursor:pointer; margin-top:14px; }
.btn-primary { background:#667eea; color:#fff; } .btn-danger { background:#e25555; color:#fff; }
.back { display:block; text-align:center; margin-top:20px; color:#667eea; text-decoration:none; font-size:14px; }
.codes-box { background:#fff8e6; border:1px solid #ffe3a3; border-radius:10px; padding:16px; margin-bottom:18px; }
.codes-box strong { font-size:15px; } .codes-box p { color:#8a6d2f; font-size:13px; margin:6px 0 12px; }
.codes { display:grid; grid-template-columns:1fr 1fr; gap:8px; }
.codes span { font-family:monospace; background:#fff; border:1px solid #ffe3a3; border-radius:6px; padding:8px; text-align:center; letter-spacing:2px; font-size:14px; }
</style>
</head> </head>
<body> <body class="app-body focus-page">
<div class="card"> <div class="focus-card card">
<h1>🔐 Zwei-Faktor-Authentifizierung</h1> <div class="focus-head">
<span class="focus-icon"><i class="fas fa-shield-halved"></i></span>
<div>
<h1>Zwei-Faktor-Authentifizierung</h1>
<p class="sub">Konto: <?= htmlspecialchars($user['email']) ?></p> <p class="sub">Konto: <?= htmlspecialchars($user['email']) ?></p>
</div>
</div>
<?php if ($setupRequired && !$totpEnabled): ?> <?php if ($setupRequired && !$totpEnabled): ?>
<div class="alert alert-error">Aus Sicherheitsgründen ist 2FA für Administratoren verpflichtend. Bitte jetzt einrichten.</div> <div class="alert alert-error">Aus Sicherheitsgründen ist 2FA für Administratoren verpflichtend. Bitte jetzt einrichten.</div>
@ -133,7 +120,7 @@ input[type=text] { width:100%; padding:13px; border:2px solid #e0e0e0; border-ra
<?php if (!empty($backupCodes)): ?> <?php if (!empty($backupCodes)): ?>
<div class="codes-box"> <div class="codes-box">
<strong>🔑 Recovery-Codes</strong> <strong><i class="fas fa-key"></i> Recovery-Codes</strong>
<p>Bewahren Sie diese sicher auf. Jeder Code funktioniert <em>einmal</em>, falls Sie keinen Zugriff auf Ihre App haben.</p> <p>Bewahren Sie diese sicher auf. Jeder Code funktioniert <em>einmal</em>, falls Sie keinen Zugriff auf Ihre App haben.</p>
<div class="codes"> <div class="codes">
<?php foreach ($backupCodes as $c): ?><span><?= htmlspecialchars($c) ?></span><?php endforeach; ?> <?php foreach ($backupCodes as $c): ?><span><?= htmlspecialchars($c) ?></span><?php endforeach; ?>
@ -142,22 +129,22 @@ input[type=text] { width:100%; padding:13px; border:2px solid #e0e0e0; border-ra
<?php endif; ?> <?php endif; ?>
<?php if (!$hasPassword): ?> <?php if (!$hasPassword): ?>
<div class="status off"> Nicht verfügbar</div> <div class="status off"><i class="fas fa-circle-minus"></i> Nicht verfügbar</div>
<p class="sub">Ihr Konto meldet sich über Microsoft 365 an. 2FA wird dort in Ihrem Microsoft-Konto verwaltet.</p> <p class="sub">Ihr Konto meldet sich über Microsoft 365 an. 2FA wird dort in Ihrem Microsoft-Konto verwaltet.</p>
<?php elseif ($totpEnabled): ?> <?php elseif ($totpEnabled): ?>
<div class="status on"> Aktiv</div> <div class="status on"><i class="fas fa-circle-check"></i> Aktiv</div>
<p class="sub">Bei jeder Anmeldung wird zusätzlich ein Code aus Ihrer Authenticator-App abgefragt.<br> <p class="sub">Bei jeder Anmeldung wird zusätzlich ein Code aus Ihrer Authenticator-App abgefragt.<br>
Verbleibende Recovery-Codes: <strong><?= (int)$auth->backupCodesRemaining($user) ?></strong></p> Verbleibende Recovery-Codes: <strong><?= (int)$auth->backupCodesRemaining($user) ?></strong></p>
<form method="post" style="margin-bottom:10px;"> <form method="post" style="margin-bottom:10px;">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($csrf) ?>"> <input type="hidden" name="csrf_token" value="<?= htmlspecialchars($csrf) ?>">
<button type="submit" name="regen_codes" class="btn btn-secondary" style="background:#eef0ff;color:#5a63d6;width:100%;">Recovery-Codes neu erzeugen</button> <button type="submit" name="regen_codes" class="btn btn-secondary btn-lg btn-block">Recovery-Codes neu erzeugen</button>
</form> </form>
<form method="post" onsubmit="return confirm('2FA wirklich deaktivieren?');"> <form method="post" onsubmit="return confirm('2FA wirklich deaktivieren?');">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($csrf) ?>"> <input type="hidden" name="csrf_token" value="<?= htmlspecialchars($csrf) ?>">
<button type="submit" name="disable_totp" class="btn btn-danger">2FA deaktivieren</button> <button type="submit" name="disable_totp" class="btn btn-danger btn-lg btn-block">2FA deaktivieren</button>
</form> </form>
<?php else: ?> <?php else: ?>
<div class="status off"> Inaktiv</div> <div class="status off"><i class="fas fa-circle-minus"></i> Inaktiv</div>
<ol> <ol>
<li>Authenticator-App öffnen (Google Authenticator, Authy, Microsoft Authenticator )</li> <li>Authenticator-App öffnen (Google Authenticator, Authy, Microsoft Authenticator )</li>
<li>QR-Code scannen <em>oder</em> Secret manuell eingeben</li> <li>QR-Code scannen <em>oder</em> Secret manuell eingeben</li>
@ -168,27 +155,28 @@ input[type=text] { width:100%; padding:13px; border:2px solid #e0e0e0; border-ra
<form method="post"> <form method="post">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($csrf) ?>"> <input type="hidden" name="csrf_token" value="<?= htmlspecialchars($csrf) ?>">
<label for="code">6-stelliger Code</label> <label for="code">6-stelliger Code</label>
<input type="text" id="code" name="code" inputmode="numeric" pattern="[0-9]*" maxlength="6" autocomplete="one-time-code" required> <input type="text" id="code" name="code" class="code-input" inputmode="numeric" pattern="[0-9]*" maxlength="6" autocomplete="one-time-code" required placeholder="123456">
<button type="submit" name="enable_totp" class="btn btn-primary">2FA aktivieren</button> <button type="submit" name="enable_totp" class="btn btn-primary btn-lg btn-block" style="margin-top:14px;">2FA aktivieren</button>
</form> </form>
<script> <script>
new QRCode(document.getElementById('qrcode'), { new QRCode(document.getElementById('qrcode'), {
text: <?= json_encode($otpUri) ?>, width: 180, height: 180, text: <?= json_encode($otpUri) ?>, width: 168, height: 168,
colorDark: '#101625', colorLight: '#ffffff',
correctLevel: QRCode.CorrectLevel.M correctLevel: QRCode.CorrectLevel.M
}); });
</script> </script>
<?php endif; ?> <?php endif; ?>
<?php if ($dbSessions): ?> <?php if ($dbSessions): ?>
<hr style="margin:20px 0;border:none;border-top:1px solid #eee;"> <hr>
<p class="sub">Aktive Sitzungen: <strong><?= (int)$activeSessions ?></strong></p> <p class="sub">Aktive Sitzungen: <strong><?= (int)$activeSessions ?></strong></p>
<form method="post" onsubmit="return confirm('Alle anderen Sitzungen abmelden?');"> <form method="post" onsubmit="return confirm('Alle anderen Sitzungen abmelden?');">
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($csrf) ?>"> <input type="hidden" name="csrf_token" value="<?= htmlspecialchars($csrf) ?>">
<button type="submit" name="logout_others" class="btn" style="background:#eef0ff;color:#5a63d6;width:100%;">Auf allen anderen Geräten abmelden</button> <button type="submit" name="logout_others" class="btn btn-secondary btn-lg btn-block">Auf allen anderen Geräten abmelden</button>
</form> </form>
<?php endif; ?> <?php endif; ?>
<a class="back" href="../index.php"> Zurück</a> <div class="auth-links"><a class="back-link" href="../index.php"><i class="fas fa-arrow-left"></i> Zurück</a></div>
</div> </div>
</body> </body>
</html> </html>

View file

@ -58,6 +58,21 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['save_settings'])) {
$settings['public_access'] = isset($_POST['public_access']) ? '1' : '0'; $settings['public_access'] = isset($_POST['public_access']) ? '1' : '0';
} }
if ($formType === 'login') {
$settings['login_panel_enabled'] = isset($_POST['login_panel_enabled']) ? '1' : '0';
$settings['login_brand_name'] = trim($_POST['login_brand_name'] ?? '');
$settings['login_logo_url'] = trim($_POST['login_logo_url'] ?? '');
$settings['login_claim_title'] = trim($_POST['login_claim_title'] ?? '');
$settings['login_claim_text'] = trim($_POST['login_claim_text'] ?? '');
$settings['login_features'] = trim($_POST['login_features'] ?? '');
$settings['login_footer'] = trim($_POST['login_footer'] ?? '');
$settings['login_bg_image'] = trim($_POST['login_bg_image'] ?? '');
$settings['login_bg_from'] = trim($_POST['login_bg_from'] ?? '');
$settings['login_bg_to'] = trim($_POST['login_bg_to'] ?? '');
$overlay = (int)($_POST['login_bg_overlay'] ?? 40);
$settings['login_bg_overlay'] = (string)max(0, min(90, $overlay));
}
if ($formType === 'defaults') { if ($formType === 'defaults') {
$expMin = (int)($_POST['default_expire_minutes'] ?? 480); $expMin = (int)($_POST['default_expire_minutes'] ?? 480);
$defDev = (int)($_POST['default_max_uses'] ?? 1); $defDev = (int)($_POST['default_max_uses'] ?? 1);
@ -190,6 +205,17 @@ $cs = [
'email_user_notification_body' => $db->getSetting('email_user_notification_body', "Hallo {USER_NAME},\n\n{CHANGES}"), 'email_user_notification_body' => $db->getSetting('email_user_notification_body', "Hallo {USER_NAME},\n\n{CHANGES}"),
'tinymce_api_key' => $db->getSetting('tinymce_api_key', ''), 'tinymce_api_key' => $db->getSetting('tinymce_api_key', ''),
'print_template' => $db->getSetting('print_template', '<div style="text-align:center;padding:40px"><h1>{APP_TITLE}</h1><h2>WLAN Code</h2><div style="font-size:48px;font-weight:bold;margin:30px 0;font-family:monospace">{VOUCHER_CODE}</div><p><strong>Gültig bis:</strong> {EXPIRY_DATE} {EXPIRY_TIME}</p><p><strong>Site:</strong> {SITE_NAME}</p><p><strong>Geräte:</strong> {MAX_USES}</p><hr style="margin:30px 0"><div>{INSTRUCTIONS}</div></div>'), 'print_template' => $db->getSetting('print_template', '<div style="text-align:center;padding:40px"><h1>{APP_TITLE}</h1><h2>WLAN Code</h2><div style="font-size:48px;font-weight:bold;margin:30px 0;font-family:monospace">{VOUCHER_CODE}</div><p><strong>Gültig bis:</strong> {EXPIRY_DATE} {EXPIRY_TIME}</p><p><strong>Site:</strong> {SITE_NAME}</p><p><strong>Geräte:</strong> {MAX_USES}</p><hr style="margin:30px 0"><div>{INSTRUCTIONS}</div></div>'),
'login_panel_enabled' => $db->getSetting('login_panel_enabled', '1'),
'login_brand_name' => $db->getSetting('login_brand_name', ''),
'login_logo_url' => $db->getSetting('login_logo_url', ''),
'login_claim_title' => $db->getSetting('login_claim_title', ''),
'login_claim_text' => $db->getSetting('login_claim_text', ''),
'login_features' => $db->getSetting('login_features', ''),
'login_footer' => $db->getSetting('login_footer', ''),
'login_bg_image' => $db->getSetting('login_bg_image', ''),
'login_bg_from' => $db->getSetting('login_bg_from', '#3b2f8f'),
'login_bg_to' => $db->getSetting('login_bg_to', '#6d5ce7'),
'login_bg_overlay' => $db->getSetting('login_bg_overlay', '40'),
'cron_token' => $db->getSetting('cron_token', ''), 'cron_token' => $db->getSetting('cron_token', ''),
'last_cron_sync' => $db->getSetting('last_cron_sync', ''), 'last_cron_sync' => $db->getSetting('last_cron_sync', ''),
]; ];
@ -207,54 +233,18 @@ $adminBase = '';
<?php if (!empty($cs['tinymce_api_key'])): ?> <?php if (!empty($cs['tinymce_api_key'])): ?>
<script src="https://cdn.tiny.cloud/1/<?= htmlspecialchars($cs['tinymce_api_key']) ?>/tinymce/6/tinymce.min.js"></script> <script src="https://cdn.tiny.cloud/1/<?= htmlspecialchars($cs['tinymce_api_key']) ?>/tinymce/6/tinymce.min.js"></script>
<?php else: ?> <?php else: ?>
<script src="https://cdn.tiny.cloud/1/no-api-key/tinymce/6/tinymce.min.js"></script> <!-- Ohne API-Key die GPL-Variante von cdnjs laden: gleiche Funktionen, kein Hinweis-Banner. -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/tinymce/6.8.3/tinymce.min.js" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script>window.TINYMCE_BASE_URL = 'https://cdnjs.cloudflare.com/ajax/libs/tinymce/6.8.3';</script>
<?php endif; ?> <?php endif; ?>
<?php include __DIR__ . '/../includes/admin_nav.php'; ?> <?php include __DIR__ . '/../includes/admin_nav.php'; ?>
<style>
.page-header { margin-bottom: 30px; }
.page-title { font-size: 28px; font-weight: 600; color: var(--text-primary); margin-bottom: 8px; }
.alert { padding: 14px 20px; border-radius: 10px; margin-bottom: 25px; font-size: 14px; display: flex; align-items: center; gap: 10px; }
.alert-error { background: #fee; border: 1px solid #fcc; color: #c33; }
.alert-success { background: #efe; border: 1px solid #cfc; color: #3c3; }
.tab-container { background: var(--bg-card); border-radius: 15px; box-shadow: 0 2px 10px var(--shadow); border: 1px solid var(--border-color); overflow: hidden; }
.tab-navigation { display: flex; background: var(--bg-table-head); border-bottom: 2px solid var(--border-color); overflow-x: auto; position: sticky; top: 70px; z-index: 50; }
.tab-button { padding: 15px 20px; background: transparent; border: none; border-bottom: 3px solid transparent; cursor: pointer; font-size: 13px; font-weight: 500; color: var(--text-secondary); transition: all 0.3s; white-space: nowrap; display: flex; align-items: center; gap: 7px; }
.tab-button:hover { background: rgba(102,126,234,0.1); color: var(--accent); }
.tab-button.active { color: var(--accent); border-bottom-color: var(--accent); background: var(--bg-card); }
.tab-content { display: none; padding: 30px; animation: fadeIn 0.2s; }
.tab-content.active { display: block; }
@keyframes fadeIn { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } }
.form-group { margin-bottom: 20px; }
label { display: block; margin-bottom: 7px; color: var(--text-secondary); font-weight: 500; font-size: 14px; }
input[type="text"], input[type="url"], input[type="password"], input[type="number"], input[type="email"], select, textarea { width: 100%; padding: 11px 14px; border: 2px solid var(--border-color); border-radius: 8px; font-size: 14px; transition: border-color 0.2s; font-family: inherit; background: var(--bg-input); color: var(--text-primary); }
input:focus, textarea:focus, select:focus { outline: none; border-color: var(--accent); }
textarea { resize: vertical; min-height: 100px; }
.checkbox-group { display: flex; align-items: center; gap: 10px; }
.checkbox-group input { width: auto; accent-color: var(--accent); }
.help-text { font-size: 12px; color: var(--text-muted); margin-top: 5px; }
.info-box { background: #e7f3ff; border: 1px solid #b3d9ff; border-radius: 8px; padding: 15px; margin-bottom: 20px; }
.info-box h4 { color: #0066cc; margin-bottom: 8px; font-size: 14px; }
.info-box p { color: #004d99; font-size: 13px; line-height: 1.5; }
.section-divider { border: none; border-top: 2px solid var(--border-color); margin: 28px 0; }
.form-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 20px; }
.placeholder-info { background: #fff9e6; border: 1px solid #ffe066; border-radius: 8px; padding: 14px; margin: 14px 0; }
.placeholder-info h4 { color: #996600; margin-bottom: 8px; font-size: 14px; }
.placeholder-info code { background: #fff; padding: 2px 6px; border-radius: 3px; font-size: 12px; color: #d63384; }
.placeholder-list { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 8px; }
.btn-primary { background: var(--accent); color: white; }
.btn-primary:hover { background: var(--accent-hover); }
.btn-danger { background: #dc3545; color: white; }
.btn-small { padding: 6px 12px; font-size: 13px; }
.token-box { background: var(--bg-hover); padding: 14px; border-radius: 6px; font-family: monospace; word-break: break-all; color: var(--text-primary); }
code { background: var(--code-bg); color: var(--accent); padding: 2px 6px; border-radius: 4px; font-size: 13px; }
</style>
</head>
<div class="page-header"> <div class="page-header">
<div>
<h1 class="page-title"><?= __('settings_title') ?></h1> <h1 class="page-title"><?= __('settings_title') ?></h1>
<p style="color: var(--text-muted); font-size: 14px;"><?= __('settings_subtitle') ?></p> <p class="page-subtitle"><?= __('settings_subtitle') ?></p>
</div>
</div> </div>
<?php if ($error): ?> <?php if ($error): ?>
@ -268,6 +258,7 @@ $adminBase = '';
<div class="tab-navigation" id="tabNav"> <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 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="defaults"><i class="fas fa-sliders-h"></i> <?= __('settings_tab_defaults') ?></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="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="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="smtp"><i class="fas fa-envelope"></i> <?= __('settings_tab_smtp') ?></button>
@ -331,6 +322,92 @@ $adminBase = '';
</form> </form>
</div> </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>
<p style="color: var(--text-muted); font-size: 14px; margin-bottom: 24px;"><?= __('settings_login_intro') ?></p>
<form method="post">
<input type="hidden" name="csrf_token" value="<?= $auth->getCsrfToken() ?>">
<input type="hidden" name="form_type" value="login">
<div class="checkbox-group" style="margin-bottom: 20px;">
<input type="checkbox" name="login_panel_enabled" id="login_panel_enabled" <?= $cs['login_panel_enabled'] === '1' ? 'checked' : '' ?>>
<label for="login_panel_enabled" style="margin:0;"><?= __('settings_login_panel') ?></label>
</div>
<div class="form-grid">
<div class="form-group">
<label><?= __('settings_login_brand') ?></label>
<input type="text" name="login_brand_name" value="<?= htmlspecialchars($cs['login_brand_name']) ?>" placeholder="<?= htmlspecialchars($cs['app_title']) ?>">
<div class="help-text"><?= __('settings_login_brand_hint') ?></div>
</div>
<div class="form-group">
<label><?= __('settings_login_logo') ?></label>
<input type="url" name="login_logo_url" value="<?= htmlspecialchars($cs['login_logo_url']) ?>" placeholder="https://example.com/logo.svg">
<div class="help-text"><?= __('settings_login_logo_hint') ?></div>
</div>
</div>
<hr class="section-divider">
<div class="form-group">
<label><?= __('settings_login_claim_title') ?></label>
<input type="text" name="login_claim_title" value="<?= htmlspecialchars($cs['login_claim_title']) ?>" placeholder="<?= htmlspecialchars(__('auth_claim_title')) ?>">
</div>
<div class="form-group">
<label><?= __('settings_login_claim_text') ?></label>
<textarea name="login_claim_text" rows="3" placeholder="<?= htmlspecialchars(__('auth_claim_text')) ?>"><?= htmlspecialchars($cs['login_claim_text']) ?></textarea>
</div>
<div class="form-group">
<label><?= __('settings_login_features') ?></label>
<textarea name="login_features" rows="4" placeholder="<?= htmlspecialchars(__('auth_feature_1') . "\n" . __('auth_feature_2') . "\n" . __('auth_feature_3')) ?>"><?= htmlspecialchars($cs['login_features']) ?></textarea>
<div class="help-text"><?= __('settings_login_features_hint') ?></div>
</div>
<div class="form-group">
<label><?= __('settings_login_footer') ?></label>
<input type="text" name="login_footer" value="<?= htmlspecialchars($cs['login_footer']) ?>" placeholder="© <?= date('Y') ?> <?= htmlspecialchars($cs['app_title']) ?>">
</div>
<hr class="section-divider">
<div class="form-group">
<label><?= __('settings_login_bg_image') ?></label>
<input type="url" name="login_bg_image" value="<?= htmlspecialchars($cs['login_bg_image']) ?>" placeholder="https://example.com/empfang.jpg">
<div class="help-text"><?= __('settings_login_bg_image_hint') ?></div>
</div>
<div class="form-grid">
<div class="form-group">
<label><?= __('settings_login_bg_from') ?></label>
<div class="color-field">
<input type="color" class="color-swatch" data-target="login_bg_from"
value="<?= preg_match('/^#[0-9a-fA-F]{6}$/', $cs['login_bg_from']) ? htmlspecialchars($cs['login_bg_from']) : '#3b2f8f' ?>">
<input type="text" id="login_bg_from" name="login_bg_from" value="<?= htmlspecialchars($cs['login_bg_from']) ?>" placeholder="#3b2f8f">
</div>
</div>
<div class="form-group">
<label><?= __('settings_login_bg_to') ?></label>
<div class="color-field">
<input type="color" class="color-swatch" data-target="login_bg_to"
value="<?= preg_match('/^#[0-9a-fA-F]{6}$/', $cs['login_bg_to']) ? htmlspecialchars($cs['login_bg_to']) : '#6d5ce7' ?>">
<input type="text" id="login_bg_to" name="login_bg_to" value="<?= htmlspecialchars($cs['login_bg_to']) ?>" placeholder="#6d5ce7">
</div>
</div>
<div class="form-group">
<label><?= __('settings_login_overlay') ?></label>
<input type="number" name="login_bg_overlay" min="0" max="90" value="<?= (int)$cs['login_bg_overlay'] ?>">
<div class="help-text"><?= __('settings_login_overlay_hint') ?></div>
</div>
</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>
<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') ?>
</a>
</div>
</form>
</div>
<!-- Cron-Sync --> <!-- Cron-Sync -->
<div id="tab-cron" class="tab-content"> <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"></i> <?= __('settings_tab_cron') ?></h2>
@ -486,7 +563,7 @@ $adminBase = '';
</div> </div>
</div> </div>
</div><!-- main-content --> </main>
<script src="../assets/global.js"></script> <script src="../assets/global.js"></script>
<script> <script>
@ -502,6 +579,16 @@ document.querySelectorAll('.tab-button').forEach(btn => {
}); });
}); });
// Farbwähler und Hex-Feld synchron halten
document.querySelectorAll('.color-swatch').forEach(swatch => {
const field = document.getElementById(swatch.dataset.target);
if (!field) return;
swatch.addEventListener('input', () => { field.value = swatch.value; });
field.addEventListener('input', () => {
if (/^#[0-9a-fA-F]{6}$/.test(field.value.trim())) swatch.value = field.value.trim();
});
});
// Restore tab from hash // Restore tab from hash
window.addEventListener('DOMContentLoaded', function() { window.addEventListener('DOMContentLoaded', function() {
const hash = location.hash.substring(1); const hash = location.hash.substring(1);
@ -551,15 +638,25 @@ async function testCronJob() {
} }
function initTinyMCE() { function initTinyMCE() {
tinymce.init({ if (typeof tinymce === 'undefined') return;
const dark = document.documentElement.getAttribute('data-theme') === 'dark';
const config = {
selector: '.tinymce-editor', selector: '.tinymce-editor',
height: 350, height: 350,
menubar: false, menubar: false,
plugins: ['advlist','autolink','lists','link','searchreplace','visualblocks','code','fullscreen','table','help','wordcount'], plugins: ['advlist','autolink','lists','link','searchreplace','visualblocks','code','fullscreen','table','help','wordcount'],
toolbar: 'undo redo | blocks | bold italic forecolor | alignleft aligncenter alignright | bullist numlist | removeformat | help', toolbar: 'undo redo | blocks | bold italic forecolor | alignleft aligncenter alignright | bullist numlist | removeformat | help',
content_style: 'body { font-family: -apple-system, sans-serif; font-size: 14px; line-height: 1.6; }', content_style: "body { font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; font-size: 14px; line-height: 1.6; }",
skin: dark ? 'oxide-dark' : 'oxide',
content_css: dark ? 'dark' : 'default',
branding: false, promotion: false branding: false, promotion: false
}); };
if (window.TINYMCE_BASE_URL) {
tinymce.baseURL = window.TINYMCE_BASE_URL;
config.base_url = window.TINYMCE_BASE_URL;
config.suffix = '.min';
}
tinymce.init(config);
} }
</script> </script>
</body> </body>

View file

@ -101,50 +101,6 @@ $currentPage = 'sites';
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?= __('sites_title') ?> <?= htmlspecialchars($appTitle) ?></title> <title><?= __('sites_title') ?> <?= htmlspecialchars($appTitle) ?></title>
<?php include __DIR__ . '/../includes/admin_nav.php'; ?> <?php include __DIR__ . '/../includes/admin_nav.php'; ?>
<style>
.page-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 28px; flex-wrap: wrap; gap: 12px; }
.page-title { font-size: 26px; font-weight: 700; color: var(--text-primary); }
.alert { padding: 13px 18px; border-radius: 10px; font-size: 14px; margin-bottom: 20px; }
.alert-error { background: #fee; border: 1px solid #fcc; color: #c33; }
.alert-success { background: #efe; border: 1px solid #cfc; color: #3c3; }
.sites-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(330px, 1fr)); gap: 18px; }
.site-card { background: var(--bg-card); border: 2px solid var(--border-color); border-radius: 14px; padding: 20px; transition: border-color .2s, box-shadow .2s; }
.site-card:hover { border-color: var(--accent); box-shadow: 0 4px 14px rgba(102,126,234,.15); }
.site-card-header { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 14px; }
.site-name { font-size: 17px; font-weight: 600; color: var(--text-primary); margin-bottom: 4px; }
.site-id-label { font-size: 12px; color: var(--text-muted); font-family: monospace; }
.site-info { margin: 14px 0; font-size: 13px; color: var(--text-secondary); }
.site-info-item { display: flex; align-items: center; gap: 8px; margin-bottom: 7px; }
.site-actions { display: flex; gap: 7px; margin-top: 14px; padding-top: 14px; border-top: 1px solid var(--border-color); flex-wrap: wrap; }
.badge { display: inline-block; padding: 3px 9px; border-radius: 5px; font-size: 11px; font-weight: 500; margin: 2px; }
.badge-success { background: #d4edda; color: #155724; }
.badge-warning { background: #fff3cd; color: #856404; }
.badge-info { background: var(--bg-badge-info); color: var(--text-badge-info); }
.btn { padding: 8px 15px; border-radius: 8px; border: none; font-weight: 500; cursor: pointer; text-decoration: none; display: inline-flex; align-items: center; gap: 7px; transition: all .2s; font-size: 13px; }
.btn-primary { background: var(--accent); color: white; }
.btn-primary:hover { background: var(--accent-hover); }
.btn-secondary { background: var(--bg-hover); color: var(--text-secondary); border: 1px solid var(--border-color); }
.btn-secondary:hover { background: var(--border-color); }
.btn-danger { background: var(--danger); color: white; }
.btn-success { background: var(--success); color: white; }
.btn-sm { padding: 6px 11px; font-size: 12px; }
.modal { display: none; position: fixed; inset: 0; background: var(--modal-overlay); z-index: 1000; align-items: center; justify-content: center; }
.modal.active { display: flex; }
.modal-content { background: var(--bg-card); border-radius: 14px; max-width: 580px; width: 90%; max-height: 90vh; overflow-y: auto; border: 1px solid var(--border-color); }
.modal-header { padding: 22px 25px; border-bottom: 1px solid var(--border-color); display: flex; justify-content: space-between; align-items: center; }
.modal-title { font-size: 19px; font-weight: 600; color: var(--text-primary); }
.modal-close { background: none; border: none; font-size: 22px; cursor: pointer; color: var(--text-muted); }
.modal-body { padding: 22px 25px; }
.form-group { margin-bottom: 17px; }
.form-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
label { display: block; margin-bottom: 7px; color: var(--text-secondary); font-weight: 500; font-size: 14px; }
input[type="text"], input[type="password"], input[type="url"] { width: 100%; padding: 11px; border: 2px solid var(--border-color); border-radius: 8px; font-size: 14px; background: var(--bg-input); color: var(--text-primary); transition: border-color .2s; }
input:focus { outline: none; border-color: var(--accent); }
.checkbox-group { display: flex; align-items: center; gap: 10px; }
.checkbox-group input { width: auto; }
.empty-card { background: var(--bg-card); border-radius: 14px; border: 1px solid var(--border-color); padding: 60px 20px; text-align: center; color: var(--text-muted); }
@media(max-width:768px){ .main-content{ margin-left:0!important; } .form-grid{ grid-template-columns:1fr; } }
</style>
<div class="page-header"> <div class="page-header">
<h1 class="page-title"><?= __('sites_title') ?></h1> <h1 class="page-title"><?= __('sites_title') ?></h1>
@ -210,7 +166,7 @@ $currentPage = 'sites';
<?= $site['is_active'] ? __('sites_deactivate') : __('sites_activate') ?> <?= $site['is_active'] ? __('sites_deactivate') : __('sites_activate') ?>
</a> </a>
<a href="?delete=<?= $site['id'] ?>&token=<?= $auth->getCsrfToken() ?>" <a href="?delete=<?= $site['id'] ?>&token=<?= $auth->getCsrfToken() ?>"
class="btn btn-danger btn-sm" class="btn btn-danger-soft btn-sm"
onclick="return confirm('Möchten Sie diese Site wirklich löschen?')"> onclick="return confirm('Möchten Sie diese Site wirklich löschen?')">
<i class="fas fa-trash"></i> <i class="fas fa-trash"></i>
</a> </a>
@ -220,7 +176,7 @@ $currentPage = 'sites';
</div> </div>
<?php endif; ?> <?php endif; ?>
</div><!-- /main-content --> </main>
<!-- Add Site Modal --> <!-- Add Site Modal -->
<div id="addSiteModal" class="modal"> <div id="addSiteModal" class="modal">

View file

@ -100,51 +100,11 @@ $adminBase = '';
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?= __('templates_title') ?> - <?= htmlspecialchars($appTitle) ?></title> <title><?= __('templates_title') ?> - <?= htmlspecialchars($appTitle) ?></title>
<?php include __DIR__ . '/../includes/admin_nav.php'; ?> <?php include __DIR__ . '/../includes/admin_nav.php'; ?>
<style>
.page-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 30px; flex-wrap: wrap; gap: 15px; }
.page-title { font-size: 28px; font-weight: 600; color: var(--text-primary); }
.alert { padding: 14px 20px; border-radius: 10px; margin-bottom: 25px; font-size: 14px; }
.alert-error { background: #fee; border: 1px solid #fcc; color: #c33; }
.alert-success { background: #efe; border: 1px solid #cfc; color: #3c3; }
.card { background: var(--bg-card); border-radius: 15px; box-shadow: 0 2px 10px var(--shadow); border: 1px solid var(--border-color); overflow: hidden; margin-bottom: 25px; }
.card-header { padding: 20px 25px; border-bottom: 1px solid var(--border-color); display: flex; justify-content: space-between; align-items: center; }
.card-title { font-size: 18px; font-weight: 600; color: var(--text-primary); }
.table { width: 100%; border-collapse: collapse; }
.table th { text-align: left; padding: 12px 15px; background: var(--bg-table-head); color: var(--text-secondary); font-weight: 600; font-size: 12px; text-transform: uppercase; letter-spacing: 0.5px; }
.table td { padding: 14px 15px; border-bottom: 1px solid var(--border-color); color: var(--text-primary); font-size: 14px; }
.table tr:last-child td { border-bottom: none; }
.table tr:hover td { background: var(--bg-hover); }
.badge { display: inline-block; padding: 3px 10px; border-radius: 6px; font-size: 11px; font-weight: 500; }
.badge-success { background: #d4edda; color: #155724; }
.badge-secondary { background: var(--bg-hover); color: var(--text-muted); }
.btn-primary { background: var(--accent); color: white; }
.btn-primary:hover { background: var(--accent-hover); }
.btn-danger { background: var(--danger); color: white; }
.btn-small { padding: 6px 12px; font-size: 12px; }
.modal { display: none; position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: var(--modal-overlay); z-index: 500; align-items: center; justify-content: center; }
.modal.active { display: flex; }
.modal-content { background: var(--bg-card); border-radius: 15px; max-width: 520px; width: 90%; max-height: 90vh; overflow-y: auto; }
.modal-header { padding: 22px 25px; border-bottom: 1px solid var(--border-color); display: flex; justify-content: space-between; align-items: center; }
.modal-title { font-size: 18px; font-weight: 600; color: var(--text-primary); }
.modal-close { background: none; border: none; font-size: 22px; cursor: pointer; color: var(--text-muted); }
.modal-body { padding: 25px; }
.form-group { margin-bottom: 18px; }
label { display: block; margin-bottom: 7px; color: var(--text-secondary); font-weight: 500; font-size: 14px; }
input[type="text"], input[type="number"], textarea, select { width: 100%; padding: 11px 14px; border: 2px solid var(--border-color); border-radius: 8px; font-size: 14px; background: var(--bg-input); color: var(--text-primary); transition: border-color 0.2s; font-family: inherit; }
input:focus, textarea:focus { outline: none; border-color: var(--accent); }
.checkbox-group { display: flex; align-items: center; gap: 10px; }
.checkbox-group input { width: auto; accent-color: var(--accent); }
.help-text { font-size: 12px; color: var(--text-muted); margin-top: 4px; }
.empty-state { text-align: center; padding: 60px 20px; color: var(--text-muted); }
.empty-state i { font-size: 48px; margin-bottom: 20px; opacity: 0.3; display: block; }
.duration-badge { display: inline-flex; align-items: center; gap: 5px; background: var(--bg-hover); padding: 3px 10px; border-radius: 20px; font-size: 12px; color: var(--text-secondary); }
</style>
</head>
<div class="page-header"> <div class="page-header">
<div> <div>
<h1 class="page-title"><?= __('templates_title') ?></h1> <h1 class="page-title"><?= __('templates_title') ?></h1>
<p style="color: var(--text-muted); font-size: 14px; margin-top: 5px;"><?= __('templates_subtitle') ?></p> <p class="page-subtitle"><?= __('templates_subtitle') ?></p>
</div> </div>
<button onclick="openAddModal()" class="btn btn-primary"> <button onclick="openAddModal()" class="btn btn-primary">
<i class="fas fa-plus"></i> <?= __('templates_add') ?> <i class="fas fa-plus"></i> <?= __('templates_add') ?>
@ -172,6 +132,7 @@ $adminBase = '';
</div> </div>
<?php else: ?> <?php else: ?>
<div style="overflow-x: auto;"> <div style="overflow-x: auto;">
<div class="table-container">
<table class="table"> <table class="table">
<thead> <thead>
<tr> <tr>
@ -216,17 +177,18 @@ $adminBase = '';
class="btn btn-secondary btn-small"><i class="fas fa-edit"></i></button> class="btn btn-secondary btn-small"><i class="fas fa-edit"></i></button>
<a href="?delete=<?= $t['id'] ?>&token=<?= $auth->getCsrfToken() ?>" <a href="?delete=<?= $t['id'] ?>&token=<?= $auth->getCsrfToken() ?>"
onclick="return confirm('Profil wirklich löschen?')" onclick="return confirm('Profil wirklich löschen?')"
class="btn btn-danger btn-small"><i class="fas fa-trash"></i></a> class="btn btn-danger-soft btn-small"><i class="fas fa-trash"></i></a>
</td> </td>
</tr> </tr>
<?php endforeach; ?> <?php endforeach; ?>
</tbody> </tbody>
</table> </table>
</div> </div>
</div>
<?php endif; ?> <?php endif; ?>
</div> </div>
</div><!-- main-content --> </main>
<!-- Modal: Hinzufügen --> <!-- Modal: Hinzufügen -->
<div id="addModal" class="modal"> <div id="addModal" class="modal">

View file

@ -171,52 +171,6 @@ $currentPage = 'users';
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?= __('users_title') ?> <?= htmlspecialchars($appTitle) ?></title> <title><?= __('users_title') ?> <?= htmlspecialchars($appTitle) ?></title>
<?php include __DIR__ . '/../includes/admin_nav.php'; ?> <?php include __DIR__ . '/../includes/admin_nav.php'; ?>
<style>
.page-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 28px; flex-wrap: wrap; gap: 12px; }
.page-title { font-size: 26px; font-weight: 700; color: var(--text-primary); }
.card { background: var(--bg-card); border-radius: 14px; box-shadow: 0 2px 10px var(--shadow); border: 1px solid var(--border-color); overflow: hidden; margin-bottom: 24px; }
.card-header { padding: 18px 22px; border-bottom: 1px solid var(--border-color); }
.card-title { font-size: 16px; font-weight: 600; color: var(--text-primary); }
.alert { padding: 13px 18px; border-radius: 10px; font-size: 14px; margin-bottom: 20px; }
.alert-error { background: #fee; border: 1px solid #fcc; color: #c33; }
.alert-success { background: #efe; border: 1px solid #cfc; color: #3c3; }
.table { width: 100%; border-collapse: collapse; }
.table th { text-align: left; padding: 12px 15px; background: var(--bg-table-head); color: var(--text-muted); font-weight: 600; font-size: 12px; text-transform: uppercase; letter-spacing: .5px; }
.table td { padding: 13px 15px; border-bottom: 1px solid var(--border-color); color: var(--text-primary); font-size: 14px; }
.table tr:last-child td { border-bottom: none; }
.table tr:hover { background: var(--bg-hover); }
.badge { display: inline-block; padding: 3px 9px; border-radius: 5px; font-size: 11px; font-weight: 500; margin: 2px; }
.badge-success { background: #d4edda; color: #155724; }
.badge-warning { background: #fff3cd; color: #856404; }
.badge-danger { background: #f8d7da; color: #721c24; }
.badge-info { background: var(--bg-badge-info); color: var(--text-badge-info); }
.btn { padding: 8px 16px; border-radius: 8px; border: none; font-weight: 500; cursor: pointer; text-decoration: none; display: inline-flex; align-items: center; gap: 7px; transition: all .2s; font-size: 13px; }
.btn-primary { background: var(--accent); color: white; }
.btn-primary:hover { background: var(--accent-hover); }
.btn-secondary { background: var(--bg-hover); color: var(--text-secondary); border: 1px solid var(--border-color); }
.btn-secondary:hover { background: var(--border-color); }
.btn-danger { background: var(--danger); color: white; }
.btn-warning { background: var(--warning); color: #333; }
.btn-sm { padding: 5px 10px; font-size: 12px; }
.modal { display: none; position: fixed; inset: 0; background: var(--modal-overlay); z-index: 1000; align-items: center; justify-content: center; }
.modal.active { display: flex; }
.modal-content { background: var(--bg-card); border-radius: 14px; max-width: 580px; width: 90%; max-height: 90vh; overflow-y: auto; border: 1px solid var(--border-color); }
.modal-header { padding: 22px 25px; border-bottom: 1px solid var(--border-color); display: flex; justify-content: space-between; align-items: center; }
.modal-title { font-size: 19px; font-weight: 600; color: var(--text-primary); }
.modal-close { background: none; border: none; font-size: 22px; cursor: pointer; color: var(--text-muted); }
.modal-body { padding: 22px 25px; }
.form-group { margin-bottom: 18px; }
label { display: block; margin-bottom: 7px; color: var(--text-secondary); font-weight: 500; font-size: 14px; }
input[type="text"], input[type="email"], input[type="password"] { width: 100%; padding: 11px; border: 2px solid var(--border-color); border-radius: 8px; font-size: 14px; background: var(--bg-input); color: var(--text-primary); transition: border-color .2s; }
input:focus { outline: none; border-color: var(--accent); }
.checkbox-group { display: flex; align-items: center; gap: 10px; margin-bottom: 8px; }
.checkbox-group input { width: auto; }
.site-selection { border: 1px solid var(--border-color); border-radius: 8px; padding: 12px; max-height: 180px; overflow-y: auto; background: var(--bg-hover); }
.empty-state { text-align: center; padding: 50px 20px; color: var(--text-muted); }
.empty-state i { font-size: 40px; margin-bottom: 15px; opacity: .3; display: block; }
.action-btns { display: flex; gap: 5px; flex-wrap: wrap; }
@media(max-width:768px){ .main-content{ margin-left:0!important; } .table th:nth-child(5),.table td:nth-child(5){ display:none; } }
</style>
<div class="page-header"> <div class="page-header">
<h1 class="page-title"><?= __('users_title') ?></h1> <h1 class="page-title"><?= __('users_title') ?></h1>
@ -239,6 +193,7 @@ $currentPage = '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"></i><p><?= __('users_none_found') ?></p></div>
<?php else: ?> <?php else: ?>
<div style="overflow-x:auto;"> <div style="overflow-x:auto;">
<div class="table-container">
<table class="table"> <table class="table">
<thead> <thead>
<tr> <tr>
@ -320,7 +275,7 @@ $currentPage = 'users';
</a> </a>
<?php endif; ?> <?php endif; ?>
<a href="?delete=<?= $user['id'] ?>&token=<?= $auth->getCsrfToken() ?>" <a href="?delete=<?= $user['id'] ?>&token=<?= $auth->getCsrfToken() ?>"
class="btn btn-danger btn-sm" title="<?= __('btn_delete') ?>" class="btn btn-danger-soft btn-sm" title="<?= __('btn_delete') ?>"
onclick="return confirm('Benutzer wirklich löschen?')"> onclick="return confirm('Benutzer wirklich löschen?')">
<i class="fas fa-trash"></i> <i class="fas fa-trash"></i>
</a> </a>
@ -332,11 +287,12 @@ $currentPage = 'users';
</tbody> </tbody>
</table> </table>
</div> </div>
</div>
<?php endif; ?> <?php endif; ?>
</div> </div>
</div> </div>
</div><!-- /main-content --> </main>
<!-- Add User Modal --> <!-- Add User Modal -->
<div id="addUserModal" class="modal"> <div id="addUserModal" class="modal">

View file

@ -130,80 +130,20 @@ $currentPage = 'vouchers';
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?= __('vouchers_title') ?> <?= htmlspecialchars($appTitle) ?></title> <title><?= __('vouchers_title') ?> <?= htmlspecialchars($appTitle) ?></title>
<?php include __DIR__ . '/../includes/admin_nav.php'; ?> <?php include __DIR__ . '/../includes/admin_nav.php'; ?>
<style>
.page-header { margin-bottom: 28px; }
.page-title { font-size: 26px; font-weight: 700; color: var(--text-primary); display: flex; align-items: center; gap: 12px; }
.page-subtitle { color: var(--text-muted); font-size: 14px; margin-top: 4px; }
.live-indicator { display: inline-flex; align-items: center; gap: 6px; font-size: 12px; color: var(--success); }
.live-indicator .dot { width: 8px; height: 8px; background: var(--success); border-radius: 50%; animation: pulse 2s infinite; }
@keyframes pulse { 0%,100%{opacity:1}50%{opacity:.5} }
.card { background: var(--bg-card); border-radius: 14px; box-shadow: 0 2px 10px var(--shadow); border: 1px solid var(--border-color); margin-bottom: 22px; overflow: hidden; }
.card-header { padding: 18px 22px; border-bottom: 1px solid var(--border-color); display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 10px; }
.card-title { font-size: 16px; font-weight: 600; color: var(--text-primary); }
.card-body { padding: 22px; }
.stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px,1fr)); gap: 16px; margin-bottom: 22px; }
.stat-card { background: var(--bg-card); padding: 18px; border-radius: 12px; box-shadow: 0 2px 8px var(--shadow); border: 1px solid var(--border-color); }
.stat-label { color: var(--text-muted); font-size: 13px; margin-bottom: 6px; }
.stat-value { font-size: 28px; font-weight: 700; color: var(--text-primary); }
.stat-value.valid { color: var(--success); }
.stat-value.used { color: var(--warning); }
.stat-value.expired { color: var(--danger); }
.site-selector { display: flex; gap: 12px; align-items: center; flex-wrap: wrap; }
.site-selector select { padding: 10px 14px; border: 2px solid var(--border-color); border-radius: 8px; font-size: 14px; min-width: 220px; cursor: pointer; background: var(--bg-input); color: var(--text-primary); }
.site-selector select:focus { outline: none; border-color: var(--accent); }
.search-bar { padding: 10px 14px; border: 2px solid var(--border-color); border-radius: 8px; font-size: 14px; background: var(--bg-input); color: var(--text-primary); min-width: 200px; }
.search-bar:focus { outline: none; border-color: var(--accent); }
.btn { padding: 9px 18px; border-radius: 8px; border: none; font-weight: 500; cursor: pointer; text-decoration: none; display: inline-flex; align-items: center; gap: 7px; transition: all .2s; font-size: 13px; }
.btn-primary { background: var(--accent); color: white; }
.btn-primary:hover { background: var(--accent-hover); }
.btn-secondary { background: var(--bg-hover); color: var(--text-secondary); border: 1px solid var(--border-color); }
.btn-secondary:hover { background: var(--border-color); }
.btn-danger { background: var(--danger); color: white; }
.btn-danger:hover { opacity: .88; }
.btn-success { background: var(--success); color: white; }
.btn-sm { padding: 5px 11px; font-size: 12px; }
.btn:disabled { opacity: .55; cursor: not-allowed; }
.table-container { overflow-x: auto; }
.table { width: 100%; border-collapse: collapse; }
.table th { text-align: left; padding: 11px 14px; background: var(--bg-table-head); color: var(--text-muted); font-weight: 600; font-size: 11px; text-transform: uppercase; letter-spacing: .5px; white-space: nowrap; }
.table td { padding: 13px 14px; border-bottom: 1px solid var(--border-color); color: var(--text-primary); font-size: 13px; }
.table tr:last-child td { border-bottom: none; }
.table tr:hover { background: var(--bg-hover); }
.table tr.deleting { opacity: .45; pointer-events: none; }
.badge { display: inline-block; padding: 3px 9px; border-radius: 5px; font-size: 11px; font-weight: 500; }
.badge-success { background: #d4edda; color: #155724; }
.badge-warning { background: #fff3cd; color: #856404; }
.badge-danger { background: #f8d7da; color: #721c24; }
.badge-info { background: var(--bg-badge-info); color: var(--text-badge-info); }
code { background: var(--bg-hover); padding: 4px 8px; border-radius: 4px; font-family: 'Courier New', monospace; font-size: 12px; letter-spacing: 1px; }
.empty-state { text-align: center; padding: 60px 20px; color: var(--text-muted); }
.empty-state i { font-size: 42px; margin-bottom: 18px; opacity: .3; display: block; }
.loading { display: flex; align-items: center; justify-content: center; padding: 60px; color: var(--text-muted); gap: 12px; }
.loading i { font-size: 22px; animation: spin 1s linear infinite; }
@keyframes spin { from{transform:rotate(0)}to{transform:rotate(360deg)} }
.filter-row { display: flex; gap: 8px; flex-wrap: wrap; }
.filter-btn { padding: 7px 14px; border: 2px solid var(--border-color); background: var(--bg-card); color: var(--text-secondary); border-radius: 8px; cursor: pointer; font-size: 13px; transition: all .2s; }
.filter-btn:hover { border-color: var(--accent); }
.filter-btn.active { border-color: var(--accent); background: var(--accent); color: white; }
.usage-info { display: flex; align-items: center; gap: 6px; }
.usage-bar { width: 50px; height: 5px; background: var(--border-color); border-radius: 3px; overflow: hidden; }
.usage-bar-fill { height: 100%; background: var(--accent); border-radius: 3px; }
.voucher-note { max-width: 180px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.no-sites-warning { background: #fff3cd; border: 1px solid #ffc107; border-radius: 12px; padding: 30px; text-align: center; color: #856404; }
@media(max-width:768px){ .main-content{ margin-left:0!important; } }
</style>
<div class="page-header"> <div class="page-header">
<div>
<h1 class="page-title"> <h1 class="page-title">
<?= __('vouchers_title') ?> <?= __('vouchers_title') ?>
<span class="live-indicator"><span class="dot"></span>LIVE</span> <span class="live-indicator"><span class="dot"></span>LIVE</span>
</h1> </h1>
<p class="page-subtitle"><?= __('vouchers_subtitle') ?></p> <p class="page-subtitle"><?= __('vouchers_subtitle') ?></p>
</div>
</div> </div>
<?php if (empty($sites)): ?> <?php if (empty($sites)): ?>
<div class="no-sites-warning"> <div class="no-sites-warning">
<i class="fas fa-exclamation-triangle" style="font-size:48px;margin-bottom:15px;opacity:.7;display:block;"></i> <i class="fas fa-triangle-exclamation" style="font-size:26px;margin-bottom:12px;display:block;"></i>
<h3><?= __('vouchers_no_sites') ?></h3> <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"></i> <?= __('nav_sites') ?></a>
</div> </div>
@ -227,7 +167,7 @@ $currentPage = 'vouchers';
<?php endforeach; ?> <?php endforeach; ?>
</select> </select>
<input type="search" id="searchInput" class="search-bar" placeholder="<?= __('vouchers_search') ?>" oninput="filterAndRender()" style="display:none;"> <input type="search" id="searchInput" class="search-bar" placeholder="<?= __('vouchers_search') ?>" oninput="filterAndRender()" style="display:none;">
<button id="refreshBtn" class="btn btn-success" onclick="loadVouchers(true)" disabled> <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"></i> <?= __('btn_refresh') ?>
</button> </button>
</div> </div>
@ -258,7 +198,7 @@ $currentPage = 'vouchers';
</div> </div>
<?php endif; ?> <?php endif; ?>
</div><!-- /main-content --> </main>
<div id="toast-container"></div> <div id="toast-container"></div>
<script src="../assets/global.js"></script> <script src="../assets/global.js"></script>
@ -404,7 +344,7 @@ function renderVouchers() {
<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>${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;"> <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="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 btn-sm" title="<?= __('btn_delete') ?>"><i class="fas fa-trash"></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> </td>
</tr>`; </tr>`;
}); });

File diff suppressed because it is too large Load diff

View file

@ -17,7 +17,12 @@ function updateDarkModeBtn() {
const btn = document.getElementById('darkModeBtn'); const btn = document.getElementById('darkModeBtn');
if (!btn) return; if (!btn) return;
const isDark = document.documentElement.getAttribute('data-theme') === 'dark'; const isDark = document.documentElement.getAttribute('data-theme') === 'dark';
btn.textContent = isDark ? '☀️' : '🌙'; const icon = btn.querySelector('i');
if (icon) {
icon.className = isDark ? 'fas fa-sun' : 'fas fa-moon';
} else {
btn.textContent = isDark ? '☀' : '☾';
}
btn.title = isDark ? 'Light Mode' : 'Dark Mode'; btn.title = isDark ? 'Light Mode' : 'Dark Mode';
} }
@ -42,8 +47,8 @@ document.addEventListener('DOMContentLoaded', updateDarkModeBtn);
const icons = { const icons = {
success: '✓', success: '✓',
error: '✕', error: '✕',
info: '', info: 'i',
warning: '' warning: '!'
}; };
window.showToast = function(type, title, message, duration) { window.showToast = function(type, title, message, duration) {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 220 KiB

After

Width:  |  Height:  |  Size: 377 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 216 KiB

After

Width:  |  Height:  |  Size: 377 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 281 KiB

After

Width:  |  Height:  |  Size: 316 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 312 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 234 KiB

After

Width:  |  Height:  |  Size: 306 KiB

Before After
Before After

Binary file not shown.

After

Width:  |  Height:  |  Size: 464 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

After

Width:  |  Height:  |  Size: 1 MiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

After

Width:  |  Height:  |  Size: 244 KiB

Before After
Before After

Binary file not shown.

After

Width:  |  Height:  |  Size: 315 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 260 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 280 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1,024 KiB

After

Width:  |  Height:  |  Size: 287 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 229 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 972 KiB

After

Width:  |  Height:  |  Size: 302 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

After

Width:  |  Height:  |  Size: 750 KiB

Before After
Before After

Binary file not shown.

After

Width:  |  Height:  |  Size: 411 KiB

View file

@ -83,30 +83,15 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?= __('reset_title') ?> <?= htmlspecialchars($appTitle) ?></title> <title><?= __('reset_title') ?> <?= htmlspecialchars($appTitle) ?></title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<link rel="stylesheet" href="assets/global.css"> <link rel="stylesheet" href="assets/global.css">
<script>(function(){ const t=localStorage.getItem('theme')||'light'; document.documentElement.setAttribute('data-theme',t); })();</script> <script>(function(){ const t=localStorage.getItem('theme')||'light'; document.documentElement.setAttribute('data-theme',t); })();</script>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); min-height: 100vh; display: flex; align-items: center; justify-content: center; padding: 20px; }
.box { background: var(--bg-card); border-radius: 20px; box-shadow: 0 20px 60px var(--shadow-lg); max-width: 420px; width: 100%; padding: 45px 40px; text-align: center; }
.logo { max-width: 180px; height: auto; margin-bottom: 25px; }
h1 { color: var(--text-primary); font-size: 26px; margin-bottom: 8px; }
.subtitle { color: var(--text-muted); font-size: 14px; margin-bottom: 28px; line-height: 1.5; }
.form-group { margin-bottom: 18px; text-align: left; }
label { display: block; margin-bottom: 7px; color: var(--text-secondary); font-weight: 500; font-size: 14px; }
input[type="email"] { width: 100%; padding: 13px; border: 2px solid var(--border-color); border-radius: 10px; font-size: 15px; background: var(--bg-input); color: var(--text-primary); transition: border-color 0.2s; }
input:focus { outline: none; border-color: var(--accent); }
.btn { width: 100%; padding: 14px; background: var(--accent); color: white; border: none; border-radius: 10px; font-size: 15px; font-weight: 600; cursor: pointer; transition: all 0.2s; margin-top: 8px; }
.btn:hover { background: var(--accent-hover); transform: translateY(-2px); }
.alert { padding: 12px 16px; border-radius: 8px; margin-bottom: 20px; font-size: 14px; text-align: left; }
.alert-error { background: #fee; border: 1px solid #fcc; color: #c33; }
.alert-success { background: #efe; border: 1px solid #cfc; color: #3c3; }
.back-link { display: block; margin-top: 22px; color: var(--accent); text-decoration: none; font-size: 14px; }
.back-link:hover { text-decoration: underline; }
</style>
</head> </head>
<body> <body class="app-body focus-page">
<div class="box"> <div class="focus-card card">
<?php if ($logoUrl): ?> <?php if ($logoUrl): ?>
<img src="<?= htmlspecialchars($logoUrl) ?>" alt="Logo" class="logo"> <img src="<?= htmlspecialchars($logoUrl) ?>" alt="Logo" class="logo">
<?php else: ?> <?php else: ?>
@ -129,11 +114,11 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
<label for="email"><?= __('reset_email_label') ?></label> <label for="email"><?= __('reset_email_label') ?></label>
<input type="email" id="email" name="email" required autofocus placeholder="name@example.com"> <input type="email" id="email" name="email" required autofocus placeholder="name@example.com">
</div> </div>
<button type="submit" class="btn"><?= __('reset_send_btn') ?></button> <button type="submit" class="btn btn-primary btn-lg btn-block"><?= __('reset_send_btn') ?></button>
</form> </form>
<?php endif; ?> <?php endif; ?>
<a href="login.php" class="back-link"><?= __('reset_back_login') ?></a> <div class="auth-links"><a href="login.php" class="back-link"><i class="fas fa-arrow-left"></i> <?= __('reset_back_login') ?></a></div>
</div> </div>
<script src="assets/global.js"></script> <script src="assets/global.js"></script>
</body> </body>

View file

@ -8,138 +8,126 @@ $currentPage = $currentPage ?? '';
$faviconUrl = isset($db) ? $db->getSetting('favicon_url', '') : ''; $faviconUrl = isset($db) ? $db->getSetting('favicon_url', '') : '';
$currentUser = isset($auth) ? $auth->getCurrentUser() : null; $currentUser = isset($auth) ? $auth->getCurrentUser() : null;
$lang = I18n::getLanguage(); $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'],
['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]); }
}
}
?> ?>
<?php if ($faviconUrl): ?> <?php if ($faviconUrl): ?>
<link rel="icon" type="image/x-icon" href="<?= htmlspecialchars($faviconUrl) ?>"> <link rel="icon" type="image/x-icon" href="<?= htmlspecialchars($faviconUrl) ?>">
<?php endif; ?> <?php endif; ?>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<link rel="stylesheet" href="<?= $adminBase ?? '../' ?>assets/global.css"> <link rel="stylesheet" href="<?= $rootBase ?>assets/global.css">
<script> <script>
(function(){ (function(){
const t = localStorage.getItem('theme') || 'light'; const t = localStorage.getItem('theme') || 'light';
document.documentElement.setAttribute('data-theme', t); document.documentElement.setAttribute('data-theme', t);
})(); })();
</script> </script>
<style>
/* Base admin layout using CSS variables */
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif; background: var(--bg-body); color: var(--text-primary); }
.header { background: var(--bg-header); border-bottom: 1px solid var(--border-color); padding: 0 30px; height: 70px; display: flex; align-items: center; justify-content: space-between; position: sticky; top: 0; z-index: 150; box-shadow: 0 2px 10px var(--shadow); }
.header-left { display: flex; align-items: center; gap: 12px; }
.header-title { font-size: 20px; font-weight: 600; color: var(--text-primary); }
.header-right { display: flex; align-items: center; gap: 10px; }
.sidebar { position: fixed; left: 0; top: 70px; bottom: 0; width: 260px; background: var(--bg-sidebar); border-right: 1px solid var(--border-color); padding: 20px 0; overflow-y: auto; z-index: 100; }
.sidebar-nav { list-style: none; }
.sidebar-nav li { margin-bottom: 2px; }
.sidebar-nav a { display: flex; align-items: center; gap: 12px; padding: 11px 25px; color: var(--text-secondary); text-decoration: none; transition: all 0.2s; font-size: 14px; border-radius: 0 8px 8px 0; margin-right: 12px; }
.sidebar-nav a:hover, .sidebar-nav a.active { background: var(--bg-hover); color: var(--accent); }
.sidebar-nav i { width: 18px; text-align: center; font-size: 15px; }
.sidebar-section { padding: 16px 25px 6px; font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 1px; color: var(--text-muted); }
.main-content { margin-left: 260px; padding: 30px; min-height: calc(100vh - 70px); }
.user-menu { display: flex; align-items: center; gap: 10px; padding: 6px 12px; background: var(--bg-hover); border-radius: 10px; }
.user-avatar { width: 32px; height: 32px; border-radius: 50%; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); display: flex; align-items: center; justify-content: center; color: white; font-weight: 600; font-size: 14px; flex-shrink: 0; }
.user-name { font-weight: 500; font-size: 13px; color: var(--text-primary); }
.btn { padding: 8px 16px; border-radius: 8px; border: none; font-weight: 500; cursor: pointer; text-decoration: none; display: inline-flex; align-items: center; gap: 7px; transition: all 0.2s; font-size: 13px; }
.btn-secondary { background: var(--bg-hover); color: var(--text-secondary); border: 1px solid var(--border-color); }
.btn-secondary:hover { background: var(--border-color); color: var(--text-primary); }
</style>
</head> </head>
<body> <body>
<!-- Sidebar overlay for mobile -->
<div class="sidebar-overlay" onclick="closeMobileSidebar()"></div> <div class="sidebar-overlay" onclick="closeMobileSidebar()"></div>
<div class="header"> <aside class="sidebar" id="adminSidebar">
<a href="<?= $rootBase ?>index.php" class="sidebar-brand">
<span class="brand-mark"><i class="fas fa-wifi"></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' : '' ?>">
<i class="fas <?= $icon ?>"></i> <?= __($labelKey) ?>
</a>
</li>
<?php endforeach; ?>
</ul>
<?php endforeach; ?>
</nav>
<?php if ($currentUser): ?>
<div class="sidebar-foot">
<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') ?>" style="width:30px;height:30px;font-size:12px;">
<i class="fas fa-arrow-right-from-bracket"></i>
</a>
</div>
</div>
<?php endif; ?>
</aside>
<header class="topbar">
<div class="header-left"> <div class="header-left">
<button class="mobile-menu-btn" onclick="toggleMobileSidebar()" title="Menu"> <button class="mobile-menu-btn" onclick="toggleMobileSidebar()" title="Menu">
<i class="fas fa-bars"></i> <i class="fas fa-bars"></i>
</button> </button>
<div class="header-title"> <div class="breadcrumb">
<i class="fas fa-shield-alt" style="color: var(--accent);"></i> <span><?= __('nav_administration') ?></span>
<?= __('nav_administration') ?> <span class="sep">/</span>
<span class="current"><?= htmlspecialchars($currentLabel) ?></span>
</div> </div>
</div> </div>
<div class="header-right"> <div class="header-right">
<!-- Language switcher -->
<div class="lang-switcher"> <div class="lang-switcher">
<?php foreach (I18n::getAvailable() as $code => $label): ?> <?php foreach (I18n::getAvailable() as $code => $label): ?>
<button class="lang-btn <?= $lang === $code ? 'active' : '' ?>" <button class="lang-btn <?= $lang === $code ? 'active' : '' ?>"
onclick="switchLanguage('<?= $code ?>')"><?= strtoupper($code) ?></button> onclick="switchLanguage('<?= $code ?>')"><?= strtoupper($code) ?></button>
<?php endforeach; ?> <?php endforeach; ?>
</div> </div>
<!-- Dark mode toggle --> <button id="darkModeBtn" class="dark-mode-toggle" onclick="toggleDarkMode()" title="Dark Mode">
<button id="darkModeBtn" class="dark-mode-toggle" onclick="toggleDarkMode()" title="Dark Mode">🌙</button> <i class="fas fa-moon"></i>
<!-- Back link --> </button>
<a href="<?= $adminBase ?? '../' ?>index.php" class="btn btn-secondary"> <a href="<?= $rootBase ?>index.php" class="btn btn-secondary">
<i class="fas fa-arrow-left"></i> <i class="fas fa-arrow-left"></i>
<span class="hide-mobile"><?= __('nav_back') ?></span> <span class="hide-mobile"><?= __('nav_back') ?></span>
</a> </a>
<!-- User menu -->
<?php if ($currentUser): ?>
<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>
</div> </div>
<?php endif; ?> </header>
</div>
</div>
<div class="sidebar" id="adminSidebar"> <main class="main-content">
<nav>
<div class="sidebar-section">Main</div>
<ul class="sidebar-nav">
<li><a href="<?= $adminBase ?? '' ?>index.php" class="<?= $currentPage === 'dashboard' ? 'active' : '' ?>">
<i class="fas fa-home"></i> <?= __('nav_dashboard') ?>
</a></li>
<li><a href="<?= $adminBase ?? '' ?>sites.php" class="<?= $currentPage === 'sites' ? 'active' : '' ?>">
<i class="fas fa-map-marker-alt"></i> <?= __('nav_sites') ?>
</a></li>
<li><a href="<?= $adminBase ?? '' ?>users.php" class="<?= $currentPage === 'users' ? 'active' : '' ?>">
<i class="fas fa-users"></i> <?= __('nav_users') ?>
</a></li>
<li><a href="<?= $adminBase ?? '' ?>vouchers.php" class="<?= $currentPage === 'vouchers' ? 'active' : '' ?>">
<i class="fas fa-ticket-alt"></i> <?= __('nav_vouchers') ?>
</a></li>
</ul>
<div class="sidebar-section" style="margin-top: 10px;">Tools</div>
<ul class="sidebar-nav">
<li><a href="<?= $adminBase ?? '' ?>templates.php" class="<?= $currentPage === 'templates' ? 'active' : '' ?>">
<i class="fas fa-layer-group"></i> <?= __('nav_templates') ?>
</a></li>
<li><a href="<?= $adminBase ?? '' ?>audit_log.php" class="<?= $currentPage === 'audit_log' ? 'active' : '' ?>">
<i class="fas fa-history"></i> <?= __('nav_audit_log') ?>
</a></li>
<li><a href="<?= $adminBase ?? '' ?>reports.php" class="<?= $currentPage === 'reports' ? 'active' : '' ?>">
<i class="fas fa-chart-line"></i> <?= __('nav_reports') ?>
</a></li>
<li><a href="<?= $adminBase ?? '' ?>import.php" class="<?= $currentPage === 'import' ? 'active' : '' ?>">
<i class="fas fa-file-import"></i> <?= __('nav_import') ?>
</a></li>
<li><a href="<?= $adminBase ?? '' ?>settings.php" class="<?= $currentPage === 'settings' ? 'active' : '' ?>">
<i class="fas fa-cog"></i> <?= __('nav_settings') ?>
</a></li>
<li><a href="<?= $adminBase ?? '' ?>api_keys.php" class="<?= $currentPage === 'api_keys' ? 'active' : '' ?>">
<i class="fas fa-key"></i> <?= __('nav_api_keys') ?>
</a></li>
<li><a href="<?= $adminBase ?? '' ?>security.php" class="<?= $currentPage === 'security' ? 'active' : '' ?>">
<i class="fas fa-user-shield"></i> <?= __('nav_security') ?>
</a></li>
<li><a href="<?= $adminBase ?? '' ?>integrations.php" class="<?= $currentPage === 'integrations' ? 'active' : '' ?>">
<i class="fas fa-plug"></i> <?= __('nav_integrations') ?>
</a></li>
<li><a href="<?= $adminBase ?? '' ?>backup.php" class="<?= $currentPage === 'backup' ? 'active' : '' ?>">
<i class="fas fa-database"></i> <?= __('nav_backup') ?>
</a></li>
<li><a href="<?= $adminBase ?? '' ?>update.php" class="<?= $currentPage === 'update' ? 'active' : '' ?>">
<i class="fas fa-sync-alt"></i> <?= __('nav_update') ?>
</a></li>
</ul>
</nav>
</div>
<div class="main-content">
<style>
.hide-mobile { }
@media(max-width:600px) { .hide-mobile { display: none; } }
</style>

175
index.php
View file

@ -284,112 +284,69 @@ function buildPrintCard($template, $data, $instructionHeader, $instructionText,
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?= htmlspecialchars($appTitle) ?></title> <title><?= htmlspecialchars($appTitle) ?></title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<link rel="stylesheet" href="assets/global.css"> <link rel="stylesheet" href="assets/global.css">
<?php if ($captchaMode === 'hcaptcha' && $hcaptchaSiteKey !== ''): ?> <?php if ($captchaMode === 'hcaptcha' && $hcaptchaSiteKey !== ''): ?>
<script src="https://js.hcaptcha.com/1/api.js" async defer></script> <script src="https://js.hcaptcha.com/1/api.js" async defer></script>
<?php endif; ?> <?php endif; ?>
<script>(function(){ const t=localStorage.getItem('theme')||'light'; document.documentElement.setAttribute('data-theme',t); })();</script> <script>(function(){ const t=localStorage.getItem('theme')||'light'; document.documentElement.setAttribute('data-theme',t); })();</script>
<?php if ($voucherCreated): ?> <?php if ($voucherCreated): ?>
<script src="https://cdnjs.cloudflare.com/ajax/libs/qrcodejs/1.0.0/qrcode.min.js" integrity="sha512-CNgIRecGo7nphbeZ04Sc13ka07paqdeTu0WR1IM4kNcpmBAUSHSe2keRB6Q5pBUtIxCY7bQMsVB0ANBpd6JDg==" crossorigin="anonymous" referrerpolicy="no-referrer"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/qrcodejs/1.0.0/qrcode.min.js" integrity="sha512-CNgIRecGo7nphbeZ04Sc13ka07paqdeTu0WR1IM4kNcpmBAUSHSQX0FslNhTDadL4O5SAGapGt4FodqL8My0mA==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<?php endif; ?> <?php endif; ?>
<style> <style>
* { margin: 0; padding: 0; box-sizing: border-box; } /* Seitenspezifisch: Druckansicht der Voucher-Karten.
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); min-height: 100vh; padding: 20px; } Die Druckvorlage ist am Bildschirm ausgeblendet und erscheint nur im Druck. */
.header { max-width: 1200px; margin: 0 auto 30px; display: flex; justify-content: space-between; align-items: center; background: rgba(255,255,255,0.15); backdrop-filter: blur(10px); padding: 15px 25px; border-radius: 15px; } #printArea { display: none; }
.header-title { color: white; font-size: 20px; font-weight: 600; }
.header-right { display: flex; align-items: center; gap: 10px; }
.btn-header { background: white; color: #667eea; padding: 10px 20px; border-radius: 8px; text-decoration: none; font-weight: 500; font-size: 14px; transition: all 0.2s; border: none; cursor: pointer; }
.btn-header:hover { transform: translateY(-2px); box-shadow: 0 4px 12px rgba(0,0,0,0.2); }
.container { max-width: 600px; margin: 0 auto; background: var(--bg-card); border-radius: 20px; box-shadow: 0 20px 60px var(--shadow-lg); padding: 40px; }
h1 { text-align: center; color: var(--text-primary); margin-bottom: 30px; font-size: 28px; }
.logo { max-width: 250px; display: block; margin: 0 auto 30px; }
.alert { padding: 14px; border-radius: 10px; margin-bottom: 25px; font-size: 14px; }
.alert-error { background: #fee; border: 1px solid #fcc; color: #c33; }
.alert-success { background: #efe; border: 1px solid #cfc; color: #3c3; }
.form-group { margin-bottom: 20px; }
label { display: block; margin-bottom: 8px; color: var(--text-secondary); font-weight: 500; font-size: 14px; }
input[type="text"], input[type="number"], input[type="email"], select { width: 100%; padding: 14px; border: 2px solid var(--border-color); border-radius: 10px; font-size: 15px; transition: all 0.2s; background: var(--bg-input); color: var(--text-primary); }
input:focus, select:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px rgba(102,126,234,0.1); }
.btn { width: 100%; padding: 16px; background: var(--accent); color: white; border: none; border-radius: 10px; font-size: 16px; font-weight: 600; cursor: pointer; transition: all 0.2s; }
.btn:hover { background: var(--accent-hover); transform: translateY(-2px); box-shadow: 0 4px 12px rgba(102,126,234,0.4); }
.btn:disabled { background: #ccc; cursor: not-allowed; transform: none; }
.btn-outline { background: transparent; color: var(--accent); border: 2px solid var(--accent); margin-top: 10px; }
.btn-outline:hover { background: var(--accent); color: white; }
.mode-tabs { display: flex; gap: 8px; margin-bottom: 25px; background: var(--bg-hover); padding: 6px; border-radius: 12px; }
.mode-tab { flex: 1; padding: 10px; border: none; border-radius: 8px; cursor: pointer; font-size: 14px; font-weight: 500; background: transparent; color: var(--text-secondary); transition: all 0.2s; }
.mode-tab.active { background: var(--bg-card); color: var(--accent); box-shadow: 0 2px 8px var(--shadow); }
.template-dropdown { margin-bottom: 20px; padding: 15px; background: var(--bg-hover); border-radius: 12px; border: 2px solid var(--border-color); }
.template-dropdown label { color: var(--text-secondary); font-size: 13px; margin-bottom: 6px; }
.template-hint { font-size: 12px; color: var(--text-muted); margin-top: 6px; }
.voucher-result { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 30px; border-radius: 15px; text-align: center; margin-bottom: 25px; }
.voucher-code { font-size: 32px; font-weight: bold; letter-spacing: 2px; margin: 20px 0; font-family: 'Courier New', monospace; cursor: pointer; }
.voucher-code:hover { opacity: 0.85; }
.voucher-info { font-size: 14px; opacity: 0.9; margin-top: 10px; }
.qr-wrapper { display: flex; flex-direction: column; align-items: center; margin: 20px 0 0; }
.qr-wrapper canvas, .qr-wrapper img { border: 6px solid white; border-radius: 8px; }
.qr-label { font-size: 12px; opacity: 0.8; margin-top: 8px; }
.instruction-box { background: var(--bg-hover); padding: 20px; border-radius: 10px; margin-top: 25px; }
.instruction-box h3 { color: var(--text-primary); margin-bottom: 10px; font-size: 16px; }
.instruction-box p, .instruction-box div { color: var(--text-secondary); line-height: 1.6; font-size: 14px; }
.empty-state { text-align: center; padding: 40px; color: var(--text-muted); }
.email-option { background: var(--bg-hover); border: 2px solid var(--border-color); border-radius: 12px; padding: 20px; margin: 20px 0; transition: all 0.2s; }
.email-option.active { background: var(--bg-hover); border-color: var(--accent); }
.email-checkbox-wrapper { display: flex; align-items: center; gap: 12px; cursor: pointer; }
.email-checkbox-wrapper input[type="checkbox"] { width: 20px; height: 20px; cursor: pointer; accent-color: var(--accent); }
.email-checkbox-wrapper label { margin: 0; cursor: pointer; font-size: 15px; font-weight: 600; color: var(--text-primary); display: flex; align-items: center; gap: 8px; }
.email-input-wrapper { max-height: 0; overflow: hidden; opacity: 0; transition: all 0.3s; }
.email-input-wrapper.show { max-height: 120px; opacity: 1; margin-top: 15px; }
.bulk-table { width: 100%; border-collapse: collapse; margin-top: 15px; }
.bulk-table th { background: var(--bg-hover); color: var(--text-secondary); padding: 10px 12px; text-align: left; font-size: 12px; text-transform: uppercase; }
.bulk-table td { padding: 12px; border-bottom: 1px solid var(--border-color); color: var(--text-primary); font-size: 14px; }
.bulk-table tr:last-child td { border-bottom: none; }
.bulk-table code { background: var(--bg-hover); padding: 4px 8px; border-radius: 4px; font-family: monospace; letter-spacing: 1px; cursor: pointer; }
.bulk-table code:hover { opacity: 0.75; }
.copy-hint { font-size: 11px; color: var(--text-muted); margin-top: 4px; }
@keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } }
@media print { @media print {
#printArea { display: block; }
body * { visibility: hidden; } body * { visibility: hidden; }
#printArea, #printArea * { visibility: visible; } #printArea, #printArea * { visibility: visible; }
#printArea { position: absolute; left: 0; top: 0; width: 100%; background: white; } #printArea { position: absolute; left: 0; top: 0; width: 100%; background: #fff; }
.no-print { display: none !important; } .no-print { display: none !important; }
.print-page-break { page-break-after: always; } .print-page-break { page-break-after: always; }
} }
@media (max-width: 480px) {
.container { padding: 25px 20px; border-radius: 15px; }
.header { padding: 12px 15px; }
}
</style> </style>
</head> </head>
<body> <body class="app-body">
<div style="position:fixed;top:15px;right:20px;display:flex;gap:8px;z-index:10;" class="no-print"> <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-name"><?= htmlspecialchars($appTitle) ?></span>
</a>
<div class="app-actions">
<div class="lang-switcher"> <div class="lang-switcher">
<?php foreach (I18n::getAvailable() as $code => $label): ?> <?php foreach (I18n::getAvailable() as $code => $label): ?>
<button class="lang-btn <?= I18n::getLanguage() === $code ? 'active' : '' ?>" <button class="lang-btn <?= I18n::getLanguage() === $code ? 'active' : '' ?>"
onclick="switchLanguage('<?= $code ?>')"><?= strtoupper($code) ?></button> onclick="switchLanguage('<?= $code ?>')"><?= strtoupper($code) ?></button>
<?php endforeach; ?> <?php endforeach; ?>
</div> </div>
<button id="darkModeBtn" class="dark-mode-toggle" onclick="toggleDarkMode()" title="Dark Mode">🌙</button> <button id="darkModeBtn" class="dark-mode-toggle" onclick="toggleDarkMode()" title="Dark Mode">
</div> <i class="fas fa-moon"></i>
</button>
<?php if ($currentUser): ?> <?php if ($currentUser): ?>
<div class="header no-print">
<div class="header-title">👋 <?= __('hello', ['name' => htmlspecialchars($currentUser['name'])]) ?></div>
<div class="header-right">
<?php if ($auth->isAdmin()): ?> <?php if ($auth->isAdmin()): ?>
<a href="admin/" class="btn-header">⚙️ <?= __('nav_administration') ?></a> <a href="admin/" class="btn btn-secondary">
<i class="fas fa-sliders"></i> <span class="hide-mobile"><?= __('nav_administration') ?></span>
</a>
<?php endif; ?> <?php endif; ?>
<a href="logout.php" class="btn-header"><?= __('btn_logout') ?></a> <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>
</div> </div>
</div> <?php elseif ($publicAccess): ?>
<?php elseif ($publicAccess): ?> <a href="login.php" class="btn btn-secondary">
<div class="header no-print"> <i class="fas fa-right-to-bracket"></i> <?= __('btn_login') ?>
<div class="header-title"><?= htmlspecialchars($appTitle) ?></div> </a>
<div class="header-right"> <?php endif; ?>
<a href="login.php" class="btn-header">🔐 <?= __('btn_login') ?></a>
</div> </div>
</div> </header>
<?php endif; ?>
<div class="container"> <div class="container">
<?php if ($logoUrl && !$voucherCreated && !$bulkCreated): ?> <?php if ($logoUrl && !$voucherCreated && !$bulkCreated): ?>
@ -397,7 +354,10 @@ function buildPrintCard($template, $data, $instructionHeader, $instructionText,
<?php endif; ?> <?php endif; ?>
<?php if (!$voucherCreated && !$bulkCreated): ?> <?php if (!$voucherCreated && !$bulkCreated): ?>
<div class="app-card-head">
<h1><?= htmlspecialchars($appTitle) ?></h1> <h1><?= htmlspecialchars($appTitle) ?></h1>
<p><?= __('app_subtitle') ?></p>
</div>
<?php endif; ?> <?php endif; ?>
<?php if ($error): ?> <?php if ($error): ?>
@ -410,13 +370,19 @@ function buildPrintCard($template, $data, $instructionHeader, $instructionText,
</div> </div>
<div class="voucher-result no-print"> <div class="voucher-result no-print">
<div style="font-size:18px;margin-bottom:10px;"><?= __('voucher_success_title') ?></div> <div class="result-label"><i class="fas fa-circle-check"></i> <?= __('voucher_success_title') ?></div>
<div class="voucher-code" id="voucherCode" onclick="copyCode()" title="Klicken zum Kopieren"> <div class="voucher-code" id="voucherCode" onclick="copyCode()" title="Klicken zum Kopieren">
<?= htmlspecialchars($voucherCode) ?> <?= htmlspecialchars($voucherCode) ?>
</div> </div>
<div class="voucher-info"> <div class="voucher-info">
<?= str_replace('{minutes}', $voucherData['expire_min'], __('voucher_validity')) ?> <i class="fas fa-copy"></i> <?= __('voucher_copy_hint') ?>
</div> </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>
</div>
<div class="ticket-divider"></div>
<div class="qr-wrapper no-print"> <div class="qr-wrapper no-print">
<div id="qrcode"></div> <div id="qrcode"></div>
<div class="qr-label"><?= __('voucher_qr_label') ?></div> <div class="qr-label"><?= __('voucher_qr_label') ?></div>
@ -430,15 +396,22 @@ function buildPrintCard($template, $data, $instructionHeader, $instructionText,
</div> </div>
<?php endif; ?> <?php endif; ?>
<form method="get" class="no-print" style="margin-top:15px;"> <div class="no-print" style="display:flex;gap:10px;margin-top:18px;">
<button type="submit" class="btn"><?= __('btn_new_code') ?></button> <form method="get" style="flex:1;">
</form> <button type="submit" class="btn btn-primary btn-lg btn-block">
<button onclick="window.print()" class="btn btn-outline no-print" style="margin-top:10px;"> <i class="fas fa-plus"></i> <?= __('btn_new_code') ?>
🖨️ <?= __('voucher_print_btn') ?>
</button> </button>
</form>
<button onclick="window.print()" class="btn btn-secondary btn-lg">
<i class="fas fa-print"></i> <?= __('voucher_print_btn') ?>
</button>
</div>
<?php elseif ($bulkCreated): ?> <?php elseif ($bulkCreated): ?>
<h1 style="font-size:22px;margin-bottom:20px;"><?= str_replace('{count}', count($bulkVouchers), __('bulk_results')) ?></h1> <div class="app-card-head">
<h1><?= str_replace('{count}', count($bulkVouchers), __('bulk_results')) ?></h1>
<p><?= __('bulk_results_hint') ?></p>
</div>
<div id="printArea"> <div id="printArea">
<?php foreach ($bulkVouchers as $idx => $bv): ?> <?php foreach ($bulkVouchers as $idx => $bv): ?>
@ -472,21 +445,21 @@ function buildPrintCard($template, $data, $instructionHeader, $instructionText,
<?php endforeach; ?> <?php endforeach; ?>
</tbody> </tbody>
</table> </table>
<p class="copy-hint" style="margin-top:8px;">Code anklicken zum Kopieren</p> <p class="copy-hint"><i class="fas fa-copy"></i> <?= __('voucher_copy_hint') ?></p>
</div> </div>
<div class="no-print" style="display:flex;gap:10px;margin-top:20px;"> <div class="no-print" style="display:flex;gap:10px;margin-top:20px;">
<button onclick="window.print()" class="btn" style="flex:1;"> <button onclick="window.print()" class="btn btn-primary btn-lg" style="flex:1;">
🖨️ <?= __('bulk_print_all') ?> <i class="fas fa-print"></i> <?= __('bulk_print_all') ?>
</button> </button>
<form method="get" style="flex:1;"> <form method="get" style="flex:1;">
<button type="submit" class="btn btn-outline" style="width:100%;"><?= __('btn_new_code') ?></button> <button type="submit" class="btn btn-secondary btn-lg btn-block"><?= __('btn_new_code') ?></button>
</form> </form>
</div> </div>
<?php elseif (empty($sites)): ?> <?php elseif (empty($sites)): ?>
<div class="empty-state"> <div class="empty-state">
<div style="font-size:60px;margin-bottom:20px;opacity:0.3;">📶</div> <div class="empty-icon"><i class="fas fa-wifi"></i></div>
<p><?= __('voucher_no_sites') ?><br> <p><?= __('voucher_no_sites') ?><br>
<?php if ($auth->isAdmin()): ?> <?php if ($auth->isAdmin()): ?>
<a href="admin/" style="color:var(--accent);"><?= __('voucher_no_sites_admin') ?></a> <a href="admin/" style="color:var(--accent);"><?= __('voucher_no_sites_admin') ?></a>
@ -570,7 +543,7 @@ function buildPrintCard($template, $data, $instructionHeader, $instructionText,
<div class="email-checkbox-wrapper"> <div class="email-checkbox-wrapper">
<input type="checkbox" id="send_email" name="send_email" onchange="toggleEmailField()"> <input type="checkbox" id="send_email" name="send_email" onchange="toggleEmailField()">
<label for="send_email"> <label for="send_email">
✉️ <?= __('voucher_email_send') ?> <i class="fas fa-envelope" style="color:var(--accent);"></i> <?= __('voucher_email_send') ?>
</label> </label>
</div> </div>
<div class="email-input-wrapper" id="email_field"> <div class="email-input-wrapper" id="email_field">
@ -587,7 +560,7 @@ function buildPrintCard($template, $data, $instructionHeader, $instructionText,
<div class="email-option"> <div class="email-option">
<div class="email-checkbox-wrapper"> <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'"> <input type="checkbox" id="send_sms" name="send_sms" onchange="document.getElementById('sms_field').style.display=this.checked?'block':'none'">
<label for="send_sms">📱 Code per SMS versenden</label> <label for="send_sms"><i class="fas fa-comment-sms" style="color:var(--accent);"></i> Code per SMS versenden</label>
</div> </div>
<div id="sms_field" style="display:none;margin-top:12px;"> <div id="sms_field" style="display:none;margin-top:12px;">
<label for="recipient_phone">Telefonnummer (international, z.B. +49170)</label> <label for="recipient_phone">Telefonnummer (international, z.B. +49170)</label>
@ -596,8 +569,8 @@ function buildPrintCard($template, $data, $instructionHeader, $instructionText,
</div> </div>
<?php endif; ?> <?php endif; ?>
<button type="submit" class="btn" id="submitBtn"> <button type="submit" class="btn btn-primary btn-lg btn-block" id="submitBtn">
<?= __('voucher_create_btn') ?> <i class="fas fa-ticket"></i> <?= __('voucher_create_btn') ?>
</button> </button>
</form> </form>
</div> </div>
@ -617,7 +590,7 @@ function buildPrintCard($template, $data, $instructionHeader, $instructionText,
<label for="bulk_count"><?= __('bulk_quantity') ?></label> <label for="bulk_count"><?= __('bulk_quantity') ?></label>
<input type="number" id="bulk_count" name="bulk_count" <input type="number" id="bulk_count" name="bulk_count"
min="1" max="20" value="5" required> min="1" max="20" value="5" required>
<p style="font-size:12px;color:var(--text-muted);margin-top:5px;"><?= __('bulk_quantity_hint') ?></p> <p class="form-hint"><?= __('bulk_quantity_hint') ?></p>
</div> </div>
<div class="form-group"> <div class="form-group">
@ -646,7 +619,7 @@ function buildPrintCard($template, $data, $instructionHeader, $instructionText,
</select> </select>
</div> </div>
<button type="submit" class="btn" id="bulkSubmitBtn"> <button type="submit" class="btn btn-primary btn-lg btn-block" id="bulkSubmitBtn">
<?= str_replace('{count}', '<span id="bulkCountLabel">5</span>', __('bulk_create_btn')) ?> <?= str_replace('{count}', '<span id="bulkCountLabel">5</span>', __('bulk_create_btn')) ?>
</button> </button>
</form> </form>
@ -670,7 +643,7 @@ function buildPrintCard($template, $data, $instructionHeader, $instructionText,
new QRCode(document.getElementById('qrcode'), { new QRCode(document.getElementById('qrcode'), {
text: '<?= addslashes($voucherCode) ?>', text: '<?= addslashes($voucherCode) ?>',
width: 160, height: 160, width: 160, height: 160,
colorDark: '#ffffff', colorLight: 'transparent', colorDark: '#101625', colorLight: '#ffffff',
correctLevel: QRCode.CorrectLevel.M correctLevel: QRCode.CorrectLevel.M
}); });
}); });
@ -742,7 +715,7 @@ function buildPrintCard($template, $data, $instructionHeader, $instructionText,
const btn = document.getElementById('submitBtn'); const btn = document.getElementById('submitBtn');
if (!btn || btn.disabled) { e.preventDefault(); return; } if (!btn || btn.disabled) { e.preventDefault(); return; }
btn.disabled = true; btn.disabled = true;
btn.innerHTML = ' <?= __('voucher_creating') ?>'; btn.innerHTML = '<i class="fas fa-circle-notch fa-spin"></i> <?= __('voucher_creating') ?>';
}); });
document.getElementById('bulkForm')?.addEventListener('submit', function(e) { document.getElementById('bulkForm')?.addEventListener('submit', function(e) {
@ -750,7 +723,7 @@ function buildPrintCard($template, $data, $instructionHeader, $instructionText,
if (!btn || btn.disabled) { e.preventDefault(); return; } if (!btn || btn.disabled) { e.preventDefault(); return; }
const count = document.getElementById('bulk_count').value; const count = document.getElementById('bulk_count').value;
btn.disabled = true; btn.disabled = true;
btn.innerHTML = ' <?= addslashes(str_replace('{count}', "' + count + '", __('bulk_creating'))) ?>'; btn.innerHTML = '<i class="fas fa-circle-notch fa-spin"></i> <?= addslashes(str_replace('{count}', "' + count + '", __('bulk_creating'))) ?>';
}); });
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function() {

View file

@ -185,153 +185,54 @@ if ($step === 5 && $_SERVER['REQUEST_METHOD'] === 'POST') {
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>UniFi Voucher System - Installation</title> <title>UniFi Voucher System - Installation</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<link rel="stylesheet" href="assets/global.css">
<style> <style>
* { margin: 0; padding: 0; box-sizing: border-box; } /* Installer-spezifisch: Fortschrittsanzeige und Abschnitte */
body { .install-head { display: flex; align-items: center; gap: 14px; margin-bottom: 4px; }
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif; .progress { position: relative; display: flex; justify-content: space-between; margin: 26px 0 28px; }
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
.container {
background: white;
border-radius: 16px;
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
max-width: 600px;
width: 100%;
padding: 40px;
}
h1 { color: #333; margin-bottom: 10px; font-size: 28px; }
h2 { color: #667eea; margin-bottom: 20px; font-size: 20px; font-weight: 500; }
.progress {
display: flex;
justify-content: space-between;
margin: 30px 0;
position: relative;
}
.progress::before { .progress::before {
content: ''; content: ''; position: absolute; top: 14px; left: 14px; right: 14px;
position: absolute; height: 2px; background: var(--border-color); z-index: 0;
top: 15px;
left: 0;
right: 0;
height: 2px;
background: #e0e0e0;
z-index: 0;
} }
.progress-step { .progress-step {
width: 30px; position: relative; z-index: 1;
height: 30px; width: 28px; height: 28px; border-radius: 50%;
border-radius: 50%; display: flex; align-items: center; justify-content: center;
background: #e0e0e0; background: var(--bg-card); border: 1px solid var(--border-color);
display: flex; color: var(--text-muted); font-size: 12px; font-weight: 650;
align-items: center;
justify-content: center;
font-weight: bold;
color: #999;
position: relative;
z-index: 1;
}
.progress-step.active {
background: #667eea;
color: white;
}
.progress-step.completed {
background: #4caf50;
color: white;
}
.form-group {
margin-bottom: 20px;
}
label {
display: block;
margin-bottom: 8px;
color: #555;
font-weight: 500;
}
input[type="text"],
input[type="email"],
input[type="password"],
textarea {
width: 100%;
padding: 12px;
border: 2px solid #e0e0e0;
border-radius: 8px;
font-size: 14px;
transition: border-color 0.3s;
}
input:focus, textarea:focus {
outline: none;
border-color: #667eea;
}
textarea {
resize: vertical;
min-height: 80px;
}
.checkbox-group {
display: flex;
align-items: center;
}
.checkbox-group input {
width: auto;
margin-right: 10px;
}
.btn {
background: #667eea;
color: white;
padding: 14px 30px;
border: none;
border-radius: 8px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: background 0.3s;
width: 100%;
}
.btn:hover {
background: #5568d3;
}
.error {
background: #fee;
border: 1px solid #fcc;
color: #c33;
padding: 12px;
border-radius: 8px;
margin-bottom: 20px;
}
.success {
background: #efe;
border: 1px solid #cfc;
color: #3c3;
padding: 12px;
border-radius: 8px;
margin-bottom: 20px;
}
.help-text {
font-size: 12px;
color: #999;
margin-top: 4px;
} }
.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 { .section {
background: #f8f9fa; padding: 18px;
padding: 20px;
border-radius: 8px;
margin-bottom: 20px; margin-bottom: 20px;
background: var(--bg-subtle);
border: 1px solid var(--border-color);
border-radius: var(--r-md);
} }
.section h3 { .section h3 { font-size: 14px; margin-bottom: 14px; }
margin-bottom: 15px; .error, .success {
color: #333; padding: 12px 15px; margin-bottom: 20px;
font-size: 16px; 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> </style>
</head> </head>
<body> <body class="app-body focus-page">
<div class="container"> <div class="focus-card card">
<h1>🚀 UniFi Voucher System</h1> <div class="install-head">
<h2>Installation</h2> <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">
<div class="progress-step <?= $step >= 1 ? 'completed' : '' ?>">1</div> <div class="progress-step <?= $step >= 1 ? 'completed' : '' ?>">1</div>
@ -354,7 +255,7 @@ if ($step === 5 && $_SERVER['REQUEST_METHOD'] === 'POST') {
<strong> Installation erfolgreich abgeschlossen!</strong><br> <strong> Installation erfolgreich abgeschlossen!</strong><br>
Sie können sich jetzt mit Ihren Admin-Zugangsdaten anmelden. Sie können sich jetzt mit Ihren Admin-Zugangsdaten anmelden.
</div> </div>
<a href="index.php" class="btn">Zum Login</a> <a href="index.php" class="btn btn-primary btn-lg btn-block">Zum Login</a>
<?php elseif ($step === 1): ?> <?php elseif ($step === 1): ?>
<form method="post"> <form method="post">
<input type="hidden" name="step" value="2"> <input type="hidden" name="step" value="2">
@ -382,7 +283,7 @@ if ($step === 5 && $_SERVER['REQUEST_METHOD'] === 'POST') {
<input type="password" name="db_pass"> <input type="password" name="db_pass">
</div> </div>
<button type="submit" class="btn">Weiter </button> <button type="submit" class="btn btn-primary btn-lg btn-block">Weiter <i class="fas fa-arrow-right"></i></button>
</form> </form>
<?php elseif ($step === 2): ?> <?php elseif ($step === 2): ?>
@ -411,7 +312,7 @@ if ($step === 5 && $_SERVER['REQUEST_METHOD'] === 'POST') {
<input type="password" name="admin_password_confirm" required> <input type="password" name="admin_password_confirm" required>
</div> </div>
<button type="submit" class="btn">Weiter </button> <button type="submit" class="btn btn-primary btn-lg btn-block">Weiter <i class="fas fa-arrow-right"></i></button>
</form> </form>
<?php elseif ($step === 3): ?> <?php elseif ($step === 3): ?>
@ -461,7 +362,7 @@ if ($step === 5 && $_SERVER['REQUEST_METHOD'] === 'POST') {
<div class="help-text">Leer lassen, wenn M365-Login nicht verwendet werden soll</div> <div class="help-text">Leer lassen, wenn M365-Login nicht verwendet werden soll</div>
</div> </div>
<button type="submit" class="btn">Weiter </button> <button type="submit" class="btn btn-primary btn-lg btn-block">Weiter <i class="fas fa-arrow-right"></i></button>
</form> </form>
<?php elseif ($step === 4): ?> <?php elseif ($step === 4): ?>
@ -474,7 +375,7 @@ if ($step === 5 && $_SERVER['REQUEST_METHOD'] === 'POST') {
Die Datenbank und alle notwendigen Dateien werden erstellt. Die Datenbank und alle notwendigen Dateien werden erstellt.
</p> </p>
<button type="submit" class="btn">Installation abschließen</button> <button type="submit" class="btn btn-primary btn-lg btn-block">Installation abschließen</button>
</form> </form>
<?php endif; ?> <?php endif; ?>
</div> </div>

View file

@ -1,6 +1,9 @@
<?php <?php
return [ return [
// Navigation // Navigation
'nav_group_overview'=> 'Übersicht',
'nav_group_manage' => 'Verwaltung',
'nav_group_system' => 'System',
'nav_dashboard' => 'Dashboard', 'nav_dashboard' => 'Dashboard',
'nav_sites' => 'Sites verwalten', 'nav_sites' => 'Sites verwalten',
'nav_users' => 'Benutzer verwalten', 'nav_users' => 'Benutzer verwalten',
@ -98,7 +101,9 @@ return [
'voucher_email_hint' => 'gast@example.com', 'voucher_email_hint' => 'gast@example.com',
'voucher_create_btn' => 'Voucher erstellen', 'voucher_create_btn' => 'Voucher erstellen',
'voucher_creating' => 'Erstelle Voucher...', 'voucher_creating' => 'Erstelle Voucher...',
'voucher_success_title' => '✓ Ihr Zugangs-Code', 'voucher_success_title' => 'Zugangs-Code erstellt',
'voucher_copy_hint' => 'Code antippen zum Kopieren',
'app_subtitle' => 'WLAN-Zugangscodes in Sekunden erstellen für Gäste, Teams und Events.',
'voucher_validity' => 'Gültig für {minutes} Minuten ab Erstellung', 'voucher_validity' => 'Gültig für {minutes} Minuten ab Erstellung',
'voucher_qr_label' => 'QR-Code scannen zum Verbinden', 'voucher_qr_label' => 'QR-Code scannen zum Verbinden',
'voucher_print_btn' => 'Code ausdrucken', 'voucher_print_btn' => 'Code ausdrucken',
@ -119,6 +124,7 @@ return [
'bulk_success' => '{count} Vouchers erfolgreich erstellt!', 'bulk_success' => '{count} Vouchers erfolgreich erstellt!',
'bulk_print_all' => 'Alle ausdrucken', 'bulk_print_all' => 'Alle ausdrucken',
'bulk_results' => 'Erstellte Vouchers ({count})', 'bulk_results' => 'Erstellte Vouchers ({count})',
'bulk_results_hint' => 'Alle Codes sind sofort gültig und können gedruckt oder kopiert werden.',
// Templates // Templates
'templates_title' => 'Voucher-Profile', 'templates_title' => 'Voucher-Profile',
@ -214,6 +220,25 @@ return [
// Settings // Settings
'settings_title' => 'Einstellungen', 'settings_title' => 'Einstellungen',
'settings_subtitle' => 'System-Konfiguration und Personalisierung', 'settings_subtitle' => 'System-Konfiguration und Personalisierung',
'settings_tab_login' => 'Login-Seite',
'settings_login_intro' => 'Aussehen und Texte der Anmeldeseite. Leere Felder verwenden die Standardwerte.',
'settings_login_panel' => 'Linke Bildspalte (Split-Screen) anzeigen',
'settings_login_brand' => 'Firmenname',
'settings_login_brand_hint' => 'Leer = Anwendungstitel wird verwendet.',
'settings_login_logo' => 'Logo (URL)',
'settings_login_logo_hint' => 'Leer = allgemeines Logo aus dem Tab „Allgemein".',
'settings_login_claim_title' => 'Überschrift',
'settings_login_claim_text' => 'Beschreibungstext',
'settings_login_features' => 'Stichpunkte',
'settings_login_features_hint'=> 'Ein Stichpunkt pro Zeile. Leer lassen, um keine Liste anzuzeigen.',
'settings_login_footer' => 'Fußzeile',
'settings_login_bg_image' => 'Hintergrundbild (URL)',
'settings_login_bg_image_hint'=> 'Optional. Wird formatfüllend angezeigt; ohne Bild gilt der Farbverlauf.',
'settings_login_bg_from' => 'Verlauf: Startfarbe',
'settings_login_bg_to' => 'Verlauf: Endfarbe',
'settings_login_overlay' => 'Abdunklung des Bildes (%)',
'settings_login_overlay_hint' => '090 %. Höhere Werte machen den Text auf hellen Bildern besser lesbar.',
'settings_login_preview' => 'Vorschau öffnen',
'settings_tab_general' => 'Allgemein', 'settings_tab_general' => 'Allgemein',
'settings_tab_defaults' => 'Voucher-Standards', 'settings_tab_defaults' => 'Voucher-Standards',
'settings_tab_cron' => 'Cron-Sync', 'settings_tab_cron' => 'Cron-Sync',
@ -244,13 +269,18 @@ return [
// Login / Auth // Login / Auth
'login_title' => 'Anmelden', 'login_title' => 'Anmelden',
'auth_claim_title' => 'WLAN-Gastzugänge, zentral verwaltet.',
'auth_claim_text' => 'Vouchers für alle UniFi-Sites erstellen, versenden und auswerten aus einer Oberfläche.',
'auth_feature_1' => 'Voucher per E-Mail, SMS oder QR-Code',
'auth_feature_2' => 'Rollen, 2FA und vollständiges Audit-Log',
'auth_feature_3' => 'Live-Statistiken je Site und Benutzer',
'login_subtitle' => 'Melden Sie sich an, um fortzufahren', 'login_subtitle' => 'Melden Sie sich an, um fortzufahren',
'login_email' => 'E-Mail', 'login_email' => 'E-Mail',
'login_password' => 'Passwort', 'login_password' => 'Passwort',
'login_btn' => 'Anmelden', 'login_btn' => 'Anmelden',
'login_ms' => 'Mit Microsoft anmelden', 'login_ms' => 'Mit Microsoft anmelden',
'login_local' => 'Mit Benutzername und Passwort anmelden', 'login_local' => 'Mit Benutzername und Passwort anmelden',
'login_back' => 'Zurück zur Code-Erstellung', 'login_back' => 'Zurück zur Code-Erstellung',
'login_forgot' => 'Passwort vergessen?', 'login_forgot' => 'Passwort vergessen?',
'login_error_empty' => 'Bitte E-Mail und Passwort eingeben', 'login_error_empty' => 'Bitte E-Mail und Passwort eingeben',
'login_error_rate' => 'Zu viele Fehlversuche. Bitte warten Sie 10 Minuten.', 'login_error_rate' => 'Zu viele Fehlversuche. Bitte warten Sie 10 Minuten.',
@ -262,7 +292,7 @@ return [
'reset_email_label' => 'E-Mail-Adresse', 'reset_email_label' => 'E-Mail-Adresse',
'reset_send_btn' => 'Reset-Link senden', 'reset_send_btn' => 'Reset-Link senden',
'reset_success' => 'Falls ein Konto mit dieser E-Mail existiert, erhalten Sie in Kürze eine E-Mail.', 'reset_success' => 'Falls ein Konto mit dieser E-Mail existiert, erhalten Sie in Kürze eine E-Mail.',
'reset_back_login' => 'Zurück zum Login', 'reset_back_login' => 'Zurück zum Login',
'reset_new_pw' => 'Neues Passwort festlegen', 'reset_new_pw' => 'Neues Passwort festlegen',
'reset_new_pw_label'=> 'Neues Passwort', 'reset_new_pw_label'=> 'Neues Passwort',
'reset_confirm_label'=> 'Passwort bestätigen', 'reset_confirm_label'=> 'Passwort bestätigen',

View file

@ -1,6 +1,9 @@
<?php <?php
return [ return [
// Navigation // Navigation
'nav_group_overview'=> 'Overview',
'nav_group_manage' => 'Management',
'nav_group_system' => 'System',
'nav_dashboard' => 'Dashboard', 'nav_dashboard' => 'Dashboard',
'nav_sites' => 'Manage Sites', 'nav_sites' => 'Manage Sites',
'nav_users' => 'Manage Users', 'nav_users' => 'Manage Users',
@ -98,7 +101,9 @@ return [
'voucher_email_hint' => 'guest@example.com', 'voucher_email_hint' => 'guest@example.com',
'voucher_create_btn' => 'Create Voucher', 'voucher_create_btn' => 'Create Voucher',
'voucher_creating' => 'Creating Voucher...', 'voucher_creating' => 'Creating Voucher...',
'voucher_success_title' => '✓ Your Access Code', 'voucher_success_title' => 'Access code created',
'voucher_copy_hint' => 'Tap the code to copy it',
'app_subtitle' => 'Create Wi-Fi access codes in seconds for guests, teams and events.',
'voucher_validity' => 'Valid for {minutes} minutes from creation', 'voucher_validity' => 'Valid for {minutes} minutes from creation',
'voucher_qr_label' => 'Scan QR code to connect', 'voucher_qr_label' => 'Scan QR code to connect',
'voucher_print_btn' => 'Print Code', 'voucher_print_btn' => 'Print Code',
@ -119,6 +124,7 @@ return [
'bulk_success' => '{count} vouchers created successfully!', 'bulk_success' => '{count} vouchers created successfully!',
'bulk_print_all' => 'Print All', 'bulk_print_all' => 'Print All',
'bulk_results' => 'Created Vouchers ({count})', 'bulk_results' => 'Created Vouchers ({count})',
'bulk_results_hint' => 'All codes are valid immediately and can be printed or copied.',
// Templates // Templates
'templates_title' => 'Voucher Profiles', 'templates_title' => 'Voucher Profiles',
@ -214,6 +220,25 @@ return [
// Settings // Settings
'settings_title' => 'Settings', 'settings_title' => 'Settings',
'settings_subtitle' => 'System configuration and customization', 'settings_subtitle' => 'System configuration and customization',
'settings_tab_login' => 'Login page',
'settings_login_intro' => 'Appearance and wording of the sign-in page. Empty fields fall back to the defaults.',
'settings_login_panel' => 'Show left image column (split screen)',
'settings_login_brand' => 'Company name',
'settings_login_brand_hint' => 'Empty = application title is used.',
'settings_login_logo' => 'Logo (URL)',
'settings_login_logo_hint' => 'Empty = general logo from the "General" tab.',
'settings_login_claim_title' => 'Headline',
'settings_login_claim_text' => 'Description',
'settings_login_features' => 'Bullet points',
'settings_login_features_hint'=> 'One bullet per line. Leave empty to hide the list.',
'settings_login_footer' => 'Footer',
'settings_login_bg_image' => 'Background image (URL)',
'settings_login_bg_image_hint'=> 'Optional. Displayed full-bleed; without an image the gradient is used.',
'settings_login_bg_from' => 'Gradient: start colour',
'settings_login_bg_to' => 'Gradient: end colour',
'settings_login_overlay' => 'Image dimming (%)',
'settings_login_overlay_hint' => '090%. Higher values keep text readable on bright images.',
'settings_login_preview' => 'Open preview',
'settings_tab_general' => 'General', 'settings_tab_general' => 'General',
'settings_tab_defaults' => 'Voucher Defaults', 'settings_tab_defaults' => 'Voucher Defaults',
'settings_tab_cron' => 'Cron Sync', 'settings_tab_cron' => 'Cron Sync',
@ -244,13 +269,18 @@ return [
// Login / Auth // Login / Auth
'login_title' => 'Sign In', 'login_title' => 'Sign In',
'auth_claim_title' => 'Wi-Fi guest access, centrally managed.',
'auth_claim_text' => 'Create, deliver and analyse vouchers for every UniFi site from a single interface.',
'auth_feature_1' => 'Vouchers via email, SMS or QR code',
'auth_feature_2' => 'Roles, 2FA and a complete audit log',
'auth_feature_3' => 'Live statistics per site and user',
'login_subtitle' => 'Sign in to continue', 'login_subtitle' => 'Sign in to continue',
'login_email' => 'Email', 'login_email' => 'Email',
'login_password' => 'Password', 'login_password' => 'Password',
'login_btn' => 'Sign In', 'login_btn' => 'Sign In',
'login_ms' => 'Sign in with Microsoft', 'login_ms' => 'Sign in with Microsoft',
'login_local' => 'Sign in with username and password', 'login_local' => 'Sign in with username and password',
'login_back' => 'Back to Code Creation', 'login_back' => 'Back to Code Creation',
'login_forgot' => 'Forgot password?', 'login_forgot' => 'Forgot password?',
'login_error_empty' => 'Please enter email and password', 'login_error_empty' => 'Please enter email and password',
'login_error_rate' => 'Too many failed attempts. Please wait 10 minutes.', 'login_error_rate' => 'Too many failed attempts. Please wait 10 minutes.',
@ -262,7 +292,7 @@ return [
'reset_email_label' => 'Email Address', 'reset_email_label' => 'Email Address',
'reset_send_btn' => 'Send Reset Link', 'reset_send_btn' => 'Send Reset Link',
'reset_success' => 'If an account with this email exists, you will receive an email shortly.', 'reset_success' => 'If an account with this email exists, you will receive an email shortly.',
'reset_back_login' => 'Back to Login', 'reset_back_login' => 'Back to Login',
'reset_new_pw' => 'Set New Password', 'reset_new_pw' => 'Set New Password',
'reset_new_pw_label'=> 'New Password', 'reset_new_pw_label'=> 'New Password',
'reset_confirm_label'=> 'Confirm Password', 'reset_confirm_label'=> 'Confirm Password',

139
login.php
View file

@ -10,7 +10,10 @@ require_once __DIR__ . '/includes/I18n.php';
try { try {
$auth = new Auth(); $auth = new Auth();
if ($auth->isLoggedIn()) { header('Location: index.php'); exit; } // Angemeldete Admins koennen die Login-Seite mit ?preview=1 ansehen
// (Vorschau aus den Einstellungen), ohne abgemeldet zu werden.
$isPreview = isset($_GET['preview']) && $auth->isLoggedIn() && $auth->isAdmin();
if ($auth->isLoggedIn() && !$isPreview) { header('Location: index.php'); exit; }
} catch (Exception $e) { } catch (Exception $e) {
die('Fehler beim Initialisieren: ' . $e->getMessage()); die('Fehler beim Initialisieren: ' . $e->getMessage());
} }
@ -117,6 +120,34 @@ try {
$showLocalLogin = isset($_GET['local']) && $_GET['local'] === '1'; $showLocalLogin = isset($_GET['local']) && $_GET['local'] === '1';
// --- Individualisierung der Login-Seite -------------------------------
// Alle Werte sind optional; leer bedeutet "Standard verwenden".
$loginBrand = $db->getSetting('login_brand_name', '') ?: $appTitle;
$loginLogo = $db->getSetting('login_logo_url', '') ?: $logoUrl;
$showPanel = $db->getSetting('login_panel_enabled', '1') === '1';
$claimTitle = $db->getSetting('login_claim_title', '') ?: __('auth_claim_title');
$claimText = $db->getSetting('login_claim_text', '') ?: __('auth_claim_text');
$featureRaw = trim((string)$db->getSetting('login_features', ''));
if ($featureRaw !== '') {
$loginFeatures = array_values(array_filter(array_map('trim', preg_split('/\r\n|\r|\n/', $featureRaw))));
} else {
$loginFeatures = [__('auth_feature_1'), __('auth_feature_2'), __('auth_feature_3')];
}
$loginFooter = $db->getSetting('login_footer', '') ?: ('© ' . date('Y') . ' ' . $loginBrand);
$loginBgImage = trim((string)$db->getSetting('login_bg_image', ''));
$loginBgFrom = $db->getSetting('login_bg_from', '') ?: '#3b2f8f';
$loginBgTo = $db->getSetting('login_bg_to', '') ?: '#6d5ce7';
$loginOverlay = max(0, min(90, (int)$db->getSetting('login_bg_overlay', '40')));
$visualStyle = '--login-from:' . htmlspecialchars($loginBgFrom, ENT_QUOTES)
. ';--login-to:' . htmlspecialchars($loginBgTo, ENT_QUOTES)
. ';--login-overlay:' . ($loginOverlay / 100);
if ($loginBgImage !== '') {
$visualStyle .= ";--login-image:url('" . htmlspecialchars($loginBgImage, ENT_QUOTES) . "')";
}
} catch (Exception $e) { } catch (Exception $e) {
die('Datenbankfehler: ' . $e->getMessage()); die('Datenbankfehler: ' . $e->getMessage());
} }
@ -127,57 +158,65 @@ try {
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?= __('login_title') ?> <?= htmlspecialchars($appTitle) ?></title> <title><?= __('login_title') ?> <?= htmlspecialchars($appTitle) ?></title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<link rel="stylesheet" href="assets/global.css"> <link rel="stylesheet" href="assets/global.css">
<script>(function(){ const t=localStorage.getItem('theme')||'light'; document.documentElement.setAttribute('data-theme',t); })();</script> <script>(function(){ const t=localStorage.getItem('theme')||'light'; document.documentElement.setAttribute('data-theme',t); })();</script>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); min-height: 100vh; display: flex; align-items: center; justify-content: center; padding: 20px; }
.login-container { background: var(--bg-card); border-radius: 20px; box-shadow: 0 20px 60px var(--shadow-lg); max-width: 420px; width: 100%; padding: 50px 40px; text-align: center; }
.logo { max-width: 200px; height: auto; margin-bottom: 30px; }
h1 { color: var(--text-primary); font-size: 28px; margin-bottom: 10px; }
.subtitle { color: var(--text-muted); font-size: 14px; margin-bottom: 30px; }
.form-group { margin-bottom: 20px; text-align: left; }
label { display: block; margin-bottom: 8px; color: var(--text-secondary); font-weight: 500; font-size: 14px; }
input[type="email"], input[type="password"] { width: 100%; padding: 14px; border: 2px solid var(--border-color); border-radius: 10px; font-size: 15px; background: var(--bg-input); color: var(--text-primary); transition: all 0.2s; }
input:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px rgba(102,126,234,0.1); }
.btn { width: 100%; padding: 14px; background: var(--accent); color: white; border: none; border-radius: 10px; font-size: 16px; font-weight: 600; cursor: pointer; transition: all 0.2s; margin-top: 10px; }
.btn:hover { background: var(--accent-hover); transform: translateY(-2px); box-shadow: 0 4px 12px rgba(102,126,234,0.4); }
.btn-microsoft { background: #2f2f2f; color: white; border: none; margin-top: 0; text-decoration: none; display: inline-flex; align-items: center; justify-content: center; gap: 12px; padding: 16px 24px; border-radius: 10px; width: 100%; font-size: 15px; font-weight: 500; cursor: pointer; transition: all 0.2s; }
.btn-microsoft:hover { background: #1a1a1a; transform: translateY(-2px); }
.btn-microsoft svg { width: 20px; height: 20px; }
.divider { margin: 22px 0; text-align: center; position: relative; }
.divider::before { content: ''; position: absolute; top: 50%; left: 0; right: 0; height: 1px; background: var(--border-color); }
.divider span { background: var(--bg-card); padding: 0 15px; color: var(--text-muted); font-size: 13px; position: relative; z-index: 1; }
.alert { padding: 12px 16px; border-radius: 8px; margin-bottom: 20px; font-size: 14px; }
.alert-error { background: #fee; border: 1px solid #fcc; color: #c33; }
.alert-success { background: #efe; border: 1px solid #cfc; color: #3c3; }
.back-link { display: block; margin-top: 20px; color: var(--accent); text-decoration: none; font-size: 14px; }
.back-link:hover { text-decoration: underline; }
.local-login-link { display: block; margin-top: 20px; color: var(--text-muted); text-decoration: none; font-size: 13px; }
.local-login-link:hover { color: var(--accent); text-decoration: underline; }
.forgot-link { display: block; margin-top: 12px; text-align: right; color: var(--text-muted); font-size: 13px; text-decoration: none; }
.forgot-link:hover { color: var(--accent); text-decoration: underline; }
.header-tools { position: absolute; top: 20px; right: 20px; display: flex; gap: 8px; }
</style>
</head> </head>
<body> <body class="auth-body<?= $showPanel ? '' : ' auth-body-single' ?>">
<div style="position:fixed;top:15px;right:20px;display:flex;gap:8px;z-index:10;">
<?php if ($showPanel): ?>
<section class="auth-visual<?= $loginBgImage !== '' ? ' has-image' : '' ?>" style="<?= $visualStyle ?>">
<div class="auth-brand">
<?php if ($loginLogo): ?>
<img src="<?= htmlspecialchars($loginLogo) ?>" alt="<?= htmlspecialchars($loginBrand) ?>" class="auth-brand-logo">
<?php else: ?>
<span class="brand-mark"><i class="fas fa-wifi"></i></span>
<span><?= htmlspecialchars($loginBrand) ?></span>
<?php endif; ?>
</div>
<div class="auth-claim">
<?php if ($claimTitle !== ''): ?><h2><?= htmlspecialchars($claimTitle) ?></h2><?php endif; ?>
<?php if ($claimText !== ''): ?><p><?= htmlspecialchars($claimText) ?></p><?php endif; ?>
<?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>
<?php endforeach; ?>
</ul>
<?php endif; ?>
</div>
<div class="auth-foot"><?= htmlspecialchars($loginFooter) ?></div>
</section>
<?php endif; ?>
<section class="auth-panel">
<div class="auth-tools">
<div class="lang-switcher"> <div class="lang-switcher">
<?php foreach (I18n::getAvailable() as $code => $label): ?> <?php foreach (I18n::getAvailable() as $code => $label): ?>
<button class="lang-btn <?= I18n::getLanguage() === $code ? 'active' : '' ?>" <button class="lang-btn <?= I18n::getLanguage() === $code ? 'active' : '' ?>"
onclick="switchLanguage('<?= $code ?>')"><?= strtoupper($code) ?></button> onclick="switchLanguage('<?= $code ?>')"><?= strtoupper($code) ?></button>
<?php endforeach; ?> <?php endforeach; ?>
</div> </div>
<button id="darkModeBtn" class="dark-mode-toggle" onclick="toggleDarkMode()" title="Dark Mode">🌙</button> <button id="darkModeBtn" class="dark-mode-toggle" onclick="toggleDarkMode()" title="Dark Mode">
</div> <i class="fas fa-moon"></i>
</button>
</div>
<div class="login-container"> <div class="login-container">
<?php if ($logoUrl): ?> <?php if (!$showPanel): ?>
<img src="<?= htmlspecialchars($logoUrl) ?>" alt="Logo" class="logo"> <?php if ($loginLogo): ?>
<img src="<?= htmlspecialchars($loginLogo) ?>" alt="<?= htmlspecialchars($loginBrand) ?>" class="logo">
<?php else: ?> <?php else: ?>
<h1><?= htmlspecialchars($appTitle) ?></h1> <div class="auth-mark">
<span class="brand-mark"><i class="fas fa-wifi"></i></span>
<span><?= htmlspecialchars($loginBrand) ?></span>
</div>
<?php endif; ?> <?php endif; ?>
<?php endif; ?>
<h1><?= __('login_title') ?></h1>
<p class="subtitle"><?= __('login_subtitle') ?></p> <p class="subtitle"><?= __('login_subtitle') ?></p>
<?php if ($error): ?> <?php if ($error): ?>
@ -189,20 +228,19 @@ try {
<?php if ($show2fa): ?> <?php if ($show2fa): ?>
<form method="post"> <form method="post">
<p style="color:var(--text-secondary,#666);font-size:14px;margin-bottom:18px;"> <p class="subtitle">
Bitte geben Sie den 6-stelligen Code aus Ihrer Authenticator-App ein Bitte geben Sie den 6-stelligen Code aus Ihrer Authenticator-App ein
oder einen Ihrer Recovery-Codes. oder einen Ihrer Recovery-Codes.
</p> </p>
<div class="form-group"> <div class="form-group">
<label for="totp_code">Code</label> <label for="totp_code">Code</label>
<input type="text" id="totp_code" name="totp_code" maxlength="9" <input type="text" id="totp_code" name="totp_code" maxlength="9" class="code-input"
autocomplete="one-time-code" required autofocus autocomplete="one-time-code" required autofocus
placeholder="123456 oder XXXX-XXXX" placeholder="123456">
style="letter-spacing:3px;text-align:center;font-size:18px;">
</div> </div>
<button type="submit" class="btn">Bestätigen</button> <button type="submit" class="btn btn-primary btn-lg">Bestätigen</button>
</form> </form>
<a href="login.php" class="local-login-link">Abbrechen</a> <div class="auth-links"><a href="login.php" class="local-login-link">Abbrechen</a></div>
<?php elseif ($m365Enabled && !$showLocalLogin): ?> <?php elseif ($m365Enabled && !$showLocalLogin): ?>
<a href="<?= htmlspecialchars($m365LoginUrl) ?>" class="btn-microsoft"> <a href="<?= htmlspecialchars($m365LoginUrl) ?>" class="btn-microsoft">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 23 23"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 23 23">
@ -213,7 +251,7 @@ try {
</svg> </svg>
<?= __('login_ms') ?> <?= __('login_ms') ?>
</a> </a>
<a href="?local=1" class="local-login-link"><?= __('login_local') ?></a> <div class="auth-links"><a href="?local=1" class="local-login-link"><?= __('login_local') ?></a></div>
<?php else: ?> <?php else: ?>
<form method="post"> <form method="post">
<div class="form-group"> <div class="form-group">
@ -227,7 +265,7 @@ try {
<?php if ($smtpEnabled): ?> <?php if ($smtpEnabled): ?>
<a href="forgot_password.php" class="forgot-link"><?= __('login_forgot') ?></a> <a href="forgot_password.php" class="forgot-link"><?= __('login_forgot') ?></a>
<?php endif; ?> <?php endif; ?>
<button type="submit" class="btn"><?= __('login_btn') ?></button> <button type="submit" class="btn btn-primary btn-lg"><?= __('login_btn') ?></button>
</form> </form>
<?php if ($m365Enabled): ?> <?php if ($m365Enabled): ?>
@ -246,15 +284,16 @@ try {
<?php if (!$show2fa && $oidcEnabled): ?> <?php if (!$show2fa && $oidcEnabled): ?>
<div class="divider"><span><?= __('or') ?></span></div> <div class="divider"><span><?= __('or') ?></span></div>
<a href="<?= htmlspecialchars($oidcLoginUrl) ?>" class="btn" style="display:block;text-align:center;text-decoration:none;background:#444;"> <a href="<?= htmlspecialchars($oidcLoginUrl) ?>" class="btn btn-secondary btn-lg">
🔑 <?= htmlspecialchars($oidcName) ?> <i class="fas fa-key"></i> <?= htmlspecialchars($oidcName) ?>
</a> </a>
<?php endif; ?> <?php endif; ?>
<?php if ($publicAccess): ?> <?php if ($publicAccess): ?>
<a href="index.php" class="back-link"><?= __('login_back') ?></a> <div class="auth-links"><a href="index.php" class="back-link"><i class="fas fa-arrow-left"></i> <?= __('login_back') ?></a></div>
<?php endif; ?> <?php endif; ?>
</div> </div>
</section>
<script src="assets/global.js"></script> <script src="assets/global.js"></script>
</body> </body>

View file

@ -120,162 +120,22 @@ try {
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Login - <?= htmlspecialchars($appTitle) ?></title> <title>Login - <?= htmlspecialchars($appTitle) ?></title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<link rel="stylesheet" href="assets/global.css">
<style> <style>
* { margin: 0; padding: 0; box-sizing: border-box; } /* Diagnose-Ausgabe am Seitenende */
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
}
.login-container {
background: white;
border-radius: 20px;
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
max-width: 420px;
width: 100%;
padding: 50px 40px;
text-align: center;
}
.logo {
max-width: 200px;
height: auto;
margin-bottom: 30px;
}
h1 {
color: #333;
font-size: 28px;
margin-bottom: 10px;
}
.subtitle {
color: #666;
font-size: 14px;
margin-bottom: 30px;
}
.form-group {
margin-bottom: 20px;
text-align: left;
}
label {
display: block;
margin-bottom: 8px;
color: #555;
font-weight: 500;
font-size: 14px;
}
input[type="email"],
input[type="password"] {
width: 100%;
padding: 14px;
border: 2px solid #e0e0e0;
border-radius: 10px;
font-size: 15px;
transition: all 0.3s;
}
input:focus {
outline: none;
border-color: #667eea;
box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
}
.btn {
width: 100%;
padding: 14px;
background: #667eea;
color: white;
border: none;
border-radius: 10px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s;
margin-top: 10px;
}
.btn:hover {
background: #5568d3;
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
}
.btn-microsoft {
background: white;
color: #333;
border: 2px solid #e0e0e0;
margin-top: 15px;
text-decoration: none;
display: inline-flex;
align-items: center;
justify-content: center;
}
.btn-microsoft:hover {
background: #f8f9fa;
border-color: #667eea;
transform: translateY(-2px);
text-decoration: none;
}
.divider {
margin: 25px 0;
text-align: center;
position: relative;
}
.divider::before {
content: '';
position: absolute;
top: 50%;
left: 0;
right: 0;
height: 1px;
background: #e0e0e0;
}
.divider span {
background: white;
padding: 0 15px;
color: #999;
font-size: 13px;
position: relative;
z-index: 1;
}
.alert {
padding: 12px;
border-radius: 8px;
margin-bottom: 20px;
font-size: 14px;
}
.alert-error {
background: #fee;
border: 1px solid #fcc;
color: #c33;
}
.alert-success {
background: #efe;
border: 1px solid #cfc;
color: #3c3;
}
.back-link {
display: block;
margin-top: 20px;
color: #667eea;
text-decoration: none;
font-size: 14px;
}
.back-link:hover {
text-decoration: underline;
}
.debug-info { .debug-info {
background: #f8f9fa; margin-top: 22px; padding: 14px;
border: 1px solid #e0e0e0; background: var(--bg-subtle); border: 1px solid var(--border-color); border-radius: var(--r-md);
padding: 15px; font-family: var(--font-mono); font-size: 12px; color: var(--text-secondary); text-align: left;
margin-top: 20px;
border-radius: 8px;
text-align: left;
font-size: 12px;
color: #666;
} }
</style> </style>
</head> </head>
<body> <body class="app-body focus-page">
<div class="login-container"> <div class="focus-card card login-container">
<?php if ($logoUrl): ?> <?php if ($logoUrl): ?>
<img src="<?= htmlspecialchars($logoUrl) ?>" alt="Logo" class="logo"> <img src="<?= htmlspecialchars($logoUrl) ?>" alt="Logo" class="logo">
<?php else: ?> <?php else: ?>
@ -303,18 +163,18 @@ try {
<input type="password" id="password" name="password" required> <input type="password" id="password" name="password" required>
</div> </div>
<button type="submit" class="btn">Anmelden</button> <button type="submit" class="btn btn-primary btn-lg btn-block">Anmelden</button>
</form> </form>
<?php if ($m365Enabled): ?> <?php if ($m365Enabled): ?>
<div class="divider"><span>oder</span></div> <div class="divider"><span>oder</span></div>
<a href="<?= htmlspecialchars($m365LoginUrl) ?>" class="btn btn-microsoft"> <a href="<?= htmlspecialchars($m365LoginUrl) ?>" class="btn btn-microsoft">
🔷 Mit Microsoft 365 anmelden <i class="fab fa-microsoft"></i> Mit Microsoft 365 anmelden
</a> </a>
<?php endif; ?> <?php endif; ?>
<?php if ($publicAccess): ?> <?php if ($publicAccess): ?>
<a href="index.php" class="back-link"> Zurück zur Code-Erstellung</a> <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>
<?php endif; ?> <?php endif; ?>
<!-- Debug Info (kann nach erfolgreicher Einrichtung entfernt werden) --> <!-- Debug Info (kann nach erfolgreicher Einrichtung entfernt werden) -->

View file

@ -32,52 +32,21 @@ $redirectUri = $protocol . '://' . $host . $scriptPath . '/m365_callback.php';
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>M365 Debug</title> <title>M365 Debug</title>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap">
<link rel="stylesheet" href="assets/global.css">
<style> <style>
body { body { padding: 32px 20px; }
font-family: monospace; .section { max-width: 820px; margin: 0 auto 18px; padding: 20px; background: var(--bg-card);
padding: 20px; border: 1px solid var(--border-color); border-radius: var(--r-lg); box-shadow: var(--shadow-sm); }
background: #f5f5f5; .section h2 { font-size: 15px; margin-bottom: 12px; }
} .ok { color: var(--success); font-weight: 600; }
.section { .error { color: var(--danger); font-weight: 600; }
background: white; .info { color: var(--text-muted); }
padding: 20px; .url { padding: 10px 12px; background: var(--bg-subtle); border: 1px solid var(--border-color);
margin-bottom: 20px; border-radius: var(--r-sm); font-family: var(--font-mono); font-size: 12.5px; word-break: break-all; }
border-radius: 8px;
border: 1px solid #ddd;
}
h2 {
margin-top: 0;
color: #333;
}
.ok {
color: green;
font-weight: bold;
}
.error {
color: red;
font-weight: bold;
}
.info {
color: #666;
font-size: 12px;
margin-top: 5px;
}
code {
background: #f0f0f0;
padding: 2px 6px;
border-radius: 3px;
}
.copyable {
background: #f9f9f9;
border: 1px solid #ddd;
padding: 10px;
border-radius: 4px;
margin: 10px 0;
word-break: break-all;
}
</style> </style>
</head> </head>
<body> <body class="app-body">
<h1>🔍 Microsoft 365 OAuth Debug</h1> <h1>🔍 Microsoft 365 OAuth Debug</h1>
<div class="section"> <div class="section">

View file

@ -69,38 +69,25 @@ if ($valid && $_SERVER['REQUEST_METHOD'] === 'POST') {
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?= __('reset_new_pw') ?> <?= htmlspecialchars($appTitle) ?></title> <title><?= __('reset_new_pw') ?> <?= htmlspecialchars($appTitle) ?></title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<link rel="stylesheet" href="assets/global.css"> <link rel="stylesheet" href="assets/global.css">
<script>(function(){ const t=localStorage.getItem('theme')||'light'; document.documentElement.setAttribute('data-theme',t); })();</script> <script>(function(){ const t=localStorage.getItem('theme')||'light'; document.documentElement.setAttribute('data-theme',t); })();</script>
<style> <style>
* { margin: 0; padding: 0; box-sizing: border-box; } .pw-strength { height: 4px; border-radius: var(--r-pill); margin-top: 8px; background: var(--border-color); transition: width .3s, background-color .3s; }
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); min-height: 100vh; display: flex; align-items: center; justify-content: center; padding: 20px; }
.box { background: var(--bg-card); border-radius: 20px; box-shadow: 0 20px 60px var(--shadow-lg); max-width: 420px; width: 100%; padding: 45px 40px; text-align: center; }
.logo { max-width: 180px; height: auto; margin-bottom: 25px; }
h1 { color: var(--text-primary); font-size: 24px; margin-bottom: 8px; }
.subtitle { color: var(--text-muted); font-size: 14px; margin-bottom: 28px; }
.form-group { margin-bottom: 18px; text-align: left; }
label { display: block; margin-bottom: 7px; color: var(--text-secondary); font-weight: 500; font-size: 14px; }
input[type="password"] { width: 100%; padding: 13px; border: 2px solid var(--border-color); border-radius: 10px; font-size: 15px; background: var(--bg-input); color: var(--text-primary); transition: border-color 0.2s; }
input:focus { outline: none; border-color: var(--accent); }
.btn { width: 100%; padding: 14px; background: var(--accent); color: white; border: none; border-radius: 10px; font-size: 15px; font-weight: 600; cursor: pointer; transition: all 0.2s; margin-top: 8px; }
.btn:hover { background: var(--accent-hover); transform: translateY(-2px); }
.alert { padding: 12px 16px; border-radius: 8px; margin-bottom: 20px; font-size: 14px; text-align: left; }
.alert-error { background: #fee; border: 1px solid #fcc; color: #c33; }
.alert-success { background: #efe; border: 1px solid #cfc; color: #3c3; }
.back-link { display: block; margin-top: 22px; color: var(--accent); text-decoration: none; font-size: 14px; }
.back-link:hover { text-decoration: underline; }
.pw-strength { height: 4px; border-radius: 2px; margin-top: 6px; transition: all 0.3s; background: var(--border-color); }
.pw-strength.weak { background: var(--danger); width: 30%; } .pw-strength.weak { background: var(--danger); width: 30%; }
.pw-strength.medium { background: var(--warning); width: 65%; } .pw-strength.medium { background: var(--warning); width: 65%; }
.pw-strength.strong { background: var(--success); width: 100%; } .pw-strength.strong { background: var(--success); width: 100%; }
</style> </style>
</head> </head>
<body> <body class="app-body focus-page">
<div class="box"> <div class="focus-card card">
<?php if ($logoUrl): ?> <?php if ($logoUrl): ?>
<img src="<?= htmlspecialchars($logoUrl) ?>" alt="Logo" class="logo"> <img src="<?= htmlspecialchars($logoUrl) ?>" alt="Logo" class="logo">
<?php else: ?> <?php else: ?>
<div style="font-size:28px;font-weight:700;color:var(--text-primary);margin-bottom:10px;"><?= htmlspecialchars($appTitle) ?></div> <div class="focus-icon" style="margin-bottom:14px;"><i class="fas fa-key"></i></div>
<?php endif; ?> <?php endif; ?>
<h1><?= __('reset_new_pw') ?></h1> <h1><?= __('reset_new_pw') ?></h1>
@ -124,10 +111,10 @@ if ($valid && $_SERVER['REQUEST_METHOD'] === 'POST') {
<label><?= __('reset_confirm_label') ?></label> <label><?= __('reset_confirm_label') ?></label>
<input type="password" name="confirm_password" required> <input type="password" name="confirm_password" required>
</div> </div>
<button type="submit" class="btn"><?= __('reset_set_btn') ?></button> <button type="submit" class="btn btn-primary btn-lg btn-block"><?= __('reset_set_btn') ?></button>
</form> </form>
<?php elseif ($success): ?> <?php elseif ($success): ?>
<a href="login.php" class="btn" style="display:block;text-decoration:none;text-align:center;"><?= __('btn_login') ?></a> <a href="login.php" class="btn btn-primary btn-lg btn-block"><?= __('btn_login') ?></a>
<?php endif; ?> <?php endif; ?>
<a href="login.php" class="back-link"><?= __('reset_back_login') ?></a> <a href="login.php" class="back-link"><?= __('reset_back_login') ?></a>

View file

@ -13,7 +13,15 @@ if (file_exists(__DIR__ . '/config.php')) {
$auth->requireAdmin(); $auth->requireAdmin();
} }
echo "<h1>System Test</h1>"; echo '<!DOCTYPE html><html lang="de"><head><meta charset="UTF-8">'
. '<meta name="viewport" content="width=device-width, initial-scale=1.0">'
. '<title>System-Test</title>'
. '<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap">'
. '<link rel="stylesheet" href="assets/global.css">'
. '<style>.focus-card h2{font-size:14px;margin:22px 0 8px;} .focus-card h2:first-of-type{margin-top:0;}'
. 'pre{white-space:pre-wrap;}</style>'
. '</head><body class="app-body focus-page">'
. '<div class="focus-card card wide"><h1 style="font-size:20px;margin-bottom:18px;">System-Test</h1>';
// 1. PHP Version // 1. PHP Version
echo "<h2>1. PHP Version</h2>"; echo "<h2>1. PHP Version</h2>";
@ -270,6 +278,8 @@ try {
} }
echo "<hr>"; echo "<hr>";
echo "<h2>✓ Test abgeschlossen</h2>"; echo "<h2>Test abgeschlossen</h2>";
echo "<p><a href='login.php'>Zum Login</a> | <a href='index.php'>Zur Startseite</a></p>"; echo "<p><a class='btn btn-secondary' href='login.php'>Zum Login</a> "
. "<a class='btn btn-primary' href='index.php'>Zur Startseite</a></p>";
echo "</div></body></html>";
?> ?>

View file

@ -5,37 +5,87 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="refresh" content="15"> <meta http-equiv="refresh" content="15">
<title>Wartungsmodus</title> <title>Wartungsmodus</title>
<!-- Bewusst vollstaendig eigenstaendig: waehrend des Updates koennen
assets/ und externe Ressourcen kurzzeitig nicht verfuegbar sein. -->
<style> <style>
* { margin: 0; padding: 0; box-sizing: border-box; } *, *::before, *::after { box-sizing: border-box; }
body { body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; margin: 0;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh; min-height: 100vh;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
padding: 20px; padding: 24px;
font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
color: #101625;
background:
radial-gradient(760px 420px at 12% -8%, #eeeefc, transparent 62%),
radial-gradient(620px 420px at 100% 6%, rgba(139,92,246,.12), transparent 64%),
#f6f7f9;
-webkit-font-smoothing: antialiased;
} }
.card { .card {
background: white;
border-radius: 20px;
box-shadow: 0 20px 60px rgba(0,0,0,0.3);
max-width: 460px;
width: 100%; width: 100%;
padding: 48px 40px; max-width: 440px;
padding: 40px 34px;
text-align: center; text-align: center;
background: #fff;
border: 1px solid #e5e8ee;
border-radius: 20px;
box-shadow: 0 28px 60px -24px rgba(16,24,40,.28), 0 8px 24px -12px rgba(16,24,40,.12);
}
.icon {
width: 52px; height: 52px;
margin: 0 auto 22px;
display: flex; align-items: center; justify-content: center;
border-radius: 14px;
background: linear-gradient(135deg, #5b5bd6 0%, #8b5cf6 100%);
box-shadow: 0 10px 24px -10px rgba(91,91,214,.9);
}
.icon svg { width: 24px; height: 24px; fill: none; stroke: #fff; stroke-width: 1.9; stroke-linecap: round; stroke-linejoin: round; }
h1 { margin: 0 0 10px; font-size: 21px; font-weight: 650; letter-spacing: -0.02em; }
p { margin: 0; color: #525c6e; font-size: 14px; line-height: 1.6; }
.bar {
position: relative;
height: 4px;
margin: 26px 0 16px;
border-radius: 999px;
background: #eef0f4;
overflow: hidden;
}
.bar::after {
content: '';
position: absolute; top: 0; left: 0;
width: 38%; height: 100%;
border-radius: 999px;
background: linear-gradient(90deg, #5b5bd6, #8b5cf6);
animation: slide 1.6s ease-in-out infinite;
}
@keyframes slide {
0% { transform: translateX(-100%); }
100% { transform: translateX(300%); }
}
.hint { font-size: 12.5px; color: #8a93a3; }
@media (prefers-color-scheme: dark) {
body { color: #e9ecf3; background: radial-gradient(760px 420px at 12% -8%, #1a1c30, transparent 62%), #0a0c11; }
.card { background: #12151d; border-color: #232838; }
p { color: #a2abbd; }
.bar { background: #1a1f2b; }
.hint { color: #6f7889; }
} }
.icon { font-size: 56px; margin-bottom: 20px; }
h1 { color: #333; font-size: 24px; margin-bottom: 12px; }
p { color: #666; line-height: 1.6; font-size: 15px; }
.hint { margin-top: 24px; font-size: 13px; color: #999; }
</style> </style>
</head> </head>
<body> <body>
<div class="card"> <div class="card">
<div class="icon">🛠️</div> <div class="icon">
<svg viewBox="0 0 24 24" aria-hidden="true">
<path d="M14.7 6.3a4 4 0 0 1 5.3 5.3l-8 8a2.8 2.8 0 0 1-4-4l8-8"/>
<path d="M4 20l2-6"/>
</svg>
</div>
<h1>Kurz mal Wartung</h1> <h1>Kurz mal Wartung</h1>
<p>Es wird gerade ein Update eingespielt. Die Seite ist in wenigen Augenblicken wieder erreichbar.</p> <p>Es wird gerade ein Update eingespielt. Die Seite ist in wenigen Augenblicken wieder erreichbar.</p>
<div class="bar"></div>
<p class="hint">Diese Seite aktualisiert sich automatisch.</p> <p class="hint">Diese Seite aktualisiert sich automatisch.</p>
</div> </div>
</body> </body>

View file

@ -1,7 +1,7 @@
<?php <?php
/** /**
* Admin-UI fuer den Updater. Standalone-Template mit Inline-CSS/JS * Admin-UI fuer den Updater. Nutzt das gemeinsame Design-System
* (das Projekt hat kein gemeinsames Admin-Layout-/Template-System). * (assets/global.css); nur Updater-spezifische Regeln stehen inline.
* *
* Verfuegbare Variablen: $manager, $auth, $currentSha, $channel, $csrfToken * Verfuegbare Variablen: $manager, $auth, $currentSha, $channel, $csrfToken
* *
@ -20,75 +20,58 @@ $channels = \Updater\UpdateManager::CHANNELS;
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>System-Update</title> <title>System-Update</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<link rel="stylesheet" href="../assets/global.css">
<script>(function(){ const t=localStorage.getItem('theme')||'light'; document.documentElement.setAttribute('data-theme',t); })();</script>
<style> <style>
* { margin: 0; padding: 0; box-sizing: border-box; } /* Updater-spezifisch der Rest kommt aus dem gemeinsamen Design-System. */
body { .wrap { max-width: 780px; margin: 0 auto; padding: 36px 0 60px; }
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; .update-head { display: flex; align-items: center; justify-content: space-between; gap: 16px; margin-bottom: 24px; }
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); .update-head .title { display: flex; align-items: center; gap: 13px; }
min-height: 100vh; .kv-row { display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 11px 0; border-bottom: 1px solid var(--border-color); }
padding: 30px 20px; .kv-row:last-of-type { border-bottom: none; }
} .kv-row .k { color: var(--text-secondary); font-size: 13.5px; }
.wrap { max-width: 760px; margin: 0 auto; } .kv-row .v { color: var(--text-primary); font-size: 13.5px; font-weight: 600; font-family: var(--font-mono); }
.topbar { .kv-row .v select { font-family: var(--font-sans); font-weight: 500; width: auto; min-width: 150px; }
display: flex; justify-content: space-between; align-items: center; .progress-wrap { display: none; margin-top: 18px; }
margin-bottom: 24px; .progress-bar { height: 8px; border-radius: var(--r-pill); background: var(--bg-hover); overflow: hidden; }
} .progress-fill { height: 100%; width: 0; border-radius: var(--r-pill); background: var(--accent); transition: width .3s; }
.topbar h1 { color: #fff; font-size: 22px; } .progress-msg { margin-top: 8px; font-size: 12.5px; color: var(--text-muted); }
.topbar a { .alert { display: none; margin: 16px 0 0; }
background: rgba(255,255,255,.18); color: #fff; text-decoration: none; .alert.show { display: flex; }
padding: 9px 16px; border-radius: 8px; font-size: 14px;
}
.card {
background: #fff; border-radius: 16px; padding: 28px;
box-shadow: 0 14px 40px rgba(0,0,0,.2); margin-bottom: 22px;
}
.card h2 { font-size: 16px; color: #333; margin-bottom: 16px; }
.row { display: flex; justify-content: space-between; padding: 8px 0; border-bottom: 1px solid #f0f0f0; }
.row:last-child { border-bottom: none; }
.row .k { color: #777; font-size: 14px; }
.row .v { color: #222; font-size: 14px; font-weight: 600; font-family: monospace; }
.btn {
background: #667eea; color: #fff; border: none; border-radius: 9px;
padding: 12px 20px; font-size: 15px; font-weight: 600; cursor: pointer;
transition: background .2s;
}
.btn:hover { background: #5568d3; }
.btn:disabled { background: #bbb; cursor: not-allowed; }
.btn-green { background: #2e9e5b; }
.btn-green:hover { background: #257d49; }
select {
padding: 10px; border: 2px solid #e0e0e0; border-radius: 8px; font-size: 14px;
}
.muted { color: #888; font-size: 13px; margin-top: 8px; }
.progress-wrap { display: none; margin-top: 16px; }
.progress-bar { height: 14px; background: #eee; border-radius: 8px; overflow: hidden; }
.progress-fill { height: 100%; width: 0; background: #667eea; transition: width .3s; }
.progress-msg { font-size: 13px; color: #555; margin-top: 8px; }
.alert { padding: 12px 14px; border-radius: 9px; font-size: 14px; margin-top: 14px; display: none; }
.alert.show { display: block; }
.alert-error { background: #fee; border: 1px solid #fcc; color: #c33; }
.alert-ok { background: #efe; border: 1px solid #cfc; color: #2a7; }
.changelog { .changelog {
background: #f8f9fb; border-radius: 9px; padding: 14px; margin-top: 14px;
font-size: 13px; color: #444; white-space: pre-wrap; max-height: 260px; overflow: auto;
display: none; display: none;
margin-top: 16px; padding: 14px;
max-height: 260px; overflow: auto;
background: var(--bg-subtle); border: 1px solid var(--border-color); border-radius: var(--r-md);
font-size: 13px; color: var(--text-secondary); white-space: pre-wrap;
} }
.mig-item { display: flex; justify-content: space-between; padding: 6px 0; font-size: 13px; } .mig-item { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 9px 0; border-bottom: 1px solid var(--border-color); font-size: 13.5px; }
.badge { padding: 2px 8px; border-radius: 6px; font-size: 12px; font-weight: 600; } .mig-item:last-child { border-bottom: none; }
.badge-on { background: #e3f6ea; color: #2a7; } .mig-item span:first-child { font-family: var(--font-mono); font-size: 12.5px; }
.badge-off { background: #fdeaea; color: #c33; } .badge-on { background: var(--success-soft); color: var(--success); border: 1px solid var(--success-border); }
.tabs { display: flex; gap: 8px; margin-bottom: 16px; } .badge-off { background: var(--warning-soft); color: var(--warning); border: 1px solid var(--warning-border); }
.tab { padding: 8px 14px; border-radius: 8px; background: #f0f0f0; cursor: pointer; font-size: 14px; } .tabs { display: inline-flex; gap: 3px; padding: 3px; margin-bottom: 18px; background: var(--bg-hover); border: 1px solid var(--border-color); border-radius: var(--r-md); }
.tab.active { background: #667eea; color: #fff; } .tab { padding: 7px 16px; border-radius: var(--r-sm); font-size: 13px; font-weight: 550; color: var(--text-secondary); cursor: pointer; }
.tab.active { background: var(--bg-card); color: var(--text-primary); box-shadow: var(--shadow-xs); font-weight: 600; }
.pane { display: none; } .pane { display: none; }
.pane.active { display: block; } .pane.active { display: block; }
</style> </style>
</head> </head>
<body> <body class="app-body">
<div class="wrap"> <div class="wrap">
<div class="topbar"> <div class="update-head">
<h1>🔄 System-Update</h1> <div class="title">
<a href="/admin/"> Administration</a> <span class="focus-icon"><i class="fas fa-rotate"></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>
</div> </div>
<div class="tabs"> <div class="tabs">
@ -99,8 +82,8 @@ $channels = \Updater\UpdateManager::CHANNELS;
<div class="pane active" id="pane-update"> <div class="pane active" id="pane-update">
<div class="card"> <div class="card">
<h2>Aktuelle Version</h2> <h2>Aktuelle Version</h2>
<div class="row"><span class="k">Installierte Version (SHA)</span><span class="v" id="curSha"><?= htmlspecialchars($shortSha) ?></span></div> <div class="kv-row"><span class="k">Installierte Version (SHA)</span><span class="v" id="curSha"><?= htmlspecialchars($shortSha) ?></span></div>
<div class="row"><span class="k">Update-Channel</span> <div class="kv-row"><span class="k">Update-Channel</span>
<span class="v"> <span class="v">
<select id="channel"> <select id="channel">
<?php foreach ($channels as $key => $url): ?> <?php foreach ($channels as $key => $url): ?>
@ -114,8 +97,8 @@ $channels = \Updater\UpdateManager::CHANNELS;
<div class="card"> <div class="card">
<h2>Updates</h2> <h2>Updates</h2>
<button class="btn" id="btnCheck">Auf Updates prüfen</button> <button class="btn btn-primary" id="btnCheck"><i class="fas fa-magnifying-glass"></i> Auf Updates prüfen</button>
<button class="btn btn-green" id="btnInstall" style="display:none;">Update installieren</button> <button class="btn btn-success" id="btnInstall" style="display:none;margin-left:8px;"><i class="fas fa-download"></i> Update installieren</button>
<div class="changelog" id="changelog"></div> <div class="changelog" id="changelog"></div>
@ -133,7 +116,7 @@ $channels = \Updater\UpdateManager::CHANNELS;
<div class="card"> <div class="card">
<h2>Migrations-Status</h2> <h2>Migrations-Status</h2>
<div id="migList"><p class="muted">Wird geladen </p></div> <div id="migList"><p class="muted">Wird geladen </p></div>
<button class="btn" id="btnRunMig" style="margin-top:16px; display:none;">Ausstehende Migrationen ausführen</button> <button class="btn btn-primary" id="btnRunMig" style="margin-top:16px; display:none;">Ausstehende Migrationen ausführen</button>
<div class="alert alert-ok" id="migAlert"></div> <div class="alert alert-ok" id="migAlert"></div>
</div> </div>
</div> </div>
@ -171,7 +154,7 @@ $('channel').addEventListener('change', async (e) => {
// Auf Updates pruefen // Auf Updates pruefen
$('btnCheck').addEventListener('click', async () => { $('btnCheck').addEventListener('click', async () => {
clearAlerts(); clearAlerts();
$('btnCheck').disabled = true; $('btnCheck').textContent = 'Prüfe …'; $('btnCheck').disabled = true; $('btnCheck').innerHTML = '<i class="fas fa-circle-notch fa-spin"></i> Prüfe …';
try { try {
const r = await fetch('update.php?action=check'); const r = await fetch('update.php?action=check');
const d = await r.json(); const d = await r.json();
@ -193,7 +176,7 @@ $('btnCheck').addEventListener('click', async () => {
} catch (e) { } catch (e) {
showAlert('error', 'Prüfung fehlgeschlagen: ' + e.message); showAlert('error', 'Prüfung fehlgeschlagen: ' + e.message);
} finally { } finally {
$('btnCheck').disabled = false; $('btnCheck').textContent = 'Auf Updates prüfen'; $('btnCheck').disabled = false; $('btnCheck').innerHTML = '<i class="fas fa-magnifying-glass"></i> Auf Updates prüfen';
} }
}); });