From 12699f0918352d1c3d55b4b0ae1265d1806c63a7 Mon Sep 17 00:00:00 2001 From: M_Viper Date: Mon, 26 Oct 2026 21:19:21 +0000 Subject: [PATCH] Upload via GUI (340 Dateien) --- DeskHub-Webseite/kb.php | 41 ++++++++++++------ DeskHub-Webseite/kb_article.php | 19 +++++--- DeskHub-Webseite/status.php | 48 +++++++++++++++++++-- DeskHub-Webseite/ticket_view.php | 3 +- DeskHub-Webseite/tools/demo-video/record.js | 30 +++++++++++++ 5 files changed, 117 insertions(+), 24 deletions(-) diff --git a/DeskHub-Webseite/kb.php b/DeskHub-Webseite/kb.php index 3186233c..05871640 100644 --- a/DeskHub-Webseite/kb.php +++ b/DeskHub-Webseite/kb.php @@ -10,19 +10,35 @@ $pdo = get_db(); $q = trim($_GET['q'] ?? ''); if ($q !== '') { - $stmt = $pdo->prepare( - "SELECT a.*, c.name AS category_name FROM kb_articles a - JOIN kb_categories c ON c.id = a.category_id - WHERE a.is_published = 1 AND (a.title LIKE ? OR a.body LIKE ?) - ORDER BY a.views DESC" - ); - $like = '%' . $q . '%'; - $stmt->execute([$like, $like]); - $articles = $stmt->fetchAll(); + $loadSearch = static function () use ($pdo, $q): array { + $stmt = $pdo->prepare( + "SELECT a.*, c.name AS category_name FROM kb_articles a + JOIN kb_categories c ON c.id = a.category_id + WHERE a.is_published = 1 AND (a.title LIKE ? OR a.body LIKE ?) + ORDER BY a.views DESC" + ); + $like = '%' . $q . '%'; + $stmt->execute([$like, $like]); + return $stmt->fetchAll(); + }; + $articles = function_exists('dhcm_cache_remember') + ? dhcm_cache_remember('kb.search.' . $q, $loadSearch) + : $loadSearch(); $categories = []; } else { - $categories = $pdo->query('SELECT * FROM kb_categories ORDER BY sort_order, name')->fetchAll(); - $artStmt = $pdo->prepare('SELECT * FROM kb_articles WHERE category_id = ? AND is_published = 1 ORDER BY title'); + $loadIndex = static function () use ($pdo): array { + $categories = $pdo->query('SELECT * FROM kb_categories ORDER BY sort_order, name')->fetchAll(); + $articlesByCategory = []; + foreach ($pdo->query('SELECT * FROM kb_articles WHERE is_published = 1 ORDER BY title')->fetchAll() as $article) { + $articlesByCategory[(int) $article['category_id']][] = $article; + } + return ['categories' => $categories, 'articles' => $articlesByCategory]; + }; + $kbIndex = function_exists('dhcm_cache_remember') + ? dhcm_cache_remember('kb.index', $loadIndex) + : $loadIndex(); + $categories = $kbIndex['categories'] ?? []; + $articlesByCategory = $kbIndex['articles'] ?? []; $articles = null; } @@ -54,8 +70,7 @@ require __DIR__ . '/includes/header_customer.php';
$cat): ?> execute([$cat['id']]); - $arts = $artStmt->fetchAll(); + $arts = $articlesByCategory[(int) $cat['id']] ?? []; if (!$arts) continue; $anyArticles = true; ?> diff --git a/DeskHub-Webseite/kb_article.php b/DeskHub-Webseite/kb_article.php index 7d7cf326..ed30f1d8 100644 --- a/DeskHub-Webseite/kb_article.php +++ b/DeskHub-Webseite/kb_article.php @@ -9,13 +9,18 @@ if (get_setting('kb_enabled', '1') !== '1') { $pdo = get_db(); $slug = $_GET['slug'] ?? ''; -$stmt = $pdo->prepare( - 'SELECT a.*, c.name AS category_name FROM kb_articles a - JOIN kb_categories c ON c.id = a.category_id - WHERE a.slug = ? AND a.is_published = 1' -); -$stmt->execute([$slug]); -$article = $stmt->fetch(); +$loadArticle = static function () use ($pdo, $slug): ?array { + $stmt = $pdo->prepare( + 'SELECT a.*, c.name AS category_name FROM kb_articles a + JOIN kb_categories c ON c.id = a.category_id + WHERE a.slug = ? AND a.is_published = 1' + ); + $stmt->execute([$slug]); + return $stmt->fetch() ?: null; +}; +$article = function_exists('dhcm_cache_remember') + ? dhcm_cache_remember('kb.article.' . $slug, $loadArticle) + : $loadArticle(); if (!$article) { flash('error', 'Dieser Artikel wurde nicht gefunden.'); diff --git a/DeskHub-Webseite/status.php b/DeskHub-Webseite/status.php index 88561b44..7823f491 100644 --- a/DeskHub-Webseite/status.php +++ b/DeskHub-Webseite/status.php @@ -24,6 +24,8 @@ foreach ($components as &$component) { } unset($component); $incidents = status_incidents($pdo, 14); +$maintenanceWindows = status_maintenance_windows($pdo); +$activeMaintenance = status_active_maintenance_map($pdo); $open = array_values(array_filter($incidents, fn($i) => $i['status'] !== 'resolved')); $past = array_values(array_filter($incidents, fn($i) => $i['status'] === 'resolved')); $incStates = status_incident_states(); @@ -33,10 +35,12 @@ if (($_GET['live'] ?? '') === '1') { header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0'); $liveComponents = []; foreach ($components as $component) { - $componentState = $states[$component['status']] ?? $states['operational']; + $componentStatus = (string) $component['status']; + if (($componentStatus === 'operational') && (isset($activeMaintenance[0]) || isset($activeMaintenance[(int) $component['id']]))) $componentStatus = 'maintenance'; + $componentState = $states[$componentStatus] ?? $states['operational']; $liveComponents[] = [ 'id' => (int) $component['id'], - 'status' => (string) $component['status'], + 'status' => $componentStatus, 'statusLabel' => (string) $componentState[0], 'color' => (string) $componentState[1], 'uptime' => $component['timeline']['uptime'], @@ -64,11 +68,21 @@ if (($_GET['live'] ?? '') === '1') { ], $incident['updates']), ]; } + $liveMaintenance = []; + foreach ($maintenanceWindows as $window) { + $liveMaintenance[] = [ + 'title' => (string) $window['title'], 'body' => (string) ($window['body'] ?? ''), + 'componentName' => (string) ($window['component_name'] ?? ''), + 'startsAt' => format_dt((string) $window['starts_at']), 'endsAt' => format_dt((string) $window['ends_at']), + 'active' => strtotime((string) $window['starts_at']) <= time() && strtotime((string) $window['ends_at']) >= time(), + ]; + } echo json_encode([ 'updatedAt' => date('d.m.Y H:i:s'), 'overall' => $overall, 'components' => $liveComponents, 'incidents' => $liveIncidents, + 'maintenanceWindows' => $liveMaintenance, ], JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT); exit; } @@ -83,6 +97,17 @@ require __DIR__ . '/includes/header_customer.php';
Stand: Uhr · Live-Aktualisierung aktiv
+
+ = time(); ?> +
+ : +
bis
+
Betrifft:
Betrifft: gesamte Plattform
+
+
+ +
+
@@ -104,7 +129,7 @@ require __DIR__ . '/includes/header_customer.php'; $group): ?>

- $c): $st = $states[$c['status']] ?? $states['operational']; ?> + $c): $componentStatus = (string) $c['status']; if ($componentStatus === 'operational' && (isset($activeMaintenance[0]) || isset($activeMaintenance[(int) $c['id']]))) $componentStatus = 'maintenance'; $st = $states[$componentStatus] ?? $states['operational']; ?>
@@ -150,6 +175,7 @@ require __DIR__ . '/includes/header_customer.php'; const overallLabel = document.getElementById('status-live-overall-label'); const updatedAt = document.getElementById('status-live-updated-at'); const incidentList = document.getElementById('status-live-incidents'); + const maintenanceList = document.getElementById('status-live-maintenance'); let polling = false; function addText(parent, tag, text, className, style) { @@ -183,6 +209,21 @@ require __DIR__ . '/includes/header_customer.php'; } } + function renderMaintenance(windows) { + if (!maintenanceList) return; + maintenanceList.replaceChildren(); + for (const window of windows || []) { + const card = document.createElement('div'); + card.className = 'card'; + card.style.cssText = 'padding:14px 18px;margin:0 0 12px;border-left:6px solid #2563eb'; + addText(card, 'strong', (window.active ? 'Laufende Wartung: ' : 'Geplante Wartung: ') + window.title); + addText(card, 'div', (window.active ? 'Endet ' : 'Geplant von ') + window.startsAt + ' bis ' + window.endsAt, 'muted', 'font-size:.85rem;margin-top:4px'); + addText(card, 'div', 'Betrifft: ' + (window.componentName || 'gesamte Plattform'), 'muted', 'font-size:.82rem'); + if (window.body) addText(card, 'div', window.body, '', 'margin-top:6px;white-space:pre-line'); + maintenanceList.appendChild(card); + } + } + async function refreshStatus() { if (polling || document.visibilityState === 'hidden') return; polling = true; @@ -219,6 +260,7 @@ require __DIR__ . '/includes/header_customer.php'; if (uptime) uptime.textContent = component.uptime === null ? 'Noch keine Pr\u00fcfungen' : Number(component.uptime).toLocaleString('de-DE', {minimumFractionDigits: 2, maximumFractionDigits: 2}) + ' %'; } renderIncidents(data.incidents); + renderMaintenance(data.maintenanceWindows); } catch (_) { // Bei einem voruebergehenden Netzfehler bleibt die zuletzt geladene Anzeige sichtbar. } finally { diff --git a/DeskHub-Webseite/ticket_view.php b/DeskHub-Webseite/ticket_view.php index 8da56818..61c88579 100644 --- a/DeskHub-Webseite/ticket_view.php +++ b/DeskHub-Webseite/ticket_view.php @@ -89,10 +89,11 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && !in_array($ticket['status'], ['clos } // Ein Kunden-Reply auf ein beantwortetes Ticket setzt den Status zurück auf "Offen" - $wasReopened = in_array($ticket['status'], ['answered', 'resolved']); + $wasReopened = in_array($ticket['status'], ['answered', 'resolved', 'pending']); $newStatus = $wasReopened ? 'open' : $ticket['status']; $stmt = $pdo->prepare('UPDATE tickets SET status = ?, updated_at = ? WHERE id = ?'); $stmt->execute([$newStatus, $now, $ticket['id']]); + ticket_sla_status_transition($pdo, (int) $ticket['id'], (string) $ticket['status'], $newStatus); log_activity($pdo, $ticket['id'], null, 'Kunde hat geantwortet'); if ($wasReopened) { diff --git a/DeskHub-Webseite/tools/demo-video/record.js b/DeskHub-Webseite/tools/demo-video/record.js index 0a607d6f..69337290 100644 --- a/DeskHub-Webseite/tools/demo-video/record.js +++ b/DeskHub-Webseite/tools/demo-video/record.js @@ -228,6 +228,32 @@ app.whenReady().then(async () => { await sleep(1400); await hide(); + // ── 3b. Weitere Funktionen der Weboberfläche ── + chapter('features'); + const tour = async (url, title, sub, { y = 0, dwell = 2600 } = {}) => { + await go(url); await caption(title, sub); await sleep(1100); + if (y) { await scroll('page', y, 1700); await sleep(900); await scroll('page', 0, 1000); } else await sleep(dwell); + await hide(); + }; + await tour('/staff/tickets.php', 'Alle Tickets', 'Suchen, filtern, sortieren und Sammelaktionen', { dwell: 3200 }); + await tour('/staff/reports.php', 'Berichte', 'Auslastung, Antwortzeiten und Zufriedenheit', { y: 520 }); + await tour('/staff/sla_plans.php', 'SLA-Pläne', 'Reaktions- und Lösungszeiten je Priorität', { dwell: 3000 }); + await tour('/staff/escalation_rules.php', 'Eskalation und Regeln', 'Überfällige Tickets automatisch weitergeben', { dwell: 3000 }); + await tour('/staff/canned_responses.php', 'Textbausteine', 'Fertige Antworten für wiederkehrende Fragen', { dwell: 3000 }); + await tour('/staff/kb_manage.php', 'Wissensdatenbank', 'Artikel für Kunden und den Assistenten', { dwell: 3000 }); + await tour('/staff/customers.php', 'Kunden und Organisationen', 'Alle Tickets eines Kunden auf einen Blick', { dwell: 3000 }); + await tour('/staff/tasks.php', 'Aufgaben', 'Eigene To-dos neben den Tickets', { dwell: 2800 }); + await tour('/staff/time_entries.php', 'Zeiterfassung', 'Aufwand pro Ticket und Kunde', { dwell: 2800 }); + await tour('/staff/status_page.php', 'Status-Seite', 'Störungen öffentlich melden', { dwell: 3000 }); + await tour('/staff/departments.php', 'Abteilungen, Teams und Rollen', 'Rechte genau festlegen', { dwell: 2800 }); + await tour('/staff/api_keys.php', 'Schnittstelle und Webhooks', 'DeskHub mit anderen Systemen verbinden', { dwell: 2800 }); + await tour('/staff/audit_log.php', 'Protokoll', 'Wer hat wann was geändert', { dwell: 2800 }); + await tour('/staff/security_check.php', 'Sicherheitsprüfung', 'Zeigt Schwachstellen der Installation', { dwell: 3000 }); + await tour('/staff/backup.php', 'Datensicherung', 'Backups per Klick, auch auf ein NAS', { dwell: 3200 }); + await tour('/staff/plugins.php', 'Plugins', 'Erweiterungen ohne Eingriff am Server', { dwell: 3000 }); + await tour('/staff/apps.php', 'Apps und Minecraft-Plugin', 'Windows, Android und Minecraft zum Herunterladen', { dwell: 3200 }); + await tour('/staff/settings.php', 'Einstellungen', 'Name, Farben, E-Mail, Sicherheit und mehr', { y: 500 }); + // ── 4. Windows-Programm ── chapter('windows'); await go('', { file: MOCK }); @@ -252,6 +278,10 @@ app.whenReady().then(async () => { evt('whoosh'); await win.loadURL(card('

Android-App

  • Neues, helles Design mit schwebender Navigation
  • Wiedervorlage, Beobachten, „Dazu passend“
  • Digitalen Assistenten komplett verwalten
  • Widget in drei Größen, hell und dunkel
')); await sleep(3700); + chapter('extras'); + evt('whoosh'); + await win.loadURL(card('

Und noch mehr

  • Chrome-Erweiterung: Zähler, Suche, Ticket anlegen
  • Minecraft-Plugin für Paper, Spigot und BungeeCord
  • Windows-Programm auch für Linux und Mac
  • Plugins und Sprachpakete, zum Beispiel Englisch
  • Updates der Apps direkt aus DeskHub
')); + await sleep(3700); chapter('outro'); evt('whoosh'); await win.loadURL(card('
Kundenportal · Weboberfläche · Windows · Android
Mehr in der README
'));