Upload via GUI (452 Dateien)
This commit is contained in:
@@ -0,0 +1,65 @@
|
||||
<h1>Sicherheit, Backup, Update und Import</h1>
|
||||
|
||||
<h2>Sicherheit und Datenschutz im Überblick</h2>
|
||||
<ul>
|
||||
<li><b>Sperrliste:</b> Einzelne E-Mail-Adressen oder ganze Domains blockieren; Mails von dort erzeugen kein Ticket mehr.</li>
|
||||
<li><b>Anmelde-Wächter, 2FA, Passkeys, Gerätekontrolle, „Von allen Geräten abmelden“:</b> siehe Kapitel 4, 9 und 12. Passkeys (Server 1.0.77, Subdomain-Erkennung ab 1.0.81) gelten nur für die Domain, unter der sie angelegt wurden; ein Domainwechsel macht sie ungültig. Sie laufen im Browser mit Windows Hello, Handy und Passwortmanagern wie Bitwarden.</li>
|
||||
<li><b>Automatische Abmeldung</b> bei Inaktivität (einstellbar).</li>
|
||||
<li><b>Verschlüsselung:</b> SMTP-/IMAP-Passwörter und Tokens liegen verschlüsselt in der Datenbank. API-Schlüssel nur als Prüfsumme.</li>
|
||||
<li><b>Schutz vor HTML-Angriffen:</b> Antworten und HTML-Mails laufen vor der Anzeige durch einen strengen Filter; Skripte, Formulare und eingebettete Inhalte werden entfernt.</li>
|
||||
<li><b>Abteilungsgrenzen:</b> Mitarbeiter sehen Tickets, Anhänge, Ausdrucke und Kundenprofile nur für ihre Abteilungen – auch über direkte Links.</li>
|
||||
<li><b>Sicherheitsprüfung</b> im Adminportal (Kapitel 12) und die Checkliste „Betrieb absichern“ (Kapitel 2).</li>
|
||||
</ul>
|
||||
|
||||
<h3>Datenschutz-Werkzeuge</h3>
|
||||
<ul>
|
||||
<li><b>Automatische Anonymisierung</b> (Einstellungen → Datenschutz): persönliche Daten in geschlossenen Tickets werden nach der eingestellten Zeit unkenntlich gemacht.</li>
|
||||
<li><b>Auskunft und Löschung auf Anfrage</b> (optional, standardmäßig aus): Im Kundenprofil sehen nur Administratoren <b>„Alle Daten exportieren (JSON)“</b> – Stammdaten, alle Tickets mit Nachrichten, Anhangsnamen, Mitleser, Bewertung und Notizen (interne Notizen als intern gekennzeichnet, Dateiinhalte fehlen) – und <b>„Kundendaten löschen“</b>: Bestätigung durch Eingabe der E-Mail-Adresse; Tickets bleiben anonymisiert mit Nummer, Betreff, Status und Zeiten; Nachrichten, Dateien und Notizen des Kunden sowie das Konto werden entfernt; offene Tickets nur mit ausdrücklichem Erzwingen. Beides steht im Audit-Log.</li>
|
||||
<li><b>Fragen-Log des Assistenten:</b> nur Frage und Ergebnis, keine Namen; Aufbewahrung einstellbar.</li>
|
||||
<li><b>Push und Apps:</b> Standardmäßig verlassen Betreff und Kundenname den Server nicht.</li>
|
||||
</ul>
|
||||
<div class="warn">Dateien, die Mitarbeiter gesendet haben, und Namen in Mitarbeiter-Notizen oder Zitaten bleiben von der Löschung unberührt und sind bei Bedarf von Hand zu prüfen.</div>
|
||||
|
||||
<h2>Backup</h2>
|
||||
<p>Adminportal → <b>Backup</b> (nur Administratoren).</p>
|
||||
<fig n="admin-backup">Die Backup-Seite mit Zeitplan und vorhandenen Sicherungen.</fig>
|
||||
<ul>
|
||||
<li><b>Manuell:</b> „Backup jetzt erstellen“, herunterladen.</li>
|
||||
<li><b>Automatisch:</b> Zeitplan (täglich, wöchentlich, monatlich), optional mit Upload per FTP auf einen zweiten Server und Rotation (alte Backups nach einer einstellbaren Anzahl löschen). Außerdem entsteht automatisch ein Backup vor jedem System-Update, jedem Datenbank-Update und jeder Plugin-Aktivierung.</li>
|
||||
<li><b>Inhalt:</b> Datenbank (SQLite-Schnappschuss bzw. SQL-Dump bei MySQL) und alle Dateien der Installation – <b>ohne</b> den Ordner <code>backups/</code> selbst. Bereits komprimierte Dateien werden nur abgelegt.</li>
|
||||
<li><b>Nur ein Backup gleichzeitig:</b> Cronjob, Hintergrunddienst, Knopf und NAS-Plugin teilen sich eine Sperre.</li>
|
||||
<li><b>Prüfen:</b> Jedes neue Backup wird sofort geprüft; ältere (über 7 Tage) im Hintergrund nachgeprüft. Die Liste zeigt „✔ geprüft“ oder „✖ defekt“; ein defektes Backup meldet DeskHub per E-Mail.</li>
|
||||
<li><b>Wiederherstellen:</b> Spielt Datenbank und (wählbar) Uploads, Plugins und App-Downloads zurück. Davor entsteht automatisch ein Sicherheits-Backup des aktuellen Stands; zur Bestätigung gebt ihr <code>WIEDERHERSTELLEN</code> ein. Programmcode und <code>config.php</code> werden nie überschrieben; ein Backup einer anderen Datenbankart (SQLite ↔ MySQL) wird abgelehnt.</li>
|
||||
</ul>
|
||||
<div class="tip">Hinter Cloudflare beendet eine Anfrage nach 100 Sekunden mit der Fehlerseite „524“; ein manuelles Backup läuft auf dem Server trotzdem zu Ende. Prüft dann die Liste, statt noch einmal zu klicken. Zuverlässiger sind der Zeitplan mit echtem Cronjob oder das Synology-NAS-Plugin.</div>
|
||||
|
||||
<h2>System-Update und Datenbank-Update</h2>
|
||||
<fig n="admin-system-update">System-Update und Datenbank.</fig>
|
||||
<ul>
|
||||
<li><b>Neue Programmversion installieren:</b> zeigt verfügbare Versionen und installiert die gewählte per Klick (oder per hochgeladener ZIP). Davor entsteht automatisch ein Backup. Konfiguration, Datenbank, Uploads, bereitgestellte Apps, Backups und Plugins werden nie überschrieben. Der Download läuft nur über eine geprüfte, verschlüsselte Verbindung; danach wird die Datenbank automatisch angepasst.</li>
|
||||
<li><b>Datenbank aktualisieren:</b> Die Karte „Datenbank“ zeigt, ob die Datenbank zum Programm passt; wenn nicht, erscheint ein Hinweis oben auf jeder Seite für Administratoren. Der Knopf legt zuerst ein Backup an und spielt dann alle fehlenden Änderungen ein – der richtige Weg nach einem Upload per FTP.</li>
|
||||
<li><b>Signierte Updates:</b> Ist ein Herausgeber-Schlüssel eingebaut, werden nur mit ihm signierte Releases installiert; manipulierte Pakete werden abgelehnt und im Admin-Protokoll vermerkt. Ein manuell hochgeladenes ZIP wird nicht geprüft.</li>
|
||||
<li><b>Zugang zum Update-Server:</b> Bei einem privaten Repository tragt ihr unter „Zugang zum Update-Server“ ein Lese-Token ein (verschlüsselt gespeichert).</li>
|
||||
<li>Nach jedem Update den Browser-Cache leeren (<kbd>Strg</kbd> + <kbd>F5</kbd>).</li>
|
||||
</ul>
|
||||
|
||||
<h2>Import aus osTicket</h2>
|
||||
<p>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 <code>mysqldump</code>), auch mit anderem Tabellenpräfix.</p>
|
||||
<fig n="admin-osticket-import">Der osTicket-Import-Assistent.</fig>
|
||||
<h3>Vorbereitung</h3>
|
||||
<ol class="steps">
|
||||
<li><b>Backup</b> von DeskHub erstellen.</li>
|
||||
<li><b>Mitarbeiter anlegen</b> – mit denselben E-Mail-Adressen wie in osTicket, denn sie werden darüber zugeordnet. Fehlende Mitarbeiter bleiben unzugewiesen.</li>
|
||||
<li><b>SQL-Dump</b> der osTicket-Datenbank erstellen (große Dateien lieber per FTP in den DeskHub-Hauptordner legen).</li>
|
||||
</ol>
|
||||
<h3>Durchführung</h3>
|
||||
<p>Einstellungen → „📥 osTicket Import“: (1) <b>Hochladen</b> (Dateiname im Hauptordner oder Direkt-Upload in Teilen), (2) <b>Analysieren</b> – DeskHub zeigt, was die Datei enthält, (3) <b>Optionen</b> wählen (feste Reihenfolge: Kunden → Abteilungen und Hilfethemen → Tickets → Nachrichten → Anhänge), (4) <b>Import</b> in kleinen Schritten mit Fortschritt und Zusammenfassung.</p>
|
||||
<ul>
|
||||
<li>Status: Offen → offen, Gelöst → gelöst, Geschlossen und Archiviert → geschlossen; gelöschte Tickets werden nicht importiert. Zeiten bleiben erhalten; offene Tickets bekommen neue SLA-Fristen.</li>
|
||||
<li><b>Wiederholbar:</b> Vorhandene Ticketnummern und Kunden werden übersprungen, nichts doppelt angelegt. Anhänge bis 50 MB.</li>
|
||||
<li><b>Nicht importiert:</b> eigene Formularfelder, Aufgaben, Wissensdatenbank, Benutzerkonten und Passwörter, Mitarbeiter-Konten, Vorlagen, Regeln und Einstellungen.</li>
|
||||
<li>Die Aktionen „Nur Nachrichten löschen“ und „Alle Tickets löschen“ löschen unwiderruflich, auch echte Tickets.</li>
|
||||
</ul>
|
||||
|
||||
<h2>Störungsanzeigen</h2>
|
||||
<p>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.</p>
|
||||
@@ -0,0 +1,92 @@
|
||||
<h1>Anhang</h1>
|
||||
|
||||
<h2>Tastenkürzel im Überblick</h2>
|
||||
<table>
|
||||
<tr><th>Wo</th><th>Taste</th><th>Wirkung</th></tr>
|
||||
<tr><td>Ticketliste</td><td><kbd>j</kbd> / <kbd>k</kbd></td><td>nächstes / vorheriges Ticket wählen</td></tr>
|
||||
<tr><td>Ticketliste</td><td><kbd>Enter</kbd> oder <kbd>o</kbd></td><td>Ticket öffnen</td></tr>
|
||||
<tr><td>Ticketliste</td><td><kbd>r</kbd></td><td>öffnen und antworten</td></tr>
|
||||
<tr><td>Ticketliste</td><td><kbd>x</kbd></td><td>ankreuzen</td></tr>
|
||||
<tr><td>Ticketliste</td><td><kbd>/</kbd></td><td>Suche</td></tr>
|
||||
<tr><td>Ticketliste</td><td><kbd>n</kbd></td><td>neues Ticket</td></tr>
|
||||
<tr><td>Ticket</td><td><kbd>j</kbd> / <kbd>k</kbd></td><td>durch die Nachrichten blättern</td></tr>
|
||||
<tr><td>Ticket</td><td><kbd>r</kbd></td><td>Antwort schreiben</td></tr>
|
||||
<tr><td>Ticket</td><td><kbd>u</kbd></td><td>zurück zur Liste</td></tr>
|
||||
<tr><td>Antwortfeld</td><td><kbd>Strg</kbd> + <kbd>Enter</kbd></td><td>Antwort senden</td></tr>
|
||||
<tr><td>Antwortfeld</td><td><kbd>Strg</kbd> + <kbd>V</kbd></td><td>Screenshot oder kopiertes Bild einfügen</td></tr>
|
||||
<tr><td>Bildvorschau</td><td>Pfeiltasten / <kbd>Esc</kbd></td><td>blättern / schließen</td></tr>
|
||||
<tr><td>überall</td><td><kbd>?</kbd></td><td>Kürzel-Übersicht (mit Abschaltmöglichkeit)</td></tr>
|
||||
<tr><td>Chrome-Erweiterung</td><td><kbd>Alt</kbd>+<kbd>Shift</kbd>+<kbd>N</kbd> / <kbd>D</kbd></td><td>Neues Ticket / Erweiterung öffnen</td></tr>
|
||||
</table>
|
||||
|
||||
<h2>Rollen und typische Rechte</h2>
|
||||
<table>
|
||||
<tr><th>Rolle</th><th>Darf</th></tr>
|
||||
<tr><td><b>Kunde</b></td><td>Tickets anlegen und verfolgen, antworten, bewerten; kein Zugang zum Mitarbeiterbereich</td></tr>
|
||||
<tr><td><b>Mitarbeiter (Agent)</b></td><td>Tickets seiner Abteilungen lesen, beantworten, notieren; Status, Zuweisung, Wiedervorlage; Chat, Aufgaben, eigene Berichte</td></tr>
|
||||
<tr><td><b>Eigene Rolle (z. B. Teamleitung)</b></td><td>zusätzlich laut Rechte: zuweisen, schließen, löschen, Tickets übertragen, Eskalationsregeln, digitalen Assistenten verwalten …</td></tr>
|
||||
<tr><td><b>Co-Admin</b></td><td>Rolle mit freigeschalteten Adminportal-Bereichen; nie Backup, System-Update, Ticket-Regeln, Plugins, API-Schlüssel, Webhooks</td></tr>
|
||||
<tr><td><b>Administrator</b></td><td>Alles; Ticket-Sperren übernehmen; Konten sichern und abmelden</td></tr>
|
||||
</table>
|
||||
|
||||
<h2>Wer bekommt welche Benachrichtigung?</h2>
|
||||
<table>
|
||||
<tr><th>Ereignis</th><th>Empfänger</th></tr>
|
||||
<tr><td>Neues Ticket</td><td>Alle Administratoren, Mitarbeiter der Abteilung und Mitarbeiter mit Benachrichtigung für diese Abteilung</td></tr>
|
||||
<tr><td>Kundenantwort</td><td>Der zugewiesene Mitarbeiter; ohne Zuweisung die Abteilung; Beobachter</td></tr>
|
||||
<tr><td>Zuweisung</td><td>Der neue Bearbeiter (nicht, wer sich selbst zuweist)</td></tr>
|
||||
<tr><td>Erwähnung</td><td>Der Erwähnte, wenn er Zugriff auf die Abteilung hat</td></tr>
|
||||
<tr><td>SLA überschritten</td><td>Standard: nur Administratoren per Mail (bei Urlaub die Vertretung); alternativ Bearbeiter/Abteilung per Mail und App-Push</td></tr>
|
||||
<tr><td>Ticket wartet auf Antwort</td><td>Der zugewiesene Bearbeiter (Push, optional Mail)</td></tr>
|
||||
<tr><td>Wiedervorlage fällig</td><td>Der Mitarbeiter, der das Ticket zurückgestellt hat</td></tr>
|
||||
<tr><td>Anmeldung von neuem Gerät, Sperre</td><td>Der Kontoinhaber (und bei Sperre die Administratoren)</td></tr>
|
||||
<tr><td>Postfach-Störung, defektes Backup, Dienstausfall</td><td>Die Administratoren</td></tr>
|
||||
</table>
|
||||
<p>Im Urlaubsmodus bekommt statt des Mitarbeiters die Vertretung diese Meldungen (Sicherheitsmails und Systemmeldungen an Administratoren ausgenommen).</p>
|
||||
|
||||
<h2>Glossar</h2>
|
||||
<table>
|
||||
<tr><th>Begriff</th><th>Bedeutung</th></tr>
|
||||
<tr><td>Ticket</td><td>Eine Anfrage mit Nummer, Verlauf, Status, Priorität und Abteilung</td></tr>
|
||||
<tr><td>Interne Notiz</td><td>Nachricht im Ticket, die nur das Team sieht</td></tr>
|
||||
<tr><td>Abteilung</td><td>Themenbereich, dem ein Ticket genau einmal zugeordnet ist; Grenze der Sichtbarkeit</td></tr>
|
||||
<tr><td>Hilfethema</td><td>Auswahl für Kunden beim Anlegen; legt Abteilung, Priorität, ggf. SLA und Formularfelder fest</td></tr>
|
||||
<tr><td>SLA</td><td>Service-Level-Vereinbarung: Fristen für erste Antwort und Lösung, gerechnet in Geschäftszeiten</td></tr>
|
||||
<tr><td>Wiedervorlage</td><td>Ticket „auf später legen“; es kommt zum gewählten Zeitpunkt zurück</td></tr>
|
||||
<tr><td>Beobachter</td><td>Mitarbeiter, der ein Ticket verfolgt, ohne Bearbeiter zu sein</td></tr>
|
||||
<tr><td>Cc / Mitleser</td><td>Externe E-Mail-Adresse, die öffentliche Antworten in Kopie bekommt</td></tr>
|
||||
<tr><td>Co-Admin</td><td>Mitarbeiter mit Zugriff auf einzelne Adminportal-Bereiche</td></tr>
|
||||
<tr><td>CSAT</td><td>Zufriedenheitsabfrage mit fünf Sternen</td></tr>
|
||||
<tr><td>Cron / Pseudo-Cron</td><td>Zeitgesteuerte Hintergrundaufgaben (echter Server-Cronjob oder eingebauter Ersatz)</td></tr>
|
||||
<tr><td>Webhook</td><td>Automatische Meldung von DeskHub an ein anderes System bei einem Ereignis</td></tr>
|
||||
<tr><td>Plugin</td><td>Nachrüstbare Zusatzfunktion als ZIP-Paket</td></tr>
|
||||
<tr><td>2FA / TOTP</td><td>Zwei-Faktor-Anmeldung mit Einmalcode aus einer Authenticator-App</td></tr>
|
||||
<tr><td>Passkey</td><td>Anmeldung ohne Passwort per Fingerabdruck, Gesicht oder Geräte-PIN (WebAuthn); ersetzt Passwort und 2FA-Code</td></tr>
|
||||
</table>
|
||||
|
||||
<h2>Wenn etwas nicht funktioniert</h2>
|
||||
<table>
|
||||
<tr><th>Problem</th><th>Was zu tun ist</th></tr>
|
||||
<tr><td>Eine automatische Mail kommt nicht an</td><td>Adminportal → Admin-Protokoll öffnen (genaue Fehlermeldung des Anbieters); Einstellungen → E-Mail → „Test-Mail senden“; einzelne Absenderadresse unter E-Mail-Adressen testen; Mail-Warteschlange ansehen</td></tr>
|
||||
<tr><td>E-Mails werden nicht zu Tickets</td><td>Postfächer (Abruf) → „Testen“ zeigt die Meldung des Mail-Servers; „E-Mails jetzt abrufen“ in der Ticketliste ausprobieren</td></tr>
|
||||
<tr><td>Rotes oder gelbes Banner oben</td><td>Störungsmeldung der Überwachung (Postfach, Abruf, Versand); Details unter E-Mail-Adressen und Postfächer</td></tr>
|
||||
<tr><td>„Datenbank veraltet“</td><td>System-Update → „Datenbank aktualisieren“</td></tr>
|
||||
<tr><td>Ein Plugin macht Probleme</td><td>Unter Plugins deaktivieren; lässt sich die Seite nicht öffnen, Datei <code>plugins/.disabled</code> anlegen</td></tr>
|
||||
<tr><td>„Mit Passkey anmelden“ fehlt oder schlägt fehl</td><td>Website-URL muss mit <code>https://</code> 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</td></tr>
|
||||
<tr><td>Konto gesperrt (zu viele Fehlversuche)</td><td>15 Minuten warten, „Passwort vergessen“ nutzen oder einen Administrator bitten, die Sperre aufzuheben</td></tr>
|
||||
<tr><td>Die App meldet „Ein gültiges Bearer-Token ist erforderlich“</td><td>Der Webserver reicht den Anmelde-Header nicht weiter (typisch Apache mit PHP-FPM): neue <code>.htaccess</code> hochladen bzw. auf nginx <code>fastcgi_param HTTP_AUTHORIZATION $http_authorization;</code> setzen</td></tr>
|
||||
<tr><td>Die App zeigt „Keine Verbindung“</td><td>Erreichbarkeit der Seite im Browser prüfen; sobald der Server antwortet, verschwindet das Banner von selbst</td></tr>
|
||||
<tr><td>Zähler in Chrome-Erweiterung/Windows-Programm stimmt nicht</td><td>Server und Programme aktualisieren (ab Server 1.0.69); der Zähler zeigt nur Tickets, die jetzt Aufmerksamkeit brauchen</td></tr>
|
||||
<tr><td>Nach einem Update sieht alles unverändert aus</td><td>Browser-Cache leeren (<kbd>Strg</kbd> + <kbd>F5</kbd>)</td></tr>
|
||||
</table>
|
||||
|
||||
<h2>Versionsstand dieses Handbuchs</h2>
|
||||
<table class="facts">
|
||||
<tr><td>DeskHub-Server</td><td>1.1.0</td></tr>
|
||||
<tr><td>Windows-Programm</td><td>2.12.3</td></tr>
|
||||
<tr><td>Android-App</td><td>3.5.3</td></tr>
|
||||
<tr><td>Chrome-Erweiterung</td><td>2.1.1</td></tr>
|
||||
<tr><td>WordPress-Plugin</td><td>1.3.0</td></tr>
|
||||
<tr><td>Minecraft-Plugin</td><td>1.2.0</td></tr>
|
||||
</table>
|
||||
<p class="small">Dieses Handbuch beschreibt den Funktionsumfang dieser Versionen. Alle Namen, Adressen und Zahlen in den Abbildungen sind erfunden; <code>example</code>-Domains sind reservierte Beispieladressen.</p>
|
||||
@@ -0,0 +1,12 @@
|
||||
<section class="cover">
|
||||
<div class="shape1"></div><div class="shape2"></div>
|
||||
<div class="mark">D</div>
|
||||
<h1>DeskHub<br>Handbuch</h1>
|
||||
<div class="rule"></div>
|
||||
<div class="sub">Das vollständige Benutzer- und Administrationshandbuch für das selbst gehostete Helpdesk-System: Kundenportal, Mitarbeiterbereich, Adminportal, Apps und Plugins.</div>
|
||||
<div class="meta">
|
||||
<b>Stand der beschriebenen Versionen</b><br>
|
||||
{{VERSION}}<br><br>
|
||||
Alle Abbildungen zeigen eine Demo-Installation mit erfundenen Beispieldaten.
|
||||
</div>
|
||||
</section>
|
||||
@@ -0,0 +1,21 @@
|
||||
<section class="front">
|
||||
<h1 class="plain">Über dieses Handbuch</h1>
|
||||
<p>Dieses Handbuch beschreibt <b>jede Funktion von DeskHub</b>: 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.</p>
|
||||
<h3>An wen richtet sich welches Kapitel?</h3>
|
||||
<table>
|
||||
<tr><th>Rolle</th><th>Kapitel</th></tr>
|
||||
<tr><td><b>Kunden</b></td><td>Kapitel 3 (Kundenportal)</td></tr>
|
||||
<tr><td><b>Mitarbeiter</b></td><td>Kapitel 4 bis 9 (Mitarbeiterbereich, Tickets, Chat, Berichte, Profil) und 14 (Apps)</td></tr>
|
||||
<tr><td><b>Administratoren</b></td><td>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</td></tr>
|
||||
</table>
|
||||
<h3>Schreibweisen</h3>
|
||||
<ul>
|
||||
<li>Menüwege stehen so: <b>Adminportal → Einstellungen → E-Mail</b>.</li>
|
||||
<li>Tasten stehen so: <kbd>Strg</kbd> + <kbd>Enter</kbd>. Texte, die ihr eintippt oder die in Dateien stehen, sind <code>so</code> gesetzt.</li>
|
||||
<li>Farbige Kästen: <b>Tipp</b> (Arbeitserleichterung), <b>Hinweis</b> (gut zu wissen), <b>Achtung</b> (kann Daten oder Funktionen beeinträchtigen).</li>
|
||||
</ul>
|
||||
<div class="note"><b>Alle Daten in den Abbildungen sind erfunden.</b> Namen, E-Mail-Adressen, Tickets, Firmen und Zahlen stammen aus einer eigens angelegten Demo-Installation. Die Domains <code>demo.example</code> und <code>kunde.example</code> sind reservierte Beispieladressen und gehören niemandem. Es kommen keine echten Kunden-, Mitarbeiter- oder Zugangsdaten vor.</div>
|
||||
<div class="note"><b>Was dieses Handbuch nicht zeigt:</b> Von der <b>Android-App</b> und der <b>Chrome-Erweiterung</b> gibt es hier keine Bildschirmfotos, weil für sie keine Demo-Umgebung zur Verfügung stand. Ihre Funktionen sind im Text vollständig beschrieben. Das Windows-Programm ist mit Bildern aus einer Test-Attrappe mit Beispieldaten dargestellt (es hat dort keine Verbindung zu einem Server, daher einzelne Hinweise wie „Mock: … nicht vorhanden“ in den Bildern; diese gehören nicht zum Programm).</div>
|
||||
<h3>Aufbau der Weboberfläche</h3>
|
||||
<p>DeskHub besteht aus drei Bereichen, die ihr in den folgenden Kapiteln einzeln kennenlernt: dem <b>Kundenportal</b> (Adresse <code>/</code>), dem <b>Mitarbeiterbereich</b> samt Adminportal (Adresse <code>/staff/</code>) und den <b>Apps</b> (Windows, Android, Chrome-Erweiterung sowie WordPress- und Minecraft-Plugin).</p>
|
||||
</section>
|
||||
@@ -0,0 +1,67 @@
|
||||
@page { size: A4; margin: 18mm 18mm 20mm 18mm; }
|
||||
@page :first { margin: 0; }
|
||||
:root { --teal: #0f6f69; --teal-d: #0a4d49; --ink: #1f2937; --mute: #6b7280; --line: #d9e2e1; --soft: #f1f7f6; }
|
||||
* { box-sizing: border-box; }
|
||||
html { font-family: "Segoe UI", "Helvetica Neue", Arial, sans-serif; color: var(--ink); font-size: 10pt; line-height: 1.5; }
|
||||
body { margin: 0; }
|
||||
/* Deckblatt */
|
||||
.cover { height: 297mm; width: 210mm; page-break-after: always; position: relative; color: #fff; overflow: hidden;
|
||||
background: linear-gradient(155deg, #0a4d49 0%, #0f6f69 55%, #1d9a8f 100%); }
|
||||
.cover .shape1 { position: absolute; right: -60mm; top: -40mm; width: 170mm; height: 170mm; border-radius: 50%; background: rgba(255,255,255,.07); }
|
||||
.cover .shape2 { position: absolute; left: -50mm; bottom: -60mm; width: 150mm; height: 150mm; border-radius: 50%; background: rgba(255,255,255,.06); }
|
||||
.cover .mark { position: absolute; left: 25mm; top: 30mm; width: 26mm; height: 26mm; border-radius: 6mm; background: #fff; color: var(--teal); font-weight: 800; font-size: 20mm; line-height: 26mm; text-align: center; }
|
||||
.cover h1 { position: absolute; left: 25mm; top: 78mm; margin: 0; font-size: 46pt; letter-spacing: -1px; line-height: 1.05; border: 0; color: #fff; }
|
||||
.cover .sub { position: absolute; left: 25mm; top: 134mm; width: 150mm; font-size: 17pt; font-weight: 300; line-height: 1.35; }
|
||||
.cover .rule { position: absolute; left: 25mm; top: 126mm; width: 40mm; height: 1.4mm; background: #fff; opacity: .8; }
|
||||
.cover .meta { position: absolute; left: 25mm; bottom: 28mm; width: 160mm; font-size: 10pt; line-height: 1.6; opacity: .95; }
|
||||
.cover .meta b { font-weight: 600; }
|
||||
/* Vorspann */
|
||||
.front { page-break-after: always; }
|
||||
.front h1.plain, .toc h1.plain { border: 0; margin-top: 0; }
|
||||
/* Überschriften */
|
||||
h1 { font-size: 24pt; color: var(--teal-d); margin: 0 0 6mm; padding-bottom: 3mm; border-bottom: 2px solid var(--teal); page-break-before: always; line-height: 1.15; break-after: avoid; }
|
||||
h1.plain { page-break-before: auto; }
|
||||
.front h1, .toc h1 { page-break-before: auto; }
|
||||
h2 { font-size: 15pt; color: var(--teal); margin: 8mm 0 3mm; break-after: avoid; line-height: 1.25; }
|
||||
h3 { font-size: 11pt; color: var(--ink); margin: 5mm 0 1.5mm; break-after: avoid; }
|
||||
.num { color: #8fb9b5; font-weight: 400; margin-right: 2mm; }
|
||||
p { margin: 0 0 2.5mm; orphans: 3; widows: 3; }
|
||||
ul, ol { margin: 0 0 3mm; padding-left: 6mm; }
|
||||
li { margin-bottom: 1mm; }
|
||||
code { font-family: Consolas, "Courier New", monospace; font-size: 9pt; background: #eef3f2; padding: 0 1mm; border-radius: 1mm; }
|
||||
kbd { font-family: Consolas, monospace; font-size: 8.5pt; background: #fff; border: 1px solid #b7c6c4; border-bottom-width: 2px; border-radius: 1.2mm; padding: 0 1.4mm; white-space: nowrap; }
|
||||
table { border-collapse: collapse; width: 100%; margin: 2mm 0 4mm; font-size: 9pt; break-inside: auto; }
|
||||
th { background: var(--teal); color: #fff; text-align: left; padding: 1.6mm 2.2mm; font-weight: 600; }
|
||||
td { padding: 1.5mm 2.2mm; border-bottom: 1px solid var(--line); vertical-align: top; }
|
||||
tr { break-inside: avoid; }
|
||||
tr:nth-child(even) td { background: #f7fbfa; }
|
||||
/* Bilder */
|
||||
figure { margin: 3mm 0 5mm; padding: 0; text-align: center; break-inside: avoid; }
|
||||
figure img { max-width: 100%; max-height: 205mm; width: auto; height: auto; border: 1px solid #c8d4d3; border-radius: 1.5mm; box-shadow: 0 1px 3px rgba(0,0,0,.12); }
|
||||
figure.narrow img { max-height: 120mm; }
|
||||
figcaption { font-size: 8.5pt; color: var(--mute); margin-top: 1.5mm; }
|
||||
/* Hinweisboxen */
|
||||
.tip, .warn, .note { margin: 3mm 0 4mm; padding: 2.4mm 3.5mm; border-left: 3mm solid; border-radius: 0 1.5mm 1.5mm 0; font-size: 9.5pt; break-inside: avoid; }
|
||||
.tip { background: #eaf6ef; border-color: #2f9e6b; }
|
||||
.warn { background: #fff4e5; border-color: #e08a00; }
|
||||
.note { background: #eaf2fb; border-color: #3b82c4; }
|
||||
.tip::before, .warn::before, .note::before { font-weight: 700; display: block; margin-bottom: .5mm; }
|
||||
.tip::before { content: "Tipp"; color: #1f7a50; }
|
||||
.warn::before { content: "Achtung"; color: #b36b00; }
|
||||
.note::before { content: "Hinweis"; color: #2a6aa3; }
|
||||
.steps { counter-reset: s; list-style: none; padding-left: 0; }
|
||||
.steps li { counter-increment: s; padding-left: 8mm; position: relative; margin-bottom: 1.6mm; }
|
||||
.steps li::before { content: counter(s); position: absolute; left: 0; top: 0; width: 5.4mm; height: 5.4mm; border-radius: 50%; background: var(--teal); color: #fff; font-size: 8pt; text-align: center; line-height: 5.4mm; font-weight: 600; }
|
||||
/* Inhaltsverzeichnis */
|
||||
.toc { page-break-after: always; }
|
||||
.toc a { display: flex; align-items: baseline; color: inherit; text-decoration: none; }
|
||||
.toc a.t1 { font-weight: 600; margin-top: 2.2mm; color: var(--teal-d); font-size: 10.5pt; }
|
||||
.toc a.t2 { padding-left: 8mm; font-size: 9pt; line-height: 1.35; }
|
||||
.toc .tn { width: 9mm; flex: none; }
|
||||
.toc a.t2 .tn { width: 10mm; color: var(--mute); }
|
||||
.toc .tt { flex: none; max-width: 140mm; }
|
||||
.toc .tl { flex: 1; border-bottom: 1px dotted #aab8b6; margin: 0 1.5mm; position: relative; top: -1mm; }
|
||||
.toc .tp { flex: none; min-width: 7mm; text-align: right; }
|
||||
.facts td:first-child { width: 42mm; font-weight: 600; }
|
||||
.small { font-size: 8.5pt; color: var(--mute); }
|
||||
.cols2 { column-count: 2; column-gap: 8mm; }
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
/**
|
||||
* tools/manual/install_plugins.php: installiert und aktiviert die mitgelieferten Plugins in der Demo-Installation (für die Bilder im Handbuch).
|
||||
* php tools/manual/install_plugins.php <Demo-Ordner> <Ordner mit den Plugin-ZIPs>
|
||||
*/
|
||||
$tmp = rtrim(str_replace('\\', '/', $argv[1] ?? ''), '/');
|
||||
$zips = rtrim(str_replace('\\', '/', $argv[2] ?? ''), '/');
|
||||
if (!is_file("$tmp/includes/bootstrap.php") || !is_dir($zips)) { fwrite(STDERR, "Aufruf: install_plugins.php <Demo-Ordner> <Plugin-ZIPs>\n"); exit(1); }
|
||||
putenv('DESKHUB_CATALOG_DEFAULTS=' . dirname(__DIR__, 2) . '/tests/fixtures/no-catalog.php');
|
||||
chdir($tmp);
|
||||
date_default_timezone_set('Europe/Berlin');
|
||||
require_once "$tmp/includes/bootstrap.php";
|
||||
$pdo = get_db();
|
||||
$want = ['tagesbericht', 'schichtuebergabe', 'rundmail', 'spamfilter', 'virenscan', 'ueberwachung', 'datenschutz', 'vertretung', 'community-bot', 'authentik', 'nas-backup', 'virustotal', 'cache-manager', 'code-issue-sync', 'cloudflare-protection', 'lang-en'];
|
||||
foreach ($want as $slug) {
|
||||
$best = null;
|
||||
foreach (glob("$zips/{$slug}-*.zip") ?: [] as $z) if ($best === null || version_compare(preg_replace('/^.*-(\d+\.\d+\.\d+)\.zip$/', '$1', basename($z)), preg_replace('/^.*-(\d+\.\d+\.\d+)\.zip$/', '$1', basename($best))) > 0) $best = $z;
|
||||
if (!$best) { echo "übersprungen (kein ZIP): $slug\n"; continue; }
|
||||
$r = plugin_install_zip($pdo, $best);
|
||||
if (empty($r['ok'])) { echo "FEHLER $slug: " . json_encode($r, JSON_UNESCAPED_UNICODE) . "\n"; continue; }
|
||||
$zip = new ZipArchive();
|
||||
$zip->open($best);
|
||||
$perms = [];
|
||||
for ($i = 0; $i < $zip->numFiles; $i++) if (preg_match('#(^|/)plugin\.json$#', $zip->getNameIndex($i))) { $perms = (json_decode($zip->getFromIndex($i), true)['permissions'] ?? []); break; }
|
||||
$a = plugin_set_active($pdo, $r['slug'], true, $perms);
|
||||
echo ($a['ok'] ? 'aktiv ' : 'inaktiv ') . $r['slug'] . (empty($a['ok']) ? ' – ' . json_encode($a, JSON_UNESCAPED_UNICODE) : '') . "\n";
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
'use strict';
|
||||
/**
|
||||
* tools/manual/print_pdf.js: wandelt handbuch.html in eine A4-PDF um (mit Kopf-/Fußzeile, Lesezeichen aus den Überschriften).
|
||||
* electron tools/manual/print_pdf.js <handbuch.html> <ausgabe.pdf> [cover.html cover.pdf]
|
||||
*/
|
||||
const { app, BrowserWindow } = require('electron');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const src = path.resolve(process.argv[2]);
|
||||
const dst = path.resolve(process.argv[3]);
|
||||
|
||||
app.whenReady().then(async () => {
|
||||
const win = new BrowserWindow({ show: false, width: 1000, height: 1400, webPreferences: { offscreen: false } });
|
||||
await win.loadFile(src);
|
||||
await new Promise((r) => setTimeout(r, 1500));
|
||||
const pdf = await win.webContents.printToPDF({
|
||||
pageSize: 'A4',
|
||||
printBackground: true,
|
||||
preferCSSPageSize: true,
|
||||
displayHeaderFooter: true,
|
||||
headerTemplate: '<div></div>',
|
||||
footerTemplate: '<div style="width:100%;font-size:8px;color:#6b7280;font-family:Segoe UI,Arial,sans-serif;padding:0 18mm;display:flex;justify-content:space-between"><span>DeskHub Handbuch · alle Beispieldaten sind erfunden</span><span>Seite <span class="pageNumber"></span> von <span class="totalPages"></span></span></div>',
|
||||
generateDocumentOutline: true,
|
||||
generateTaggedPDF: true,
|
||||
});
|
||||
fs.writeFileSync(dst, pdf);
|
||||
if (process.argv[4]) {
|
||||
const w2 = new BrowserWindow({ show: false, width: 900, height: 1200 });
|
||||
await w2.loadFile(path.resolve(process.argv[4]));
|
||||
await new Promise((r) => setTimeout(r, 500));
|
||||
fs.writeFileSync(path.resolve(process.argv[5]), await w2.webContents.printToPDF({ pageSize: 'A4', printBackground: true, preferCSSPageSize: true }));
|
||||
}
|
||||
console.log('PDF geschrieben: ' + dst + ' (' + Math.round(pdf.length / 1024) + ' KB)');
|
||||
app.quit();
|
||||
});
|
||||
@@ -0,0 +1,260 @@
|
||||
<?php
|
||||
/**
|
||||
* tools/manual/seed_manual.php
|
||||
*
|
||||
* Ergänzt die Demo-Installation von tools/demo-video/seed_demo.php um viele weitere erfundene Daten (Abteilungen, Themen, Vorlagen, Regeln,
|
||||
* Berichte, Kunden, Tickets, Plugins …), damit die Bilder im Handbuch jede Seite gefüllt zeigen. Alles ist erfunden: Namen, Firmen,
|
||||
* Adressen (example.org, demo.example), keine echten Daten.
|
||||
* php tools/manual/seed_manual.php <Demo-Ordner>
|
||||
* Zugang danach: lena@demo.example / Demo-Passwort-1234 (Administratorin), kunde: erika.muster@kunde.example / Demo-Passwort-1234
|
||||
*/
|
||||
$tmp = rtrim(str_replace('\\', '/', $argv[1] ?? ''), '/');
|
||||
if ($tmp === '' || !is_file("$tmp/database/deskhub.sqlite")) { fwrite(STDERR, "Demo-Ordner fehlt. Erst tools/demo-video/seed_demo.php ausführen.\n"); exit(1); }
|
||||
date_default_timezone_set('Europe/Berlin');
|
||||
$p = new PDO("sqlite:$tmp/database/deskhub.sqlite");
|
||||
$p->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
||||
$p->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);
|
||||
$ts = fn(string $rel) => date('Y-m-d H:i:s', strtotime($rel));
|
||||
$warn = [];
|
||||
$try = function (string $what, callable $fn) use (&$warn) { try { $fn(); } catch (Throwable $e) { $warn[] = $what . ': ' . $e->getMessage(); } };
|
||||
$one = fn(string $sql, array $args = []) => (function () use ($p, $sql, $args) { $s = $p->prepare($sql); $s->execute($args); return $s->fetchColumn(); })();
|
||||
$set = $p->prepare('INSERT OR REPLACE INTO settings (setting_key, setting_value) VALUES (?, ?)');
|
||||
$id = fn(string $table, string $col, string $val) => (int) $one("SELECT id FROM {$table} WHERE {$col} = ?", [$val]);
|
||||
$H = password_hash('Demo-Passwort-1234', PASSWORD_DEFAULT);
|
||||
|
||||
// ── Abteilungen, Prioritäten, SLA ───────────────────────────────────────────
|
||||
$try('Abteilungen', function () use ($p, $ts) {
|
||||
$p->exec("UPDATE departments SET email = 'support@demo.example', notes = 'Erste Anlaufstelle für alle Anfragen' WHERE name = 'Allgemeiner Support'");
|
||||
$p->exec("UPDATE departments SET email = 'buchhaltung@demo.example', notes = 'Rechnungen, Zahlungen, Mahnungen' WHERE name = 'Buchhaltung'");
|
||||
$p->exec("UPDATE departments SET email = 'technik@demo.example', notes = 'Störungen und technische Fragen' WHERE name = 'Technik'");
|
||||
});
|
||||
$try('SLA-Pläne', function () use ($p, $ts, $id) {
|
||||
$p->exec("INSERT INTO sla_plans (name, response_hours, resolution_hours, is_active, notes, created_at, updated_at) VALUES ('Standard', 8, 48, 1, 'Für normale Anfragen', '{$ts('-60 days')}', '{$ts('-60 days')}'), ('Premium', 1, 8, 1, 'Für Kunden mit Servicevertrag', '{$ts('-60 days')}', '{$ts('-10 days')}')");
|
||||
$p->exec("UPDATE departments SET sla_plan_id = " . $id('sla_plans', 'name', 'Standard') . " WHERE name = 'Allgemeiner Support'");
|
||||
});
|
||||
$dept = fn(string $n) => $id('departments', 'name', $n);
|
||||
$prio = fn(string $n) => $id('priorities', 'name', $n);
|
||||
$staffId = fn(string $n) => $id('staff', 'name', $n);
|
||||
$lena = $staffId('Lena Hartmann');
|
||||
$tim = $staffId('Tim Roth');
|
||||
|
||||
// ── Hilfethemen, Felder, Listen ─────────────────────────────────────────────
|
||||
$try('Hilfethemen', function () use ($p, $ts, $dept, $prio) {
|
||||
$ins = $p->prepare('INSERT INTO help_topics (name, department_id, priority_id, is_active, sort_order, is_public, notes, created_at, updated_at) VALUES (?,?,?,1,?,1,?,?,?)');
|
||||
foreach ([['Rechnung und Zahlung', 'Buchhaltung', 'Hoch', 1, 'Fragen zu Rechnungen und Zahlungen'], ['Technische Störung', 'Technik', 'Dringend', 2, 'Etwas funktioniert nicht'], ['Allgemeine Frage', 'Allgemeiner Support', 'Normal', 3, 'Alles andere'], ['Kündigung und Vertrag', 'Allgemeiner Support', 'Normal', 4, 'Verträge ändern oder beenden']] as [$n, $d, $pr, $so, $no])
|
||||
$ins->execute([$n, $dept($d), $prio($pr), $so, $no, $ts('-60 days'), $ts('-60 days')]);
|
||||
});
|
||||
$try('Listen und Felder', function () use ($p, $ts, $id) {
|
||||
$p->exec("INSERT INTO custom_lists (name, sort_order, created_at) VALUES ('Betriebssystem', 1, '{$ts('-50 days')}')");
|
||||
$lid = (int) $p->lastInsertId();
|
||||
foreach (['Windows', 'macOS', 'Linux', 'Android', 'iOS'] as $i => $v) $p->prepare('INSERT INTO custom_list_items (list_id, value, sort_order) VALUES (?,?,?)')->execute([$lid, $v, $i]);
|
||||
$ht = $id('help_topics', 'name', 'Rechnung und Zahlung');
|
||||
$ht2 = $id('help_topics', 'name', 'Technische Störung');
|
||||
$p->prepare("INSERT INTO custom_fields (label, field_type, is_required, is_active, sort_order, created_at, help_topic_id, help_text) VALUES ('Rechnungsnummer', 'text', 1, 1, 1, ?, ?, 'Zu finden oben auf der Rechnung')")->execute([$ts('-50 days'), $ht]);
|
||||
$p->prepare("INSERT INTO custom_fields (label, field_type, list_id, is_required, is_active, sort_order, created_at, help_topic_id) VALUES ('Betriebssystem', 'list', ?, 0, 1, 2, ?, ?)")->execute([$lid, $ts('-50 days'), $ht2]);
|
||||
});
|
||||
|
||||
// ── Vorlagen, Teams, Rollen ─────────────────────────────────────────────────
|
||||
$try('Vorlagen', function () use ($p, $ts, $dept) {
|
||||
$ins = $p->prepare('INSERT INTO canned_responses (title, body, department_id, created_at) VALUES (?,?,?,?)');
|
||||
foreach ([
|
||||
['Danke für deine Nachricht', '<p>Hallo {{kundenname}},</p><p>vielen Dank für deine Nachricht. Wir kümmern uns darum und melden uns schnellstmöglich.</p><p>{{signatur}}</p>', null],
|
||||
['Rechnung erneut senden', '<p>Hallo {{kundenname}},</p><p>wir haben dir die Rechnung noch einmal als PDF angehängt. Sie steht außerdem in deinem Kundenbereich unter „Rechnungen“.</p>', 'Buchhaltung'],
|
||||
['Rückfrage zur Störung', '<p>Hallo {{kundenname}},</p><p>damit wir die Störung nachstellen können: Welches Gerät und welche Version nutzt du? Hilft ein Neustart?</p>', 'Technik'],
|
||||
['Ticket gelöst', '<p>Hallo {{kundenname}},</p><p>wir haben dein Anliegen gelöst. Falls noch etwas offen ist, antworte einfach auf diese Nachricht.</p>', null],
|
||||
['Kündigung bestätigt', '<p>Hallo {{kundenname}},</p><p>wir bestätigen deine Kündigung zum Ende der Laufzeit. Schade, dass du gehst!</p>', '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'), '<p>Hallo Frau Klein, kurze Nachfrage: Konnten Sie die Adresse inzwischen ändern?</p>', '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', '<p>Text</p>', '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', '<p><b>Muster Support GmbH</b><br>Beispielstraße 1<br>12345 Beispielstadt</p><p>Alle Angaben sind erfunden.</p>', 1, 1, $ts('-60 days')]);
|
||||
$ins->execute(['datenschutz', 'Datenschutz', '<p>Hier steht in einer echten Installation eure Datenschutzerklärung.</p>', 1, 2, $ts('-60 days')]);
|
||||
$ins->execute(['agb', 'Allgemeine Geschäftsbedingungen', '<p>Beispieltext.</p>', 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";
|
||||
Reference in New Issue
Block a user