prepare('SELECT COUNT(*) FROM staff WHERE LOWER(email) = LOWER(?) AND id != ?'); $dup->execute([$email, $staff['id']]); if ((int)$dup->fetchColumn()) $errors[] = 'Diese E-Mail-Adresse wird bereits von einem anderen Mitarbeiter verwendet.'; } if (!$errors) { $pdo->prepare('UPDATE staff SET name=?, email=?, phone=? WHERE id=?') ->execute([$name, $email, $phone ?: null, $staff['id']]); audit_log($staff['id'], 'Profil aktualisiert: Name/E-Mail/Telefon'); flash('success', 'Kontodaten gespeichert.'); redirect('/staff/profile.php?tab=account'); } $tab = 'account'; } // ── Passwort ───────────────────────────────────────────────── if ($action === 'save_password') { $current = $_POST['current_password'] ?? ''; $new = $_POST['new_password'] ?? ''; $confirm = $_POST['confirm_password'] ?? ''; if (!password_verify($current, $staff['password_hash'])) { $errors[] = 'Das aktuelle Passwort ist falsch.'; } $pwError = validate_password_strength($new); if ($pwError) { $errors[] = $pwError; } if ($new !== $confirm) { $errors[] = 'Die Passwörter stimmen nicht überein.'; } if (!$errors) { $pdo->prepare('UPDATE staff SET password_hash=? WHERE id=?') ->execute([password_hash($new, PASSWORD_DEFAULT), $staff['id']]); audit_log($staff['id'], 'Passwort geändert'); flash('success', 'Passwort geändert.'); redirect('/staff/profile.php?tab=account'); } $tab = 'account'; } // ── Avatar ──────────────────────────────────────────────────── if ($action === 'save_avatar') { if (!empty($_FILES['avatar']['name'])) { $allowed = ['jpg','jpeg','png','gif','webp']; $ext = strtolower(pathinfo($_FILES['avatar']['name'], PATHINFO_EXTENSION)); if (!in_array($ext, $allowed)) { $errors[] = 'Erlaubte Bildformate: JPG, PNG, GIF, WEBP.'; } elseif ($_FILES['avatar']['size'] > 2 * 1024 * 1024) { $errors[] = 'Maximale Dateigröße: 2 MB.'; } elseif (($imgErr = favicon_validate_file($_FILES['avatar']['tmp_name'], $ext)) !== null) { $errors[] = $imgErr; } else { $dir = __DIR__ . '/../assets/avatars/'; if (!is_dir($dir)) mkdir($dir, 0755, true); // Alten Avatar löschen if ($staff['avatar_path']) { $old = __DIR__ . '/..' . $staff['avatar_path']; if (file_exists($old)) unlink($old); } $filename = 'avatar_' . $staff['id'] . '_' . time() . '.' . $ext; move_uploaded_file($_FILES['avatar']['tmp_name'], $dir . $filename); $path = '/assets/avatars/' . $filename; $pdo->prepare('UPDATE staff SET avatar_path=? WHERE id=?')->execute([$path, $staff['id']]); flash('success', 'Avatar aktualisiert.'); redirect('/staff/profile.php?tab=account'); } } $tab = 'account'; } if ($action === 'delete_avatar') { if ($staff['avatar_path']) { $old = __DIR__ . '/..' . $staff['avatar_path']; if (file_exists($old)) unlink($old); } $pdo->prepare('UPDATE staff SET avatar_path=NULL WHERE id=?')->execute([$staff['id']]); flash('success', 'Avatar entfernt.'); redirect('/staff/profile.php?tab=account'); } // ── Urlaub/Abwesenheit ───────────────────────────────────── if ($action === 'save_settings') { $onVacation = !empty($_POST['is_on_vacation']) ? 1 : 0; $substituteId = ($_POST['vacation_substitute_id'] ?? '') !== '' ? (int)$_POST['vacation_substitute_id'] : null; $beforeRow = $pdo->prepare('SELECT is_on_vacation, vacation_substitute_id FROM staff WHERE id = ?'); $beforeRow->execute([$staff['id']]); $beforeRow = $beforeRow->fetch() ?: ['is_on_vacation' => 0, 'vacation_substitute_id' => null]; $pdo->prepare('UPDATE staff SET is_on_vacation=?, vacation_substitute_id=? WHERE id=?') ->execute([$onVacation, $substituteId, $staff['id']]); audit_log($staff['id'], $onVacation ? 'Abwesenheit aktiviert' : 'Abwesenheit deaktiviert'); // Offene Tickets an die Vertretung weitergeben bzw. nach dem Urlaub zurückholen [$handed, $back] = vacation_apply_change($pdo, (int) $staff['id'], [$beforeRow['is_on_vacation'], $beforeRow['vacation_substitute_id']], [$onVacation, $substituteId], !empty($_POST['hand_over'])); flash('success', 'Einstellungen gespeichert.' . ($handed ? ' ' . $handed . ' offene Tickets wurden an die Vertretung weitergegeben.' : '') . ($back ? ' ' . $back . ' Tickets sind wieder bei Ihnen.' : '')); redirect('/staff/profile.php?tab=settings'); } // ── Android-Push: welche Popups, stille Zeiten ───────────────── if ($action === 'save_push_prefs') { $ev = array_map('strval', (array) ($_POST['push_events'] ?? [])); $qf = trim((string) ($_POST['quiet_from'] ?? '')); $qt = trim((string) ($_POST['quiet_to'] ?? '')); if (($qf !== '' || $qt !== '') && (!fcm_time_valid($qf) || !fcm_time_valid($qt))) { flash('error', 'Stille Zeiten bitte als Uhrzeit von/bis angeben (z. B. 22:00 und 06:00).'); redirect('/staff/profile.php?tab=account'); } fcm_save_prefs($pdo, (int) $staff['id'], $ev, $qf, $qt, array_keys(fcm_event_types_for($staff))); flash('success', 'Push-Einstellungen gespeichert.'); redirect('/staff/profile.php?tab=account'); } // ── Von allen Geräten abmelden ───────────────────────────────── if ($action === 'sign_out_everywhere') { $keep = ($_POST['scope'] ?? 'others') !== 'all'; $r = staff_sign_out_everywhere($pdo, (int) $staff['id'], (int) $staff['id'], $keep); if ($keep) { flash('success', 'Alle anderen Geräte sind abgemeldet. Beendete App-Anmeldungen: ' . $r['apps'] . '. Diese Sitzung bleibt angemeldet.'); redirect('/staff/profile.php?tab=security'); } staff_logout(); flash('success', 'Sie sind von allen Geräten abgemeldet. Bitte melden Sie sich neu an.'); redirect('/staff/login.php'); } // ── Gemerktes Gerät entfernen (Anmelde-Wächter) ──────────────── if ($action === 'forget_device') { $pdo->prepare('DELETE FROM login_devices WHERE id = ? AND staff_id = ?')->execute([(int) ($_POST['device_id'] ?? 0), $staff['id']]); flash('success', 'Gerät entfernt.'); redirect('/staff/profile.php?tab=security'); } // ── Passkey entfernen (das Passwort wird bestätigt) ─────────── if ($action === 'delete_passkey') { $pass = $_POST['confirm_password'] ?? ''; $me = $pdo->prepare('SELECT password_hash FROM staff WHERE id=?'); $me->execute([$staff['id']]); $me = $me->fetch(); if (!password_verify($pass, $me['password_hash'])) { flash('error', 'Falsches Passwort.'); } else { $del = $pdo->prepare('DELETE FROM staff_passkeys WHERE id = ? AND staff_id = ?'); $del->execute([(int) ($_POST['passkey_id'] ?? 0), $staff['id']]); if ($del->rowCount() > 0) { audit_log($staff['id'], 'Passkey entfernt'); login_guard_record($pdo, 'passkey_removed', (int) $staff['id'], (string) $staff['email'], 'web', 'Passkey vom Mitarbeiter selbst entfernt'); flash('success', 'Passkey entfernt.'); } } redirect('/staff/profile.php?tab=security'); } // ── 2FA aktivieren ───────────────────────────────────────────── if ($action === 'enable_2fa') { $code = trim($_POST['totp_code'] ?? ''); $secret = $_SESSION['totp_setup_secret'] ?? ''; if (!$secret) { flash('error', 'Sitzungsfehler. Bitte nochmals versuchen.'); } elseif (!totp_verify($secret, $code)) { flash('error', 'Code ungültig. Bitte erneut versuchen.'); } else { $pdo->prepare('UPDATE staff SET totp_secret=?, totp_enabled=1 WHERE id=?')->execute([$secret, $staff['id']]); unset($_SESSION['totp_setup_secret']); audit_log($staff['id'], 'Zwei-Faktor-Authentifizierung aktiviert'); flash('success', '2FA wurde erfolgreich aktiviert.'); } redirect('/staff/profile.php?tab=security'); } // ── 2FA deaktivieren ─────────────────────────────────────────── if ($action === 'disable_2fa') { $pass = $_POST['confirm_password'] ?? ''; $me = $pdo->prepare('SELECT password_hash FROM staff WHERE id=?'); $me->execute([$staff['id']]); $me = $me->fetch(); if (!password_verify($pass, $me['password_hash'])) { flash('error', 'Falsches Passwort.'); } else { $pdo->prepare('UPDATE staff SET totp_secret=NULL, totp_enabled=0 WHERE id=?')->execute([$staff['id']]); audit_log($staff['id'], 'Zwei-Faktor-Authentifizierung deaktiviert'); flash('success', '2FA wurde deaktiviert.'); } redirect('/staff/profile.php?tab=security'); } // ── Signatur ────────────────────────────────────────────────── if ($action === 'save_signature') { $sig = sanitize_staff_html(trim($_POST['signature'] ?? '')); $pdo->prepare('UPDATE staff SET signature=? WHERE id=?')->execute([$sig ?: null, $staff['id']]); flash('success', 'Signatur gespeichert.'); redirect('/staff/profile.php?tab=signature'); } } // Aktuellen Stand neu laden (nach Redirect relevant wenn $_SESSION cached) $staffRow = $pdo->prepare('SELECT * FROM staff WHERE id = ?'); $staffRow->execute([$staff['id']]); $me = $staffRow->fetch(); $pageTitle = 'Mein Profil'; $activePage = 'profile'; require __DIR__ . '/_header.php'; ?>

