'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 .= '
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 ? 'Ihre Abwesenheit ist beendet. Diese Tickets sind wieder bei Ihnen: