111 lines
4.8 KiB
PHP
111 lines
4.8 KiB
PHP
<?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];
|
||
}
|