fetchOne( "SELECT k.*, s.name AS site_name, s.is_active AS site_active, t.name AS template_name, t.max_uses AS tpl_max_uses, t.expire_minutes AS tpl_expire_minutes, t.qos_rate_max_down, t.qos_rate_max_up, t.qos_usage_quota FROM kiosks k INNER JOIN sites s ON s.id = k.site_id LEFT JOIN voucher_templates t ON t.id = k.template_id WHERE k.token = ? AND k.is_active = 1", [$token] ); if (!$row || (int)$row['site_active'] !== 1) { return null; } return $row; } /** Wie viele Codes hat dieser Kiosk heute schon ausgegeben? */ public static function usedToday($db, int $kioskId): int { $row = $db->fetchOne( "SELECT COUNT(*) AS c FROM vouchers WHERE kiosk_id = ? AND DATE(created_at) = CURDATE()", [$kioskId] ); return (int)($row['c'] ?? 0); } /** * Darf gerade ein Code geholt werden? * * @return array{allowed:bool,reason:string,wait:int} * reason: '' | 'cooldown' | 'daily_limit' */ public static function checkLimits($db, array $kiosk): array { $cooldown = max(0, (int)$kiosk['cooldown_seconds']); if ($cooldown > 0 && !empty($kiosk['last_used_at'])) { $elapsed = time() - strtotime((string)$kiosk['last_used_at']); if ($elapsed >= 0 && $elapsed < $cooldown) { return ['allowed' => false, 'reason' => 'cooldown', 'wait' => $cooldown - $elapsed]; } } $limit = max(0, (int)$kiosk['daily_limit']); if ($limit > 0 && self::usedToday($db, (int)$kiosk['id']) >= $limit) { return ['allowed' => false, 'reason' => 'daily_limit', 'wait' => 0]; } return ['allowed' => true, 'reason' => '', 'wait' => 0]; } /** Nach erfolgreicher Ausgabe den Zeitstempel fortschreiben. */ public static function markUsed($db, int $kioskId): void { $db->execute("UPDATE kiosks SET last_used_at = NOW() WHERE id = ?", [$kioskId]); } /** * Voucher-Eckdaten eines Kiosks: entweder aus dem verknüpften Profil * oder aus den globalen Standardwerten. */ public static function voucherSettings($db, array $kiosk): array { $maxUses = (int)($kiosk['tpl_max_uses'] ?? 0); $expire = (int)($kiosk['tpl_expire_minutes'] ?? 0); if ($maxUses < 1) { $maxUses = max(1, (int)$db->getSetting('default_max_uses', 1)); } if ($expire < 1) { $expire = max(1, (int)$db->getSetting('default_expire_minutes', 480)); } return [ 'max_uses' => $maxUses, 'expire_minutes' => $expire, 'qos' => [ 'down' => max(0, (int)($kiosk['qos_rate_max_down'] ?? 0)), 'up' => max(0, (int)($kiosk['qos_rate_max_up'] ?? 0)), 'quota_mb' => max(0, (int)($kiosk['qos_usage_quota'] ?? 0)), ], ]; } /** * 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 { if ($baseUrl === '') { $protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? 'https' : 'http'; $host = $_SERVER['HTTP_HOST'] ?? 'localhost'; $path = dirname($_SERVER['SCRIPT_NAME'] ?? '/', 2); $path = $path === '/' || $path === '\\' ? '' : $path; $baseUrl = $protocol . '://' . $host . $path; } return rtrim($baseUrl, '/') . '/kiosk.php?k=' . $token; } }