Files
deskhub/DeskHub-Webseite/includes/staff_activity.php
T
2026-10-23 17:22:19 +00:00

123 lines
6.6 KiB
PHP

<?php
/**
* includes/staff_activity.php
*
* Aktivität der Mitarbeiter und automatischer Offline-Status: Wer länger als die eingestellte Zeit (Standard 30 Minuten) nichts
* gemacht hat, wird auf "Offline" gesetzt und bekommt seinen vorherigen Status zurück, sobald er wieder aktiv ist. Wer den Status
* von Hand ändert (oder ein Admin ihn ändert), behält ihn, bis er ihn selbst wieder ändert.
*
* Als "aktiv" zählt echte Nutzung: Seitenaufrufe im Browser, die Klick-/Tipp-Pings der Weboberfläche (staff/session_ping.php),
* Aktionen in der App (Schreib-Aufrufe der API) und die Berührungs-Pings der App (api/staff/v1/presence.php). Reines Abfragen im
* Hintergrund (Polling, Push-Registrierung) hält niemanden online.
*/
const STAFF_AVAILABILITY_STATUSES = ['available', 'busy', 'break', 'offline'];
/** Minuten ohne Aktivität bis "Offline" (0 = Funktion aus). */
function staff_auto_offline_minutes(): int
{
$m = (int) get_setting('staff_auto_offline_minutes', '30');
return max(0, min(1440, $m));
}
/**
* Merkt sich, dass der Mitarbeiter gerade aktiv ist (höchstens einmal pro Minute in die Datenbank geschrieben) und holt ihn aus
* dem automatischen Offline zurück. Fehler (z. B. Migration noch nicht gelaufen) stören nie.
*/
function staff_touch_activity(PDO $pdo, int $staffId, bool $force = false): void
{
static $last = [];
$now = time();
if (!$force && isset($last[$staffId]) && $now - $last[$staffId] < 60) return;
$last[$staffId] = $now;
try {
$cut = date('Y-m-d H:i:s', $now - 60);
$pdo->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';
}