getAttribute(PDO::ATTR_DRIVER_NAME); if ($driver === 'sqlite') { $pdo->prepare( 'INSERT INTO ticket_work_sessions (ticket_id, staff_id, started_at, last_seen_at) VALUES (?,?,?,?) ON CONFLICT(ticket_id, staff_id) DO UPDATE SET last_seen_at = excluded.last_seen_at' )->execute([$ticketId, $staffId, $now, $now]); } else { $pdo->prepare( 'INSERT INTO ticket_work_sessions (ticket_id, staff_id, started_at, last_seen_at) VALUES (?,?,?,?) ON DUPLICATE KEY UPDATE last_seen_at = VALUES(last_seen_at)' )->execute([$ticketId, $staffId, $now, $now]); } } /** * Schließt eine offene Arbeits-Sitzung ab und protokolliert die verstrichene Zeit * (auf ganze Minuten gerundet, mindestens 1). Sitzungen unter 1 Minute werden * verworfen statt als "0 Min."-Rauschen gespeichert. $endAt erlaubt, statt "jetzt" * den letzten bekannten Aktivitätszeitpunkt zu verwenden (Stale-Sweep). */ function finalize_work_session(PDO $pdo, int $ticketId, int $staffId, ?string $endAt = null): void { $stmt = $pdo->prepare('SELECT * FROM ticket_work_sessions WHERE ticket_id = ? AND staff_id = ?'); $stmt->execute([$ticketId, $staffId]); $session = $stmt->fetch(); if (!$session) return; $pdo->prepare('DELETE FROM ticket_work_sessions WHERE ticket_id = ? AND staff_id = ?')->execute([$ticketId, $staffId]); $end = $endAt ?? db_now(); $minutes = (int) round((strtotime($end) - strtotime($session['started_at'])) / 60); if ($minutes < 1) return; $pdo->prepare( 'INSERT INTO ticket_time_entries (ticket_id, staff_id, minutes, note, logged_at) VALUES (?,?,?,?,?)' )->execute([$ticketId, $staffId, $minutes, 'Automatisch erfasst', db_now()]); log_activity($pdo, $ticketId, $staffId, "Zeit automatisch erfasst: {$minutes} Min."); } /** Schließt alle offenen Sitzungen eines Mitarbeiters außer der auf $exceptTicketId ab (Ticket gewechselt). */ function finalize_other_open_sessions(PDO $pdo, int $staffId, int $exceptTicketId): void { $stmt = $pdo->prepare('SELECT ticket_id FROM ticket_work_sessions WHERE staff_id = ? AND ticket_id != ?'); $stmt->execute([$staffId, $exceptTicketId]); foreach ($stmt->fetchAll(PDO::FETCH_COLUMN) as $otherTicketId) { finalize_work_session($pdo, (int) $otherTicketId, $staffId); } } /** Schließt ALLE offenen Sitzungen eines Mitarbeiters ab (z.B. beim Logout). */ function finalize_all_sessions_for_staff(PDO $pdo, int $staffId): void { $stmt = $pdo->prepare('SELECT ticket_id FROM ticket_work_sessions WHERE staff_id = ?'); $stmt->execute([$staffId]); foreach ($stmt->fetchAll(PDO::FETCH_COLUMN) as $ticketId) { finalize_work_session($pdo, (int) $ticketId, $staffId); } } /** * Räumt "verwaiste" Sitzungen auf (Tab geschlossen, Browser zu, Rechner aus – * ohne dass jemand explizit geantwortet oder sich abgemeldet hat). Wird * regelmäßig vom Auto-Cron (includes/pseudo_cron.php) aufgerufen. Nutzt den * letzten bekannten Aktivitätszeitpunkt (last_seen_at) als Sitzungsende, * nicht "jetzt" – die erfasste Zeit spiegelt also die tatsächliche Aktivität * wider, nicht die Zeit bis zum nächsten Cron-Durchlauf. * * @return array{finalized: int, log: string[]} */ function finalize_stale_work_sessions(PDO $pdo, bool $verbose = false): array { $log = []; $threshold = date('Y-m-d H:i:s', time() - WORK_SESSION_STALE_MINUTES * 60); $stmt = $pdo->prepare('SELECT ticket_id, staff_id, last_seen_at FROM ticket_work_sessions WHERE last_seen_at < ?'); $stmt->execute([$threshold]); $stale = $stmt->fetchAll(); foreach ($stale as $row) { finalize_work_session($pdo, (int) $row['ticket_id'], (int) $row['staff_id'], $row['last_seen_at']); } if ($verbose) { $log[] = "[" . date('Y-m-d H:i:s') . "] " . count($stale) . " inaktive Zeiterfassungs-Sitzung(en) abgeschlossen."; } return ['finalized' => count($stale), 'log' => $log]; }