Mein Profil

'👤 Konto', 'settings' => '⚙ Einstellungen', 'signature' => '✍ Signatur', 'security' => '🔒 Sicherheit'] as $t => $label): ?>

Profilbild

Avatar

Kontodaten

Passwort ändern

Browser-Push-Benachrichtigungen

Erhalte Desktop-Benachrichtigungen bei neuen Tickets und Antworten — auch wenn der Browser-Tab nicht aktiv ist. Funktioniert in Chrome, Firefox und Edge. Erfordert HTTPS.

🔕
Deaktiviert
Klicken Sie auf Aktivieren um Benachrichtigungen zu erhalten
Benachrichtigungen erhalten bei:
✅ Neues Ticket in meiner Abteilung
✅ Neue Antwort auf mir zugewiesenes Ticket

Push-Benachrichtigungen sind auf diesem Server nicht verfügbar (OpenSSL-Erweiterung fehlt oder PHP zu alt).

📱 Android-App: Welche Popups?

Wähle, wofür die App ein Popup zeigen soll.

$__label): ?>
Stille Zeiten (keine Popups)
von bis

Beide Felder leer = immer Popups. Die Zeit gilt nach der Serverzeit (), auch über Mitternacht (z. B. 22:00 bis 06:00).

Abwesenheit / Urlaub

