Some checks are pending
CI / PHP Lint (push) Waiting to run
CI / PHP Lint (pull_request) Waiting to run
CI / PHP Lint-1 (pull_request) Waiting to run
CI / PHP Lint-1 (push) Waiting to run
CI / Unit Tests & Static Analysis (pull_request) Waiting to run
CI / Unit Tests & Static Analysis (push) Waiting to run
Jede Display-Seite bringt jetzt ihr eigenes Erscheinungsbild mit – der Empfang sieht anders aus als der Tagungsraum nebenan: - eigenes Logo (leer = Logo aus den Einstellungen) - formatfüllendes Hintergrundbild mit einstellbarer Abdunklung (0–90 %), damit die Karte auf hellen Fotos lesbar bleibt - eigene Akzentfarbe für den Knopf (leer = Farbe aus dem Design-Tab) - Karte wahlweise hell oder dunkel; auf Fotos wirkt dunkel meist ruhiger Logo und Hintergrund lassen sich hochladen oder als URL hinterlegen; beim Löschen einer Display-Seite verschwinden die hochgeladenen Dateien mit. Nebenbei aufgeräumt: das Bildfeld (Vorschau + Upload + URL + Entfernen) lag als Funktion in admin/settings.php und wird jetzt von beiden Seiten genutzt – Ui::imageField() für die Darstellung, Upload::resolveField() für die Auswertung. Sicherheit: die Akzentfarbe landet in einem style-Attribut, deshalb wird sie sowohl beim Speichern als auch beim Ausgeben auf eine echte Hex-Farbe geprüft; ein Test hält das fest. Migration 0006, database.sql nachgezogen, 4 neue Tests (42 gesamt), Screenshots ergänzt, Version 2.8.0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
500 lines
25 KiB
PHP
500 lines
25 KiB
PHP
<?php
|
||
/**
|
||
* Verwaltung der öffentlichen Display-Seiten ("Kiosk").
|
||
*
|
||
* Jeder Kiosk gehört zu einer Site, hat einen geheimen Link und gibt über
|
||
* kiosk.php Zugangscodes aus – ohne Anmeldung, aber mit Tageslimit und
|
||
* Wartezeit zwischen zwei Codes.
|
||
*/
|
||
error_reporting(E_ALL);
|
||
ini_set('display_errors', 0);
|
||
ini_set('log_errors', 1);
|
||
|
||
require_once __DIR__ . '/../config.php';
|
||
require_once __DIR__ . '/../includes/Database.php';
|
||
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();
|
||
I18n::init();
|
||
|
||
$db = Database::getInstance();
|
||
$appTitle = $db->getSetting('app_title', 'UniFi Voucher System');
|
||
|
||
$error = '';
|
||
$success = '';
|
||
|
||
/** Formularwerte einsammeln – für Anlegen und Bearbeiten identisch. */
|
||
function kioskInput(): array
|
||
{
|
||
return [
|
||
'site_id' => (int)($_POST['site_id'] ?? 0),
|
||
'template_id' => (int)($_POST['template_id'] ?? 0) ?: null,
|
||
'name' => trim((string)($_POST['name'] ?? '')),
|
||
'headline' => trim((string)($_POST['headline'] ?? '')),
|
||
'subline' => trim((string)($_POST['subline'] ?? '')),
|
||
'daily_limit' => max(0, (int)($_POST['daily_limit'] ?? Kiosk::DEFAULT_DAILY_LIMIT)),
|
||
'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');
|
||
} else {
|
||
try {
|
||
$in = kioskInput();
|
||
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,
|
||
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'], $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');
|
||
} catch (Exception $e) {
|
||
$error = $e->getMessage();
|
||
}
|
||
}
|
||
}
|
||
|
||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['edit_kiosk'])) {
|
||
if (!$auth->validateCsrfToken($_POST['csrf_token'] ?? '')) {
|
||
$error = __('error_csrf');
|
||
} else {
|
||
try {
|
||
$id = (int)($_POST['kiosk_id'] ?? 0);
|
||
$in = kioskInput();
|
||
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']);
|
||
$success = __('kiosks_updated');
|
||
} catch (Exception $e) {
|
||
$error = $e->getMessage();
|
||
}
|
||
}
|
||
}
|
||
|
||
// Neuen Link erzeugen – der alte gilt damit sofort nicht mehr.
|
||
if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['renew_token'])) {
|
||
if (!$auth->validateCsrfToken($_POST['csrf_token'] ?? '')) {
|
||
$error = __('error_csrf');
|
||
} else {
|
||
$id = (int)($_POST['kiosk_id'] ?? 0);
|
||
$db->execute("UPDATE kiosks SET token = ? WHERE id = ?", [Kiosk::newToken(), $id]);
|
||
$auth->writeAuditLog($_SESSION['user_id'], 'kiosk_updated', 'kiosk', $id, 'Link erneuert');
|
||
$success = __('kiosks_token_renewed');
|
||
}
|
||
}
|
||
|
||
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');
|
||
} else {
|
||
$error = __('error_csrf');
|
||
}
|
||
}
|
||
|
||
$sites = $db->fetchAll("SELECT id, name FROM sites WHERE is_active = 1 ORDER BY name");
|
||
$templates = $db->fetchAll("SELECT id, name, max_uses, expire_minutes FROM voucher_templates WHERE is_active = 1 ORDER BY name");
|
||
$kiosks = $db->fetchAll(
|
||
"SELECT k.*, s.name AS site_name, t.name AS template_name,
|
||
(SELECT COUNT(*) FROM vouchers v WHERE v.kiosk_id = k.id) AS total_vouchers,
|
||
(SELECT COUNT(*) FROM vouchers v WHERE v.kiosk_id = k.id AND DATE(v.created_at) = CURDATE()) AS today_vouchers
|
||
FROM kiosks k
|
||
INNER JOIN sites s ON s.id = k.site_id
|
||
LEFT JOIN voucher_templates t ON t.id = k.template_id
|
||
ORDER BY k.is_active DESC, k.name"
|
||
);
|
||
|
||
$csrf = $auth->getCsrfToken();
|
||
$currentPage = 'kiosks';
|
||
$adminBase = '';
|
||
?>
|
||
<!DOCTYPE html>
|
||
<html lang="<?= I18n::getLanguage() ?>">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title><?= __('kiosks_title') ?> – <?= htmlspecialchars($appTitle) ?></title>
|
||
<?= Ui::script('assets/vendor/qrcodejs/qrcode.min.js', '../') ?>
|
||
<?php require __DIR__ . '/../includes/admin_nav.php'; ?>
|
||
|
||
<div class="page-header">
|
||
<div>
|
||
<h1 class="page-title"><?= __('kiosks_title') ?></h1>
|
||
<p class="page-subtitle"><?= __('kiosks_subtitle') ?></p>
|
||
</div>
|
||
<button onclick="openAddModal()" class="btn btn-primary">
|
||
<i class="fas fa-plus" aria-hidden="true"></i> <?= __('kiosks_add') ?>
|
||
</button>
|
||
</div>
|
||
|
||
<?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 (empty($sites)): ?>
|
||
<div class="empty-card">
|
||
<div class="empty-icon"><i class="fas fa-location-dot" aria-hidden="true"></i></div>
|
||
<p><?= __('kiosks_no_sites') ?></p>
|
||
<a href="sites.php" class="btn btn-primary" style="margin-top:16px;">
|
||
<i class="fas fa-plus" aria-hidden="true"></i> <?= __('sites_add') ?>
|
||
</a>
|
||
</div>
|
||
<?php elseif (empty($kiosks)): ?>
|
||
<div class="empty-card">
|
||
<div class="empty-icon"><i class="fas fa-display" aria-hidden="true"></i></div>
|
||
<p><?= __('kiosks_empty') ?></p>
|
||
<button onclick="openAddModal()" class="btn btn-primary" style="margin-top:16px;">
|
||
<i class="fas fa-plus" aria-hidden="true"></i> <?= __('kiosks_add') ?>
|
||
</button>
|
||
</div>
|
||
<?php else: ?>
|
||
<div class="sites-grid">
|
||
<?php foreach ($kiosks as $k): ?>
|
||
<?php $url = Kiosk::publicUrl($k['token']); ?>
|
||
<div class="site-card">
|
||
<div class="site-card-header">
|
||
<div>
|
||
<div class="site-name"><?= htmlspecialchars($k['name']) ?></div>
|
||
<div class="site-id-label"><?= htmlspecialchars($k['site_name']) ?></div>
|
||
</div>
|
||
<span class="badge <?= $k['is_active'] ? 'badge-success' : 'badge-neutral' ?>">
|
||
<?= $k['is_active'] ? __('status_active') : __('status_inactive') ?>
|
||
</span>
|
||
</div>
|
||
|
||
<div class="site-info">
|
||
<div class="site-info-item">
|
||
<i class="fas fa-layer-group" aria-hidden="true"></i>
|
||
<?= $k['template_name'] ? htmlspecialchars($k['template_name']) : __('kiosks_no_template') ?>
|
||
</div>
|
||
<div class="site-info-item">
|
||
<i class="fas fa-gauge-high" aria-hidden="true"></i>
|
||
<?= (int)$k['today_vouchers'] ?><?= (int)$k['daily_limit'] > 0 ? ' / ' . (int)$k['daily_limit'] : '' ?>
|
||
<?= __('kiosks_today') ?>
|
||
</div>
|
||
<div class="site-info-item">
|
||
<i class="fas fa-ticket" aria-hidden="true"></i>
|
||
<?= (int)$k['total_vouchers'] ?> <?= __('kiosks_total') ?>
|
||
</div>
|
||
</div>
|
||
|
||
<label class="muted" style="display:block;margin-bottom:6px;"><?= __('kiosks_link') ?></label>
|
||
<div class="kiosk-link-row">
|
||
<input type="text" class="input" readonly value="<?= htmlspecialchars($url) ?>"
|
||
id="link-<?= (int)$k['id'] ?>" onclick="this.select()">
|
||
<button class="btn btn-secondary" type="button"
|
||
onclick="copyToClipboard('<?= htmlspecialchars($url, ENT_QUOTES) ?>', '<?= __('js_copied') ?>')"
|
||
title="<?= __('js_copy') ?>" aria-label="<?= __('js_copy') ?>">
|
||
<i class="fas fa-copy" aria-hidden="true"></i>
|
||
</button>
|
||
</div>
|
||
|
||
<div class="site-actions">
|
||
<a class="btn btn-secondary btn-sm" href="<?= htmlspecialchars($url) ?>" target="_blank" rel="noopener">
|
||
<i class="fas fa-arrow-up-right-from-square" aria-hidden="true"></i> <?= __('kiosks_open') ?>
|
||
</a>
|
||
<button class="btn btn-secondary btn-sm" type="button"
|
||
onclick="showQr('<?= htmlspecialchars($url, ENT_QUOTES) ?>', '<?= htmlspecialchars($k['name'], ENT_QUOTES) ?>')">
|
||
<i class="fas fa-qrcode" aria-hidden="true"></i> <?= __('kiosks_qr') ?>
|
||
</button>
|
||
<button class="btn btn-secondary btn-sm" type="button"
|
||
onclick='openEditModal(<?= json_encode([
|
||
"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"],
|
||
"is_active" => (int)$k["is_active"],
|
||
], JSON_HEX_APOS | JSON_HEX_QUOT) ?>)'>
|
||
<i class="fas fa-edit" aria-hidden="true"></i> <?= __('btn_edit') ?>
|
||
</button>
|
||
<form method="post" style="display:inline;"
|
||
onsubmit="return confirm('<?= __('kiosks_renew_confirm') ?>');">
|
||
<input type="hidden" name="csrf_token" value="<?= htmlspecialchars($csrf) ?>">
|
||
<input type="hidden" name="kiosk_id" value="<?= (int)$k['id'] ?>">
|
||
<button class="btn btn-secondary btn-sm" type="submit" name="renew_token">
|
||
<i class="fas fa-rotate" aria-hidden="true"></i> <?= __('kiosks_renew') ?>
|
||
</button>
|
||
</form>
|
||
<a class="btn btn-danger-soft btn-sm"
|
||
href="?delete=<?= (int)$k['id'] ?>&token=<?= urlencode($csrf) ?>"
|
||
onclick="return confirm('<?= __('kiosks_delete_confirm') ?>');"
|
||
title="<?= __('btn_delete') ?>" aria-label="<?= __('btn_delete') ?>">
|
||
<i class="fas fa-trash" aria-hidden="true"></i>
|
||
</a>
|
||
</div>
|
||
</div>
|
||
<?php endforeach; ?>
|
||
</div>
|
||
<?php endif; ?>
|
||
|
||
<!-- Anlegen / Bearbeiten -->
|
||
<div class="modal" id="kioskModal">
|
||
<div class="modal-content" role="dialog" aria-modal="true" aria-labelledby="kioskModalTitle">
|
||
<div class="modal-header">
|
||
<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" 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="">
|
||
|
||
<div class="form-group">
|
||
<label for="name"><?= __('kiosks_name') ?></label>
|
||
<input type="text" id="name" name="name" required placeholder="<?= __('kiosks_name_placeholder') ?>">
|
||
<div class="help-text"><?= __('kiosks_name_hint') ?></div>
|
||
</div>
|
||
|
||
<div class="form-grid">
|
||
<div class="form-group">
|
||
<label for="site_id"><?= __('label_site') ?></label>
|
||
<select id="site_id" name="site_id" required>
|
||
<?php foreach ($sites as $s): ?>
|
||
<option value="<?= (int)$s['id'] ?>"><?= htmlspecialchars($s['name']) ?></option>
|
||
<?php endforeach; ?>
|
||
</select>
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="template_id"><?= __('kiosks_template') ?></label>
|
||
<select id="template_id" name="template_id">
|
||
<option value="0"><?= __('kiosks_no_template') ?></option>
|
||
<?php foreach ($templates as $t): ?>
|
||
<option value="<?= (int)$t['id'] ?>">
|
||
<?= htmlspecialchars($t['name']) ?> – <?= (int)$t['max_uses'] ?> <?= __('label_devices') ?>,
|
||
<?= (int)$t['expire_minutes'] ?> <?= __('minutes_short') ?>
|
||
</option>
|
||
<?php endforeach; ?>
|
||
</select>
|
||
<div class="help-text"><?= __('kiosks_template_hint') ?></div>
|
||
</div>
|
||
</div>
|
||
|
||
<hr class="section-divider">
|
||
|
||
<div class="form-group">
|
||
<label for="headline"><?= __('kiosks_headline') ?></label>
|
||
<input type="text" id="headline" name="headline" placeholder="<?= htmlspecialchars(__('kiosk_default_headline')) ?>">
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="subline"><?= __('kiosks_subline') ?></label>
|
||
<textarea id="subline" name="subline" rows="2" placeholder="<?= htmlspecialchars(__('kiosk_default_subline')) ?>"></textarea>
|
||
</div>
|
||
|
||
<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>
|
||
<input type="number" id="daily_limit" name="daily_limit" min="0" value="<?= Kiosk::DEFAULT_DAILY_LIMIT ?>">
|
||
<div class="help-text"><?= __('kiosks_daily_limit_hint') ?></div>
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="cooldown_seconds"><?= __('kiosks_cooldown') ?></label>
|
||
<input type="number" id="cooldown_seconds" name="cooldown_seconds" min="0" max="3600" value="<?= Kiosk::DEFAULT_COOLDOWN ?>">
|
||
<div class="help-text"><?= __('kiosks_cooldown_hint') ?></div>
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="display_seconds"><?= __('kiosks_display') ?></label>
|
||
<input type="number" id="display_seconds" name="display_seconds" min="10" max="600" value="<?= Kiosk::DEFAULT_DISPLAY_SECONDS ?>">
|
||
<div class="help-text"><?= __('kiosks_display_hint') ?></div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="checkbox-group" id="activeRow" style="display:none;">
|
||
<input type="checkbox" id="is_active" name="is_active" checked>
|
||
<label for="is_active"><?= __('kiosks_active') ?></label>
|
||
</div>
|
||
</div>
|
||
<div class="modal-footer">
|
||
<button type="button" class="btn btn-secondary" onclick="closeModal()"><?= __('btn_cancel') ?></button>
|
||
<button type="submit" name="add_kiosk" id="submitAdd" class="btn btn-primary">
|
||
<i class="fas fa-save" aria-hidden="true"></i> <?= __('btn_save') ?>
|
||
</button>
|
||
<button type="submit" name="edit_kiosk" id="submitEdit" class="btn btn-primary" style="display:none;">
|
||
<i class="fas fa-save" aria-hidden="true"></i> <?= __('btn_save') ?>
|
||
</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- QR-Code des Links -->
|
||
<div class="modal" id="qrModal">
|
||
<div class="modal-content" style="max-width:420px;" role="dialog" aria-modal="true">
|
||
<div class="modal-header">
|
||
<h2 class="modal-title" id="qrTitle"><?= __('kiosks_qr') ?></h2>
|
||
<button class="modal-close" type="button" onclick="closeQr()" aria-label="<?= __('btn_cancel') ?>">×</button>
|
||
</div>
|
||
<div class="modal-body" style="text-align:center;">
|
||
<div id="qrTarget" style="display:inline-block;padding:14px;background:#fff;border-radius:12px;line-height:0;"></div>
|
||
<p class="help-text" style="margin-top:14px;"><?= __('kiosks_qr_hint') ?></p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
</main>
|
||
|
||
<div id="toast-container" role="status" aria-live="polite"></div>
|
||
<script src="../assets/global.js"></script>
|
||
<script>
|
||
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';
|
||
document.getElementById('kioskModal').classList.add('active');
|
||
}
|
||
|
||
function openEditModal(data) {
|
||
document.getElementById('kioskModalTitle').textContent = <?= json_encode(__('kiosks_edit')) ?>;
|
||
document.getElementById('kiosk_id').value = data.id;
|
||
document.getElementById('name').value = data.name || '';
|
||
document.getElementById('site_id').value = data.site_id;
|
||
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;
|
||
document.getElementById('is_active').checked = data.is_active === 1;
|
||
document.getElementById('submitAdd').style.display = 'none';
|
||
document.getElementById('submitEdit').style.display = '';
|
||
document.getElementById('activeRow').style.display = '';
|
||
document.getElementById('kioskModal').classList.add('active');
|
||
}
|
||
|
||
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 = '';
|
||
document.getElementById('qrTitle').textContent = name;
|
||
new QRCode(target, { text: url, width: 260, height: 260, colorDark: '#101625', colorLight: '#ffffff' });
|
||
document.getElementById('qrModal').classList.add('active');
|
||
}
|
||
|
||
function closeQr() { document.getElementById('qrModal').classList.remove('active'); }
|
||
|
||
document.addEventListener('keydown', function (e) {
|
||
if (e.key === 'Escape') { closeModal(); closeQr(); }
|
||
});
|
||
document.querySelectorAll('.modal').forEach(function (m) {
|
||
m.addEventListener('click', function (e) { if (e.target === m) m.classList.remove('active'); });
|
||
});
|
||
</script>
|
||
</body>
|
||
</html>
|