Merge pull request 'Redesign: gemeinsames Design-System für Frontend und Backend' (#1) from redesign/design-system into main
95
Readme.md
|
|
@ -8,7 +8,7 @@
|
||||||

|

|
||||||

|

|
||||||

|

|
||||||

|

|
||||||

|

|
||||||
|
|
||||||
</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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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">
|
||||||
<h1 class="page-title"><?= __('audit_title') ?></h1>
|
<div>
|
||||||
<p style="color: var(--text-muted); font-size: 14px;"><?= __('audit_subtitle') ?></p>
|
<h1 class="page-title"><?= __('audit_title') ?></h1>
|
||||||
|
<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>
|
||||||
|
|
@ -197,6 +168,7 @@ $actionLabels = [
|
||||||
<?php endforeach; ?>
|
<?php endforeach; ?>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<?php if ($pages > 1): ?>
|
<?php if ($pages > 1): ?>
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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 & 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 & 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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
146
admin/index.php
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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 & 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 & 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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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">
|
||||||
<p class="sub">Konto: <?= htmlspecialchars($user['email']) ?></p>
|
<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>
|
||||||
|
</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>
|
||||||
|
|
|
||||||
|
|
@ -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">
|
||||||
<h1 class="page-title"><?= __('settings_title') ?></h1>
|
<div>
|
||||||
<p style="color: var(--text-muted); font-size: 14px;"><?= __('settings_subtitle') ?></p>
|
<h1 class="page-title"><?= __('settings_title') ?></h1>
|
||||||
|
<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>
|
||||||
|
|
|
||||||
|
|
@ -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">
|
||||||
|
|
|
||||||
|
|
@ -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">
|
||||||
|
|
|
||||||
|
|
@ -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">
|
||||||
|
|
|
||||||
|
|
@ -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">
|
||||||
<h1 class="page-title">
|
<div>
|
||||||
<?= __('vouchers_title') ?>
|
<h1 class="page-title">
|
||||||
<span class="live-indicator"><span class="dot"></span>LIVE</span>
|
<?= __('vouchers_title') ?>
|
||||||
</h1>
|
<span class="live-indicator"><span class="dot"></span>LIVE</span>
|
||||||
<p class="page-subtitle"><?= __('vouchers_subtitle') ?></p>
|
</h1>
|
||||||
|
<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>`;
|
||||||
});
|
});
|
||||||
|
|
|
||||||
1933
assets/global.css
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 220 KiB After Width: | Height: | Size: 377 KiB |
|
Before Width: | Height: | Size: 216 KiB After Width: | Height: | Size: 377 KiB |
|
Before Width: | Height: | Size: 281 KiB After Width: | Height: | Size: 316 KiB |
|
Before Width: | Height: | Size: 1.1 MiB After Width: | Height: | Size: 312 KiB |
|
Before Width: | Height: | Size: 234 KiB After Width: | Height: | Size: 306 KiB |
BIN
docs/screenshots/login-branding.png
Normal file
|
After Width: | Height: | Size: 464 KiB |
|
Before Width: | Height: | Size: 1.2 MiB After Width: | Height: | Size: 1 MiB |
|
Before Width: | Height: | Size: 1.2 MiB After Width: | Height: | Size: 244 KiB |
BIN
docs/screenshots/settings-login.png
Normal file
|
After Width: | Height: | Size: 315 KiB |
BIN
docs/screenshots/settings.png
Normal file
|
After Width: | Height: | Size: 260 KiB |
|
Before Width: | Height: | Size: 1.1 MiB After Width: | Height: | Size: 280 KiB |
|
Before Width: | Height: | Size: 1,024 KiB After Width: | Height: | Size: 287 KiB |
|
Before Width: | Height: | Size: 1.1 MiB After Width: | Height: | Size: 229 KiB |
|
Before Width: | Height: | Size: 972 KiB After Width: | Height: | Size: 302 KiB |
|
Before Width: | Height: | Size: 1.3 MiB After Width: | Height: | Size: 750 KiB |
BIN
docs/screenshots/vouchers.png
Normal file
|
After Width: | Height: | Size: 411 KiB |
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
|
||||||
<?php endif; ?>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</header>
|
||||||
|
|
||||||
<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>
|
|
||||||
|
|
|
||||||
195
index.php
|
|
@ -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">
|
||||||
<div class="lang-switcher">
|
<a href="index.php" class="brand">
|
||||||
<?php foreach (I18n::getAvailable() as $code => $label): ?>
|
<span class="brand-mark"><i class="fas fa-wifi"></i></span>
|
||||||
<button class="lang-btn <?= I18n::getLanguage() === $code ? 'active' : '' ?>"
|
<span class="brand-name"><?= htmlspecialchars($appTitle) ?></span>
|
||||||
onclick="switchLanguage('<?= $code ?>')"><?= strtoupper($code) ?></button>
|
</a>
|
||||||
<?php endforeach; ?>
|
<div class="app-actions">
|
||||||
</div>
|
<div class="lang-switcher">
|
||||||
<button id="darkModeBtn" class="dark-mode-toggle" onclick="toggleDarkMode()" title="Dark Mode">🌙</button>
|
<?php foreach (I18n::getAvailable() as $code => $label): ?>
|
||||||
</div>
|
<button class="lang-btn <?= I18n::getLanguage() === $code ? 'active' : '' ?>"
|
||||||
|
onclick="switchLanguage('<?= $code ?>')"><?= strtoupper($code) ?></button>
|
||||||
<?php if ($currentUser): ?>
|
<?php endforeach; ?>
|
||||||
<div class="header no-print">
|
</div>
|
||||||
<div class="header-title">👋 <?= __('hello', ['name' => htmlspecialchars($currentUser['name'])]) ?></div>
|
<button id="darkModeBtn" class="dark-mode-toggle" onclick="toggleDarkMode()" title="Dark Mode">
|
||||||
<div class="header-right">
|
<i class="fas fa-moon"></i>
|
||||||
<?php if ($auth->isAdmin()): ?>
|
</button>
|
||||||
<a href="admin/" class="btn-header">⚙️ <?= __('nav_administration') ?></a>
|
<?php if ($currentUser): ?>
|
||||||
|
<?php if ($auth->isAdmin()): ?>
|
||||||
|
<a href="admin/" class="btn btn-secondary">
|
||||||
|
<i class="fas fa-sliders"></i> <span class="hide-mobile"><?= __('nav_administration') ?></span>
|
||||||
|
</a>
|
||||||
|
<?php endif; ?>
|
||||||
|
<div class="user-menu">
|
||||||
|
<div class="user-avatar"><?= strtoupper(mb_substr($currentUser['name'], 0, 1)) ?></div>
|
||||||
|
<div class="user-name hide-mobile"><?= htmlspecialchars($currentUser['name']) ?></div>
|
||||||
|
<a href="logout.php" class="icon-btn" title="<?= __('btn_logout') ?>" style="width:28px;height:28px;font-size:11px;">
|
||||||
|
<i class="fas fa-arrow-right-from-bracket"></i>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<?php elseif ($publicAccess): ?>
|
||||||
|
<a href="login.php" class="btn btn-secondary">
|
||||||
|
<i class="fas fa-right-to-bracket"></i> <?= __('btn_login') ?>
|
||||||
|
</a>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
<a href="logout.php" class="btn-header"><?= __('btn_logout') ?></a>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</header>
|
||||||
<?php elseif ($publicAccess): ?>
|
|
||||||
<div class="header no-print">
|
|
||||||
<div class="header-title"><?= htmlspecialchars($appTitle) ?></div>
|
|
||||||
<div class="header-right">
|
|
||||||
<a href="login.php" class="btn-header">🔐 <?= __('btn_login') ?></a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<?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): ?>
|
||||||
<h1><?= htmlspecialchars($appTitle) ?></h1>
|
<div class="app-card-head">
|
||||||
|
<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() {
|
||||||
|
|
|
||||||
183
install.php
|
|
@ -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>
|
||||||
|
|
|
||||||
36
lang/de.php
|
|
@ -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' => '0–90 %. 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',
|
||||||
|
|
|
||||||
36
lang/en.php
|
|
@ -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' => '0–90%. 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',
|
||||||
|
|
|
||||||
151
login.php
|
|
@ -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;">
|
|
||||||
<div class="lang-switcher">
|
<?php if ($showPanel): ?>
|
||||||
<?php foreach (I18n::getAvailable() as $code => $label): ?>
|
<section class="auth-visual<?= $loginBgImage !== '' ? ' has-image' : '' ?>" style="<?= $visualStyle ?>">
|
||||||
<button class="lang-btn <?= I18n::getLanguage() === $code ? 'active' : '' ?>"
|
<div class="auth-brand">
|
||||||
onclick="switchLanguage('<?= $code ?>')"><?= strtoupper($code) ?></button>
|
<?php if ($loginLogo): ?>
|
||||||
<?php endforeach; ?>
|
<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">
|
||||||
|
<?php foreach (I18n::getAvailable() as $code => $label): ?>
|
||||||
|
<button class="lang-btn <?= I18n::getLanguage() === $code ? 'active' : '' ?>"
|
||||||
|
onclick="switchLanguage('<?= $code ?>')"><?= strtoupper($code) ?></button>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
<button id="darkModeBtn" class="dark-mode-toggle" onclick="toggleDarkMode()" title="Dark Mode">
|
||||||
|
<i class="fas fa-moon"></i>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<button id="darkModeBtn" class="dark-mode-toggle" onclick="toggleDarkMode()" title="Dark Mode">🌙</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): ?>
|
||||||
<?php else: ?>
|
<img src="<?= htmlspecialchars($loginLogo) ?>" alt="<?= htmlspecialchars($loginBrand) ?>" class="logo">
|
||||||
<h1><?= htmlspecialchars($appTitle) ?></h1>
|
<?php else: ?>
|
||||||
|
<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>
|
||||||
|
|
|
||||||
170
login_simple.php
|
|
@ -1,7 +1,7 @@
|
||||||
<?php
|
<?php
|
||||||
// Umfassendes Error Reporting
|
// Umfassendes Error Reporting
|
||||||
error_reporting(E_ALL);
|
error_reporting(E_ALL);
|
||||||
ini_set('display_errors', 0);
|
ini_set('display_errors', 0);
|
||||||
ini_set('log_errors', 1);
|
ini_set('log_errors', 1);
|
||||||
ini_set('log_errors', 1);
|
ini_set('log_errors', 1);
|
||||||
|
|
||||||
|
|
@ -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) -->
|
||||||
|
|
|
||||||
|
|
@ -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">
|
||||||
|
|
|
||||||
|
|
@ -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; }
|
.pw-strength.weak { background: var(--danger); width: 30%; }
|
||||||
.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.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>
|
||||||
|
|
|
||||||
16
test.php
|
|
@ -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>";
|
||||||
?>
|
?>
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||