query('SELECT id, name FROM staff WHERE is_active=1 ORDER BY name')->fetchAll(); ?>

Neue Tickets werden während Ihrer Abwesenheit automatisch dieser Person zugewiesen. Sie sieht dafür auch Ihre Abteilungen.

Urlaubsmodus ist aktuell aktiv. Sie erhalten keine automatischen Ticket-Zuweisungen.

Übersicht

Rolle Administrator' : 'Mitarbeiter' ?>
Mitglied seit
prepare('SELECT COUNT(*) FROM tickets WHERE assigned_staff_id = ?'); $ticketCount->execute([$me['id']]); $myCount = $ticketCount->fetchColumn(); ?>
Zugewiesene Tickets

E-Mail-Signatur

Wird automatisch an Ihre ausgehenden Ticket-Antworten angehängt. Ist keine Mitarbeiter-Signatur gesetzt, greift die Abteilungs-Signatur.

Bilder einfügen mit 🖼️. Zum Verkleinern oder Vergrößern das Bild anklicken: Mit dem runden Anfasser ziehen oder 25 / 50 / 75 % / Original wählen.

Vorschau:

prepare('SELECT totp_enabled, totp_secret, email FROM staff WHERE id=?'); $me2->execute([$staff['id']]); $me2 = $me2->fetch(); if (!$me2['totp_enabled'] && empty($_SESSION['totp_setup_secret'])) { $_SESSION['totp_setup_secret'] = totp_generate_secret(); } $setupSecret = $_SESSION['totp_setup_secret'] ?? ''; ?>

