prepare('UPDATE staff SET last_active_at = ? WHERE id = ? AND (last_active_at IS NULL OR last_active_at < ?)') ->execute([db_now(), $staffId, $cut]); // War der Mitarbeiter automatisch offline gesetzt, kommt sein vorheriger Status zurück $pdo->prepare("UPDATE staff_availability SET status = COALESCE(prev_status, 'available'), auto_offline = 0, prev_status = NULL, updated_at = ? WHERE staff_id = ? AND auto_offline = 1") ->execute([db_now(), $staffId]); } catch (Throwable $e) { error_log('staff_touch_activity: ' . $e->getMessage()); } } /** * Setzt alle Mitarbeiter offline, die länger als die eingestellte Zeit nicht aktiv waren. Läuft höchstens alle 30 Sekunden und wird * von allen Stellen aufgerufen, die den Status lesen. Mitarbeiter, die noch nie aktiv gemeldet wurden (last_active_at leer), bleiben * unverändert. @return int Anzahl der neu auf Offline gesetzten Mitarbeiter */ function staff_run_auto_offline(PDO $pdo, bool $force = false): int { static $lastRun = 0; $minutes = staff_auto_offline_minutes(); if ($minutes <= 0 || (!$force && time() - $lastRun < 30)) return 0; $lastRun = time(); try { $cutoff = date('Y-m-d H:i:s', time() - $minutes * 60); $stmt = $pdo->prepare("UPDATE staff_availability SET prev_status = status, status = 'offline', auto_offline = 1, updated_at = ? WHERE status <> 'offline' AND staff_id IN (SELECT id FROM staff WHERE is_active = 1 AND last_active_at IS NOT NULL AND last_active_at < ?)"); $stmt->execute([db_now(), $cutoff]); return $stmt->rowCount(); } catch (Throwable $e) { error_log('staff_run_auto_offline: ' . $e->getMessage()); return 0; } } /** * Setzt den Status eines Mitarbeiters von Hand (er selbst oder ein Admin). Ein manueller Status ist nie "automatisch" und wird * deshalb nicht von selbst zurückgesetzt. Gibt false bei ungültigem Status zurück. */ function staff_set_status(PDO $pdo, int $staffId, string $status, string $note = ''): bool { if (!in_array($status, STAFF_AVAILABILITY_STATUSES, true)) return false; $note = mb_substr(trim($note), 0, 120); $now = db_now(); $drv = $pdo->getAttribute(PDO::ATTR_DRIVER_NAME); $sql = $drv === 'sqlite' ? 'INSERT INTO staff_availability (staff_id, status, note, updated_at, auto_offline, prev_status) VALUES (?,?,?,?,0,NULL) ON CONFLICT(staff_id) DO UPDATE SET status=excluded.status, note=excluded.note, updated_at=excluded.updated_at, auto_offline=0, prev_status=NULL' : 'INSERT INTO staff_availability (staff_id, status, note, updated_at, auto_offline, prev_status) VALUES (?,?,?,?,0,NULL) ON DUPLICATE KEY UPDATE status=VALUES(status), note=VALUES(note), updated_at=VALUES(updated_at), auto_offline=0, prev_status=NULL'; $pdo->prepare($sql)->execute([$staffId, $status, $note !== '' ? $note : null, $now]); return true; } /** * Ist die aktuelle Anfrage das Laden einer Seite (im Gegensatz zu einem Abruf, den eine geöffnete Seite im Hintergrund per fetch stellt)? * Egal, ob der Nutzer sie ausgelöst hat oder der Browser selbst (Tab wird nach dem Ruhezustand neu geladen, Seite lädt sich per Skript neu). */ function staff_is_document_request(): bool { $mode = $_SERVER['HTTP_SEC_FETCH_MODE'] ?? ''; if ($mode !== '') return $mode === 'navigate' || ($_SERVER['HTTP_SEC_FETCH_DEST'] ?? '') === 'document'; // Ältere Browser ohne Sec-Fetch-Header: Seitenaufrufe erkennt man daran, dass kein X-Requested-With gesetzt ist und HTML erwartet wird return empty($_SERVER['HTTP_X_REQUESTED_WITH']) && str_contains((string) ($_SERVER['HTTP_ACCEPT'] ?? ''), 'text/html'); } /** Ist die aktuelle Anfrage eine echte Seitennavigation des Nutzers (und kein Hintergrund-Abruf per fetch)? */ function staff_is_user_navigation(): bool { if (($_SERVER['REQUEST_METHOD'] ?? 'GET') !== 'GET' && ($_SERVER['REQUEST_METHOD'] ?? '') !== 'POST') return false; $mode = $_SERVER['HTTP_SEC_FETCH_MODE'] ?? ''; // Sec-Fetch-User (?1) sendet der Browser nur bei Aufrufen, die der Nutzer selbst ausgelöst hat (Klick, Adresszeile, F5, Formular), // nicht bei Neuladen oder Weiterleitung per Skript (z. B. "neue Antwort, Seite aktualisieren") if ($mode !== '') return $mode === 'navigate' && ($_SERVER['HTTP_SEC_FETCH_USER'] ?? '') === '?1'; // Ältere Browser ohne Sec-Fetch-Header: Seitenaufrufe erkennt man daran, dass kein X-Requested-With gesetzt ist und HTML erwartet wird return empty($_SERVER['HTTP_X_REQUESTED_WITH']) && str_contains((string) ($_SERVER['HTTP_ACCEPT'] ?? ''), 'text/html'); } /** "vor 5 Min", "vor 2 Std", "vor 3 Tagen" oder '' wenn unbekannt. */ function staff_last_active_label(?string $lastActiveAt): string { if (!$lastActiveAt) return ''; $t = strtotime($lastActiveAt); if (!$t) return ''; $d = max(0, time() - $t); if ($d < 90) return 'gerade eben'; if ($d < 3600) return 'vor ' . (int) round($d / 60) . ' Min'; if ($d < 86400) return 'vor ' . (int) round($d / 3600) . ' Std'; return 'vor ' . (int) round($d / 86400) . ' Tagen'; }