Upload via GUI (65 Dateien)

This commit is contained in:
2026-11-05 21:23:54 +00:00
parent 95faa40e98
commit 84448ad45e
15 changed files with 943 additions and 0 deletions
@@ -0,0 +1,22 @@
CREATE TABLE IF NOT EXISTS plugin_vertretung_absences (
id INTEGER PRIMARY KEY AUTOINCREMENT,
staff_id INTEGER NOT NULL,
substitute_id INTEGER NOT NULL,
start_date TEXT NOT NULL,
end_date TEXT NOT NULL,
note TEXT NOT NULL DEFAULT '',
status TEXT NOT NULL DEFAULT 'planned',
created_by INTEGER NULL,
created_at TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_plugin_vertretung_status ON plugin_vertretung_absences (status, start_date);
CREATE TABLE IF NOT EXISTS plugin_vertretung_moves (
id INTEGER PRIMARY KEY AUTOINCREMENT,
absence_id INTEGER NOT NULL,
ticket_id INTEGER NOT NULL,
from_staff_id INTEGER NOT NULL,
to_staff_id INTEGER NOT NULL,
created_at TEXT NOT NULL,
returned_at TEXT NULL
);
CREATE INDEX IF NOT EXISTS idx_plugin_vertretung_moves_absence ON plugin_vertretung_moves (absence_id, returned_at)
@@ -0,0 +1,93 @@
<?php
// Seite des Vertretungs-Plugins. Verfügbar: $staff, $pdo.
$isAdmin = ($staff['role'] ?? '') === 'admin';
$message = null;
$error = null;
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
verify_csrf();
$action = $_POST['action'] ?? '';
if ($action === 'create') {
$staffId = $isAdmin ? (int) ($_POST['staff_id'] ?? 0) : (int) $staff['id'];
$r = vertretung_create($pdo, $staff, $staffId, (int) ($_POST['substitute_id'] ?? 0), (string) ($_POST['start_date'] ?? ''), (string) ($_POST['end_date'] ?? ''), (string) ($_POST['note'] ?? ''));
if ($r['ok']) $message = 'Die Abwesenheit ist eingetragen.'; else $error = $r['error'];
} elseif ($action === 'cancel') {
if (vertretung_cancel($pdo, $staff, (int) ($_POST['id'] ?? 0))) $message = 'Die Abwesenheit wurde beendet, offene Tickets sind wieder bei der ursprünglichen Person.';
else $error = 'Diese Abwesenheit kann nicht beendet werden.';
}
}
$people = $pdo->query('SELECT id, name FROM staff WHERE is_active = 1 ORDER BY name')->fetchAll();
$labels = vertretung_status_labels();
$names = [];
foreach ($people as $p) $names[(int) $p['id']] = $p['name'];
$name = fn($id) => e($names[(int) $id] ?? vertretung_name($pdo, (int) $id));
$current = $pdo->query("SELECT a.*, (SELECT COUNT(*) FROM plugin_vertretung_moves m WHERE m.absence_id = a.id) AS moved, (SELECT COUNT(*) FROM plugin_vertretung_moves m WHERE m.absence_id = a.id AND m.returned_at IS NULL) AS holding FROM plugin_vertretung_absences a WHERE a.status IN ('planned','active') ORDER BY a.start_date, a.id")->fetchAll();
$past = $pdo->query("SELECT a.*, (SELECT COUNT(*) FROM plugin_vertretung_moves m WHERE m.absence_id = a.id) AS moved FROM plugin_vertretung_absences a WHERE a.status NOT IN ('planned','active') ORDER BY a.id DESC LIMIT 20")->fetchAll();
?>
<h1>Abwesenheit und Vertretung</h1>
<?php if ($message): ?><div class="flash success"><?= e($message) ?></div><?php endif; ?>
<?php if ($error): ?><div class="flash error"><?= e($error) ?></div><?php endif; ?>
<form method="post" class="card" style="max-width:900px;margin-bottom:16px">
<?= csrf_field() ?>
<input type="hidden" name="action" value="create">
<h3>Abwesenheit eintragen</h3>
<p class="muted" style="font-size:.85rem">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).</p>
<?php if (!function_exists('staff_substitution_department_ids')): ?><div class="flash warning">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.</div><?php endif; ?>
<div style="display:flex;gap:14px;flex-wrap:wrap">
<?php if ($isAdmin): ?>
<div><label>Wer ist abwesend?</label><select name="staff_id"><?php foreach ($people as $p): ?><option value="<?= (int) $p['id'] ?>" <?= (int) $p['id'] === (int) $staff['id'] ? 'selected' : '' ?>><?= e($p['name']) ?></option><?php endforeach; ?></select></div>
<?php endif; ?>
<div><label>Vertretung</label><select name="substitute_id" required><option value="">– wählen –</option><?php foreach ($people as $p): if ((int) $p['id'] === (int) $staff['id'] && !$isAdmin) continue; ?><option value="<?= (int) $p['id'] ?>"><?= e($p['name']) ?></option><?php endforeach; ?></select></div>
<div><label>Von</label><input type="date" name="start_date" value="<?= e(vertretung_today()) ?>" required></div>
<div><label>Bis (einschließlich)</label><input type="date" name="end_date" required></div>
</div>
<label style="margin-top:8px">Notiz (optional)</label><input type="text" name="note" maxlength="250" placeholder="zum Beispiel Sommerurlaub">
<div class="btn-row"><button type="submit" class="btn">Eintragen</button></div>
</form>
<div class="card" style="margin-bottom:16px">
<h3>Laufend und geplant</h3>
<?php if (!$current): ?><p class="muted">Keine Abwesenheiten eingetragen.</p><?php else: ?>
<table style="width:100%">
<thead><tr><th>Wer</th><th>Vertretung</th><th>Zeitraum</th><th>Stand</th><th>Tickets</th><th>Die Vertretung sieht</th><th></th></tr></thead>
<tbody>
<?php foreach ($current as $a):
$dq = $pdo->prepare('SELECT d.name FROM departments d WHERE d.id = (SELECT department_id FROM staff WHERE id = ?) OR d.id IN (SELECT department_id FROM staff_department WHERE staff_id = ?) ORDER BY d.name');
$dq->execute([(int) $a['staff_id'], (int) $a['staff_id']]);
$deptNames = $dq->fetchAll(PDO::FETCH_COLUMN);
$absentAdmin = ($pdo->query('SELECT role FROM staff WHERE id = ' . (int) $a['staff_id'])->fetchColumn() === 'admin');
?>
<tr>
<td><?= $name($a['staff_id']) ?><?php if ($a['note']): ?><div class="muted" style="font-size:.76rem"><?= e((string) $a['note']) ?></div><?php endif; ?></td>
<td><?= $name($a['substitute_id']) ?></td>
<td><?= e(vertretung_date((string) $a['start_date'])) ?> – <?= e(vertretung_date((string) $a['end_date'])) ?></td>
<td><?= e($labels[$a['status']] ?? $a['status']) ?></td>
<td><?= $a['status'] === 'active' ? (int) $a['holding'] . ' bei der Vertretung' : '–' ?></td>
<td style="font-size:.82rem"><?= $absentAdmin ? 'Abteilungen der übernommenen Tickets' : ($deptNames ? e(implode(', ', $deptNames)) : '–') ?></td>
<td>
<?php if (vertretung_can_manage($staff, (int) $a['staff_id'])): ?>
<form method="post" style="display:inline" onsubmit="return confirm('Abwesenheit beenden? Offene Tickets gehen zurück.')"><?= csrf_field() ?><input type="hidden" name="action" value="cancel"><input type="hidden" name="id" value="<?= (int) $a['id'] ?>"><button class="btn secondary" type="submit"><?= $a['status'] === 'active' ? 'Jetzt beenden' : 'Löschen' ?></button></form>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
</div>
<?php if ($past): ?>
<div class="card">
<h3>Frühere Abwesenheiten</h3>
<table style="width:100%">
<thead><tr><th>Wer</th><th>Vertretung</th><th>Zeitraum</th><th>Ergebnis</th><th>Weitergegeben</th></tr></thead>
<tbody>
<?php foreach ($past as $a): ?>
<tr><td><?= $name($a['staff_id']) ?></td><td><?= $name($a['substitute_id']) ?></td><td><?= e(vertretung_date((string) $a['start_date'])) ?> – <?= e(vertretung_date((string) $a['end_date'])) ?></td><td><?= e($labels[$a['status']] ?? $a['status']) ?></td><td><?= (int) $a['moved'] ?> Tickets</td></tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
+9
View File
@@ -0,0 +1,9 @@
{
"slug": "vertretung",
"name": "Urlaub und Vertretung",
"version": "1.1.0",
"description": "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. Die Vertretung sieht während der Abwesenheit auch die Abteilungen der abwesenden Person, selbst ohne eigenen Zugang. Baut auf dem Urlaubsstatus im Profil und auf der automatischen Zuweisung der Abteilungen auf (Server ab 1.0.55).",
"author": "DeskHub",
"requires": "1.0.55",
"permissions": ["hooks.tickets", "hooks.cron", "ui.menu", "db.migrations"]
}
+16
View File
@@ -0,0 +1,16 @@
<?php
// Urlaub und Vertretung: Zeiträume für Abwesenheiten, Umverteilung und Rückgabe von Tickets.
require_once __DIR__ . '/lib.php';
plugin_add_hook('cron.run', function () {
vertretung_tick(get_db());
});
// Ticket ist (von Regel, Hand oder API) einer abwesenden Person zugewiesen → an die Vertretung weitergeben
foreach (['ticket.created', 'ticket.assigned'] as $hook) {
plugin_add_hook($hook, function (array $payload) {
if (!empty($payload['ticketId'])) vertretung_redirect(get_db(), (int) $payload['ticketId']);
});
}
// Jeder Mitarbeiter pflegt seine eigene Abwesenheit, Administratoren alle
plugin_register_menu('Abwesenheit', 'vertretung');