Das Helpdesk-System zum Selbsthosten
Demo mit erfundenen Beispieldaten ${VERSION ? '· Version ' + VERSION : ''}
`], + ['40_android', `Installationsanleitung und Funktionen: siehe README
`], + ]; + for (const [name, body] of cards) { + await win.loadURL(page(body)); + await sleep(500); + fs.writeFileSync(path.join(OUT, name + '.png'), (await win.webContents.capturePage()).toPNG()); + } + console.log(`${cards.length} Titelbilder in ${OUT}`); + app.quit(); +}); diff --git a/DeskHub-Webseite/tools/demo-video/seed_demo.php b/DeskHub-Webseite/tools/demo-video/seed_demo.php new file mode 100644 index 00000000..9da7a43c --- /dev/null +++ b/DeskHub-Webseite/tools/demo-video/seed_demo.php @@ -0,0 +1,120 @@ + (Zugang: lena@demo.example / Demo-Passwort-1234) + */ +$root = dirname(__DIR__, 2); +$tmp = $argv[1] ?? (sys_get_temp_dir() . '/deskhub-demo'); +function rrm(string $d): void { if (!is_dir($d)) return; foreach (scandir($d) as $e) { if ($e === '.' || $e === '..') continue; is_dir("$d/$e") ? rrm("$d/$e") : @unlink("$d/$e"); } @rmdir($d); } +rrm($tmp); +mkdir($tmp, 0755, true); +$skip = '#^(config/config\.php|database/[^/]+\.sqlite[^/]*|backups|uploads|tests|tools|plugins/[^/]+|docs|\.git)(/|$)#'; +foreach (new RecursiveIteratorIterator(new RecursiveDirectoryIterator($root, FilesystemIterator::SKIP_DOTS), RecursiveIteratorIterator::SELF_FIRST) as $f) { + $rel = str_replace('\\', '/', substr($f->getPathname(), strlen($root) + 1)); + if (preg_match($skip, $rel) && !preg_match('#^(backups|uploads|plugins)/(\.htaccess|README\.md)$#', $rel)) continue; + if ($f->isDir()) { @mkdir("$tmp/$rel", 0755, true); continue; } + @mkdir(dirname("$tmp/$rel"), 0755, true); + copy($f->getPathname(), "$tmp/$rel"); +} +@mkdir("$tmp/uploads"); @mkdir("$tmp/config"); +file_put_contents("$tmp/config/config.php", ' bin2hex(random_bytes(32)), + 'db' => ['driver' => 'sqlite', 'sqlite_path' => "$tmp/database/deskhub.sqlite"], + 'site' => ['name' => 'Muster Support', 'url' => 'http://127.0.0.1:8097', 'admin_email' => 'lena@demo.example', 'timezone' => 'Europe/Berlin'], + 'uploads' => ['max_size_bytes' => 10485760, 'allowed_extensions' => ['png'], 'path' => "$tmp/uploads"], +], true) . ';'); +putenv('DESKHUB_CONFIG_JSON'); putenv('DESKHUB_SQLITE_PATH'); +require_once "$root/database/sqlite_schema.php"; +require_once "$root/database/migrate.php"; +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); +$p->exec('PRAGMA foreign_keys = ON'); +$p->exec(convert_schema_to_sqlite(file_get_contents("$root/database/schema.sql"))); +run_migrations($p, true); + +$ts = fn(string $rel) => date('Y-m-d H:i:s', strtotime($rel)); +$dept = fn(string $n) => (int) $p->query("SELECT id FROM departments WHERE name = " . $p->quote($n))->fetchColumn(); +$prio = fn(string $n) => (int) $p->query("SELECT id FROM priorities WHERE name = " . $p->quote($n))->fetchColumn(); + +// Mitarbeiter +$p->exec('DELETE FROM staff'); +$ins = $p->prepare('INSERT INTO staff (name, email, password_hash, role, department_id, is_active, created_at) VALUES (?,?,?,?,?,1,?)'); +$staff = []; +foreach ([['Lena Hartmann', 'lena@demo.example', 'admin', 'Allgemeiner Support'], ['Tim Roth', 'tim@demo.example', 'agent', 'Allgemeiner Support'], ['Mira Schulz', 'mira@demo.example', 'agent', 'Technik']] as [$n, $e, $r, $d]) { + $ins->execute([$n, $e, password_hash('Demo-Passwort-1234', PASSWORD_DEFAULT), $r, $dept($d), $ts('-30 days')]); + $staff[$n] = (int) $p->lastInsertId(); +} +$av = $p->prepare("INSERT OR REPLACE INTO staff_availability (staff_id, status, updated_at) VALUES (?,?,?)"); +$av->execute([$staff['Lena Hartmann'], 'available', $ts('now')]); +$av->execute([$staff['Tim Roth'], 'busy', $ts('now')]); +$av->execute([$staff['Mira Schulz'], 'break', $ts('now')]); + +// Einstellungen +$set = $p->prepare('INSERT OR REPLACE INTO settings (setting_key, setting_value) VALUES (?, ?)'); +foreach (['assistant_enabled' => '1', 'live_chat_enabled' => '0', 'kb_enabled' => '1', 'display_name' => 'Muster Support', 'assistant_name' => 'Mia, die digitale Assistentin', + 'assistant_greeting' => 'Hallo! Ich bin Mia. Ich beantworte Fragen zu Lieferung, Rechnung und Konto oder erstelle ein Ticket für dich.', 'assistant_department_id' => (string) $dept('Allgemeiner Support'), + 'assistant_priority_id' => (string) $prio('Normal'), 'assistant_synonyms' => "Rechnung, Beleg, Quittung\nKündigung, kündigen, Vertrag beenden", + 'remind_enabled' => '1', 'remind_after_days' => '3', 'auto_close_enabled' => '1', 'auto_close_days' => '7'] as $k => $v) $set->execute([$k, $v]); + +// Wissensdatenbank +$p->exec("DELETE FROM kb_articles"); $p->exec("DELETE FROM kb_categories"); +$p->exec("INSERT INTO kb_categories (name, slug, sort_order) VALUES ('Konto', 'konto', 1), ('Bestellung und Versand', 'versand', 2), ('Rechnungen', 'rechnungen', 3)"); +$cat = fn(string $s) => (int) $p->query("SELECT id FROM kb_categories WHERE slug = " . $p->quote($s))->fetchColumn(); +$art = $p->prepare('INSERT INTO kb_articles (category_id, title, slug, body, is_published, views, created_at, updated_at) VALUES (?,?,?,?,1,?,?,?)'); +foreach ([ + ['konto', 'Passwort zurücksetzen', 'passwort-zuruecksetzen', 'Klicke auf der Anmeldeseite auf Passwort vergessen. Wir senden dir einen Link per E-Mail, mit dem du ein neues Passwort festlegst. Der Link gilt eine Stunde.
', 128], + ['konto', 'E-Mail-Adresse ändern', 'email-aendern', 'Unter Mein Profil kannst du deine E-Mail-Adresse ändern. Zur Sicherheit bestätigst du die neue Adresse per Link.
', 41], + ['versand', 'Wie lange dauert der Versand?', 'versandzeiten', 'Bestellungen kommen in der Regel in 2 bis 3 Werktagen an. Bei Auslandsversand dauert es bis zu 7 Werktage.
', 203], + ['versand', 'Meine Sendung ist nicht angekommen', 'sendung-fehlt', 'Prüfe zuerst die Sendungsverfolgung. Kommt das Paket nach 5 Werktagen nicht an, erstelle bitte ein Ticket mit der Bestellnummer.
', 77], + ['rechnungen', 'Rechnungen herunterladen', 'rechnungen', 'Alle Rechnungen findest du im Kundenbereich unter Rechnungen. Dort lädst du sie als PDF herunter.
', 156], +] as [$c, $t, $s, $b, $v]) $art->execute([$cat($c), $t, $s, $b, $v, $ts('-20 days'), $ts('-5 days')]); + +// zusätzliche Antworten des Assistenten +$ans = $p->prepare('INSERT INTO assistant_answers (title, keywords, answer, is_active, hits, created_at, updated_at) VALUES (?,?,?,?,?,?,?)'); +foreach ([ + ['Wie sind eure Öffnungszeiten?', 'geöffnet, Uhrzeit, erreichbar', "Wir sind Montag bis Freitag von 9 bis 17 Uhr für dich da.\nAußerhalb dieser Zeit beantwortet dich Mia oder du erstellst ein Ticket.", 1, 34], + ['Kann ich eine Bestellung stornieren?', 'Storno, abbrechen, stornieren', 'Solange die Bestellung noch nicht versendet ist, kannst du sie im Kundenbereich unter „Bestellungen“ stornieren.', 1, 19], + ['Wo finde ich meine Rechnung?', 'Beleg, Quittung', 'Deine Rechnungen findest du im Kundenbereich unter „Rechnungen“ als PDF.', 1, 52], +] as $a) $ans->execute([$a[0], $a[1], $a[2], $a[3], $a[4], $ts('-15 days'), $ts('-2 days')]); + +// Fragen-Log +$lg = $p->prepare('INSERT INTO assistant_log (question, result, source, matched_title, ticket_number, helpful, created_at) VALUES (?,?,?,?,?,?,?)'); +foreach ([ + ['Wie lange dauert der Versand?', 'kb', 'kb', 'Wie lange dauert der Versand?', null, 1, '-2 hours'], ['Wo finde ich meine Rechnung?', 'custom', 'custom', 'Wo finde ich meine Rechnung?', null, 1, '-3 hours'], + ['Wann seid ihr erreichbar?', 'custom', 'custom', 'Wie sind eure Öffnungszeiten?', null, 1, '-5 hours'], ['Passwort vergessen was tun', 'kb', 'kb', 'Passwort zurücksetzen', null, null, '-1 day'], + ['Wie kann ich meinen Vertrag kündigen?', 'none', null, null, null, null, '-1 day'], ['Wie kann ich meinen Vertrag kündigen?', 'none', null, null, null, null, '-2 days'], + ['Gibt es Rabatt für Schulen?', 'none', null, null, null, null, '-2 days'], ['Mein Paket ist beschädigt', 'kb', 'kb', 'Meine Sendung ist nicht angekommen', null, 0, '-3 days'], + ['Ich brauche Hilfe von einem Menschen', 'ticket_request', null, null, null, null, '-3 days'], ['Mein Paket ist beschädigt', 'ticket_created', null, null, '260001', null, '-3 days'], +] as [$q, $r, $s, $m, $tn, $h, $when]) $lg->execute([$q, $r, $s, $m, $tn, $h, $ts($when)]); + +// Tickets +$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, snoozed_until, snoozed_by) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)'); +$th = $p->prepare('INSERT INTO ticket_threads (ticket_id, author_type, staff_id, message, is_internal_note, created_at) VALUES (?,?,?,?,?,?)'); +$defs = [ + ['260101', 'Rechnung für März fehlt', 'Erika Muster', 'Buchhaltung', 'Hoch', 'open', 'Lena Hartmann', '-3 hours', '+5 hours', [['customer', null, 'Guten Tag, in meinem Kundenbereich fehlt die Rechnung für März. Können Sie mir diese bitte zuschicken? Danke!', 0, '-3 hours'], ['staff', 'Lena Hartmann', 'Hallo Frau Muster, ich schaue gleich nach und melde mich noch heute bei Ihnen.', 0, '-2 hours'], ['staff', 'Lena Hartmann', 'Intern: Rechnung wurde nicht automatisch erzeugt, bitte Buchhaltung prüfen lassen.', 1, '-90 minutes']]], + ['260102', 'Passwort zurücksetzen funktioniert nicht', 'Jonas Weber', 'Allgemeiner Support', 'Dringend', 'open', 'Tim Roth', '-26 hours', '-2 hours', [['customer', null, 'Der Link zum Zurücksetzen meines Passworts kommt nie an. Ich habe schon dreimal gedrückt.', 0, '-26 hours']]], + ['260103', 'Lieferstatus meiner Bestellung', 'Sophie Klein', 'Allgemeiner Support', 'Normal', 'answered', 'Tim Roth', '-30 hours', '+20 hours', [['customer', null, 'Wo ist meine Bestellung 48211? Sie sollte gestern kommen.', 0, '-30 hours'], ['staff', 'Tim Roth', 'Hallo Sophie, Ihre Bestellung ist beim Zusteller und kommt morgen. Hier die Sendungsnummer: 0034 5521 8890.', 0, '-28 hours']]], + ['260104', 'Frage zur Kündigung', 'Markus Lehmann', 'Allgemeiner Support', 'Normal', 'open', null, '-45 minutes', '+23 hours', [['customer', null, 'Wie kann ich meinen Vertrag zum Monatsende kündigen?', 0, '-45 minutes']]], + ['260105', 'App stürzt beim Start ab', 'Lea Wagner', 'Technik', 'Hoch', 'open', 'Mira Schulz', '-5 hours', '+3 hours', [['customer', null, 'Seit dem letzten Update stürzt die App auf meinem Handy direkt nach dem Start ab (Android 14).', 0, '-5 hours'], ['staff', 'Mira Schulz', 'Danke für den Hinweis. Wir können das reproduzieren und arbeiten an einem Fix.', 0, '-4 hours']]], + ['260106', 'Rückruf gewünscht', 'Dirk Neumann', 'Allgemeiner Support', 'Niedrig', 'open', 'Lena Hartmann', '-2 days', '+1 day', [['customer', null, 'Bitte rufen Sie mich nächste Woche wegen meines Angebots zurück.', 0, '-2 days']], '+1 day 08:00'], + ['259901', 'Passwort vergessen: Link kommt nicht an', 'Nina Fischer', 'Allgemeiner Support', 'Normal', 'resolved', 'Tim Roth', '-12 days', null, [['customer', null, 'Ich habe mein Passwort vergessen und bekomme keine E-Mail.', 0, '-12 days'], ['staff', 'Tim Roth', 'Der Link war im Spam-Ordner. Ich habe ihn erneut gesendet, jetzt klappt es.', 0, '-12 days']]], + ['259902', 'Rechnung als PDF anfordern', 'Paul Schmitt', 'Buchhaltung', 'Normal', 'closed', 'Lena Hartmann', '-20 days', null, [['customer', null, 'Bitte schicken Sie mir die Rechnung Nr. 5521 als PDF.', 0, '-20 days'], ['staff', 'Lena Hartmann', 'Die Rechnung ist angehängt. Viele Grüße', 0, '-20 days']]], + ['259903', 'Paket beschädigt angekommen', 'Clara Braun', 'Allgemeiner Support', 'Hoch', 'closed', 'Tim Roth', '-25 days', null, [['customer', null, 'Der Karton war aufgerissen.', 0, '-25 days'], ['staff', 'Tim Roth', 'Das tut uns leid, Ersatz ist unterwegs.', 0, '-24 days']]], + ['259904', 'Kündigung zum Jahresende', 'Felix Vogel', 'Allgemeiner Support', 'Normal', 'closed', 'Lena Hartmann', '-40 days', null, [['customer', null, 'Ich möchte kündigen.', 0, '-40 days'], ['staff', 'Lena Hartmann', 'Kündigung bestätigt.', 0, '-39 days']]], +]; +foreach ($defs as $d) { + [$no, $subject, $name, $dn, $pn, $status, $assignee, $created, $due, $threads] = $d; + $snooze = $d[10] ?? null; + $tk->execute([$no, $subject, $name, strtolower(str_replace(' ', '.', $name)) . '@kunde.example', $dept($dn), $prio($pn), $status, 'web', $assignee ? $staff[$assignee] : null, + $ts($created), $ts($threads[count($threads) - 1][4]), $due ? $ts($due) : null, $snooze ? $ts($snooze) : null, $snooze ? $staff['Lena Hartmann'] : null]); + $id = (int) $p->lastInsertId(); + foreach ($threads as [$who, $sn, $msg, $note, $when]) $th->execute([$id, $who, $sn ? $staff[$sn] : null, $msg, $note, $ts($when)]); + if ($snooze) $p->prepare('INSERT INTO ticket_followers (ticket_id, staff_id, created_at) VALUES (?,?,?)')->execute([$id, $staff['Lena Hartmann'], $ts('-1 day')]); +} +$p->prepare('INSERT INTO ticket_followers (ticket_id, staff_id, created_at) VALUES ((SELECT id FROM tickets WHERE ticket_number = ?),?,?)')->execute(['260102', $staff['Lena Hartmann'], $ts('-20 hours')]); +echo "Demo bereit: $tmp\n";