diff --git a/Readme.md b/Readme.md
index 8cc7143..46a8a84 100644
--- a/Readme.md
+++ b/Readme.md
@@ -10,7 +10,7 @@ Entwickelt von **[Loheide.eu](https://loheide.eu)**



-
+

[](https://git.loheide.cloud/friloo/Unifi-Voucher-Tool)
@@ -29,6 +29,7 @@ Entwickelt von **[Loheide.eu](https://loheide.eu)**
- 🎟️ **Voucher-Erstellung** mit sofortiger QR-Code-Anzeige, Druckvorlage und E-Mail-Versand
- 🖥️ **Display-Seiten (Kiosk)** – öffentliche Seite je Site, an der Gäste sich mit einem Klick selbst einen Zugang holen
+- 🎨 **Jede Display-Seite eigenständig gestaltbar** – Logo, Hintergrundbild, Akzentfarbe, helle oder dunkle Karte
- 📦 **Bulk-Erstellung** – bis zu 20 Vouchers auf einmal, inkl. Sammeldruck-Layout
- 🧩 **Voucher-Profile/Templates** – vordefinierte Laufzeiten & Gerätelimits per Schnellauswahl
- 🏢 **Multi-Site-Support** – beliebig viele UniFi-Standorte zentral verwalten
@@ -88,7 +89,12 @@ Entwickelt von **[Loheide.eu](https://loheide.eu)**
@@ -253,6 +259,23 @@ Auf dem Startbildschirm steht zusätzlich ein QR-Code, der auf dieselbe Seite
zeigt. Gäste können sie damit **am eigenen Handy** öffnen – praktisch bei
Bildschirmen ohne Touch.
+### Jede Seite eigenständig gestalten
+
+Jede Display-Seite bringt ihr eigenes Erscheinungsbild mit – das Hotel am
+Empfang sieht anders aus als der Tagungsraum nebenan:
+
+| Einstellung | Wirkung |
+|---|---|
+| Logo | eigenes Logo auf der Karte (leer = Logo aus den Einstellungen) |
+| Hintergrundbild | formatfĂĽllend hinter der Karte, z. B. ein Foto des Hauses |
+| Abdunklung | 0–90 % dunkle Ebene über dem Bild, damit die Karte lesbar bleibt |
+| Akzentfarbe | färbt den Knopf dieser Seite (leer = Farbe aus dem Design-Tab) |
+| Karte | hell oder dunkel – auf Fotos wirkt die dunkle Karte meist ruhiger |
+
+Logo und Hintergrund lassen sich direkt hochladen (PNG, JPG, WEBP, GIF, SVG bis
+3 MB) oder als URL hinterlegen; beim Löschen einer Display-Seite verschwinden
+die hochgeladenen Dateien mit.
+
Die ausgegebenen Codes erscheinen normal in *Live Vouchers*, im *Reporting* und
im *Audit-Log* (Aktion „Voucher am Display geholt"), sodass jederzeit
nachvollziehbar bleibt, woher ein Zugang stammt.
@@ -660,7 +683,7 @@ dem Git-Hoster.
-**Version 2.7.0** · Autor: **Friederich Loheide** · Lizenz: **MIT**
+**Version 2.8.0** · Autor: **Friederich Loheide** · Lizenz: **MIT**
Entwickelt von **[Loheide.eu](https://loheide.eu)**
diff --git a/VERSION b/VERSION
index 24ba9a3..834f262 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-2.7.0
+2.8.0
diff --git a/admin/kiosks.php b/admin/kiosks.php
index 5597422..50019af 100644
--- a/admin/kiosks.php
+++ b/admin/kiosks.php
@@ -16,6 +16,7 @@ require_once __DIR__ . '/../includes/Auth.php';
require_once __DIR__ . '/../includes/I18n.php';
require_once __DIR__ . '/../includes/Ui.php';
require_once __DIR__ . '/../includes/Kiosk.php';
+require_once __DIR__ . '/../includes/Upload.php';
$auth = new Auth();
$auth->requireAdmin();
@@ -40,9 +41,20 @@ function kioskInput(): array
'cooldown_seconds' => max(0, min(3600, (int)($_POST['cooldown_seconds'] ?? Kiosk::DEFAULT_COOLDOWN))),
'display_seconds' => max(10, min(600, (int)($_POST['display_seconds'] ?? Kiosk::DEFAULT_DISPLAY_SECONDS))),
'is_active' => isset($_POST['is_active']) ? 1 : 0,
+ 'bg_overlay' => max(0, min(90, (int)($_POST['bg_overlay'] ?? 45))),
+ 'accent_color' => self_accent($_POST['accent_color'] ?? ''),
+ 'card_style' => ($_POST['card_style'] ?? 'light') === 'dark' ? 'dark' : 'light',
];
}
+/** Nur echte Hex-Farben durchlassen – der Wert landet in einem style-Attribut. */
+function self_accent($value): ?string
+{
+ $value = strtolower(trim((string)$value));
+
+ return preg_match('/^#[0-9a-f]{6}$/', $value) ? $value : null;
+}
+
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['add_kiosk'])) {
if (!$auth->validateCsrfToken($_POST['csrf_token'] ?? '')) {
$error = __('error_csrf');
@@ -52,12 +64,18 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['add_kiosk'])) {
if ($in['name'] === '') throw new Exception(__('error_name_req'));
if ($in['site_id'] <= 0) throw new Exception(__('error_site_req'));
+ $logo = Upload::resolveField('logo_url', '', 'image');
+ $bg = Upload::resolveField('background_url', '', 'image');
+
$db->execute(
- "INSERT INTO kiosks (site_id, template_id, name, token, headline, subline, daily_limit, cooldown_seconds, display_seconds, is_active, created_by)
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?)",
+ "INSERT INTO kiosks (site_id, template_id, name, token, headline, subline,
+ logo_url, background_url, bg_overlay, accent_color, card_style,
+ daily_limit, cooldown_seconds, display_seconds, is_active, created_by)
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?)",
[$in['site_id'], $in['template_id'], $in['name'], Kiosk::newToken(),
- $in['headline'], $in['subline'], $in['daily_limit'], $in['cooldown_seconds'],
- $in['display_seconds'], $_SESSION['user_id']]
+ $in['headline'], $in['subline'], $logo, $bg, $in['bg_overlay'],
+ $in['accent_color'], $in['card_style'], $in['daily_limit'],
+ $in['cooldown_seconds'], $in['display_seconds'], $_SESSION['user_id']]
);
$auth->writeAuditLog($_SESSION['user_id'], 'kiosk_created', 'kiosk', null, $in['name']);
$success = __('kiosks_added');
@@ -77,11 +95,17 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['edit_kiosk'])) {
if ($in['name'] === '') throw new Exception(__('error_name_req'));
if ($in['site_id'] <= 0) throw new Exception(__('error_site_req'));
+ $current = $db->fetchOne("SELECT logo_url, background_url FROM kiosks WHERE id = ?", [$id]) ?: [];
+ $logo = Upload::resolveField('logo_url', (string)($current['logo_url'] ?? ''), 'image');
+ $bg = Upload::resolveField('background_url', (string)($current['background_url'] ?? ''), 'image');
+
$db->execute(
"UPDATE kiosks SET site_id=?, template_id=?, name=?, headline=?, subline=?,
+ logo_url=?, background_url=?, bg_overlay=?, accent_color=?, card_style=?,
daily_limit=?, cooldown_seconds=?, display_seconds=?, is_active=?
WHERE id=?",
[$in['site_id'], $in['template_id'], $in['name'], $in['headline'], $in['subline'],
+ $logo, $bg, $in['bg_overlay'], $in['accent_color'], $in['card_style'],
$in['daily_limit'], $in['cooldown_seconds'], $in['display_seconds'], $in['is_active'], $id]
);
$auth->writeAuditLog($_SESSION['user_id'], 'kiosk_updated', 'kiosk', $id, $in['name']);
@@ -106,6 +130,11 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['renew_token'])) {
if (isset($_GET['delete'], $_GET['token'])) {
if ($auth->validateCsrfToken($_GET['token'])) {
+ $old = $db->fetchOne("SELECT logo_url, background_url FROM kiosks WHERE id = ?", [(int)$_GET['delete']]);
+ if ($old) {
+ Upload::delete((string)($old['logo_url'] ?? ''));
+ Upload::delete((string)($old['background_url'] ?? ''));
+ }
$db->execute("DELETE FROM kiosks WHERE id = ?", [(int)$_GET['delete']]);
$auth->writeAuditLog($_SESSION['user_id'], 'kiosk_deleted', 'kiosk', (int)$_GET['delete'], '');
$success = __('kiosks_deleted');
@@ -223,6 +252,9 @@ $adminBase = '';
"id" => (int)$k["id"], "site_id" => (int)$k["site_id"],
"template_id" => (int)$k["template_id"], "name" => $k["name"],
"headline" => $k["headline"], "subline" => $k["subline"],
+ "logo_url" => $k["logo_url"], "background_url" => $k["background_url"],
+ "bg_overlay" => (int)$k["bg_overlay"], "accent_color" => $k["accent_color"],
+ "card_style" => $k["card_style"],
"daily_limit" => (int)$k["daily_limit"],
"cooldown_seconds" => (int)$k["cooldown_seconds"],
"display_seconds" => (int)$k["display_seconds"],
@@ -257,7 +289,7 @@ $adminBase = '';
= __('kiosks_add') ?>
×
-