🔒 Zwei-Faktor-Authentifizierung (2FA)

2FA ist aktuell aktiv.

Um 2FA zu deaktivieren, bestätigen Sie mit Ihrem Passwort:

Schützen Sie Ihr Konto mit einem zeitbasierten Einmalcode (TOTP – kompatibel mit Google Authenticator, Authy, etc.).

  1. Installieren Sie Google Authenticator oder Authy
  2. Scannen Sie den QR-Code oder geben Sie den Schlüssel manuell ein
  3. Tragen Sie den 6-stelligen Code ein und klicken Sie „2FA aktivieren"
QR-Code
Manueller Schlüssel:
prepare('SELECT * FROM login_devices WHERE staff_id = ? ORDER BY last_seen DESC'); $myDevices->execute([$staff['id']]); $myDevices = $myDevices->fetchAll(); $myEvents = $pdo->prepare('SELECT * FROM login_events WHERE staff_id = ? ORDER BY id DESC LIMIT 10'); $myEvents->execute([$staff['id']]); $myEvents = $myEvents->fetchAll(); $eventLabels = login_guard_event_labels(); ?>

🔑 Passkeys

Mit einem Passkey melden Sie sich ohne Passwort an: per Fingerabdruck, Gesicht oder Geräte-PIN (Windows Hello, Handy, Sicherheitsschlüssel). Der Passkey ersetzt Passwort und 2FA-Code; Passwort und 2FA bleiben als Rückfall bestehen. Ein Administrator kann Ihre Passkeys bei Verlust entfernen.

Passkeys gelten für die Domain (die Adresse, unter der Sie DeskHub aufrufen).

Passkeys sind in dieser Installation zurzeit ausgeschaltet. Vorhandene Passkeys bleiben gespeichert, gelten aber erst wieder, wenn ein Administrator sie einschaltet.
Passkeys brauchen eine HTTPS-Adresse. Die Website-URL in den Einstellungen beginnt noch nicht mit https://.
NameHinzugefügtZuletzt benutzt

Noch kein Passkey eingerichtet.

Von allen Geräten abmelden

Beendet alle Anmeldungen in Browsern und in den Apps (Android, Windows-Programm, Chrome-Erweiterung) und stoppt die Push-Benachrichtigungen auf diesen Geräten. Sinnvoll, wenn ein Gerät verloren ging oder Sie sich irgendwo angemeldet gelassen haben. Ihr Passwort bleibt unverändert.

Angemeldete Apps:
·

🛡️ Meine Geräte und Anmeldungen

Von diesen Geräten haben Sie sich schon angemeldet. Meldet sich jemand von einem neuen Gerät oder Ort an, bekommen Sie eine Mail. Ein Gerät, das Sie nicht kennen, können Sie hier entfernen; ändern Sie dann auch Ihr Passwort.

Noch keine Geräte gemerkt.

GerätLetzter OrtZuletzt gesehen
, seit

Letzte Anmeldungen

()