query('SELECT * FROM departments ORDER BY name')->fetchAll(); $customRoles = $pdo->query('SELECT * FROM roles ORDER BY name')->fetchAll(); $teams = $pdo->query('SELECT id, name, is_active FROM teams ORDER BY name')->fetchAll(); $permLabels = permission_labels(); /** Setzt die zusätzlichen Abteilungen (Erweiterter Zugriff) neu. */ function save_staff_access(PDO $pdo, int $id, ?int $primaryDeptId, array $post): void { $selected = array_unique(array_filter(array_map('intval', $post['extra_departments'] ?? []))); $pdo->prepare('DELETE FROM staff_department WHERE staff_id = ?')->execute([$id]); $ins = $pdo->prepare('INSERT INTO staff_department (staff_id, department_id, notify, role_id) VALUES (?,?,?,?)'); $validRoles = array_map('intval', $pdo->query('SELECT id FROM roles')->fetchAll(PDO::FETCH_COLUMN)); foreach ($selected as $dId) { if ($dId === (int) $primaryDeptId) continue; // Hauptabteilung wird nicht doppelt gespeichert $rid = (int) ($post['role_' . $dId] ?? 0); $ins->execute([$id, $dId, !empty($post['notify_' . $dId]) ? 1 : 0, in_array($rid, $validRoles, true) ? $rid : null]); } } /** Setzt die Team-Mitgliedschaften eines Mitarbeiters neu. */ function save_staff_teams(PDO $pdo, int $id, array $teamIds): void { $pdo->prepare('DELETE FROM team_members WHERE staff_id = ?')->execute([$id]); $ins = $pdo->prepare('INSERT INTO team_members (team_id, staff_id) VALUES (?,?)'); foreach (array_unique(array_filter(array_map('intval', $teamIds))) as $tid) $ins->execute([$tid, $id]); } if ($_SERVER['REQUEST_METHOD'] === 'POST') { verify_csrf(); $action = $_POST['action'] ?? ''; if ($action === 'create' || $action === 'update') { $id = (int) ($_POST['id'] ?? 0); $name = trim($_POST['name'] ?? ''); $email = trim($_POST['email'] ?? ''); // Nur echte Administratoren dürfen die Administrator-Rolle vergeben – sonst könnte sich ein // per Co-Admin-Recht (nur manage_staff) berechtigter Agent selbst (oder andere) zum // Vollzugriff-Admin machen. Beim Bearbeiten durch einen Co-Admin bleibt die vorhandene // Rolle des Ziel-Kontos unverändert, statt sie stillschweigend auf "Mitarbeiter" zu setzen. if ($isFullAdmin) { $role = ($_POST['role'] ?? '') === 'admin' ? 'admin' : 'agent'; } elseif ($action === 'update') { $currentRoleStmt = $pdo->prepare('SELECT role FROM staff WHERE id = ?'); $currentRoleStmt->execute([$id]); $role = $currentRoleStmt->fetchColumn() === 'admin' ? 'admin' : 'agent'; } else { $role = 'agent'; } $roleId = ($_POST['role_id'] ?? '') !== '' ? (int) $_POST['role_id'] : null; $deptId = ($_POST['department_id'] ?? '') !== '' ? (int) $_POST['department_id'] : null; $isActive = ($_POST['status'] ?? 'active') === 'active' ? 1 : 0; $password = $_POST['password'] ?? ''; $signature = sanitize_staff_html(trim($_POST['signature'] ?? '')); if ($name === '') $errors[] = 'Bitte einen Namen angeben.'; if (!filter_var($email, FILTER_VALIDATE_EMAIL)) $errors[] = 'Bitte eine gültige E-Mail-Adresse angeben.'; if ($action === 'create' || $password !== '') { $pwErr = validate_password_strength($password); if ($pwErr) $errors[] = $pwErr; } if ($action === 'update' && $id === (int) $me['id'] && !$isActive) $errors[] = 'Sie können Ihr eigenes Konto nicht deaktivieren.'; $dup = $pdo->prepare('SELECT COUNT(*) FROM staff WHERE LOWER(email) = LOWER(?) AND id <> ?'); $dup->execute([$email, $id]); if ($dup->fetchColumn() > 0) $errors[] = 'Ein Mitarbeiter mit dieser E-Mail-Adresse existiert bereits.'; if (!$errors) { if ($action === 'create') { $pdo->prepare('INSERT INTO staff (name, email, password_hash, role, role_id, department_id, signature, is_active, created_at) VALUES (?,?,?,?,?,?,?,?,?)') ->execute([$name, $email, password_hash($password, PASSWORD_DEFAULT), $role, $roleId, $deptId, $signature ?: null, $isActive, db_now()]); $id = (int) $pdo->lastInsertId(); audit_log($me['id'], "Mitarbeiter angelegt: {$name} ({$email})"); flash('success', 'Mitarbeiter angelegt.'); } else { $sql = 'UPDATE staff SET name=?, email=?, role=?, role_id=?, department_id=?, signature=?, is_active=?'; $params = [$name, $email, $role, $roleId, $deptId, $signature ?: null, $isActive]; if ($password !== '') { $sql .= ', password_hash=?'; $params[] = password_hash($password, PASSWORD_DEFAULT); } $params[] = $id; $pdo->prepare($sql . ' WHERE id=?')->execute($params); audit_log($me['id'], "Mitarbeiter aktualisiert: {$name} ({$email})"); flash('success', 'Änderungen gespeichert.'); } save_staff_access($pdo, $id, $deptId, $_POST); if ($canManageTeams) save_staff_teams($pdo, $id, (array) ($_POST['teams'] ?? [])); redirect('/staff/staff_manage.php'); } } // Eigenständiger Zugriffs-Speicher-Aufruf (Rückwärtskompatibilität) if ($action === 'save_access') { $id = (int) $_POST['id']; $stmt = $pdo->prepare('SELECT department_id FROM staff WHERE id = ?'); $stmt->execute([$id]); save_staff_access($pdo, $id, (int) $stmt->fetchColumn(), $_POST); audit_log($me['id'], "Abteilungszugriff geändert für Mitarbeiter #{$id}"); flash('success', 'Abteilungszugriff gespeichert.'); redirect('/staff/staff_manage.php?edit=' . $id . '&tab=access'); } if ($action === 'unlock_login') { $id = (int) $_POST['id']; $exists = $pdo->prepare('SELECT COUNT(*) FROM staff WHERE id = ?'); $exists->execute([$id]); if (!(int) $exists->fetchColumn()) { flash('error', 'Mitarbeiter nicht gefunden.'); } else { login_guard_unlock($pdo, $id, (int) $me['id']); flash('success', 'Die Anmeldesperre ist aufgehoben. Der Mitarbeiter kann sich sofort wieder anmelden.'); } redirect('/staff/staff_manage.php'); } if ($action === 'sign_out_all') { $id = (int) $_POST['id']; $exists = $pdo->prepare('SELECT COUNT(*) FROM staff WHERE id = ?'); $exists->execute([$id]); if (!(int) $exists->fetchColumn()) { flash('error', 'Mitarbeiter nicht gefunden.'); } else { $r = staff_sign_out_everywhere($pdo, $id, (int) $me['id'], $id === (int) $me['id']); flash('success', 'Mitarbeiter von allen Geräten abgemeldet. Beendet: App-Anmeldungen ' . $r['apps'] . ', Browser-Push ' . $r['browsers'] . ', App-Geräte ' . $r['devices'] . '.'); } redirect('/staff/staff_manage.php'); } if ($action === 'remove_passkey') { $id = (int) $_POST['id']; $q = $pdo->prepare('SELECT id, name, email FROM staff WHERE id = ?'); $q->execute([$id]); $target = $q->fetch(); $del = $pdo->prepare('DELETE FROM staff_passkeys WHERE id = ? AND staff_id = ?'); if ($target) $del->execute([(int) ($_POST['passkey_id'] ?? 0), $id]); if ($target && $del->rowCount() > 0) { audit_log((int) $me['id'], "Passkey von Mitarbeiter #{$id} entfernt"); login_guard_record($pdo, 'passkey_removed', $id, (string) $target['email'], 'web', '1 Passkey durch Administrator #' . (int) $me['id'] . ' entfernt'); if ($id !== (int) $me['id'] && trim((string) $target['email']) !== '') { send_mail((string) $target['email'], 'Ein Passkey wurde entfernt', email_wrap('

Passkey entfernt

Ein Administrator hat einen Ihrer Passkeys in DeskHub entfernt. Sie können sich weiter mit Passwort (und 2FA) anmelden. Haben Sie das nicht erwartet, wenden Sie sich an Ihre Administration.

')); } flash('success', 'Passkey entfernt.'); } else { flash('error', 'Passkey nicht gefunden.'); } redirect('/staff/staff_manage.php?edit=' . $id); } if ($action === 'remove_passkeys') { $id = (int) $_POST['id']; $q = $pdo->prepare('SELECT id, name, email FROM staff WHERE id = ?'); $q->execute([$id]); $target = $q->fetch(); if (!$target) { flash('error', 'Mitarbeiter nicht gefunden.'); } else { $n = webauthn_remove_all($pdo, $id); if ($n > 0) { audit_log((int) $me['id'], "Passkeys von Mitarbeiter #{$id} entfernt ({$n})"); login_guard_record($pdo, 'passkey_removed', $id, (string) $target['email'], 'web', $n . ' Passkey(s) durch Administrator #' . (int) $me['id'] . ' entfernt'); if ($id !== (int) $me['id'] && trim((string) $target['email']) !== '') { send_mail((string) $target['email'], 'Ihre Passkeys wurden entfernt', email_wrap('

Passkeys entfernt

Ein Administrator hat Ihre Passkeys in DeskHub entfernt. Sie können sich weiter mit Passwort (und 2FA) anmelden und im Profil unter „Sicherheit“ neue Passkeys einrichten. Haben Sie das nicht erwartet, wenden Sie sich an Ihre Administration.

')); } flash('success', $n . ' Passkey' . ($n === 1 ? '' : 's') . ' von ' . $target['name'] . ' entfernt. Die Anmeldung mit Passwort bleibt möglich.'); } else { flash('info', 'Dieser Mitarbeiter hat keine Passkeys.'); } } redirect(($_POST['back'] ?? '') === 'edit' ? '/staff/staff_manage.php?edit=' . $id : '/staff/staff_manage.php'); } if ($action === 'delete') { $id = (int) $_POST['id']; if ($id === (int) $me['id']) { flash('error', 'Sie können Ihr eigenes Konto nicht löschen.'); } else { $pdo->prepare('UPDATE tickets SET assigned_staff_id = NULL WHERE assigned_staff_id = ?')->execute([$id]); webauthn_remove_all($pdo, $id); $pdo->prepare('DELETE FROM staff WHERE id = ?')->execute([$id]); audit_log($me['id'], "Mitarbeiter gelöscht: #{$id}"); flash('success', 'Mitarbeiter gelöscht.'); } redirect('/staff/staff_manage.php'); } } $isNew = isset($_GET['new']); $editId = (int) ($_GET['edit'] ?? 0); $showForm = $isNew || $editId || ($errors && in_array($_POST['action'] ?? '', ['create', 'update'], true)); $editStaff = null; $extraAccess = []; // department_id => ['notify' => 0/1, 'role_id' => ?int] $memberTeams = []; if ($editId) { $stmt = $pdo->prepare('SELECT * FROM staff WHERE id = ?'); $stmt->execute([$editId]); $editStaff = $stmt->fetch() ?: null; if ($editStaff) { $stmt = $pdo->prepare('SELECT department_id, notify, role_id FROM staff_department WHERE staff_id = ?'); $stmt->execute([$editId]); foreach ($stmt->fetchAll() as $row) $extraAccess[(int) $row['department_id']] = ['notify' => (int) $row['notify'], 'role_id' => $row['role_id'] !== null ? (int) $row['role_id'] : null]; $stmt = $pdo->prepare('SELECT team_id FROM team_members WHERE staff_id = ?'); $stmt->execute([$editId]); $memberTeams = array_map('intval', $stmt->fetchAll(PDO::FETCH_COLUMN)); } } if ($_POST && $errors) { // eingegebene Auswahl bei Fehlern erhalten $extraAccess = []; foreach (array_filter(array_map('intval', $_POST['extra_departments'] ?? [])) as $dId) { $extraAccess[$dId] = ['notify' => !empty($_POST['notify_' . $dId]) ? 1 : 0, 'role_id' => ($_POST['role_' . $dId] ?? '') !== '' ? (int) $_POST['role_' . $dId] : null]; } $memberTeams = array_map('intval', (array) ($_POST['teams'] ?? [])); } $allStaff = $pdo->query( "SELECT s.*, d.name AS department_name, (SELECT r.name FROM roles r WHERE r.id = s.role_id) AS role_name, (SELECT COUNT(*) FROM team_members tm WHERE tm.staff_id = s.id) AS team_count, (SELECT COUNT(*) FROM tickets t WHERE t.assigned_staff_id = s.id AND t.is_spam = 0 AND t.status NOT IN ('resolved','closed')) AS open_tickets FROM staff s LEFT JOIN departments d ON d.id = s.department_id ORDER BY s.name" )->fetchAll(); // Weitere Abteilungen je Mitarbeiter (zusätzlich zur Hauptabteilung) $extraDepts = []; foreach ($pdo->query('SELECT sd.staff_id, d.name FROM staff_department sd JOIN departments d ON d.id = sd.department_id ORDER BY d.name')->fetchAll() as $r) { $extraDepts[(int) $r['staff_id']][] = (string) $r['name']; } $fmt = fn(?string $dt) => $dt ? date('d.m.Y', strtotime($dt)) : '–'; $pageTitle = 'Mitarbeiter'; $activePage = 'staff'; require __DIR__ . '/_header.php'; ?>
$_POST[$k] ?? $default; $v = fn(string $k, string $col, $default = '') => $f($k, $editStaff[$col] ?? $default); $curStatus = $_POST ? ($_POST['status'] ?? 'active') : (($editStaff['is_active'] ?? 1) ? 'active' : 'locked'); $curRole = (string) $v('role', 'role', 'agent'); $curRoleId = (string) $v('role_id', 'role_id'); $curDept = (string) $v('department_id', 'department_id'); $startTab = in_array($_GET['tab'] ?? '', ['account', 'access', 'perms', 'teams'], true) ? $_GET['tab'] : 'account'; $rolePerms = []; foreach ($customRoles as $r) $rolePerms[$r['id']] = json_decode($r['permissions'], true) ?: []; $agentDefaults = default_permissions('agent'); $adminDefaults = default_permissions('admin'); ?>

—

Kontoinformationen

>
Abbrechen

🔑 Passkeys von

Passkeys sind zurzeit ausgeschaltet (Adminportal → Anmelde-Wächter). Vorhandene Passkeys gelten nicht, bleiben aber gespeichert.

Dieser Mitarbeiter hat keinen Passkey. Einrichten kann er ihn selbst unter Mein Profil → Sicherheit.

NameHinzugefügtZuletzt benutzt

Der Mitarbeiter bekommt eine Mail. Anmelden mit Passwort (und 2FA) bleibt möglich.

$locked, '_passkeys' => webauthn_count_passkeys($pdo, (int) $s['id'])]; } ?>

Mitarbeiter

Konten
Neuen Agenten hinzufügen
$n !== (string) ($s['department_name'] ?? ''))); $haystack = mb_strtolower($s['name'] . ' ' . $s['email'] . ' ' . ($s['department_name'] ?? '') . ' ' . implode(' ', $depts) . ' ' . ($s['role_name'] ?? '')); ?>
MitarbeiterRolleAbteilungOffene TicketsZuletzt aktivStatus
(Sie)
Administrator Mitarbeiter
🔒 2FA an
🔑 Passkey
Team
Deaktiviert Aktiv Im Urlaub
Anmeldung zeitweise gesperrt
Bearbeiten
⋯
Keine Mitarbeiter gefunden.