123 lines
6.6 KiB
PHP
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';
|
|
}
|