diff --git a/DeskHub-Webseite/.htaccess b/DeskHub-Webseite/.htaccess index fb7d8dcf..75213242 100644 --- a/DeskHub-Webseite/.htaccess +++ b/DeskHub-Webseite/.htaccess @@ -44,6 +44,10 @@ RewriteCond %{REQUEST_URI} \.(sqlite3?|db|sql|log|bak|old|orig|swp|md|sh|bat|ps1 RewriteCond %{REQUEST_URI} ^/(offen\.txt|install\.sh|install_windows\.bat|composer\.(json|lock)|package(-lock)?\.json)$ [NC] RewriteRule ^ - [F,L] +# Übrig gebliebene alte Ordner eines Updates (…alt-JJJJMMTT-HHMMSS) nicht ausliefern +RewriteCond %{REQUEST_URI} \.alt-[0-9]{8}-[0-9]{6}(/|$) [NC] +RewriteRule ^ - [F,L] + # version.php nur intern zugänglich RewriteCond %{REQUEST_URI} ^/version\.php$ [NC] RewriteRule ^ - [F,L] diff --git a/DeskHub-Webseite/tools/manual/handbuch/ch17.html b/DeskHub-Webseite/tools/manual/handbuch/ch17.html new file mode 100644 index 00000000..646e7bfa --- /dev/null +++ b/DeskHub-Webseite/tools/manual/handbuch/ch17.html @@ -0,0 +1,65 @@ +
Adminportal → Backup (nur Administratoren).
+backups/ selbst. Bereits komprimierte Dateien werden nur abgelegt.WIEDERHERSTELLEN ein. Programmcode und config.php werden nie überschrieben; ein Backup einer anderen Datenbankart (SQLite ↔ MySQL) wird abgelehnt.Wer von osTicket umzieht, übernimmt den Bestand: Kunden, Abteilungen, Hilfethemen, Tickets, Nachrichten (Kundennachrichten, Antworten, interne Notizen), Mitlesende und Anhänge. Unterstützt werden SQL-Dumps von osTicket 1.10 bis 1.18 (phpMyAdmin-Export oder mysqldump), auch mit anderem Tabellenpräfix.
Einstellungen → „📥 osTicket Import“: (1) Hochladen (Dateiname im Hauptordner oder Direkt-Upload in Teilen), (2) Analysieren – DeskHub zeigt, was die Datei enthält, (3) Optionen wählen (feste Reihenfolge: Kunden → Abteilungen und Hilfethemen → Tickets → Nachrichten → Anhänge), (4) Import in kleinen Schritten mit Fortschritt und Zusammenfassung.
+DeskHub überwacht sich selbst und zeigt Probleme als Banner im Mitarbeiterbereich und in den Apps: Postfach nicht erreichbar, Abruf veraltet, Versand fehlgeschlagen, Backup defekt, Datenbank veraltet, Plugin-Updates verfügbar. Die Apps melden außerdem, wenn der Server nicht erreichbar ist, und versuchen es automatisch erneut.
diff --git a/DeskHub-Webseite/tools/manual/handbuch/ch18.html b/DeskHub-Webseite/tools/manual/handbuch/ch18.html new file mode 100644 index 00000000..6c5cc561 --- /dev/null +++ b/DeskHub-Webseite/tools/manual/handbuch/ch18.html @@ -0,0 +1,92 @@ +| Wo | Taste | Wirkung |
|---|---|---|
| Ticketliste | j / k | nächstes / vorheriges Ticket wählen |
| Ticketliste | Enter oder o | Ticket öffnen |
| Ticketliste | r | öffnen und antworten |
| Ticketliste | x | ankreuzen |
| Ticketliste | / | Suche |
| Ticketliste | n | neues Ticket |
| Ticket | j / k | durch die Nachrichten blättern |
| Ticket | r | Antwort schreiben |
| Ticket | u | zurück zur Liste |
| Antwortfeld | Strg + Enter | Antwort senden |
| Antwortfeld | Strg + V | Screenshot oder kopiertes Bild einfügen |
| Bildvorschau | Pfeiltasten / Esc | blättern / schließen |
| überall | ? | Kürzel-Übersicht (mit Abschaltmöglichkeit) |
| Chrome-Erweiterung | Alt+Shift+N / D | Neues Ticket / Erweiterung öffnen |
| Rolle | Darf |
|---|---|
| Kunde | Tickets anlegen und verfolgen, antworten, bewerten; kein Zugang zum Mitarbeiterbereich |
| Mitarbeiter (Agent) | Tickets seiner Abteilungen lesen, beantworten, notieren; Status, Zuweisung, Wiedervorlage; Chat, Aufgaben, eigene Berichte |
| Eigene Rolle (z. B. Teamleitung) | zusätzlich laut Rechte: zuweisen, schließen, löschen, Tickets übertragen, Eskalationsregeln, digitalen Assistenten verwalten … |
| Co-Admin | Rolle mit freigeschalteten Adminportal-Bereichen; nie Backup, System-Update, Ticket-Regeln, Plugins, API-Schlüssel, Webhooks |
| Administrator | Alles; Ticket-Sperren übernehmen; Konten sichern und abmelden |
| Ereignis | Empfänger |
|---|---|
| Neues Ticket | Alle Administratoren, Mitarbeiter der Abteilung und Mitarbeiter mit Benachrichtigung für diese Abteilung |
| Kundenantwort | Der zugewiesene Mitarbeiter; ohne Zuweisung die Abteilung; Beobachter |
| Zuweisung | Der neue Bearbeiter (nicht, wer sich selbst zuweist) |
| Erwähnung | Der Erwähnte, wenn er Zugriff auf die Abteilung hat |
| SLA überschritten | Standard: nur Administratoren per Mail (bei Urlaub die Vertretung); alternativ Bearbeiter/Abteilung per Mail und App-Push |
| Ticket wartet auf Antwort | Der zugewiesene Bearbeiter (Push, optional Mail) |
| Wiedervorlage fällig | Der Mitarbeiter, der das Ticket zurückgestellt hat |
| Anmeldung von neuem Gerät, Sperre | Der Kontoinhaber (und bei Sperre die Administratoren) |
| Postfach-Störung, defektes Backup, Dienstausfall | Die Administratoren |
Im Urlaubsmodus bekommt statt des Mitarbeiters die Vertretung diese Meldungen (Sicherheitsmails und Systemmeldungen an Administratoren ausgenommen).
+ +| Begriff | Bedeutung |
|---|---|
| Ticket | Eine Anfrage mit Nummer, Verlauf, Status, Priorität und Abteilung |
| Interne Notiz | Nachricht im Ticket, die nur das Team sieht |
| Abteilung | Themenbereich, dem ein Ticket genau einmal zugeordnet ist; Grenze der Sichtbarkeit |
| Hilfethema | Auswahl für Kunden beim Anlegen; legt Abteilung, Priorität, ggf. SLA und Formularfelder fest |
| SLA | Service-Level-Vereinbarung: Fristen für erste Antwort und Lösung, gerechnet in Geschäftszeiten |
| Wiedervorlage | Ticket „auf später legen“; es kommt zum gewählten Zeitpunkt zurück |
| Beobachter | Mitarbeiter, der ein Ticket verfolgt, ohne Bearbeiter zu sein |
| Cc / Mitleser | Externe E-Mail-Adresse, die öffentliche Antworten in Kopie bekommt |
| Co-Admin | Mitarbeiter mit Zugriff auf einzelne Adminportal-Bereiche |
| CSAT | Zufriedenheitsabfrage mit fünf Sternen |
| Cron / Pseudo-Cron | Zeitgesteuerte Hintergrundaufgaben (echter Server-Cronjob oder eingebauter Ersatz) |
| Webhook | Automatische Meldung von DeskHub an ein anderes System bei einem Ereignis |
| Plugin | Nachrüstbare Zusatzfunktion als ZIP-Paket |
| 2FA / TOTP | Zwei-Faktor-Anmeldung mit Einmalcode aus einer Authenticator-App |
| Passkey | Anmeldung ohne Passwort per Fingerabdruck, Gesicht oder Geräte-PIN (WebAuthn); ersetzt Passwort und 2FA-Code |
| Problem | Was zu tun ist |
|---|---|
| Eine automatische Mail kommt nicht an | Adminportal → Admin-Protokoll öffnen (genaue Fehlermeldung des Anbieters); Einstellungen → E-Mail → „Test-Mail senden“; einzelne Absenderadresse unter E-Mail-Adressen testen; Mail-Warteschlange ansehen |
| E-Mails werden nicht zu Tickets | Postfächer (Abruf) → „Testen“ zeigt die Meldung des Mail-Servers; „E-Mails jetzt abrufen“ in der Ticketliste ausprobieren |
| Rotes oder gelbes Banner oben | Störungsmeldung der Überwachung (Postfach, Abruf, Versand); Details unter E-Mail-Adressen und Postfächer |
| „Datenbank veraltet“ | System-Update → „Datenbank aktualisieren“ |
| Ein Plugin macht Probleme | Unter Plugins deaktivieren; lässt sich die Seite nicht öffnen, Datei plugins/.disabled anlegen |
| „Mit Passkey anmelden“ fehlt oder schlägt fehl | Website-URL muss mit https:// beginnen, die aufgerufene Adresse muss dazu passen (eine Subdomain davon ist erlaubt); im Profil steht unter „Passkeys“, für welche Domain sie gelten; im Adminportal prüfen, ob „Passkeys erlauben“ an ist; Passkey im Profil prüfen; sonst mit Passwort anmelden und neu einrichten oder einen Administrator bitten, die Passkeys zu entfernen |
| Konto gesperrt (zu viele Fehlversuche) | 15 Minuten warten, „Passwort vergessen“ nutzen oder einen Administrator bitten, die Sperre aufzuheben |
| Die App meldet „Ein gültiges Bearer-Token ist erforderlich“ | Der Webserver reicht den Anmelde-Header nicht weiter (typisch Apache mit PHP-FPM): neue .htaccess hochladen bzw. auf nginx fastcgi_param HTTP_AUTHORIZATION $http_authorization; setzen |
| Die App zeigt „Keine Verbindung“ | Erreichbarkeit der Seite im Browser prüfen; sobald der Server antwortet, verschwindet das Banner von selbst |
| Zähler in Chrome-Erweiterung/Windows-Programm stimmt nicht | Server und Programme aktualisieren (ab Server 1.0.69); der Zähler zeigt nur Tickets, die jetzt Aufmerksamkeit brauchen |
| Nach einem Update sieht alles unverändert aus | Browser-Cache leeren (Strg + F5) |
| DeskHub-Server | 1.1.0 |
| Windows-Programm | 2.12.3 |
| Android-App | 3.5.3 |
| Chrome-Erweiterung | 2.1.1 |
| WordPress-Plugin | 1.3.0 |
| Minecraft-Plugin | 1.2.0 |
Dieses Handbuch beschreibt den Funktionsumfang dieser Versionen. Alle Namen, Adressen und Zahlen in den Abbildungen sind erfunden; example-Domains sind reservierte Beispieladressen.
Dieses Handbuch beschreibt jede Funktion von DeskHub: von der Installation über das Kundenportal und die tägliche Arbeit der Mitarbeiter bis zu allen Bereichen des Adminportals, den mitgelieferten Plugins und den Apps. Es ist so aufgebaut, dass Einsteiger von vorn nach hinten lesen können; erfahrene Nutzer springen über das Inhaltsverzeichnis (die Einträge sind anklickbar) direkt zum Thema.
+| Rolle | Kapitel |
|---|---|
| Kunden | Kapitel 3 (Kundenportal) |
| Mitarbeiter | Kapitel 4 bis 9 (Mitarbeiterbereich, Tickets, Chat, Berichte, Profil) und 14 (Apps) |
| Administratoren | Kapitel 2 sowie 10 bis 13 und 15 bis 17 (Installation, Adminportal, Einstellungen, Plugins, Integrationen, Sicherheit, Backup, Update); der Anhang (Kapitel 18) enthält Kürzel, Glossar und Fehlerhilfe für alle |
so gesetzt.demo.example und kunde.example sind reservierte Beispieladressen und gehören niemandem. Es kommen keine echten Kunden-, Mitarbeiter- oder Zugangsdaten vor.DeskHub besteht aus drei Bereichen, die ihr in den folgenden Kapiteln einzeln kennenlernt: dem Kundenportal (Adresse /), dem Mitarbeiterbereich samt Adminportal (Adresse /staff/) und den Apps (Windows, Android, Chrome-Erweiterung sowie WordPress- und Minecraft-Plugin).
Hallo {{kundenname}},
vielen Dank für deine Nachricht. Wir kümmern uns darum und melden uns schnellstmöglich.
{{signatur}}
', null], + ['Rechnung erneut senden', 'Hallo {{kundenname}},
wir haben dir die Rechnung noch einmal als PDF angehängt. Sie steht außerdem in deinem Kundenbereich unter „Rechnungen“.
', 'Buchhaltung'], + ['Rückfrage zur Störung', 'Hallo {{kundenname}},
damit wir die Störung nachstellen können: Welches Gerät und welche Version nutzt du? Hilft ein Neustart?
', 'Technik'], + ['Ticket gelöst', 'Hallo {{kundenname}},
wir haben dein Anliegen gelöst. Falls noch etwas offen ist, antworte einfach auf diese Nachricht.
', null], + ['Kündigung bestätigt', 'Hallo {{kundenname}},
wir bestätigen deine Kündigung zum Ende der Laufzeit. Schade, dass du gehst!
', 'Allgemeiner Support'], + ] as [$t, $b, $d]) $ins->execute([$t, $b, $d ? $dept($d) : null, $ts('-40 days')]); +}); +$try('Rollen', function () use ($p, $ts) { + $p->prepare('INSERT INTO roles (name, permissions, created_at) VALUES (?,?,?)')->execute(['Teamleitung', json_encode(['reply_ticket' => true, 'close_ticket' => true, 'post_note' => true, 'assign_ticket' => true, 'transfer_ticket' => true, 'manage_canned' => true, 'view_reports' => true, 'manage_teams' => true]), $ts('-45 days')]); + $p->prepare('INSERT INTO roles (name, permissions, created_at) VALUES (?,?,?)')->execute(['Nur lesen', json_encode(['view_reports' => true]), $ts('-45 days')]); +}); +$try('Teams', function () use ($p, $ts, $staffId) { + $p->prepare('INSERT INTO teams (name, lead_id, notes, is_active, created_at) VALUES (?,?,?,1,?)')->execute(['Support-Team', $staffId('Lena Hartmann'), 'Tagesgeschäft und Kundenanfragen', $ts('-45 days')]); + $t1 = (int) $p->lastInsertId(); + $p->prepare('INSERT INTO teams (name, lead_id, notes, is_active, created_at) VALUES (?,?,?,1,?)')->execute(['Technik-Team', $staffId('Mira Schulz'), 'Störungen und Entwicklung', $ts('-45 days')]); + $t2 = (int) $p->lastInsertId(); + foreach ([[$t1, 'Lena Hartmann'], [$t1, 'Tim Roth'], [$t2, 'Mira Schulz'], [$t2, 'Tim Roth']] as [$t, $n]) $p->prepare('INSERT INTO team_members (team_id, staff_id) VALUES (?,?)')->execute([$t, $staffId($n)]); +}); + +// ── Kunden, Organisationen ────────────────────────────────────────────────── +$try('Kunden', function () use ($p, $ts, $H, $lena) { + $p->exec("INSERT INTO organizations (name, notes, created_at) VALUES ('Beispiel Bau GmbH', 'Rahmenvertrag, Premium-Support', '{$ts('-80 days')}'), ('Musterhaus AG', 'Pilotkunde', '{$ts('-70 days')}')"); + $o1 = (int) $p->query("SELECT id FROM organizations WHERE name = 'Beispiel Bau GmbH'")->fetchColumn(); + $o2 = (int) $p->query("SELECT id FROM organizations WHERE name = 'Musterhaus AG'")->fetchColumn(); + $ins = $p->prepare('INSERT INTO users (name, email, password_hash, phone, organization_id, created_at) VALUES (?,?,?,?,?,?)'); + foreach ([['Erika Muster', 'erika.muster@kunde.example', '0123 456789', $o1, '-90 days'], ['Jonas Weber', 'jonas.weber@kunde.example', null, $o1, '-75 days'], ['Sophie Klein', 'sophie.klein@kunde.example', null, $o2, '-60 days'], ['Markus Lehmann', 'markus.lehmann@kunde.example', '0171 0000000', null, '-30 days'], ['Lea Wagner', 'lea.wagner@kunde.example', null, null, '-20 days'], ['Dirk Neumann', 'dirk.neumann@kunde.example', null, $o2, '-10 days']] as [$n, $e, $ph, $o, $c]) + $ins->execute([$n, $e, $H, $ph, $o, $ts($c)]); + $uid = (int) $p->query("SELECT id FROM users WHERE email = 'erika.muster@kunde.example'")->fetchColumn(); + $p->prepare('INSERT INTO customer_notes (user_id, staff_id, note, created_at) VALUES (?,?,?,?)')->execute([$uid, $lena, 'Wünscht Rückrufe nur vormittags. Ansprechpartnerin für die Buchhaltung.', $ts('-20 days')]); +}); + +// ── Weitere Tickets ───────────────────────────────────────────────────────── +$try('Tickets', function () use ($p, $ts, $dept, $prio, $staffId) { + $tk = $p->prepare('INSERT INTO tickets (ticket_number, subject, requester_name, requester_email, department_id, priority_id, status, source, assigned_staff_id, created_at, updated_at, resolution_due_at, first_response_due_at, first_responded_at, resolved_at, closed_at, is_overdue, user_id) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)'); + $th = $p->prepare('INSERT INTO ticket_threads (ticket_id, author_type, staff_id, message, is_internal_note, created_at) VALUES (?,?,?,?,?,?)'); + $uid = fn(string $e) => (int) $p->query("SELECT id FROM users WHERE email = " . $p->quote($e))->fetchColumn() ?: null; + foreach ([ + ['260107', 'Zahlung doppelt abgebucht', 'Erika Muster', 'Buchhaltung', 'Hoch', 'open', 'email', 'Lena Hartmann', '-4 hours', '+4 hours', null, [['customer', null, "Hallo,\nmeine Rechnung 4711 wurde zweimal abgebucht. Bitte um Erstattung.", '-4 hours']]], + ['260108', 'Webseite lädt sehr langsam', 'Jonas Weber', 'Technik', 'Dringend', 'open', 'web', 'Mira Schulz', '-9 hours', '-1 hours', null, [['customer', null, 'Seit heute Morgen braucht jede Seite über 10 Sekunden.', '-9 hours'], ['staff', 'Mira Schulz', 'Wir prüfen das gerade. Danke für den Hinweis!', '-8 hours']]], + ['260109', 'Wie ändere ich meine Lieferadresse?', 'Sophie Klein', 'Allgemeiner Support', 'Normal', 'pending', 'chat', 'Tim Roth', '-1 days', '+30 hours', null, [['customer', null, 'Ich ziehe um. Wo ändere ich die Adresse?', '-1 days'], ['staff', 'Tim Roth', 'Im Kundenbereich unter „Profil“. Klappt das bei dir?', '-23 hours']]], + ['260110', 'Angebot für 50 Lizenzen', 'Lea Wagner', 'Allgemeiner Support', 'Normal', 'open', 'phone', null, '-3 hours', '+45 hours', null, [['customer', null, 'Telefonische Anfrage: Angebot für 50 Lizenzen, Rückruf gewünscht.', '-3 hours']]], + ['260111', 'Rechnung fehlt (Monat Februar)', 'Dirk Neumann', 'Buchhaltung', 'Normal', 'on_hold', 'api', 'Lena Hartmann', '-2 days', '+3 days', null, [['customer', null, 'Die Februar-Rechnung fehlt im Kundenbereich.', '-2 days'], ['staff', 'Lena Hartmann', 'Wir klären das mit dem Rechnungsprogramm und melden uns.', '-47 hours']]], + ['259905', 'Zugang gesperrt nach Passwortwechsel', 'Markus Lehmann', 'Allgemeiner Support', 'Hoch', 'resolved', 'web', 'Tim Roth', '-8 days', null, '-7 days', [['customer', null, 'Nach dem Passwortwechsel komme ich nicht mehr rein.', '-8 days'], ['staff', 'Tim Roth', 'Ich habe den Zugang entsperrt. Bitte versuche es jetzt noch einmal.', '-7 days']]], + ['259906', 'Frage zum Datenschutz', 'Erika Muster', 'Allgemeiner Support', 'Niedrig', 'closed', 'email', 'Lena Hartmann', '-30 days', null, '-29 days', [['customer', null, 'Wo kann ich meine Daten einsehen?', '-30 days'], ['staff', 'Lena Hartmann', 'Unter „Profil → Datenschutz“. Gerne senden wir dir auch einen Auszug.', '-29 days']]], + ['259907', 'Rechnungsadresse falsch', 'Jonas Weber', 'Buchhaltung', 'Normal', 'closed', 'web', 'Lena Hartmann', '-35 days', null, '-33 days', [['customer', null, 'Auf der Rechnung steht eine alte Adresse.', '-35 days'], ['staff', 'Lena Hartmann', 'Korrigiert, die neue Rechnung ist unterwegs.', '-33 days']]], + ] as [$no, $subj, $name, $dn, $pn, $status, $src, $assignee, $created, $due, $resolved, $threads]) { + $last = $threads[count($threads) - 1][3]; + $email = strtolower(str_replace(' ', '.', $name)) . '@kunde.example'; + $tk->execute([$no, $subj, $name, $email, $dept($dn), $prio($pn), $status, $src, $assignee ? $staffId($assignee) : null, $ts($created), $ts($last), + $due ? $ts($due) : null, $ts($created . ' +1 hour'), $assignee ? $ts($created . ' +40 minutes') : null, $resolved ? $ts($resolved) : null, $status === 'closed' ? $ts($resolved ?? $last) : null, + ($due && strtotime($ts($due)) < time() && !in_array($status, ['resolved', 'closed'], true)) ? 1 : 0, $uid($email)]); + $tid = (int) $p->lastInsertId(); + foreach ($threads as [$who, $sn, $msg, $when]) $th->execute([$tid, $who, $sn ? $staffId($sn) : null, $msg, 0, $ts($when)]); + } +}); + +// ── Ticket-Zubehör: Schlagwörter, Zeiten, Verknüpfungen, Notizen, Anhang, Bewertung ─ +$tid = fn(string $no) => (int) $one('SELECT id FROM tickets WHERE ticket_number = ?', [$no]); +$try('Schlagwörter', function () use ($p, $tid) { + $p->exec("INSERT INTO tags (name, color) VALUES ('Zahlung', '#0ea5e9'), ('Störung', '#ef4444'), ('Vertrag', '#8b5cf6'), ('VIP', '#f59e0b')"); + foreach ([['260101', 'Zahlung'], ['260107', 'Zahlung'], ['260107', 'VIP'], ['260108', 'Störung'], ['260105', 'Störung'], ['260104', 'Vertrag'], ['260102', 'VIP']] as [$no, $tag]) + $p->prepare('INSERT INTO ticket_tags (ticket_id, tag_id) VALUES (?, (SELECT id FROM tags WHERE name = ?))')->execute([$tid($no), $tag]); +}); +$try('Zeiten und Notizen', function () use ($p, $ts, $tid, $staffId) { + foreach ([['260108', 'Mira Schulz', 45, 'Logs geprüft, Datenbank langsam'], ['260108', 'Mira Schulz', 30, 'Index neu aufgebaut'], ['260107', 'Lena Hartmann', 15, 'Zahlungseingang geprüft'], ['260105', 'Mira Schulz', 60, 'Absturz nachgestellt']] as [$no, $n, $m, $note]) + $p->prepare('INSERT INTO ticket_time_entries (ticket_id, staff_id, minutes, note, logged_at) VALUES (?,?,?,?,?)')->execute([$tid($no), $staffId($n), $m, $note, $ts('-3 hours')]); + foreach ([['260107', 'Lena Hartmann', 'Buchung bei der Bank prüfen: Erstattung nötig, wenn tatsächlich doppelt abgebucht.', '-3 hours'], ['260108', 'Mira Schulz', 'Ursache vermutlich fehlender Index auf der Bestelltabelle.', '-7 hours']] as [$no, $n, $note, $w]) + $p->prepare('INSERT INTO ticket_threads (ticket_id, author_type, staff_id, message, is_internal_note, created_at) VALUES (?,?,?,?,1,?)')->execute([$tid($no), 'staff', $staffId($n), $note, $ts($w)]); +}); +$try('Verknüpfungen und Mitleser', function () use ($p, $ts, $tid, $lena) { + $p->prepare('INSERT INTO ticket_links (ticket_id, linked_ticket_id, link_type, created_by, created_at) VALUES (?,?,?,?,?)')->execute([$tid('260107'), $tid('260101'), 'related', $lena, $ts('-2 hours')]); + $p->prepare('INSERT INTO ticket_collaborators (ticket_id, email, name, added_by, created_at) VALUES (?,?,?,?,?)')->execute([$tid('260101'), 'buchhaltung@kunde.example', 'Buchhaltung Muster GmbH', $lena, $ts('-2 hours')]); + $p->prepare('INSERT INTO problems (title, description, root_cause, workaround, status, created_by, updated_by, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?)')->execute(['Webseite langsam am Morgen', 'Viele Kunden melden lange Ladezeiten.', 'Fehlender Datenbank-Index', 'Seite neu laden hilft kurzzeitig', 'workaround', $lena, $lena, $ts('-8 hours'), $ts('-2 hours')]); + $pid = (int) $p->lastInsertId(); + foreach (['260108', '260102'] as $no) $p->prepare('INSERT INTO problem_tickets (problem_id, ticket_id, linked_by, linked_at) VALUES (?,?,?,?)')->execute([$pid, $tid($no), $lena, $ts('-2 hours')]); +}); +$try('Geplante Antwort', function () use ($p, $ts, $tid, $staffId) { + $p->prepare("INSERT INTO scheduled_replies (ticket_id, staff_id, message, set_status, send_at, state, created_at) VALUES (?,?,?,?,?,'pending',?)")->execute([$tid('260103'), $staffId('Tim Roth'), 'Hallo Frau Klein, kurze Nachfrage: Konnten Sie die Adresse inzwischen ändern?
', 'pending', $ts('tomorrow 09:00'), $ts('-1 hour')]); +}); +$try('Bewertungen', function () use ($p, $ts, $tid, $staffId) { + $i = 0; + foreach ([['259901', 'Tim Roth', 5, 'Super schnelle Hilfe, danke!'], ['259902', 'Lena Hartmann', 4, null], ['259903', 'Tim Roth', 5, 'Ersatz war sofort da.'], ['259904', 'Lena Hartmann', 3, 'Hat etwas gedauert.'], ['259905', 'Tim Roth', 5, null], ['259906', 'Lena Hartmann', 4, 'Freundlich und hilfreich.']] as [$no, $n, $r, $c]) { + $p->prepare('INSERT INTO ticket_csat (ticket_id, token, staff_id, rating, comment, sent_at, answered_at) VALUES (?,?,?,?,?,?,?)')->execute([$tid($no), sprintf('%040x', ++$i * 7919), $staffId($n), $r, $c, $ts('-' . (3 + $i) . ' days'), $ts('-' . (3 + $i) . ' days +2 hours')]); + } +}); +$try('Anhang', function () use ($p, $ts, $tmp, $tid) { + $png = base64_decode('iVBORw0KGgoAAAANSUhEUgAAAGQAAAA8CAIAAAAv+yGaAAAAZ0lEQVR4nO3PQQ0AIBAEoFt+zWoRDmZgUkCSM0I5vUPzbBQKpyC6V2gv0N6hvUN7h/YO7R3aO7R3aO/Q3qG9Q3uH9g7tHdo7tHdo79DeIb1Deof0DukdsvrBR/YDBwAAAABJRU5ErkJggg=='); + @file_put_contents("$tmp/uploads/beispiel-fehlermeldung.png", $png); + $thread = (int) $p->query("SELECT id FROM ticket_threads WHERE ticket_id = {$tid('260105')} AND author_type = 'customer' ORDER BY id LIMIT 1")->fetchColumn(); + $p->prepare('INSERT INTO ticket_attachments (thread_id, original_name, stored_name, filesize, created_at) VALUES (?,?,?,?,?)')->execute([$thread, 'Fehlermeldung.png', 'beispiel-fehlermeldung.png', strlen($png), $ts('-5 hours')]); + $p->prepare('INSERT INTO ticket_attachments (thread_id, original_name, stored_name, filesize, created_at) VALUES (?,?,?,?,?)')->execute([$thread, 'Protokoll.txt', 'beispiel-protokoll.txt', 2048, $ts('-5 hours')]); + @file_put_contents("$tmp/uploads/beispiel-protokoll.txt", str_repeat("Beispielprotokoll\n", 100)); +}); + +// ── Aufgaben, Ansichten, Berichte ─────────────────────────────────────────── +$try('Aufgaben', function () use ($p, $ts, $staffId, $tid) { + $ins = $p->prepare('INSERT INTO tasks (ticket_id, title, description, assigned_to, status, due_at, created_by, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?,?)'); + foreach ([[$tid('260108'), 'Index auf Bestelltabelle prüfen', 'Mit dem Hosting klären', 'Mira Schulz', 'open', '+1 day'], [null, 'Textbausteine überarbeiten', 'Neue Vorlagen für Rückfragen', 'Tim Roth', 'open', '+3 days'], [$tid('260107'), 'Erstattung prüfen', null, 'Lena Hartmann', 'open', 'today 16:00'], [null, 'Monatsbericht vorbereiten', null, 'Lena Hartmann', 'done', '-2 days']] as [$t, $title, $d, $n, $st, $due]) + $ins->execute([$t, $title, $d, $staffId($n), $st, $ts($due), $staffId('Lena Hartmann'), $ts('-3 days'), $ts('-1 day')]); +}); +$try('Ansichten', function () use ($p, $ts, $lena, $dept) { + $ins = $p->prepare('INSERT INTO saved_queues (staff_id, name, filters, sort_order, created_at, is_shared, updated_at) VALUES (?,?,?,?,?,?,?)'); + foreach ([['Meine überfälligen Tickets', ['assigned' => 'me', 'sla' => 'overdue'], 0], ['Unzugewiesen', ['assigned' => 'none', 'tab' => 'open'], 1], ['Nur Technik', ['tab' => 'open', 'departmentId' => (string) $dept('Technik')], 1]] as $i => [$n, $f, $sh]) + $ins->execute([$lena, $n, json_encode($f), $i, $ts('-10 days'), $sh, $ts('-10 days')]); +}); +$try('Berichte', function () use ($p, $ts, $lena) { + $cfg = fn($dim, $m) => json_encode(['dimension' => $dim, 'metrics' => $m, 'from' => date('Y-m-d', strtotime('-30 days')), 'to' => date('Y-m-d'), 'filters' => [], 'sort' => 'label', 'desc' => false]); + $p->prepare('INSERT INTO saved_reports (staff_id, name, config, is_shared, created_at, updated_at) VALUES (?,?,?,?,?,?)')->execute([$lena, 'Monatsbericht je Abteilung', $cfg('department', ['total', 'open', 'resolved']), 1, $ts('-12 days'), $ts('-12 days')]); + $rid = (int) $p->lastInsertId(); + $p->prepare('INSERT INTO saved_reports (staff_id, name, config, is_shared, created_at, updated_at) VALUES (?,?,?,?,?,?)')->execute([$lena, 'Tickets je Mitarbeiter', $cfg('agent', ['total', 'resolved', 'avg_first_response']), 0, $ts('-8 days'), $ts('-8 days')]); + $p->prepare("INSERT INTO report_schedules (report_id, staff_id, recipients, frequency, weekday, monthday, send_time, period, enabled, next_run_at, created_at, only_if_data, skip_unchanged) VALUES (?,?,?,?,?,?,?,?,1,?,?,1,1)")->execute([$rid, $lena, 'leitung@demo.example', 'weekly', 1, 1, '08:00', 'last_week', $ts('next monday 08:00'), $ts('-6 days')]); +}); + +// ── Integrationen, E-Mail, Zeiten, Regeln ─────────────────────────────────── +$try('API und Webhooks', function () use ($p, $ts, $lena) { + $p->prepare('INSERT INTO api_keys (name, key_hash, key_prefix, can_create_tickets, can_read_tickets, ip_whitelist, is_active, last_used_at, created_by, created_at) VALUES (?,?,?,?,?,?,?,?,?,?)')->execute(['Kontaktformular Website', hash('sha256', 'demo-1'), 'dh_3f9a', 1, 1, null, 1, $ts('-2 hours'), $lena, $ts('-30 days')]); + $p->prepare('INSERT INTO api_keys (name, key_hash, key_prefix, can_create_tickets, can_read_tickets, ip_whitelist, is_active, last_used_at, created_by, created_at) VALUES (?,?,?,?,?,?,?,?,?,?)')->execute(['Minecraft-Server', hash('sha256', 'demo-2'), 'dh_77c1', 1, 0, '203.0.113.10', 1, $ts('-1 day'), $lena, $ts('-20 days')]); + $p->prepare('INSERT INTO webhooks (name, url, secret_enc, events, is_active, last_status, last_delivery_at, fail_count, created_at) VALUES (?,?,?,?,?,?,?,?,?)')->execute(['Chat-Benachrichtigung', 'https://hooks.example.org/deskhub', 'demo', 'ticket.created,ticket.replied', 1, 200, $ts('-1 hour'), 0, $ts('-15 days')]); +}); +$try('E-Mail', function () use ($p, $ts, $dept, $prio) { + $ins = $p->prepare('INSERT INTO email_addresses (email, label, department_id, priority_id, is_default, is_active, notes, created_at) VALUES (?,?,?,?,?,1,?,?)'); + $ins->execute(['support@demo.example', 'Support', $dept('Allgemeiner Support'), $prio('Normal'), 1, 'Standardadresse', $ts('-60 days')]); + $ins->execute(['buchhaltung@demo.example', 'Buchhaltung', $dept('Buchhaltung'), $prio('Hoch'), 0, null, $ts('-60 days')]); + $ins->execute(['technik@demo.example', 'Technik', $dept('Technik'), $prio('Dringend'), 0, null, $ts('-60 days')]); + $p->prepare('INSERT INTO imap_accounts (label, host, port, encryption, username, password_enc, folder, after_fetch, department_id, priority_id, is_active, last_run_at, last_success_at, created_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)') + ->execute(['Support-Postfach', 'imap.demo.example', 993, 'ssl', 'support@demo.example', 'demo', 'INBOX', 'mark_read', $dept('Allgemeiner Support'), $prio('Normal'), 1, $ts('-4 minutes'), $ts('-4 minutes'), $ts('-60 days')]); + $p->prepare("INSERT INTO mail_queue (to_email, subject, body, status, attempts, next_attempt_at, last_error, created_at) VALUES (?,?,?,?,?,?,?,?)")->execute(['erika.muster@kunde.example', 'Ihre Anfrage #260107', 'Text
', 'waiting', 2, $ts('+20 minutes'), 'Verbindung zum Mailserver fehlgeschlagen', $ts('-30 minutes')]); +}); +$try('Zeiten', function () use ($p, $ts) { + $p->exec('DELETE FROM business_hours'); + foreach (range(0, 6) as $w) $p->prepare('INSERT INTO business_hours (weekday, is_open, open_time, close_time) VALUES (?,?,?,?)')->execute([$w, in_array($w, [1, 2, 3, 4, 5], true) ? 1 : 0, '08:00:00', '17:00:00']); + foreach ([['2026-12-25', '1. Weihnachtstag'], ['2026-12-26', '2. Weihnachtstag'], ['2027-01-01', 'Neujahr']] as [$d, $n]) $p->prepare('INSERT INTO business_holidays (holiday_date, name) VALUES (?,?)')->execute([$d, $n]); +}); +$try('Regeln', function () use ($p, $ts, $dept, $prio, $staffId, $lena) { + $p->prepare('INSERT INTO ticket_rules (name, match_type, is_active, sort_order, created_at, notes) VALUES (?,?,?,?,?,?)')->execute(['Rechnungs-Anfragen an die Buchhaltung', 'any', 1, 1, $ts('-40 days'), 'Betreff enthält Rechnung oder Zahlung']); + $r = (int) $p->lastInsertId(); + foreach (['Rechnung', 'Zahlung'] as $w) $p->prepare('INSERT INTO ticket_rule_conditions (rule_id, field, operator, value) VALUES (?,?,?,?)')->execute([$r, 'subject', 'contains', $w]); + $p->prepare('INSERT INTO ticket_rule_actions (rule_id, action_type, action_value) VALUES (?,?,?)')->execute([$r, 'set_department', (string) $dept('Buchhaltung')]); + $p->prepare('INSERT INTO ticket_rule_actions (rule_id, action_type, action_value) VALUES (?,?,?)')->execute([$r, 'set_priority', (string) $prio('Hoch')]); + $p->prepare("INSERT INTO escalation_rules (name, is_active, trigger_type, trigger_hours, condition_status, action_set_priority_id, action_notify_admin, action_notify_staff, sort_order, created_at) VALUES (?,?,?,?,?,?,?,?,?,?)")->execute(['Unbeantwortet nach 8 Stunden: Priorität erhöhen', 1, 'no_reply', 8, 'open', $prio('Dringend'), 1, 1, 1, $ts('-40 days')]); + $p->prepare("INSERT INTO escalation_rules (name, is_active, trigger_type, trigger_hours, action_notify_admin, sort_order, created_at) VALUES (?,?,?,?,?,?,?)")->execute(['Überfällig: Admin informieren', 1, 'overdue', 0, 1, 2, $ts('-40 days')]); + $p->prepare('INSERT INTO recurring_tickets (subject, message, department_id, priority_id, assigned_staff_id, interval_days, next_run, is_active, created_by, created_at) VALUES (?,?,?,?,?,?,?,1,?,?)')->execute(['Monatliche Checkliste Backups', 'Backups prüfen, Speicherplatz kontrollieren, Updates einspielen.', $dept('Technik'), $prio('Normal'), $staffId('Mira Schulz'), 30, $ts('+12 days'), $lena, $ts('-18 days')]); +}); +$try('Seiten', function () use ($p, $ts) { + $ins = $p->prepare('INSERT INTO site_pages (slug, title, content, show_in_nav, is_active, sort_order, created_at) VALUES (?,?,?,?,1,?,?)'); + $ins->execute(['impressum', 'Impressum', 'Muster Support GmbH
Beispielstraße 1
12345 Beispielstadt
Alle Angaben sind erfunden.
', 1, 1, $ts('-60 days')]); + $ins->execute(['datenschutz', 'Datenschutz', 'Hier steht in einer echten Installation eure Datenschutzerklärung.
', 1, 2, $ts('-60 days')]); + $ins->execute(['agb', 'Allgemeine Geschäftsbedingungen', 'Beispieltext.
', 0, 3, $ts('-60 days')]); +}); +$try('Statusseite', function () use ($p, $ts, $lena) { + $n = (int) $p->query('SELECT COUNT(*) FROM status_components')->fetchColumn(); + if ($n === 0) { + foreach ([['Website', 'operational'], ['Kundenportal', 'operational'], ['E-Mail-Versand', 'degraded'], ['Telefon-Hotline', 'operational']] as $i => [$nm, $st]) + $p->prepare('INSERT INTO status_components (name, status, sort_order, created_at, is_enabled) VALUES (?,?,?,?,1)')->execute([$nm, $st, $i, $ts('-60 days')]); + } + $cid = (int) $p->query("SELECT id FROM status_components ORDER BY id LIMIT 1 OFFSET 2")->fetchColumn() ?: null; + $p->prepare('INSERT INTO status_incidents (title, body, status, impact, component_id, created_by, created_at, updated_at) VALUES (?,?,?,?,?,?,?,?)')->execute(['Verzögerter E-Mail-Versand', 'Mails kommen mit Verspätung an. Wir arbeiten an der Ursache.', 'monitoring', 'minor', $cid, $lena, $ts('-3 hours'), $ts('-1 hour')]); + $iid = (int) $p->lastInsertId(); + $p->prepare('INSERT INTO status_incident_updates (incident_id, status, body, created_by, created_at) VALUES (?,?,?,?,?)')->execute([$iid, 'investigating', 'Wir haben das Problem erkannt.', $lena, $ts('-3 hours')]); + $p->prepare('INSERT INTO status_incident_updates (incident_id, status, body, created_by, created_at) VALUES (?,?,?,?,?)')->execute([$iid, 'monitoring', 'Ein Neustart des Mailservers hat geholfen. Wir beobachten.', $lena, $ts('-1 hour')]); +}); +$try('Mitarbeiter-Zubehör', function () use ($p, $ts, $staffId, $lena, $tim) { + foreach ([1, 2, 3, 4, 5] as $w) $p->prepare('INSERT INTO staff_shifts (staff_id, weekday, start_time, end_time, is_active) VALUES (?,?,?,?,1)')->execute([$staffId('Tim Roth'), $w, '08:00', '16:00']); + $p->prepare('INSERT INTO staff_chat_messages (from_staff_id, to_staff_id, message, is_read, created_at) VALUES (?,?,?,?,?)')->execute([$staffId('Tim Roth'), $staffId('Lena Hartmann'), 'Kannst du kurz auf 260105 schauen? Android 14.', 1, $ts('-2 hours')]); + $p->prepare('INSERT INTO staff_chat_messages (from_staff_id, to_staff_id, message, is_read, created_at) VALUES (?,?,?,?,?)')->execute([$staffId('Lena Hartmann'), $staffId('Tim Roth'), 'Mache ich, danke dir!', 1, $ts('-110 minutes')]); + $p->prepare('INSERT INTO banlist (match_type, value, reason, created_by, created_at) VALUES (?,?,?,?,?)')->execute(['email', 'spam@bad-example.org', 'Wiederholt Werbung', $lena, $ts('-14 days')]); + $p->prepare('INSERT INTO banlist (match_type, value, reason, created_by, created_at) VALUES (?,?,?,?,?)')->execute(['domain', 'wegwerf-mail.example', 'Wegwerf-Adressen', $lena, $ts('-14 days')]); + foreach ([['Anmeldung', 'Mitarbeiter angemeldet: Tim Roth', '-1 hours'], ['Einstellungen', 'Systemeinstellungen geändert', '-1 days'], ['Mitarbeiter', 'Mitarbeiter angelegt: Mira Schulz', '-20 days'], ['Backup', 'Backup erstellt (automatisch)', '-2 days'], ['E-Mail', 'Postfach „Support-Postfach“ abgerufen: 3 neue Mails', '-4 minutes']] as [$k, $a, $w]) + $p->prepare('INSERT INTO admin_audit_log (staff_id, action, created_at) VALUES (?,?,?)')->execute([$lena, $a, $ts($w)]); + $ev = $p->prepare('INSERT INTO login_events (staff_id, email, event, channel, ip, place, device, detail, created_at) VALUES (?,?,?,?,?,?,?,?,?)'); + $ev->execute([$lena, 'lena@demo.example', 'success', 'web', '203.0.113.5', 'Netz 203.0.x.x', 'Chrome auf Windows', 'Angemeldet', $ts('-1 hour')]); + $ev->execute([$tim, 'tim@demo.example', 'new_device', 'app', '203.0.113.9', 'Netz 203.0.x.x', 'DeskHub-App', 'Neues Gerät', $ts('-1 day')]); + $ev->execute([$tim, 'tim@demo.example', 'failed', 'web', '198.51.100.7', 'Netz 198.51.x.x', 'Firefox auf Linux', 'Falsches Passwort', $ts('-2 days')]); +}); +$try('Einstellungen', function () use ($set) { + foreach (['cron_fetch_emails_last_run' => date('Y-m-d H:i:s', strtotime('+30 days')), 'csat_enabled' => '1', 'csat_delay_minutes' => '30', 'business_hours_enabled' => '1', 'status_page_enabled' => '1', 'auto_assign_on_open' => '1', 'kb_enabled' => '1', 'live_chat_enabled' => '1', 'display_name' => 'Muster Support', 'wait_reminder_enabled' => '1', 'wait_reminder_hours' => '4'] as $k => $v) $set->execute([$k, $v]); +}); + +// Lenas Woche: ein paar gelöste Tickets mit Bewertungen, damit „Meine Woche“ gefüllt aussieht; eine Erwähnung +$try('Meine Woche', function () use ($p, $ts, $dept, $prio, $lena, $tim) { + $tk = $p->prepare('INSERT INTO tickets (ticket_number, subject, requester_name, requester_email, department_id, priority_id, status, source, assigned_staff_id, created_at, updated_at, resolution_due_at, first_response_due_at, first_responded_at, resolved_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)'); + $th = $p->prepare('INSERT INTO ticket_threads (ticket_id, author_type, staff_id, message, is_internal_note, created_at) VALUES (?,?,?,?,0,?)'); + $monday = strtotime('monday this week 09:00'); + foreach ([['260021', 'Rechnung nachgereicht', 0, 3, 5, 'Buchhaltung'], ['260022', 'Adresse geändert', 0, 2, 5, 'Allgemeiner Support'], ['260023', 'Zugang eingerichtet', 1, 5, 4, 'Allgemeiner Support'], ['260024', 'Passwort zurückgesetzt', 1, 1, 5, 'Allgemeiner Support'], ['260025', 'Frage zur Lieferung', 2, 4, 3, 'Allgemeiner Support']] as [$no, $subj, $dayOff, $hours, $rating, $dn]) { + $created = date('Y-m-d H:i:s', $monday + $dayOff * 86400); + $resolved = date('Y-m-d H:i:s', $monday + $dayOff * 86400 + $hours * 3600); + if (strtotime($resolved) > time()) { $created = date('Y-m-d H:i:s', time() - 7200 - $hours * 3600); $resolved = date('Y-m-d H:i:s', time() - 3600); } + $tk->execute([$no, $subj, 'Kunde ' . $no, 'k' . $no . '@kunde.example', $dept($dn), $prio('Normal'), 'resolved', 'web', $lena, $created, $resolved, date('Y-m-d H:i:s', strtotime($created) + 86400), date('Y-m-d H:i:s', strtotime($created) + 3600), date('Y-m-d H:i:s', strtotime($created) + 1500 + $hours * 300), $resolved]); + $tid = (int) $p->lastInsertId(); + $th->execute([$tid, 'customer', null, 'Anfrage: ' . $subj, $created]); + $th->execute([$tid, 'staff', $lena, 'Erledigt, danke für die Geduld.', date('Y-m-d H:i:s', strtotime($created) + 1500 + $hours * 300)]); + $p->prepare('INSERT INTO ticket_csat (ticket_id, token, staff_id, rating, comment, sent_at, answered_at) VALUES (?,?,?,?,?,?,?)')->execute([$tid, sprintf('%040x', crc32($no) * 31), $lena, $rating, null, $resolved, $resolved]); + } + $t = (int) $p->query("SELECT id FROM tickets WHERE ticket_number = '260108'")->fetchColumn(); + $thr = (int) $p->query("SELECT id FROM ticket_threads WHERE ticket_id = {$t} AND is_internal_note = 1 ORDER BY id LIMIT 1")->fetchColumn(); + $p->prepare('INSERT INTO staff_mentions (ticket_id, thread_id, mentioned_staff_id, mentioned_by_staff_id, is_read, created_at) VALUES (?,?,?,?,0,?)')->execute([$t, $thr, $lena, $tim, $ts('-1 hour')]); +}); + +echo "Handbuch-Daten ergänzt.\n"; +foreach ($warn as $w) echo "WARNUNG $w\n";