Merge pull request 'Display-Seiten individuell gestalten' (#8) from feature/kiosk-branding into main
27
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)**
|
|||
|
||||
<div align="center">
|
||||
<img src="docs/screenshots/kiosk-display.png" alt="Display-Seite im Ruhezustand" width="48%">
|
||||
<img src="docs/screenshots/kiosk-branded.png" alt="Display-Seite mit eigenem Bild und Farben" width="48%">
|
||||
</div>
|
||||
|
||||
<div align="center">
|
||||
<img src="docs/screenshots/kiosk-code.png" alt="Ausgegebener Zugangscode auf dem Display" width="48%">
|
||||
<img src="docs/screenshots/kiosks-form.png" alt="Display-Seite einrichten" width="48%">
|
||||
</div>
|
||||
|
||||
<div align="center">
|
||||
|
|
@ -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.
|
|||
|
||||
<div align="center">
|
||||
|
||||
**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)**
|
||||
|
||||
|
|
|
|||
2
VERSION
|
|
@ -1 +1 @@
|
|||
2.7.0
|
||||
2.8.0
|
||||
|
|
|
|||
|
|
@ -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 = '';
|
|||
<h2 class="modal-title" id="kioskModalTitle"><?= __('kiosks_add') ?></h2>
|
||||
<button class="modal-close" type="button" onclick="closeModal()" aria-label="<?= __('btn_cancel') ?>">×</button>
|
||||
</div>
|
||||
<form method="post">
|
||||
<form method="post" enctype="multipart/form-data">
|
||||
<div class="modal-body">
|
||||
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($csrf) ?>">
|
||||
<input type="hidden" name="kiosk_id" id="kiosk_id" value="">
|
||||
|
|
@ -305,6 +337,37 @@ $adminBase = '';
|
|||
|
||||
<hr class="section-divider">
|
||||
|
||||
<h3 style="font-size:14px;margin-bottom:14px;"><?= __('kiosks_appearance') ?></h3>
|
||||
|
||||
<?= Ui::imageField('logo_url', __('kiosks_logo'), '', __('kiosks_logo_hint')) ?>
|
||||
<?= Ui::imageField('background_url', __('kiosks_background'), '', __('kiosks_background_hint')) ?>
|
||||
|
||||
<div class="form-grid">
|
||||
<div class="form-group">
|
||||
<label for="bg_overlay"><?= __('kiosks_overlay') ?></label>
|
||||
<input type="number" id="bg_overlay" name="bg_overlay" min="0" max="90" value="45">
|
||||
<div class="help-text"><?= __('kiosks_overlay_hint') ?></div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="accent_color"><?= __('kiosks_accent') ?></label>
|
||||
<div class="color-field">
|
||||
<input type="color" class="color-swatch" data-target="accent_color" value="<?= Ui::DEFAULT_ACCENT ?>">
|
||||
<input type="text" id="accent_color" name="accent_color" placeholder="<?= __('kiosks_accent_default') ?>">
|
||||
</div>
|
||||
<div class="help-text"><?= __('kiosks_accent_hint') ?></div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="card_style"><?= __('kiosks_card_style') ?></label>
|
||||
<select id="card_style" name="card_style">
|
||||
<option value="light"><?= __('kiosks_card_light') ?></option>
|
||||
<option value="dark"><?= __('kiosks_card_dark') ?></option>
|
||||
</select>
|
||||
<div class="help-text"><?= __('kiosks_card_hint') ?></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr class="section-divider">
|
||||
|
||||
<div class="form-grid">
|
||||
<div class="form-group">
|
||||
<label for="daily_limit"><?= __('kiosks_daily_limit') ?></label>
|
||||
|
|
@ -364,6 +427,8 @@ function openAddModal() {
|
|||
document.getElementById('kioskModalTitle').textContent = <?= json_encode(__('kiosks_add')) ?>;
|
||||
document.querySelector('#kioskModal form').reset();
|
||||
document.getElementById('kiosk_id').value = '';
|
||||
setImageField('logo_url', '');
|
||||
setImageField('background_url', '');
|
||||
document.getElementById('submitAdd').style.display = '';
|
||||
document.getElementById('submitEdit').style.display = 'none';
|
||||
document.getElementById('activeRow').style.display = 'none';
|
||||
|
|
@ -378,6 +443,11 @@ function openEditModal(data) {
|
|||
document.getElementById('template_id').value = data.template_id || 0;
|
||||
document.getElementById('headline').value = data.headline || '';
|
||||
document.getElementById('subline').value = data.subline || '';
|
||||
document.getElementById('bg_overlay').value = data.bg_overlay;
|
||||
document.getElementById('accent_color').value = data.accent_color || '';
|
||||
document.getElementById('card_style').value = data.card_style || 'light';
|
||||
setImageField('logo_url', data.logo_url || '');
|
||||
setImageField('background_url', data.background_url || '');
|
||||
document.getElementById('daily_limit').value = data.daily_limit;
|
||||
document.getElementById('cooldown_seconds').value = data.cooldown_seconds;
|
||||
document.getElementById('display_seconds').value = data.display_seconds;
|
||||
|
|
@ -390,6 +460,25 @@ function openEditModal(data) {
|
|||
|
||||
function closeModal() { document.getElementById('kioskModal').classList.remove('active'); }
|
||||
|
||||
/**
|
||||
* Bildfeld im Modal auf den Wert des Kiosks setzen: Vorschau, URL-Feld und
|
||||
* der Entfernen-Schalter hängen am selben Namen.
|
||||
*/
|
||||
function setImageField(name, value) {
|
||||
const wrapper = document.querySelector('[name="' + name + '"]').closest('.form-group');
|
||||
const text = wrapper.querySelector('input[type="text"]');
|
||||
const preview = wrapper.querySelector('.image-preview');
|
||||
const remove = wrapper.querySelector('input[type="checkbox"]');
|
||||
if (text) text.value = value;
|
||||
if (remove) remove.checked = false;
|
||||
if (preview) {
|
||||
const src = value && !/^https?:|^\//.test(value) ? '../' + value : value;
|
||||
preview.innerHTML = value
|
||||
? '<img src="' + src + '" alt="">'
|
||||
: '<i class="fas fa-image" aria-hidden="true"></i>';
|
||||
}
|
||||
}
|
||||
|
||||
function showQr(url, name) {
|
||||
var target = document.getElementById('qrTarget');
|
||||
target.innerHTML = '';
|
||||
|
|
|
|||
|
|
@ -42,33 +42,6 @@ if (isset($_POST['ajax_smtp_test'])) {
|
|||
$error = '';
|
||||
$success = '';
|
||||
|
||||
/**
|
||||
* Liefert den neuen Wert eines Bildfeldes: Upload schlaegt URL, und ein
|
||||
* gesetzter Entfernen-Schalter loescht die bisherige Datei.
|
||||
*/
|
||||
function resolveImageField(string $name, Database $db, string $kind): string
|
||||
{
|
||||
$current = (string)$db->getSetting($name, '');
|
||||
|
||||
$uploaded = Upload::store($_FILES[$name . '_file'] ?? [], $kind);
|
||||
if ($uploaded !== '') {
|
||||
Upload::delete($current);
|
||||
return $uploaded;
|
||||
}
|
||||
|
||||
if (!empty($_POST[$name . '_remove'])) {
|
||||
Upload::delete($current);
|
||||
return '';
|
||||
}
|
||||
|
||||
$value = trim($_POST[$name] ?? '');
|
||||
if ($value !== $current && Upload::isLocal($current) && !Upload::isLocal($value)) {
|
||||
Upload::delete($current);
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
// Einstellungen speichern
|
||||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['save_settings'])) {
|
||||
if (!$auth->validateCsrfToken($_POST['csrf_token'] ?? '')) {
|
||||
|
|
@ -80,8 +53,8 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['save_settings'])) {
|
|||
|
||||
if ($formType === 'general') {
|
||||
$settings['app_title'] = trim($_POST['app_title'] ?? '');
|
||||
$settings['logo_url'] = resolveImageField('logo_url', $db, 'image');
|
||||
$settings['favicon_url'] = resolveImageField('favicon_url', $db, 'favicon');
|
||||
$settings['logo_url'] = Upload::resolveField('logo_url', (string)$db->getSetting('logo_url', ''), 'image');
|
||||
$settings['favicon_url'] = Upload::resolveField('favicon_url', (string)$db->getSetting('favicon_url', ''), 'favicon');
|
||||
$settings['instruction_header'] = trim($_POST['instruction_header'] ?? '');
|
||||
$settings['instruction_text'] = $_POST['instruction_text'] ?? '';
|
||||
$settings['public_access'] = isset($_POST['public_access']) ? '1' : '0';
|
||||
|
|
@ -99,12 +72,12 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['save_settings'])) {
|
|||
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'] = resolveImageField('login_logo_url', $db, 'image');
|
||||
$settings['login_logo_url'] = Upload::resolveField('login_logo_url', (string)$db->getSetting('login_logo_url', ''), 'image');
|
||||
$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'] = resolveImageField('login_bg_image', $db, 'image');
|
||||
$settings['login_bg_image'] = Upload::resolveField('login_bg_image', (string)$db->getSetting('login_bg_image', ''), '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);
|
||||
|
|
@ -263,41 +236,6 @@ $cs = [
|
|||
'last_cron_sync' => $db->getSetting('last_cron_sync', ''),
|
||||
];
|
||||
|
||||
/**
|
||||
* Bildfeld: Vorschau, Upload, alternativ URL – plus Entfernen-Schalter.
|
||||
*/
|
||||
function imageField(string $name, string $label, ?string $value, string $hint = '', string $accept = 'image/*'): void
|
||||
{
|
||||
$value = (string)$value;
|
||||
$preview = Ui::mediaUrl($value, '../');
|
||||
?>
|
||||
<div class="form-group">
|
||||
<label><?= htmlspecialchars($label) ?></label>
|
||||
<div class="image-field">
|
||||
<div class="image-preview">
|
||||
<?php if ($preview !== ''): ?>
|
||||
<img src="<?= htmlspecialchars($preview) ?>" alt="">
|
||||
<?php else: ?>
|
||||
<i class="fas fa-image" aria-hidden="true"></i>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<div class="image-field-controls">
|
||||
<input type="file" name="<?= $name ?>_file" accept="<?= htmlspecialchars($accept) ?>">
|
||||
<input type="text" name="<?= $name ?>" value="<?= htmlspecialchars($value) ?>"
|
||||
placeholder="<?= htmlspecialchars(__('settings_image_url_placeholder')) ?>">
|
||||
<?php if ($value !== ''): ?>
|
||||
<label class="chk"><input type="checkbox" name="<?= $name ?>_remove" value="1"> <?= __('settings_image_remove') ?></label>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php if ($hint !== ''): ?><div class="help-text"><?= htmlspecialchars($hint) ?></div><?php endif; ?>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
|
||||
// Aktiver Tab per ?tab=… (Deep-Link, funktioniert auch ohne JavaScript)
|
||||
$activeTab = preg_replace('/[^a-z_]/', '', (string)($_GET['tab'] ?? 'general')) ?: 'general';
|
||||
|
||||
$currentPage = 'settings';
|
||||
$adminBase = '';
|
||||
?>
|
||||
|
|
@ -350,8 +288,8 @@ $adminBase = '';
|
|||
<input type="hidden" name="form_type" value="general">
|
||||
<div class="form-group"><label><?= __('settings_app_title') ?></label><input type="text" name="app_title" value="<?= htmlspecialchars($cs['app_title']) ?>" required></div>
|
||||
<div class="form-grid">
|
||||
<?php imageField('logo_url', __('settings_logo_url'), $cs['logo_url'], __('settings_upload_hint')); ?>
|
||||
<?php imageField('favicon_url', __('settings_favicon_url'), $cs['favicon_url'], __('settings_favicon_hint'), 'image/x-icon,image/png,image/svg+xml'); ?>
|
||||
<?= Ui::imageField('logo_url', __('settings_logo_url'), $cs['logo_url'], __('settings_upload_hint')) ?>
|
||||
<?= Ui::imageField('favicon_url', __('settings_favicon_url'), $cs['favicon_url'], __('settings_favicon_hint'), 'image/x-icon,image/png,image/svg+xml') ?>
|
||||
</div>
|
||||
<hr class="section-divider">
|
||||
<div class="form-group"><label><?= __('settings_instr_header') ?></label><input type="text" name="instruction_header" value="<?= htmlspecialchars($cs['instruction_header']) ?>"></div>
|
||||
|
|
@ -485,7 +423,7 @@ $adminBase = '';
|
|||
<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>
|
||||
<?php imageField('login_logo_url', __('settings_login_logo'), $cs['login_logo_url'], __('settings_login_logo_hint')); ?>
|
||||
<?= Ui::imageField('login_logo_url', __('settings_login_logo'), $cs['login_logo_url'], __('settings_login_logo_hint')) ?>
|
||||
</div>
|
||||
|
||||
<hr class="section-divider">
|
||||
|
|
@ -510,7 +448,7 @@ $adminBase = '';
|
|||
|
||||
<hr class="section-divider">
|
||||
|
||||
<?php imageField('login_bg_image', __('settings_login_bg_image'), $cs['login_bg_image'], __('settings_login_bg_image_hint')); ?>
|
||||
<?= Ui::imageField('login_bg_image', __('settings_login_bg_image'), $cs['login_bg_image'], __('settings_login_bg_image_hint')) ?>
|
||||
<div class="form-grid">
|
||||
<div class="form-group">
|
||||
<label><?= __('settings_login_bg_from') ?></label>
|
||||
|
|
|
|||
|
|
@ -1641,6 +1641,52 @@ input.search-bar, .site-selector .search-bar { min-width: 240px; width: auto; fl
|
|||
box-shadow: var(--shadow-xl);
|
||||
}
|
||||
.kiosk-logo { max-height: 84px; max-width: 320px; margin: 0 auto 26px; display: block; }
|
||||
.kiosk-logo-sm { max-height: 52px; margin-bottom: 20px; }
|
||||
|
||||
/* Eigenes Hintergrundbild der Display-Seite: Bild formatfüllend, darüber
|
||||
eine abdunkelnde Ebene, damit die Karte sich abhebt. */
|
||||
.kiosk-body.has-background {
|
||||
--kiosk-overlay: 0.45;
|
||||
position: relative;
|
||||
background: var(--kiosk-bg) center / cover no-repeat fixed;
|
||||
}
|
||||
.kiosk-body.has-background::before {
|
||||
content: '';
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(8, 11, 18, var(--kiosk-overlay));
|
||||
pointer-events: none;
|
||||
}
|
||||
.kiosk-body.has-background > * { position: relative; z-index: 1; }
|
||||
.kiosk-body.has-background .kiosk-card { box-shadow: 0 40px 80px -30px rgba(0,0,0,.6); }
|
||||
.kiosk-body.has-background .kiosk-footer .app-credit,
|
||||
.kiosk-body.has-background .kiosk-footer .app-credit a { color: rgba(255,255,255,.65); }
|
||||
|
||||
/* Dunkle Karte – wirkt auf Fotos oft ruhiger als eine weiße Fläche. */
|
||||
.kiosk-dark .kiosk-card {
|
||||
background: rgba(12,15,22,.86);
|
||||
border-color: rgba(255,255,255,.12);
|
||||
backdrop-filter: blur(6px);
|
||||
color: #f4f6fb;
|
||||
}
|
||||
.kiosk-dark .kiosk-card h1,
|
||||
.kiosk-dark .kiosk-code { color: #ffffff; }
|
||||
.kiosk-dark .kiosk-subline,
|
||||
.kiosk-dark .kiosk-qr-label { color: rgba(255,255,255,.76); }
|
||||
.kiosk-dark .kiosk-countdown { color: rgba(255,255,255,.5); }
|
||||
.kiosk-dark .kiosk-meta span {
|
||||
background: rgba(255,255,255,.10);
|
||||
border-color: rgba(255,255,255,.16);
|
||||
color: rgba(255,255,255,.88);
|
||||
}
|
||||
.kiosk-dark .kiosk-phone { border-top-color: rgba(255,255,255,.14); color: rgba(255,255,255,.7); }
|
||||
.kiosk-dark .kiosk-eyebrow { background: rgba(74,222,128,.16); border-color: rgba(74,222,128,.32); color: #86efac; }
|
||||
.kiosk-dark .btn-secondary {
|
||||
background: rgba(255,255,255,.10);
|
||||
border-color: rgba(255,255,255,.18);
|
||||
color: #f4f6fb;
|
||||
}
|
||||
.kiosk-dark .btn-secondary:hover { background: rgba(255,255,255,.18); color: #fff; }
|
||||
.kiosk-mark { width: 62px; height: 62px; margin: 0 auto 26px; font-size: 26px; }
|
||||
.kiosk-headline {
|
||||
font-size: clamp(30px, 4.6vw, 46px);
|
||||
|
|
|
|||
|
|
@ -74,6 +74,11 @@ CREATE TABLE IF NOT EXISTS `kiosks` (
|
|||
`token` VARCHAR(64) NOT NULL,
|
||||
`headline` VARCHAR(255) NULL,
|
||||
`subline` VARCHAR(500) NULL,
|
||||
`logo_url` VARCHAR(500) NULL,
|
||||
`background_url` VARCHAR(500) NULL,
|
||||
`bg_overlay` TINYINT NOT NULL DEFAULT 45,
|
||||
`accent_color` VARCHAR(7) NULL,
|
||||
`card_style` ENUM('light','dark') NOT NULL DEFAULT 'light',
|
||||
`is_active` TINYINT(1) NOT NULL DEFAULT 1,
|
||||
`daily_limit` INT NOT NULL DEFAULT 100,
|
||||
`cooldown_seconds` INT NOT NULL DEFAULT 20,
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 396 KiB After Width: | Height: | Size: 396 KiB |
|
Before Width: | Height: | Size: 390 KiB After Width: | Height: | Size: 389 KiB |
|
Before Width: | Height: | Size: 319 KiB After Width: | Height: | Size: 319 KiB |
|
Before Width: | Height: | Size: 319 KiB After Width: | Height: | Size: 319 KiB |
BIN
docs/screenshots/kiosk-branded.png
Normal file
|
After Width: | Height: | Size: 213 KiB |
|
Before Width: | Height: | Size: 202 KiB After Width: | Height: | Size: 216 KiB |
|
Before Width: | Height: | Size: 254 KiB After Width: | Height: | Size: 213 KiB |
|
Before Width: | Height: | Size: 262 KiB After Width: | Height: | Size: 262 KiB |
BIN
docs/screenshots/kiosks-form.png
Normal file
|
After Width: | Height: | Size: 446 KiB |
|
Before Width: | Height: | Size: 315 KiB After Width: | Height: | Size: 190 KiB |
|
Before Width: | Height: | Size: 336 KiB After Width: | Height: | Size: 192 KiB |
|
Before Width: | Height: | Size: 285 KiB After Width: | Height: | Size: 192 KiB |
|
Before Width: | Height: | Size: 284 KiB After Width: | Height: | Size: 284 KiB |
|
Before Width: | Height: | Size: 422 KiB After Width: | Height: | Size: 421 KiB |
|
|
@ -124,6 +124,35 @@ class Kiosk
|
|||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Gestaltung einer Display-Seite: eigene Werte, sonst die des Systems.
|
||||
*
|
||||
* @return array{logo:string,background:string,overlay:float,accent:string,card:string}
|
||||
*/
|
||||
public static function appearance($db, array $kiosk): array
|
||||
{
|
||||
$accent = strtolower(trim((string)($kiosk['accent_color'] ?? '')));
|
||||
if (!preg_match('/^#[0-9a-f]{6}$/', $accent)) {
|
||||
$accent = '';
|
||||
}
|
||||
|
||||
$overlay = (int)($kiosk['bg_overlay'] ?? 45);
|
||||
$overlay = max(0, min(90, $overlay));
|
||||
|
||||
$logo = trim((string)($kiosk['logo_url'] ?? ''));
|
||||
if ($logo === '' && $db) {
|
||||
$logo = (string)$db->getSetting('logo_url', '');
|
||||
}
|
||||
|
||||
return [
|
||||
'logo' => $logo,
|
||||
'background' => trim((string)($kiosk['background_url'] ?? '')),
|
||||
'overlay' => (float)$overlay / 100, // immer float, auch bei 0
|
||||
'accent' => $accent,
|
||||
'card' => ($kiosk['card_style'] ?? 'light') === 'dark' ? 'dark' : 'light',
|
||||
];
|
||||
}
|
||||
|
||||
/** Öffentliche Adresse eines Kiosks. */
|
||||
public static function publicUrl(string $token, string $baseUrl = ''): string
|
||||
{
|
||||
|
|
|
|||
|
|
@ -116,6 +116,46 @@ class Ui
|
|||
. '</style>';
|
||||
}
|
||||
|
||||
/**
|
||||
* Bildfeld mit Vorschau, Upload und URL-Eingabe.
|
||||
* Wird von den Einstellungen und der Kiosk-Verwaltung genutzt.
|
||||
*/
|
||||
public static function imageField(
|
||||
string $name,
|
||||
string $label,
|
||||
?string $value,
|
||||
string $hint = '',
|
||||
string $accept = 'image/*',
|
||||
string $base = '../'
|
||||
): string {
|
||||
$value = (string)$value;
|
||||
$preview = self::mediaUrl($value, $base);
|
||||
$esc = static fn ($text) => htmlspecialchars((string)$text, ENT_QUOTES);
|
||||
$t = static fn ($key, $fallback) => function_exists('__') ? __($key) : $fallback;
|
||||
|
||||
$thumb = $preview !== ''
|
||||
? '<img src="' . $esc($preview) . '" alt="">'
|
||||
: '<i class="fas fa-image" aria-hidden="true"></i>';
|
||||
|
||||
$remove = $value !== ''
|
||||
? '<label class="chk"><input type="checkbox" name="' . $esc($name) . '_remove" value="1"> '
|
||||
. $esc($t('settings_image_remove', 'Bild entfernen')) . '</label>'
|
||||
: '';
|
||||
|
||||
return '<div class="form-group">'
|
||||
. '<label>' . $esc($label) . '</label>'
|
||||
. '<div class="image-field">'
|
||||
. '<div class="image-preview">' . $thumb . '</div>'
|
||||
. '<div class="image-field-controls">'
|
||||
. '<input type="file" name="' . $esc($name) . '_file" accept="' . $esc($accept) . '">'
|
||||
. '<input type="text" name="' . $esc($name) . '" value="' . $esc($value) . '" '
|
||||
. 'placeholder="' . $esc($t('settings_image_url_placeholder', 'https://… oder Datei hochladen')) . '">'
|
||||
. $remove
|
||||
. '</div></div>'
|
||||
. ($hint !== '' ? '<div class="help-text">' . $esc($hint) . '</div>' : '')
|
||||
. '</div>';
|
||||
}
|
||||
|
||||
/**
|
||||
* Version aus der Datei VERSION im Projektstamm.
|
||||
* Damit tragen Oberfläche und Release-Paket dieselbe Nummer.
|
||||
|
|
|
|||
|
|
@ -120,6 +120,39 @@ class Upload
|
|||
return 'uploads/' . $name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Neuer Wert eines Bildfeldes aus dem Formular.
|
||||
*
|
||||
* Reihenfolge: hochgeladene Datei schlaegt alles, danach der
|
||||
* Entfernen-Schalter, sonst gilt das URL-Feld. Wird eine zuvor
|
||||
* hochgeladene Datei ersetzt oder entfernt, verschwindet sie auch
|
||||
* von der Platte.
|
||||
*
|
||||
* @param string $name Feldname (erwartet <name>, <name>_file, <name>_remove)
|
||||
* @param string $current bisher gespeicherter Wert
|
||||
* @param string $kind 'image' oder 'favicon'
|
||||
*/
|
||||
public static function resolveField(string $name, string $current, string $kind = 'image'): string
|
||||
{
|
||||
$uploaded = self::store($_FILES[$name . '_file'] ?? [], $kind);
|
||||
if ($uploaded !== '') {
|
||||
self::delete($current);
|
||||
return $uploaded;
|
||||
}
|
||||
|
||||
if (!empty($_POST[$name . '_remove'])) {
|
||||
self::delete($current);
|
||||
return '';
|
||||
}
|
||||
|
||||
$value = trim((string)($_POST[$name] ?? ''));
|
||||
if ($value !== $current && self::isLocal($current) && !self::isLocal($value)) {
|
||||
self::delete($current);
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Entfernt aktive Inhalte aus SVG-Dateien (Skripte, Event-Handler,
|
||||
* externe Verweise). Lieber eine Grafik verlieren als eine XSS-Luecke.
|
||||
|
|
|
|||
27
kiosk.php
|
|
@ -87,9 +87,29 @@ if (!$notFound && $_SERVER['REQUEST_METHOD'] === 'POST') {
|
|||
|
||||
$headline = trim((string)($kiosk['headline'] ?? '')) ?: __('kiosk_default_headline');
|
||||
$subline = trim((string)($kiosk['subline'] ?? '')) ?: __('kiosk_default_subline');
|
||||
$logoUrl = $db->getSetting('logo_url', '');
|
||||
$display = max(10, (int)($kiosk['display_seconds'] ?? Kiosk::DEFAULT_DISPLAY_SECONDS));
|
||||
$selfUrl = $kiosk ? Kiosk::publicUrl($kiosk['token']) : '';
|
||||
|
||||
// Gestaltung dieser Display-Seite (eigene Werte, sonst die des Systems)
|
||||
$look = $kiosk ? Kiosk::appearance($db, $kiosk) : ['logo'=>'','background'=>'','overlay'=>0.45,'accent'=>'','card'=>'light'];
|
||||
$logoUrl = $look['logo'];
|
||||
$bodyClass = 'kiosk-body';
|
||||
$bodyStyle = '';
|
||||
if ($look['background'] !== '') {
|
||||
$bodyClass .= ' has-background';
|
||||
$bodyStyle .= "--kiosk-bg:url('" . htmlspecialchars(Ui::mediaUrl($look['background']), ENT_QUOTES) . "');"
|
||||
. '--kiosk-overlay:' . $look['overlay'] . ';';
|
||||
}
|
||||
if ($look['card'] === 'dark') {
|
||||
$bodyClass .= ' kiosk-dark';
|
||||
}
|
||||
if ($look['accent'] !== '') {
|
||||
// Nur die Akzentfarbe dieser Seite überschreiben – der Rest bleibt Design-System.
|
||||
$bodyStyle .= '--accent:' . $look['accent'] . ';'
|
||||
. '--accent-hover:color-mix(in srgb, ' . $look['accent'] . ' 84%, #000);'
|
||||
. '--accent-soft:color-mix(in srgb, ' . $look['accent'] . ' 14%, #fff);'
|
||||
. '--accent-border:color-mix(in srgb, ' . $look['accent'] . ' 32%, #fff);';
|
||||
}
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="<?= I18n::getLanguage() ?>">
|
||||
|
|
@ -103,7 +123,7 @@ $selfUrl = $kiosk ? Kiosk::publicUrl($kiosk['token']) : '';
|
|||
<?= Ui::script('assets/vendor/qrcodejs/qrcode.min.js') ?>
|
||||
<?php endif; ?>
|
||||
</head>
|
||||
<body class="kiosk-body">
|
||||
<body class="<?= $bodyClass ?>"<?= $bodyStyle !== '' ? ' style="' . $bodyStyle . '"' : '' ?>>
|
||||
|
||||
<?php if ($notFound): ?>
|
||||
<main class="kiosk-stage">
|
||||
|
|
@ -118,6 +138,9 @@ $selfUrl = $kiosk ? Kiosk::publicUrl($kiosk['token']) : '';
|
|||
<?php elseif ($voucher): ?>
|
||||
<main class="kiosk-stage">
|
||||
<div class="kiosk-card kiosk-result">
|
||||
<?php if ($logoUrl): ?>
|
||||
<img class="kiosk-logo kiosk-logo-sm" src="<?= htmlspecialchars(Ui::mediaUrl($logoUrl)) ?>" alt="<?= htmlspecialchars($appTitle) ?>">
|
||||
<?php endif; ?>
|
||||
<p class="kiosk-eyebrow"><i class="fas fa-circle-check" aria-hidden="true"></i> <?= __('kiosk_ready') ?></p>
|
||||
<div class="kiosk-code" id="voucherCode"><?= htmlspecialchars($voucher['code']) ?></div>
|
||||
<div class="kiosk-meta">
|
||||
|
|
|
|||
14
lang/de.php
|
|
@ -523,6 +523,20 @@ return [
|
|||
'audit_action_kiosk_created' => 'Display-Seite angelegt',
|
||||
'audit_action_kiosk_updated' => 'Display-Seite geändert',
|
||||
'audit_action_kiosk_deleted' => 'Display-Seite gelöscht',
|
||||
'kiosks_appearance' => 'Erscheinungsbild',
|
||||
'kiosks_logo' => 'Logo',
|
||||
'kiosks_logo_hint' => 'Leer = allgemeines Logo aus den Einstellungen.',
|
||||
'kiosks_background' => 'Hintergrundbild',
|
||||
'kiosks_background_hint' => 'Formatfüllend hinter der Karte – z.B. ein Foto des Hauses.',
|
||||
'kiosks_overlay' => 'Abdunklung des Bildes (%)',
|
||||
'kiosks_overlay_hint' => '0–90 %. Höhere Werte machen die Karte auf hellen Bildern lesbarer.',
|
||||
'kiosks_accent' => 'Akzentfarbe',
|
||||
'kiosks_accent_default' => 'Standardfarbe verwenden',
|
||||
'kiosks_accent_hint' => 'Färbt den Knopf auf dieser Seite. Leer = Farbe aus dem Design-Tab.',
|
||||
'kiosks_card_style' => 'Karte',
|
||||
'kiosks_card_light' => 'Hell',
|
||||
'kiosks_card_dark' => 'Dunkel',
|
||||
'kiosks_card_hint' => 'Auf Fotos wirkt die dunkle Karte meist ruhiger.',
|
||||
'settings_tab_general' => 'Allgemein',
|
||||
'settings_tab_defaults' => 'Voucher-Standards',
|
||||
'settings_tab_cron' => 'Cron-Sync',
|
||||
|
|
|
|||
14
lang/en.php
|
|
@ -523,6 +523,20 @@ return [
|
|||
'audit_action_kiosk_created' => 'Display page created',
|
||||
'audit_action_kiosk_updated' => 'Display page updated',
|
||||
'audit_action_kiosk_deleted' => 'Display page deleted',
|
||||
'kiosks_appearance' => 'Appearance',
|
||||
'kiosks_logo' => 'Logo',
|
||||
'kiosks_logo_hint' => 'Empty = general logo from the settings.',
|
||||
'kiosks_background' => 'Background image',
|
||||
'kiosks_background_hint' => 'Displayed full-bleed behind the card – e.g. a photo of the building.',
|
||||
'kiosks_overlay' => 'Image dimming (%)',
|
||||
'kiosks_overlay_hint' => '0–90%. Higher values keep the card readable on bright images.',
|
||||
'kiosks_accent' => 'Accent colour',
|
||||
'kiosks_accent_default' => 'Use default colour',
|
||||
'kiosks_accent_hint' => 'Colours the button on this page. Empty = colour from the Design tab.',
|
||||
'kiosks_card_style' => 'Card',
|
||||
'kiosks_card_light' => 'Light',
|
||||
'kiosks_card_dark' => 'Dark',
|
||||
'kiosks_card_hint' => 'On photos the dark card usually looks calmer.',
|
||||
'settings_tab_general' => 'General',
|
||||
'settings_tab_defaults' => 'Voucher Defaults',
|
||||
'settings_tab_cron' => 'Cron Sync',
|
||||
|
|
|
|||
|
|
@ -143,6 +143,56 @@ class KioskTest extends TestCase
|
|||
$this->assertSame(0, $settings['qos']['down']);
|
||||
}
|
||||
|
||||
public function testAppearanceFallsBackToSystemLogo(): void
|
||||
{
|
||||
$db = new FakeKioskDb(['logo_url' => 'uploads/global.png']);
|
||||
|
||||
$look = \Kiosk::appearance($db, $this->kiosk());
|
||||
|
||||
$this->assertSame('uploads/global.png', $look['logo']);
|
||||
$this->assertSame('', $look['background']);
|
||||
$this->assertSame('', $look['accent'], 'Ohne eigene Farbe bleibt das Design-System zuständig');
|
||||
$this->assertSame('light', $look['card']);
|
||||
}
|
||||
|
||||
public function testAppearanceUsesOwnValues(): void
|
||||
{
|
||||
$db = new FakeKioskDb(['logo_url' => 'uploads/global.png']);
|
||||
$kiosk = $this->kiosk([
|
||||
'logo_url' => 'uploads/hotel.svg',
|
||||
'background_url' => 'uploads/lobby.jpg',
|
||||
'bg_overlay' => 60,
|
||||
'accent_color' => '#0F766E',
|
||||
'card_style' => 'dark',
|
||||
]);
|
||||
|
||||
$look = \Kiosk::appearance($db, $kiosk);
|
||||
|
||||
$this->assertSame('uploads/hotel.svg', $look['logo']);
|
||||
$this->assertSame('uploads/lobby.jpg', $look['background']);
|
||||
$this->assertSame(0.6, $look['overlay']);
|
||||
$this->assertSame('#0f766e', $look['accent']);
|
||||
$this->assertSame('dark', $look['card']);
|
||||
}
|
||||
|
||||
public function testAppearanceRejectsUnsafeColour(): void
|
||||
{
|
||||
$db = new FakeKioskDb();
|
||||
|
||||
// Der Wert landet in einem style-Attribut – nur echte Hex-Farben durch.
|
||||
$look = \Kiosk::appearance($db, $this->kiosk(['accent_color' => 'red;background:url(evil)']));
|
||||
|
||||
$this->assertSame('', $look['accent']);
|
||||
}
|
||||
|
||||
public function testAppearanceClampsOverlay(): void
|
||||
{
|
||||
$db = new FakeKioskDb();
|
||||
|
||||
$this->assertSame(0.9, \Kiosk::appearance($db, $this->kiosk(['bg_overlay' => 250]))['overlay']);
|
||||
$this->assertSame(0.0, \Kiosk::appearance($db, $this->kiosk(['bg_overlay' => -10]))['overlay']);
|
||||
}
|
||||
|
||||
public function testPublicUrl(): void
|
||||
{
|
||||
$token = \Kiosk::newToken();
|
||||
|
|
|
|||
|
|
@ -99,6 +99,11 @@ document.addEventListener('DOMContentLoaded', function () {
|
|||
|
||||
$headline = trim((string)($kiosk['headline'] ?? ''))''')
|
||||
|
||||
# Kiosk-Verwaltung: Formular fuer den Screenshot geoeffnet zeigen
|
||||
patch(os.path.join(target, 'admin', 'kiosks.php'),
|
||||
'<div class="modal" id="kioskModal">',
|
||||
'<div class="modal<?= ($_GET[\'demo\'] ?? \'\') === \'form\' ? \' active\' : \'\' ?>" id="kioskModal">')
|
||||
|
||||
# Theme per Query-Parameter erzwingen (fuer Dark-Mode-Screenshots)
|
||||
ui = os.path.join(target, 'includes', 'Ui.php')
|
||||
patch(ui, 'var s=localStorage.getItem("theme");',
|
||||
|
|
|
|||
|
|
@ -62,12 +62,16 @@ class Database {
|
|||
return [
|
||||
['id'=>1,'site_id'=>1,'template_id'=>1,'name'=>'Empfang Erdgeschoss','token'=>'a1b2c3d4e5f60718293a4b5c6d7e8f90',
|
||||
'headline'=>'Willkommen im Hotel Seeblick','subline'=>'Tippen Sie auf den Knopf – Ihr WLAN-Code erscheint sofort.',
|
||||
'logo_url'=>'/demo-assets/logo.svg','background_url'=>'/demo-assets/background.svg',
|
||||
'bg_overlay'=>55,'accent_color'=>'#0f766e','card_style'=>'dark',
|
||||
'is_active'=>1,'daily_limit'=>150,'cooldown_seconds'=>15,'display_seconds'=>90,'last_used_at'=>null,
|
||||
'site_name'=>'Hauptstandort Nord','site_active'=>1,'template_name'=>'Tagesgast',
|
||||
'tpl_max_uses'=>2,'tpl_expire_minutes'=>480,'qos_rate_max_down'=>20000,'qos_rate_max_up'=>5000,'qos_usage_quota'=>0,
|
||||
'total_vouchers'=>412,'today_vouchers'=>23,'created_at'=>'2026-06-01 10:00:00'],
|
||||
['id'=>2,'site_id'=>2,'template_id'=>null,'name'=>'Tagungsraum West','token'=>'0f1e2d3c4b5a69788796a5b4c3d2e1f0',
|
||||
'headline'=>'','subline'=>'','is_active'=>1,'daily_limit'=>0,'cooldown_seconds'=>30,'display_seconds'=>60,
|
||||
'headline'=>'','subline'=>'','logo_url'=>null,'background_url'=>null,'bg_overlay'=>45,
|
||||
'accent_color'=>null,'card_style'=>'light',
|
||||
'is_active'=>1,'daily_limit'=>0,'cooldown_seconds'=>30,'display_seconds'=>60,
|
||||
'last_used_at'=>null,'site_name'=>'Campus West','site_active'=>1,'template_name'=>null,
|
||||
'tpl_max_uses'=>0,'tpl_expire_minutes'=>0,'qos_rate_max_down'=>0,'qos_rate_max_up'=>0,'qos_usage_quota'=>0,
|
||||
'total_vouchers'=>87,'today_vouchers'=>4,'created_at'=>'2026-07-12 09:30:00'],
|
||||
|
|
|
|||
|
|
@ -35,9 +35,11 @@ SHOTS = [
|
|||
('two-factor.png', 'admin/security.php', 1200, 900),
|
||||
('updater.png', 'admin/update.php', 1200, 780),
|
||||
('updater-available.png', 'admin/update.php?demo=available', 1200, 780),
|
||||
('kiosk-display.png', 'kiosk.php?k=a1b2c3d4e5f60718293a4b5c6d7e8f90', 1200, 900),
|
||||
('kiosk-display.png', 'kiosk.php?k=0f1e2d3c4b5a69788796a5b4c3d2e1f0', 1200, 900),
|
||||
('kiosk-code.png', 'kiosk.php?k=a1b2c3d4e5f60718293a4b5c6d7e8f90&demo=code', 1200, 900),
|
||||
('kiosk-branded.png', 'kiosk.php?k=a1b2c3d4e5f60718293a4b5c6d7e8f90', 1200, 900),
|
||||
('kiosks-admin.png', 'admin/kiosks.php', 1200, 780),
|
||||
('kiosks-form.png', 'admin/kiosks.php?demo=form', 1200, 1150),
|
||||
('mobile-vouchers.png', 'admin/users.php', 430, 860),
|
||||
('maintenance.png', 'updater/templates/maintenance.html', 1200, 700),
|
||||
]
|
||||
|
|
|
|||
8
updater/migrations/0006_kiosk_branding.sql
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
-- Display-Seiten individuell gestalten: eigenes Logo, Hintergrundbild,
|
||||
-- Akzentfarbe und helle oder dunkle Karte je Kiosk.
|
||||
|
||||
ALTER TABLE `kiosks` ADD COLUMN `logo_url` VARCHAR(500) NULL AFTER `subline`;
|
||||
ALTER TABLE `kiosks` ADD COLUMN `background_url` VARCHAR(500) NULL AFTER `logo_url`;
|
||||
ALTER TABLE `kiosks` ADD COLUMN `bg_overlay` TINYINT NOT NULL DEFAULT 45 AFTER `background_url`;
|
||||
ALTER TABLE `kiosks` ADD COLUMN `accent_color` VARCHAR(7) NULL AFTER `bg_overlay`;
|
||||
ALTER TABLE `kiosks` ADD COLUMN `card_style` ENUM('light','dark') NOT NULL DEFAULT 'light' AFTER `accent_color`;
|
||||