diff --git a/DeskHub-Webseite/README.md b/DeskHub-Webseite/README.md index 4310acd8..951ba093 100644 --- a/DeskHub-Webseite/README.md +++ b/DeskHub-Webseite/README.md @@ -299,6 +299,10 @@ Nach der Installation müsst ihr nichts tun, Option A läuft von selbst. --- +## Status und automatisch Offline + +Jeder Mitarbeiter stellt seinen Status ein (Verfügbar, Beschäftigt, Pause, Offline), im Browser unter **Verfügbarkeit & Schichten** und in der App unter Verwaltung → Mein Status. **Wer 30 Minuten lang nichts tut, geht automatisch auf Offline** und bekommt seinen vorherigen Status zurück, sobald er wieder aktiv ist. Als aktiv zählen echte Bedienung (Seitenaufrufe, Klicks und Tipps, Aktionen und Berührungen in der App); Abfragen im Hintergrund halten niemanden online. Ein von Hand gesetzter Status wird nicht von selbst zurückgesetzt. Die Zeit stellt ein Admin unter Verfügbarkeit & Schichten ein (0 = aus). Admins können dort (und in der App durch Antippen eines Teammitglieds) den Status jedes Mitarbeiters ändern; das steht im Aktivitätsprotokoll. Der Live-Chat auf der Kundenseite zeigt automatisch Offline-Mitarbeiter nicht mehr als erreichbar. + ## SLA-Pläne Ein SLA-Plan legt fest, wie schnell reagiert und gelöst werden soll. Unter Adminportal → **SLA-Pläne** legt ihr Pläne mit Reaktions- und Lösungszeit an und weist sie Abteilungen oder Hilfethemen zu. @@ -399,6 +403,18 @@ Beide Apps sind für Mitarbeiter und Admins gedacht und haben ein helles und ein - Admins und Co-Admins finden zusätzlich die **Verwaltung** mit fast allen Adminportal-Bereichen, je nach zugewiesenen Rechten: Abteilungen, Prioritäten, SLA-Pläne, Hilfethemen, E-Mail-Adressen (inkl. Test), Wissensdatenbank, E-Mail-Vorlagen, Organisationen, Mitarbeiter, Teams, Rollen, Admin-Protokoll, Eskalationsregeln, Zeiterfassung, Geschäftszeiten, Sperrliste, Aufgaben - Systemnahe Bereiche (Backup, System-Update, Plugins, API-Schlüssel, Webhooks, Postfächer, Ticket-Regeln, Ticket-Felder, Seiten, Einstellungen) gibt es bewusst nur in der Weboberfläche +**Push-Benachrichtigungen (Android):** Die Android-App zeigt Popups bei neuen Tickets, Kundenantworten und Zuweisungen, auch wenn sie geschlossen ist. Das läuft über Firebase Cloud Messaging (Google). + +- **Standard (nichts einzurichten):** Das DeskHub meldet sich beim ersten Senden automatisch beim **Push-Dienst des Herausgebers** an (`push.php` der Plugin-Webseite, die Adresse leitet sich aus `includes/catalog_defaults.php` ab) und schickt nur Geräte-Token und Ticketnummer dorthin. Der Firebase-Schlüssel liegt ausschließlich dort, nie in der App oder in diesem Paket. In der App anmelden und Benachrichtigungen erlauben (ab Android 13 fragt die App einmal danach); mit **„Testnachricht an meine Geräte“** unter **Einstellungen → Benachrichtigungen → „Android-App: Push-Benachrichtigungen“** prüfen. +- **Eigenes Firebase-Projekt (für Fortgeschrittene):** Wer eine eigene App-Variante mit eigenem Firebase-Projekt betreibt, trägt in `config/config.php` `'fcm' => ['service_account_file' => '/pfad/ausserhalb/des/webordners/dienstkonto.json']` ein (Dienstkonto-Schlüssel aus Projekteinstellungen → Dienstkonten). Eine Oberfläche zum Hochladen gibt es bewusst nicht. Dann geht nichts mehr über den Push-Dienst. Die Datei `google-services.json` der eigenen App kommt beim Bauen in den Ordner `mobile/` (nicht ausgeliefert). + +**Einstellbar pro Mitarbeiter:** Im Profil (Karte „Android-App: Welche Popups?“) und in der App unter Verwaltung → Benachrichtigungen wählt jeder, wofür er ein Popup bekommt (neues Ticket, Kundenantwort, Zuweisung, Live-Chat, SLA überschritten, Erwähnung) und legt stille Zeiten fest (Serverzeit, auch über Mitternacht). Testnachrichten kommen immer an. Auf Kundenantworten kann direkt in der Benachrichtigung geantwortet werden. Stürzt die App ab, meldet sie das beim nächsten Start an den eigenen Server (Aktivitätsprotokoll, „App-Absturz …“); es gehen keine Ticketinhalte und nichts an Dritte. + +**Rechte beim Push:** Ein Popup bekommt nur, wer das Ticket auch sehen darf (Administrator oder Mitglied der Abteilung des Tickets). Für neue Tickets, Kundenantworten und Live-Chats braucht man zusätzlich das Recht „Antworten“ (Rolle). Eine **SLA-Überschreitung** melden die App nur dem zugewiesenen Mitarbeiter, Administratoren und Mitarbeitern mit dem Recht „Eskalationsregeln verwalten“, nicht der ganzen Abteilung. **Zuweisung** und **Erwähnung** erhält nur, wer Zugriff auf die Abteilung hat. In den Einstellungen („Welche Popups?“) erscheinen jedem nur die Arten, die für ihn überhaupt in Frage kommen; Live-Chat-Popups gibt es nur, wenn der Live-Chat eingeschaltet ist. + +Wer bekommt was: ein **neues Ticket** bekommen alle Admins, Mitarbeiter der Abteilung und Mitarbeiter mit Benachrichtigung für diese Abteilung; eine **Kundenantwort** der zugewiesene Mitarbeiter (ohne Zuweisung die Abteilung); eine **Zuweisung** der neue Bearbeiter, aber nicht, wer sich selbst zuweist. Das gilt für Tickets aus Webformular, E-Mail, API und Live-Chat. Ein Tipp auf die Benachrichtigung öffnet das Ticket in der App. + +**Datenschutz:** Standardmäßig steht nur „Neues Ticket #T-123“ in der Nachricht, Betreff und Kundenname verlassen euren Server nicht (auch der Push-Dienst sieht nur Ticketnummer und Geräte-Token). Wer mehr sehen will, schaltet „Betreff und Kundenname in der Benachrichtigung anzeigen“ ein. Jedes Gerät meldet sich beim Ausloggen ab. Pro Mitarbeiter sind bis zu 10 Geräte möglich; abgemeldete oder ungültige Geräte entfernt der Server selbst. Der Versand läuft nach dem Senden der Seite und bremst nichts. **Updates:** Die Android-App wird nur dann über die alte Version installiert, wenn sie mit demselben Signaturschlüssel erstellt wurde und eine höhere Versionsnummer hat. Nach einem Update die App einmal ganz schließen und neu öffnen. --- diff --git a/DeskHub-Webseite/api/staff/v1/availability.php b/DeskHub-Webseite/api/staff/v1/availability.php index 0e358242..55efd547 100644 --- a/DeskHub-Webseite/api/staff/v1/availability.php +++ b/DeskHub-Webseite/api/staff/v1/availability.php @@ -19,8 +19,9 @@ $pdo = get_db(); const AVAILABILITY_STATUSES = ['available', 'busy', 'break', 'offline']; if ($_SERVER['REQUEST_METHOD'] === 'GET') { + staff_run_auto_offline($pdo); $agents = $pdo->query( - 'SELECT s.id, s.name, s.is_on_vacation, sa.status, sa.note, sa.updated_at + 'SELECT s.id, s.name, s.is_on_vacation, s.last_active_at, sa.auto_offline, sa.status, sa.note, sa.updated_at FROM staff s LEFT JOIN staff_availability sa ON sa.staff_id = s.id WHERE s.is_active = 1 @@ -37,6 +38,8 @@ if ($_SERVER['REQUEST_METHOD'] === 'GET') { 'note' => $a['is_on_vacation'] ? 'Urlaub' : ($a['note'] ?? ''), 'isOnVacation' => (bool) $a['is_on_vacation'], 'updatedAt' => $a['updated_at'], + 'lastActiveAt' => $a['last_active_at'] ?? null, + 'autoOffline' => !empty($a['auto_offline']), ]; $team[] = $entry; if ((int) $a['id'] === (int) $staff['id']) { @@ -57,12 +60,20 @@ if (!in_array($newStatus, AVAILABILITY_STATUSES, true)) { api_error(422, 'INVALID_STATUS', 'Ungültiger Status.'); } -$drv = $pdo->getAttribute(PDO::ATTR_DRIVER_NAME); -$sql = $drv === 'sqlite' - ? 'INSERT INTO staff_availability (staff_id, status, note, updated_at) VALUES (?,?,?,?) - ON CONFLICT(staff_id) DO UPDATE SET status=excluded.status, note=excluded.note, updated_at=excluded.updated_at' - : 'INSERT INTO staff_availability (staff_id, status, note, updated_at) VALUES (?,?,?,?) - ON DUPLICATE KEY UPDATE status=VALUES(status), note=VALUES(note), updated_at=VALUES(updated_at)'; -$pdo->prepare($sql)->execute([$staff['id'], $newStatus, $note ?: null, db_now()]); - -api_json(['ok' => true, 'status' => $newStatus]); +// Ein Admin darf den Status eines anderen Mitarbeiters setzen (staffId), alle anderen nur den eigenen +$targetId = (int) $staff['id']; +if (isset($body['staffId']) && (int) $body['staffId'] !== $targetId) { + if (($staff['role'] ?? '') !== 'admin') { + api_error(403, 'FORBIDDEN', 'Nur Administratoren dürfen den Status anderer Mitarbeiter ändern.'); + } + $targetId = (int) $body['staffId']; + $exists = $pdo->prepare('SELECT COUNT(*) FROM staff WHERE id = ? AND is_active = 1'); + $exists->execute([$targetId]); + if ((int) $exists->fetchColumn() === 0) { + api_error(404, 'STAFF_NOT_FOUND', 'Mitarbeiter nicht gefunden.'); + } + audit_log((int) $staff['id'], "Status von Mitarbeiter #$targetId auf $newStatus gesetzt"); +} +staff_set_status($pdo, $targetId, $newStatus, $note); +staff_touch_activity($pdo, (int) $staff['id']); +api_json(['ok' => true, 'status' => $newStatus, 'staffId' => $targetId]); diff --git a/DeskHub-Webseite/api/staff/v1/common.php b/DeskHub-Webseite/api/staff/v1/common.php index ad442170..548907be 100644 --- a/DeskHub-Webseite/api/staff/v1/common.php +++ b/DeskHub-Webseite/api/staff/v1/common.php @@ -138,6 +138,11 @@ function api_require_staff(): array ->execute([db_now(), $staff['mobile_token_id']]); } + // Aktionen in der App (Schreib-Aufrufe) zählen als Aktivität; reines Abfragen im Hintergrund und die Push-Anmeldung nicht + if ($_SERVER['REQUEST_METHOD'] !== 'GET' && !in_array(basename((string) ($_SERVER['SCRIPT_NAME'] ?? '')), ['push_device.php', 'crash_report.php', 'logout.php'], true)) { + staff_touch_activity($pdo, (int) $staff['id']); + } + // "Eingebauter" Ersatz für echte Server-Cronjobs (siehe includes/pseudo_cron.php): // auch App-Nutzung hält die automatischen Hintergrund-Jobs am Laufen. pseudo_cron_schedule_background(); diff --git a/DeskHub-Webseite/api/staff/v1/crash_report.php b/DeskHub-Webseite/api/staff/v1/crash_report.php new file mode 100644 index 00000000..105404fa --- /dev/null +++ b/DeskHub-Webseite/api/staff/v1/crash_report.php @@ -0,0 +1,23 @@ + true], 201); diff --git a/DeskHub-Webseite/live_chat.php b/DeskHub-Webseite/live_chat.php index 8b2e0bfb..c263144c 100644 --- a/DeskHub-Webseite/live_chat.php +++ b/DeskHub-Webseite/live_chat.php @@ -168,6 +168,7 @@ function chat_any_staff_available(PDO $pdo): bool // die eine fehlende Zeile ebenfalls als "Offline" behandelt – sonst würde der Chat // "online" anzeigen, obwohl kein einziger Mitarbeiter seinen Status je auf // "Verfügbar" gestellt hat. + staff_run_auto_offline($pdo); // Wer lange inaktiv war, gilt nicht mehr als erreichbar $stmt = $pdo->query( "SELECT COUNT(*) FROM staff s INNER JOIN staff_availability sa ON sa.staff_id = s.id diff --git a/DeskHub-Webseite/version.php b/DeskHub-Webseite/version.php index 3d839796..46d7aeba 100644 --- a/DeskHub-Webseite/version.php +++ b/DeskHub-Webseite/version.php @@ -1,7 +1,7 @@