<?php
function nav_active(string $file): string { return basename($_SERVER['SCRIPT_NAME'] ?? '') === $file ? 'active' : ''; }
function page_header(string $title): void { $navUser = $GLOBALS['user'] ?? (isset($GLOBALS['pdo']) ? current_user($GLOBALS['pdo']) : null); ?>
<!doctype html><html lang="de" data-theme="auto"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title><?= e($title) ?> · KuhRator</title><link rel="stylesheet" href="../public/assets/style.css"><script src="https://cdn.jsdelivr.net/npm/qrcode@1.2.2/build/qrcode.min.js" defer></script><script src="../public/assets/admin.js" defer></script></head><body class="app-body"><button class="mobile-menu-toggle" type="button" data-menu-toggle aria-label="Menü öffnen" aria-expanded="false"><span></span><span></span><span></span></button><button class="desktop-theme-toggle" type="button" data-theme-toggle aria-label="Darstellung umschalten">Dark Mode</button><div class="mobile-overlay" data-menu-overlay></div><div class="app-shell"><aside class="sidebar" data-sidebar><a class="brand brand-logo-only" href="dashboard.php" aria-label="KuhRator Dashboard"><span class="brand-logo-wrap"><img src="../public/assets/qrator-mark.png" alt="KuhRator Logo"></span></a><nav class="side-nav"><a class="<?= nav_active('dashboard.php') ?>" href="dashboard.php">Dashboard</a><a class="<?= nav_active('create.php') ?>" href="create.php">QR-Code erstellen</a><a class="<?= nav_active('groups.php') ?>" href="groups.php">Gruppen</a><a class="<?= nav_active('statistics.php') ?>" href="statistics.php">Statistik</a><?php if($navUser && is_superadmin($navUser)): ?><?php $updateCount = count(glob(__DIR__.'/../update/qr-code-manager-v*.zip') ?: []); ?><a class="<?= nav_active('self-update.php') ?>" href="self-update.php">Updates<?= $updateCount ? ' <span class="nav-badge">'.$updateCount.'</span>' : '' ?></a><?php endif; ?></nav><div class="side-footer"><?php if($navUser): ?><details class="account-menu" <?= in_array(basename($_SERVER['SCRIPT_NAME'] ?? ''), ['users.php','account.php','change-password.php'], true) ? 'open' : '' ?>><summary><span><?= e($navUser['email'] ?? '') ?></span><?php if(is_superadmin($navUser)): ?><small>Superadmin</small><?php else: ?><small>Benutzer</small><?php endif; ?></summary><div class="account-links"><?php if(is_superadmin($navUser)): ?><a class="<?= nav_active('users.php') ?>" href="users.php">Benutzer</a><?php endif; ?><a class="<?= nav_active('account.php') ?>" href="account.php">Konto</a><a class="<?= nav_active('change-password.php') ?>" href="change-password.php">Passwort</a></div></details><?php endif; ?><a class="logout-link <?= nav_active('faq.php') ?>" href="faq.php">FAQ</a><a class="logout-link changelog-link version-link <?= nav_active('changelog.php') ?>" href="changelog.php"><span>Changelog</span><strong>v17.2</strong></a><a class="logout-link" href="logout.php">Logout</a></div></aside><main class="wrap content"><header class="page-head"><div><p class="eyebrow">KuhRator</p><h1><?= e($title) ?></h1></div></header>
<?php }
function page_footer(): void { ?></main></div></body></html><?php }
function user_groups(PDO $pdo, int $userId): array { $stmt=$pdo->prepare('SELECT id,name FROM '.qtbl('qr_groups').' WHERE user_id=? ORDER BY name ASC'); $stmt->execute([$userId]); return $stmt->fetchAll(); }
function accessible_groups(PDO $pdo, int $userId, bool $editOnly=false): array { $groups = user_groups($pdo, $userId); foreach ($groups as &$g) { $g['access_role']='owner'; $g['owner_email']=''; $g['is_owner']=1; } unset($g); $roleSql = $editOnly ? " AND s.access_role='edit'" : ''; $stmt=$pdo->prepare('SELECT g.id,g.name,s.access_role,u.email AS owner_email,0 AS is_owner FROM '.qtbl('qr_group_shares').' s INNER JOIN '.qtbl('qr_groups').' g ON g.id=s.group_id INNER JOIN '.qtbl('users').' u ON u.id=g.user_id WHERE s.user_id=?'.$roleSql.' ORDER BY u.email ASC,g.name ASC'); $stmt->execute([$userId]); return array_merge($groups, $stmt->fetchAll()); }
function group_can_view(PDO $pdo, int $userId, int $groupId): bool { if($groupId<=0) return false; $stmt=$pdo->prepare('SELECT 1 FROM '.qtbl('qr_groups').' WHERE id=? AND user_id=? UNION SELECT 1 FROM '.qtbl('qr_group_shares').' WHERE group_id=? AND user_id=? LIMIT 1'); $stmt->execute([$groupId,$userId,$groupId,$userId]); return (bool)$stmt->fetchColumn(); }
function group_can_edit(PDO $pdo, int $userId, int $groupId): bool { if($groupId<=0) return false; $stmt=$pdo->prepare('SELECT 1 FROM '.qtbl('qr_groups').' WHERE id=? AND user_id=? UNION SELECT 1 FROM '.qtbl('qr_group_shares').' WHERE group_id=? AND user_id=? AND access_role="edit" LIMIT 1'); $stmt->execute([$groupId,$userId,$groupId,$userId]); return (bool)$stmt->fetchColumn(); }
function selected_group_id(PDO $pdo, int $userId, $raw): ?int { $id=(int)$raw; if($id<=0) return null; return group_can_edit($pdo,$userId,$id) ? $id : null; }
function selected_group_id_view(PDO $pdo, int $userId, $raw): ?int { $id=(int)$raw; if($id<=0) return null; return group_can_view($pdo,$userId,$id) ? $id : null; }
function qr_form(array $qr, string $action, string $button): void { $pdo=$GLOBALS['pdo']; $user=$GLOBALS['user']; $config=$GLOBALS['config']; $groups=accessible_groups($pdo,(int)$user['id'], true); if($_SERVER['REQUEST_METHOD']==='POST'){ foreach(['title','target_url','group_id','custom_slug','foreground_color','background_color','utm_source','utm_medium','utm_campaign','utm_term','utm_content'] as $k){ if(array_key_exists($k,$_POST)){ $qr[$k === 'custom_slug' ? 'code' : $k] = $_POST[$k]; } } $qr['transparent_background']=isset($_POST['transparent_background'])?1:0; } $base=base_url($config).'/q/'; $initialCode=$qr['code'] ?? 'vorschau'; $logoCurrent=!empty($qr['logo_path']) && !empty($qr['id']) ? 'logo.php?id='.(int)$qr['id'] : ''; ?>
<section class="qr-editor-shell">
<form method="post" action="<?= e($action) ?>" class="card formgrid qr-editor-form" enctype="multipart/form-data" data-qr-editor data-base-url="<?= e($base) ?>" data-current-code="<?= e($initialCode) ?>" data-logo-current="<?= e($logoCurrent) ?>">
<input type="hidden" name="csrf" value="<?= e(csrf_token()) ?>">
<?php if (!empty($qr['id'])): ?><input type="hidden" name="id" value="<?= (int)$qr['id'] ?>"><?php endif; ?>
<div class="form-section"><h2>Inhalt</h2><label>Titel<input name="title" required maxlength="190" value="<?= e($qr['title'] ?? '') ?>"></label>
<label>Ziel-URL<input name="target_url" required type="url" placeholder="https://..." value="<?= e($qr['target_url'] ?? '') ?>"></label>
<label>Gruppe / Ordner<select name="group_id"><option value="0">Keine Gruppe</option><?php foreach($groups as $g): ?><option value="<?= (int)$g['id'] ?>" <?= ((int)($qr['group_id'] ?? 0)===(int)$g['id'])?'selected':'' ?>><?= e($g['name']) ?><?= !empty($g['owner_email']) ? ' · geteilt von '.e($g['owner_email']) : '' ?></option><?php endforeach; ?></select></label>
<label>Eigener Slug <span class="muted">optional, 4-32 Zeichen</span><input name="custom_slug" maxlength="32" pattern="[A-Za-z0-9_-]{4,32}" placeholder="z.B. flyer-2026" value="<?= e($qr['code'] ?? '') ?>"></label>
<div class="utm-box"><h3>UTM-Tracking <span class="muted">optional</span></h3><div class="grid2"><label>Source<input name="utm_source" maxlength="120" placeholder="newsletter" value="<?= e($qr['utm_source'] ?? '') ?>"></label><label>Medium<input name="utm_medium" maxlength="120" placeholder="print" value="<?= e($qr['utm_medium'] ?? '') ?>"></label><label>Campaign<input name="utm_campaign" maxlength="190" placeholder="fruehling-2026" value="<?= e($qr['utm_campaign'] ?? '') ?>"></label><label>Term<input name="utm_term" maxlength="190" value="<?= e($qr['utm_term'] ?? '') ?>"></label><label class="full">Content<input name="utm_content" maxlength="190" placeholder="flyer-a" value="<?= e($qr['utm_content'] ?? '') ?>"></label></div></div></div>
<div class="form-section"><h2>Design</h2><div class="color-grid"><label class="color-field"><span>Vordergrundfarbe</span><span class="color-control"><input name="foreground_color" type="color" value="<?= e($qr['foreground_color'] ?? '#000000') ?>"><input class="color-hex" data-color-hex="foreground_color" maxlength="7" value="<?= e($qr['foreground_color'] ?? '#000000') ?>"></span></label>
<label class="color-field"><span>Hintergrundfarbe</span><span class="color-control"><input name="background_color" type="color" value="<?= e($qr['background_color'] ?? '#FFFFFF') ?>"><input class="color-hex" data-color-hex="background_color" maxlength="7" value="<?= e($qr['background_color'] ?? '#FFFFFF') ?>"></span></label></div>
<label class="check switch"><input name="transparent_background" type="checkbox" value="1" <?= !empty($qr['transparent_background']) ? 'checked' : '' ?>><span></span> transparenter Hintergrund</label>
<label>Logo / Bildmarke <span class="muted">optional, PNG/JPEG/GIF/WebP, max. 1 MB</span><input name="logo" type="file" accept="image/png,image/jpeg,image/gif,image/webp"></label>
<?php if (!empty($qr['logo_path'])): ?><label class="check"><input name="remove_logo" type="checkbox" value="1"> vorhandenes Logo entfernen</label><?php endif; ?></div>
<button class="btn" type="submit"><?= e($button) ?></button>
</form>
<aside class="card live-preview-card"><div class="preview-head"><div><p class="eyebrow">Live-Vorschau</p><h2>QR-Code</h2></div><span class="preview-badge">direkt</span></div><div class="qrbox live"><canvas id="qr-live" class="qr-live" width="320" height="320" data-text="<?= e($base.$initialCode) ?>"></canvas></div><p class="muted live-url" data-live-url><?= e($base.$initialCode) ?></p><p class="muted">Die Vorschau zeigt den stabilen Kurzlink. Die Ziel-URL kannst du später jederzeit ändern.</p></aside>
</section><?php }
