220 lines
12 KiB
PHP
220 lines
12 KiB
PHP
<?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;
|
||
}
|