prepare('INSERT INTO departments (name, email) VALUES (?,?)')->execute([$name, strtolower($name) . '@firma.example']); return (int) $pdo->lastInsertId(); }; $person = function (string $name, string $role, ?int $dept) use ($pdo, $now): int { $pdo->prepare('INSERT INTO staff (name, email, password_hash, role, department_id, is_active, created_at) VALUES (?,?,?,?,?,1,?)') ->execute([$name, strtolower($name) . '@firma.example', 'x', $role, $dept, $now]); return (int) $pdo->lastInsertId(); }; $staffRow = fn(int $id) => $pdo->query('SELECT * FROM staff WHERE id = ' . $id)->fetch(); $ids = function (int $id) use ($staffRow): ?array { $r = staff_department_ids($staffRow($id)); if ($r !== null) sort($r); return $r; }; $away = fn(int $id, ?int $sub) => $pdo->prepare('UPDATE staff SET is_on_vacation = ?, vacation_substitute_id = ? WHERE id = ?')->execute([$sub === null ? 0 : 1, $sub, $id]); $dA = $dept('Support'); $dB = $dept('Technik'); $dC = $dept('Buchhaltung'); $anna = $person('Anna', 'agent', $dA); // Vertretung, nur Support $ben = $person('Ben', 'agent', $dB); // im Urlaub, Technik $cora = $person('Cora', 'agent', $dC); // Kette $chef = $person('Chef', 'admin', null); $pdo->prepare('INSERT INTO staff_department (staff_id, department_id) VALUES (?,?)')->execute([$ben, $dC]); // Ben hat zusätzlich Buchhaltung // 1) Ohne Abwesenheit: nur die eigene Abteilung $check($ids($anna) === [$dA], 'Ohne Abwesenheit hat Anna nur ihre Abteilung'); $check($ids($ben) === [$dB, $dC], 'Ben hat Technik und Buchhaltung'); // 2) Ben im Urlaub, Anna ist die Vertretung $away($ben, $anna); $check($ids($anna) === [$dA, $dB, $dC], 'Die Vertretung bekommt Haupt- und zusätzliche Abteilungen des Abwesenden'); $check($ids($ben) === [$dB, $dC], 'Der Abwesende selbst behält seinen Zugriff'); $check($ids($cora) === [$dC], 'Unbeteiligte bekommen nichts dazu'); [$sql, $params] = dept_filter_sql($ids($anna), 't.department_id'); $check(str_contains($sql, 'IN') && count($params) === 3, 'Die Ticketliste filtert mit den erweiterten Abteilungen'); $check(staff_can_access_department($staffRow($anna), $dB) === true, 'Die Vertretung darf Tickets der Abteilung öffnen'); // 3) Kette: Anna ist selbst im Urlaub, Cora vertritt Anna $away($anna, $cora); $check($ids($cora) === [$dA, $dB, $dC], 'Kette: die Vertretung der Vertretung bekommt beide Abteilungen'); $away($anna, null); // 4) Ende der Abwesenheit $away($ben, null); $check($ids($anna) === [$dA], 'Nach dem Urlaub ist der zusätzliche Zugriff wieder weg'); // 5) Inaktiver Abwesender zählt nicht $away($ben, $anna); $pdo->exec('UPDATE staff SET is_active = 0 WHERE id = ' . $ben); $check($ids($anna) === [$dA], 'Ein inaktiver Abwesender gibt keinen Zugriff'); $pdo->exec('UPDATE staff SET is_active = 1 WHERE id = ' . $ben); $away($ben, null); // 6) Abwesender Administrator: nur Abteilungen der zugewiesenen offenen Tickets $away($chef, $anna); $check($ids($anna) === [$dA], 'Abwesender Admin ohne Tickets: kein zusätzlicher Zugriff'); $pdo->prepare("INSERT INTO tickets (ticket_number, subject, status, department_id, assigned_staff_id, requester_name, requester_email, priority_id, created_at, updated_at) VALUES (?,?,?,?,?,?,?,(SELECT MIN(id) FROM priorities),?,?)") ->execute(['T-1', 'Test', 'open', $dB, $anna, 'Kunde', 'kunde@x.example', $now, $now]); $check($ids($anna) === [$dA, $dB], 'Abwesender Admin: die Abteilung des zugewiesenen Tickets kommt dazu'); $pdo->exec("UPDATE tickets SET status = 'closed'"); $check($ids($anna) === [$dA], 'Geschlossene Tickets geben keinen Zugriff mehr'); $away($chef, null); // 7) Admin bleibt unbeschränkt, Selbstvertretung wird ignoriert $check($ids($chef) === null, 'Administratoren sehen weiter alle Abteilungen'); $away($anna, $anna); $check($ids($anna) === [$dA], 'Selbst als Vertretung eingetragen: kein Zugriffsgewinn'); $away($anna, null); // 8) Weitergabe und Rückgabe der Tickets (Profil-Abwesenheit) $newTicket = function (string $nr, int $deptId, ?int $assignee, string $status = 'open') use ($pdo, $now): int { $pdo->prepare("INSERT INTO tickets (ticket_number, subject, status, department_id, assigned_staff_id, requester_name, requester_email, priority_id, created_at, updated_at) VALUES (?,?,?,?,?,?,?,(SELECT MIN(id) FROM priorities),?,?)") ->execute([$nr, 'Test ' . $nr, $status, $deptId, $assignee, 'Kunde', 'kunde@x.example', $now, $now]); return (int) $pdo->lastInsertId(); }; $owner = fn(int $t) => (int) $pdo->query('SELECT assigned_staff_id FROM tickets WHERE id = ' . $t)->fetchColumn(); $t1 = $newTicket('V-1', $dB, $ben); $t2 = $newTicket('V-2', $dB, $ben); $t3 = $newTicket('V-3', $dB, $ben, 'closed'); $t4 = $newTicket('V-4', $dB, $chef); $pdo->prepare('UPDATE staff SET is_on_vacation = 1, vacation_substitute_id = ? WHERE id = ?')->execute([$anna, $ben]); [$h, $b] = vacation_apply_change($pdo, $ben, [0, null], [1, $anna], false); $check($h === 0 && $owner($t1) === $ben, 'Ohne Haken wird nichts weitergegeben'); [$h, $b] = vacation_apply_change($pdo, $ben, [0, null], [1, $anna], true); $check($h === 2 && $owner($t1) === $anna && $owner($t2) === $anna, 'Offene Tickets gehen an die Vertretung'); $check($owner($t3) === $ben && $owner($t4) === $chef, 'Geschlossene und fremde Tickets bleiben'); $check((int) $pdo->query('SELECT COUNT(*) FROM vacation_moves WHERE returned_at IS NULL')->fetchColumn() === 2, 'Die Weitergabe wird gemerkt'); $check((int) $pdo->query("SELECT COUNT(*) FROM activity_log WHERE action LIKE 'An die Vertretung%'")->fetchColumn() === 2, 'Das Ticket-Protokoll nennt die Weitergabe'); [$h] = vacation_apply_change($pdo, $ben, [1, $anna], [1, $anna], true); $check($h === 0, 'Erneutes Speichern gibt nichts doppelt weiter'); // Anna ändert eines der Tickets selbst auf erledigt, ein anderes hängt jemand um $pdo->exec("UPDATE tickets SET status = 'resolved' WHERE id = " . $t1); $pdo->prepare('UPDATE tickets SET assigned_staff_id = ? WHERE id = ?')->execute([$cora, $t2]); // Ende des Urlaubs $pdo->prepare('UPDATE staff SET is_on_vacation = 0, vacation_substitute_id = NULL WHERE id = ?')->execute([$ben]); [$h, $b] = vacation_apply_change($pdo, $ben, [1, $anna], [0, null], true); $check($b === 0 && $owner($t1) === $anna && $owner($t2) === $cora, 'Erledigte und umgehängte Tickets kommen nicht zurück'); $check((int) $pdo->query('SELECT COUNT(*) FROM vacation_moves WHERE returned_at IS NULL')->fetchColumn() === 0, 'Alle Weitergaben sind abgeschlossen'); // Zurückgeben, wenn alles offen bei der Vertretung liegt $t5 = $newTicket('V-5', $dB, $ben); $pdo->prepare('UPDATE staff SET is_on_vacation = 1, vacation_substitute_id = ? WHERE id = ?')->execute([$anna, $ben]); vacation_apply_change($pdo, $ben, [0, null], [1, $anna], true); $check($owner($t5) === $anna, 'Weitergabe (zweiter Urlaub)'); $pdo->prepare('UPDATE staff SET is_on_vacation = 0, vacation_substitute_id = NULL WHERE id = ?')->execute([$ben]); [$h, $b] = vacation_apply_change($pdo, $ben, [1, $anna], [0, null], true); $check($b === 1 && $owner($t5) === $ben, 'Offene Tickets kommen nach dem Urlaub zurück'); // Neue Vertretung: erst zurück, dann an die neue $t6 = $newTicket('V-6', $dB, $ben); $pdo->prepare('UPDATE staff SET is_on_vacation = 1, vacation_substitute_id = ? WHERE id = ?')->execute([$anna, $ben]); vacation_apply_change($pdo, $ben, [0, null], [1, $anna], true); $pdo->prepare('UPDATE staff SET vacation_substitute_id = ? WHERE id = ?')->execute([$cora, $ben]); vacation_apply_change($pdo, $ben, [1, $anna], [1, $cora], true); $check($owner($t6) === $cora, 'Bei neuer Vertretung geht das Ticket an die neue Person'); // Ohne Vertretung / Vertretung selbst im Urlaub ohne Ersatz: nichts $pdo->prepare('UPDATE staff SET is_on_vacation = 0, vacation_substitute_id = NULL WHERE id = ?')->execute([$ben]); vacation_apply_change($pdo, $ben, [1, $cora], [0, null], true); $check($owner($t6) === $ben, 'Zurück bei Ben'); $pdo->prepare('UPDATE staff SET is_on_vacation = 1, vacation_substitute_id = NULL WHERE id = ?')->execute([$ben]); [$h] = vacation_apply_change($pdo, $ben, [0, null], [1, null], true); $check($h === 0 && $owner($t6) === $ben, 'Ohne Vertretung bleibt alles beim Abwesenden'); // Das Plugin „Urlaub und Vertretung“ hat Vorrang $pdo->exec($pdo->getAttribute(PDO::ATTR_DRIVER_NAME) === 'sqlite' ? 'CREATE TABLE IF NOT EXISTS plugin_vertretung_absences (id INTEGER PRIMARY KEY AUTOINCREMENT, staff_id INTEGER, substitute_id INTEGER, start_date TEXT, end_date TEXT, note TEXT, status TEXT, created_by INTEGER, created_at TEXT)' : 'CREATE TABLE IF NOT EXISTS plugin_vertretung_absences (id INT AUTO_INCREMENT PRIMARY KEY, staff_id INT, substitute_id INT, start_date VARCHAR(20), end_date VARCHAR(20), note TEXT, status VARCHAR(20), created_by INT, created_at VARCHAR(20))'); $pdo->prepare("INSERT INTO plugin_vertretung_absences (staff_id, substitute_id, start_date, end_date, note, status, created_at) VALUES (?,?,?,?,?,?,?)")->execute([$ben, $anna, '2026-01-01', '2026-12-31', '', 'active', $now]); $pdo->prepare('UPDATE staff SET is_on_vacation = 1, vacation_substitute_id = ? WHERE id = ?')->execute([$anna, $ben]); [$h] = vacation_apply_change($pdo, $ben, [0, null], [1, $anna], true); $check($h === 0 && $owner($t6) === $ben, 'Ist eine Abwesenheit im Plugin aktiv, gibt der Server nichts selbst weiter'); @unlink($db); if ($failures) { fwrite(STDERR, "Vertretungs-Zugriff fehlgeschlagen:\n- " . implode("\n- ", $failures) . "\n"); exit(1); } echo "Vertretungs-Zugriff bestanden.\n";