'Geplant', 'active' => 'Läuft', 'done' => 'Beendet', 'cancelled' => 'Abgebrochen', 'missed' => 'Nicht angetreten']; } function vertretung_staff(PDO $pdo, int $id): ?array { $s = $pdo->prepare('SELECT id, name, email, is_active, is_on_vacation, vacation_substitute_id FROM staff WHERE id = ?'); $s->execute([$id]); return $s->fetch() ?: null; } function vertretung_name(PDO $pdo, int $id): string { return (string) (vertretung_staff($pdo, $id)['name'] ?? ('#' . $id)); } function vertretung_can_manage(array $actor, int $staffId): bool { return ($actor['role'] ?? '') === 'admin' || (int) ($actor['id'] ?? 0) === $staffId; } function vertretung_mail(PDO $pdo, int $staffId, string $subject, string $html): void { $st = vertretung_staff($pdo, $staffId); if (!$st || !(int) $st['is_active'] || trim((string) $st['email']) === '') return; try { send_mail((string) $st['email'], $subject, $html); } catch (Throwable $e) { error_log('vertretung mail: ' . $e->getMessage()); } } /** * Wer arbeitet an Stelle von $staffId? Folgt Vertretungsketten (höchstens drei Stufen, ohne Schleifen). null, wenn die Person * anwesend ist oder keine nutzbare Vertretung hat. */ function vertretung_target(PDO $pdo, int $staffId): ?int { $row = vertretung_staff($pdo, $staffId); if (!$row || !(int) $row['is_on_vacation']) return null; $seen = [$staffId]; for ($i = 0; $i < 3; $i++) { $sub = (int) ($row['vacation_substitute_id'] ?? 0); if ($sub <= 0 || in_array($sub, $seen, true)) return null; $row = vertretung_staff($pdo, $sub); if (!$row || !(int) $row['is_active']) return null; if (!(int) $row['is_on_vacation']) return $sub; $seen[] = $sub; } return null; } /** Gibt ein Ticket an $to weiter und merkt es für die Rückgabe. */ function vertretung_move(PDO $pdo, int $ticketId, int $from, int $to, int $absenceId): void { $pdo->prepare('UPDATE tickets SET assigned_staff_id = ? WHERE id = ?')->execute([$to, $ticketId]); log_activity($pdo, $ticketId, null, 'An die Vertretung ' . vertretung_name($pdo, $to) . ' weitergegeben (' . vertretung_name($pdo, $from) . ' ist abwesend)'); $pdo->prepare('INSERT INTO plugin_vertretung_moves (absence_id, ticket_id, from_staff_id, to_staff_id, created_at) VALUES (?,?,?,?,?)') ->execute([$absenceId, $ticketId, $from, $to, db_now()]); } /** Hook: Ticket gehört einer abwesenden Person → an die Vertretung. */ function vertretung_redirect(PDO $pdo, int $ticketId): void { $t = $pdo->prepare('SELECT assigned_staff_id, status, is_spam FROM tickets WHERE id = ?'); $t->execute([$ticketId]); $ticket = $t->fetch(); if (!$ticket || !$ticket['assigned_staff_id'] || !empty($ticket['is_spam']) || in_array($ticket['status'], ['resolved', 'closed'], true)) return; $from = (int) $ticket['assigned_staff_id']; $to = vertretung_target($pdo, $from); if ($to === null || $to === $from) return; $a = $pdo->prepare("SELECT id FROM plugin_vertretung_absences WHERE staff_id = ? AND status = 'active' ORDER BY id DESC LIMIT 1"); $a->execute([$from]); vertretung_move($pdo, $ticketId, $from, $to, (int) $a->fetchColumn()); } /** Prüft Eingaben für eine neue Abwesenheit. Liefert eine Fehlermeldung oder null. */ function vertretung_validate(PDO $pdo, int $staffId, int $subId, string $start, string $end): ?string { $okDate = static fn(string $d) => preg_match('/^(\d{4})-(\d{2})-(\d{2})$/', $d, $m) === 1 && checkdate((int) $m[2], (int) $m[3], (int) $m[1]); if (!$okDate($start) || !$okDate($end)) return 'Bitte Beginn und Ende als Datum angeben.'; if ($end < $start) return 'Das Ende darf nicht vor dem Beginn liegen.'; if ($end < vertretung_today()) return 'Der Zeitraum liegt schon in der Vergangenheit.'; $st = vertretung_staff($pdo, $staffId); if (!$st || !(int) $st['is_active']) return 'Dieser Mitarbeiter ist nicht aktiv.'; $sub = vertretung_staff($pdo, $subId); if (!$sub || !(int) $sub['is_active']) return 'Bitte eine aktive Vertretung wählen.'; if ($subId === $staffId) return 'Die Vertretung muss eine andere Person sein.'; $o = $pdo->prepare("SELECT COUNT(*) FROM plugin_vertretung_absences WHERE staff_id = ? AND status IN ('planned','active') AND start_date <= ? AND end_date >= ?"); $o->execute([$staffId, $end, $start]); if ((int) $o->fetchColumn() > 0) return 'In diesem Zeitraum ist für diese Person schon eine Abwesenheit eingetragen.'; return null; } /** @return array{ok:bool,error?:string,id?:int} */ function vertretung_create(PDO $pdo, array $actor, int $staffId, int $subId, string $start, string $end, string $note): array { if (!vertretung_can_manage($actor, $staffId)) return ['ok' => false, 'error' => 'Sie dürfen nur Ihre eigene Abwesenheit eintragen.']; $err = vertretung_validate($pdo, $staffId, $subId, $start, $end); if ($err !== null) return ['ok' => false, 'error' => $err]; $pdo->prepare("INSERT INTO plugin_vertretung_absences (staff_id, substitute_id, start_date, end_date, note, status, created_by, created_at) VALUES (?,?,?,?,?,'planned',?,?)") ->execute([$staffId, $subId, $start, $end, mb_substr(trim($note), 0, 250), (int) ($actor['id'] ?? 0) ?: null, db_now()]); $id = (int) $pdo->lastInsertId(); if ($start <= vertretung_today()) vertretung_tick($pdo); // beginnt sofort return ['ok' => true, 'id' => $id]; } function vertretung_cancel(PDO $pdo, array $actor, int $id): bool { $s = $pdo->prepare("SELECT * FROM plugin_vertretung_absences WHERE id = ? AND status IN ('planned','active')"); $s->execute([$id]); $abs = $s->fetch(); if (!$abs || !vertretung_can_manage($actor, (int) $abs['staff_id'])) return false; if ($abs['status'] === 'active') vertretung_deactivate($pdo, $abs, 'cancelled'); else $pdo->prepare("UPDATE plugin_vertretung_absences SET status = 'cancelled' WHERE id = ?")->execute([$id]); return true; } /** Beginn: Urlaubsstatus setzen und offene Tickets an die Vertretung verteilen. Liefert die Zahl der weitergegebenen Tickets. */ function vertretung_activate(PDO $pdo, array $abs): int { $staffId = (int) $abs['staff_id']; $pdo->prepare("UPDATE plugin_vertretung_absences SET status = 'active' WHERE id = ?")->execute([$abs['id']]); $pdo->prepare('UPDATE staff SET is_on_vacation = 1, vacation_substitute_id = ? WHERE id = ?')->execute([$abs['substitute_id'], $staffId]); $to = vertretung_target($pdo, $staffId); if ($to === null) return 0; $t = $pdo->prepare("SELECT id, ticket_number, subject FROM tickets WHERE assigned_staff_id = ? AND is_spam = 0 AND status NOT IN ('resolved','closed') ORDER BY id"); $t->execute([$staffId]); $tickets = $t->fetchAll(); foreach ($tickets as $row) vertretung_move($pdo, (int) $row['id'], $staffId, $to, (int) $abs['id']); $name = vertretung_name($pdo, $staffId); $list = ''; foreach (array_slice($tickets, 0, 15) as $row) $list .= '
  • #' . e((string) $row['ticket_number']) . ' – ' . e(mb_strimwidth((string) $row['subject'], 0, 70, '…')) . '
  • '; vertretung_mail($pdo, $to, 'Vertretung für ' . $name . ': ' . count($tickets) . ' Tickets', '

    Sie vertreten ' . e($name) . ' vom ' . e(vertretung_date((string) $abs['start_date'])) . ' bis ' . e(vertretung_date((string) $abs['end_date'])) . '.

    ' . '

    ' . count($tickets) . ' offene Tickets wurden Ihnen zugewiesen, neue Tickets von ' . e($name) . ' kommen ebenfalls zu Ihnen.

    ' . ($list ? '' : '')); return count($tickets); } /** Ende oder Abbruch: Urlaubsstatus zurücknehmen und noch offene Tickets zurückgeben. Liefert die Zahl der zurückgegebenen Tickets. */ function vertretung_deactivate(PDO $pdo, array $abs, string $newStatus): int { $staffId = (int) $abs['staff_id']; $pdo->prepare("UPDATE plugin_vertretung_absences SET status = ? WHERE id = ?")->execute([$newStatus, $abs['id']]); // nur zurücknehmen, was dieses Plugin gesetzt hat (hat jemand die Vertretung im Profil geändert, bleibt es so) $pdo->prepare('UPDATE staff SET is_on_vacation = 0, vacation_substitute_id = NULL WHERE id = ? AND is_on_vacation = 1 AND vacation_substitute_id = ?')->execute([$staffId, $abs['substitute_id']]); $m = $pdo->prepare('SELECT m.id, m.ticket_id, m.to_staff_id, t.assigned_staff_id, t.status, t.ticket_number, t.subject FROM plugin_vertretung_moves m JOIN tickets t ON t.id = m.ticket_id WHERE m.absence_id = ? AND m.returned_at IS NULL ORDER BY m.id'); $m->execute([$abs['id']]); $back = []; foreach ($m->fetchAll() as $row) { $stillThere = (int) $row['assigned_staff_id'] === (int) $row['to_staff_id'] && !in_array($row['status'], ['resolved', 'closed'], true); if ($stillThere) { $pdo->prepare('UPDATE tickets SET assigned_staff_id = ? WHERE id = ?')->execute([$staffId, $row['ticket_id']]); log_activity($pdo, (int) $row['ticket_id'], null, 'Zurück an ' . vertretung_name($pdo, $staffId) . ' (Abwesenheit beendet)'); $back[] = $row; } $pdo->prepare('UPDATE plugin_vertretung_moves SET returned_at = ? WHERE id = ?')->execute([db_now(), $row['id']]); } if ($back) { $list = ''; foreach (array_slice($back, 0, 15) as $row) $list .= '
  • #' . e((string) $row['ticket_number']) . ' – ' . e(mb_strimwidth((string) $row['subject'], 0, 70, '…')) . '
  • '; vertretung_mail($pdo, $staffId, 'Willkommen zurück: ' . count($back) . ' Tickets', '

    Ihre Abwesenheit ist beendet. Diese Tickets sind wieder bei Ihnen:

    '); } return count($back); } /** Cron: abgelaufene Abwesenheiten beenden, fällige beginnen. */ function vertretung_tick(PDO $pdo): void { $today = vertretung_today(); $q = $pdo->prepare("SELECT * FROM plugin_vertretung_absences WHERE status = 'active' AND end_date < ?"); $q->execute([$today]); foreach ($q->fetchAll() as $abs) vertretung_deactivate($pdo, $abs, 'done'); $pdo->prepare("UPDATE plugin_vertretung_absences SET status = 'missed' WHERE status = 'planned' AND end_date < ?")->execute([$today]); $q = $pdo->prepare("SELECT * FROM plugin_vertretung_absences WHERE status = 'planned' AND start_date <= ? AND end_date >= ? ORDER BY start_date, id"); $q->execute([$today, $today]); foreach ($q->fetchAll() as $abs) vertretung_activate($pdo, $abs); vertretung_rebalance($pdo); } /** * Ist die erste Vertretung zurück (oder hat sich die Kette geändert), wandern die für die Abwesende weitergegebenen Tickets, * die noch unberührt bei der bisherigen Vertretung liegen, zur jetzt zuständigen Person. */ function vertretung_rebalance(PDO $pdo): void { foreach ($pdo->query("SELECT id, staff_id FROM plugin_vertretung_absences WHERE status = 'active'")->fetchAll() as $abs) { $target = vertretung_target($pdo, (int) $abs['staff_id']); if ($target === null) continue; $m = $pdo->prepare('SELECT m.id, m.ticket_id, m.to_staff_id FROM plugin_vertretung_moves m JOIN tickets t ON t.id = m.ticket_id WHERE m.absence_id = ? AND m.returned_at IS NULL AND m.to_staff_id <> ? AND t.assigned_staff_id = m.to_staff_id'); $m->execute([$abs['id'], $target]); foreach ($m->fetchAll() as $row) { $t = $pdo->prepare('SELECT status FROM tickets WHERE id = ?'); $t->execute([$row['ticket_id']]); if (in_array($t->fetchColumn(), ['resolved', 'closed'], true)) continue; $pdo->prepare('UPDATE tickets SET assigned_staff_id = ? WHERE id = ?')->execute([$target, $row['ticket_id']]); $pdo->prepare('UPDATE plugin_vertretung_moves SET to_staff_id = ? WHERE id = ?')->execute([$target, $row['id']]); log_activity($pdo, (int) $row['ticket_id'], null, 'An die Vertretung ' . vertretung_name($pdo, $target) . ' weitergegeben (Vertretung ist zurück)'); } } } function vertretung_date(string $ymd): string { $t = strtotime($ymd . ' 12:00:00'); return $t ? date('d.m.Y', $t) : $ymd; }