Files
2026-10-24 20:15:16 +00:00

220 lines
12 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
// Urlaub und Vertretung. Alle Funktionen beginnen mit vertretung_.
// Baut auf dem Urlaubsstatus im Profil auf (staff.is_on_vacation / vacation_substitute_id): Die automatische Zuweisung der Abteilungen
// und der E-Mail-Abruf beachten ihn bereits. Dieses Plugin setzt ihn zum Beginn und nimmt ihn zum Ende zurück.
const VERTRETUNG = 'vertretung';
function vertretung_today(): string
{
return (string) ($GLOBALS['vertretung_today'] ?? date('Y-m-d'));
}
function vertretung_status_labels(): array
{
return ['planned' => '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 .= '<li>#' . e((string) $row['ticket_number']) . ' – ' . e(mb_strimwidth((string) $row['subject'], 0, 70, '…')) . '</li>';
vertretung_mail($pdo, $to, 'Vertretung für ' . $name . ': ' . count($tickets) . ' Tickets', '<p>Sie vertreten <strong>' . e($name) . '</strong> vom ' . e(vertretung_date((string) $abs['start_date'])) . ' bis ' . e(vertretung_date((string) $abs['end_date'])) . '.</p>'
. '<p>' . count($tickets) . ' offene Tickets wurden Ihnen zugewiesen, neue Tickets von ' . e($name) . ' kommen ebenfalls zu Ihnen.</p>' . ($list ? '<ul>' . $list . '</ul>' : ''));
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 .= '<li>#' . e((string) $row['ticket_number']) . ' – ' . e(mb_strimwidth((string) $row['subject'], 0, 70, '…')) . '</li>';
vertretung_mail($pdo, $staffId, 'Willkommen zurück: ' . count($back) . ' Tickets', '<p>Ihre Abwesenheit ist beendet. Diese Tickets sind wieder bei Ihnen:</p><ul>' . $list . '</ul>');
}
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;
}