Upload via GUI (258 Dateien)

This commit is contained in:
2026-10-16 19:12:18 +00:00
parent 56deade877
commit 6cced3beda
50 changed files with 11148 additions and 0 deletions
+110
View File
@@ -0,0 +1,110 @@
<?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];
}