Upload via GUI (65 Dateien)
This commit is contained in:
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"_info": "English translations for this plugin. Key = German source text, value = English. {} stands for a changing part, {#} for a number.",
|
||||
"Abwesenheit": "Absence",
|
||||
"Abwesenheit eintragen": "Enter absence",
|
||||
"Abwesenheit und Vertretung": "Absence and substitute",
|
||||
"An die Vertretung": "To the substitute",
|
||||
"Bis (einschließlich)": "Until (inclusive)",
|
||||
"Bitte Beginn und Ende als Datum angeben.": "Please enter the start and end as dates.",
|
||||
"Bitte eine aktive Vertretung wählen.": "Please choose an active substitute.",
|
||||
"Das Ende darf nicht vor dem Beginn liegen.": "The end must not be before the start.",
|
||||
"Der Zeitraum liegt schon in der Vergangenheit.": "The period is already in the past.",
|
||||
"Die Abwesenheit ist eingetragen.": "The absence has been entered.",
|
||||
"Die Abwesenheit wurde beendet, offene Tickets sind wieder bei der ursprünglichen Person.": "The absence has ended; open tickets are back with the original person.",
|
||||
"Die Vertretung muss eine andere Person sein.": "The substitute must be a different person.",
|
||||
"Diese Abwesenheit kann nicht beendet werden.": "This absence cannot be ended.",
|
||||
"Dieser Mitarbeiter ist nicht aktiv.": "This staff member is not active.",
|
||||
"Eintragen": "Enter",
|
||||
"In diesem Zeitraum ist für diese Person schon eine Abwesenheit eingetragen.": "An absence has already been entered for this person in this period.",
|
||||
"Keine Abwesenheiten eingetragen.": "No absences entered.",
|
||||
"Laufend und geplant": "Ongoing and planned",
|
||||
"Mitarbeiter tragen Abwesenheiten mit Vertretung ein. Zum Beginn werden ihre offenen Tickets an die Vertretung verteilt, neue Tickets gehen automatisch dorthin, und am Ende bekommt der Mitarbeiter seine noch offenen Tickets zurück. Baut auf dem Urlaubsstatus im Profil und auf der automatischen Zuweisung der Abteilungen auf.": "Staff enter absences with a substitute. At the start their open tickets are distributed to the substitute, new tickets go there automatically, and at the end the staff member gets back their still-open tickets. Builds on the vacation status in the profile and on the automatic assignment of the departments.",
|
||||
"Nicht angetreten": "Not started",
|
||||
"Urlaub und Vertretung": "Vacation and substitute",
|
||||
"Vertretung": "Substitute",
|
||||
"Vertretung für": "Substitute for",
|
||||
"Wer ist abwesend?": "Who is absent?",
|
||||
"zum Beispiel Sommerurlaub": "for example summer vacation",
|
||||
"– wählen –": "– choose –",
|
||||
"Die Vertretung sieht": "The substitute sees",
|
||||
"Abteilungen der übernommenen Tickets": "Departments of the tickets taken over",
|
||||
"Dieser Server ist älter als 1.0.55: Die Vertretung bekommt die Tickets, sieht sie aber nur, wenn sie auch Zugang zur Abteilung hat. Bitte den Server aktualisieren.": "This server is older than 1.0.55: the substitute receives the tickets but only sees them with access to the department. Please update the server.",
|
||||
"Zum Beginn gehen Ihre offenen Tickets an die Vertretung, neue Tickets ebenfalls (auch bei automatischer Zuweisung). Am Ende bekommen Sie die Tickets zurück, die dann noch bei der Vertretung liegen. Der Urlaubsstatus im Profil wird dafür automatisch gesetzt. Solange die Abwesenheit dauert, sieht und bearbeitet die Vertretung auch die Abteilungen der abwesenden Person, selbst wenn sie dort sonst keinen Zugang hat (Server ab 1.0.55).": "At the start your open tickets go to the substitute, as do new tickets (also with automatic assignment). At the end you get back the tickets that are still with the substitute. The vacation status in the profile is set automatically for this. While the absence lasts, the substitute also sees and handles the absent person’s departments, even without their own access (server 1.0.55 or later)."
|
||||
}
|
||||
@@ -0,0 +1,219 @@
|
||||
<?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;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
CREATE TABLE IF NOT EXISTS plugin_vertretung_absences (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
staff_id INT NOT NULL,
|
||||
substitute_id INT NOT NULL,
|
||||
start_date DATE NOT NULL,
|
||||
end_date DATE NOT NULL,
|
||||
note VARCHAR(255) NOT NULL DEFAULT '',
|
||||
status VARCHAR(12) NOT NULL DEFAULT 'planned',
|
||||
created_by INT NULL,
|
||||
created_at DATETIME NOT NULL,
|
||||
KEY idx_plugin_vertretung_status (status, start_date)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
CREATE TABLE IF NOT EXISTS plugin_vertretung_moves (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
absence_id INT NOT NULL,
|
||||
ticket_id INT NOT NULL,
|
||||
from_staff_id INT NOT NULL,
|
||||
to_staff_id INT NOT NULL,
|
||||
created_at DATETIME NOT NULL,
|
||||
returned_at DATETIME NULL,
|
||||
KEY idx_plugin_vertretung_moves_absence (absence_id, returned_at)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
|
||||
Reference in New Issue
Block a user