Files
2026-10-16 19:12:18 +00:00

111 lines
4.8 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* includes/time_tracking.php
*
* Automatische Zeiterfassung pro Ticket: Sobald ein Mitarbeiter ein Ticket
* öffnet, startet eine "Arbeits-Sitzung" (ticket_work_sessions). Beendet er
* die Bearbeitung (Antwort/Notiz senden, anderes Ticket öffnen, Logout) oder
* bleibt länger als WORK_SESSION_STALE_MINUTES untätig, wird die Sitzung
* automatisch als Eintrag in ticket_time_entries protokolliert – ganz ohne
* dass jemand manuell die Minuten eintragen muss.
*
* Kann in Einstellungen über das Setting "auto_time_tracking_enabled"
* komplett abgeschaltet werden (Standard: aktiv).
*/
const WORK_SESSION_STALE_MINUTES = 10;
/** Startet eine neue Arbeits-Sitzung, oder frischt eine bestehende auf (Startzeit bleibt erhalten). */
function start_or_touch_work_session(PDO $pdo, int $ticketId, int $staffId): void
{
if (get_setting('auto_time_tracking_enabled', '1') !== '1') return;
$now = db_now();
$driver = $pdo->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];
}