Upload via GUI (340 Dateien)

This commit is contained in:
2026-10-26 21:19:21 +00:00
parent d6df75ad64
commit 12699f0918
5 changed files with 117 additions and 24 deletions
+28 -13
View File
@@ -10,19 +10,35 @@ $pdo = get_db();
$q = trim($_GET['q'] ?? ''); $q = trim($_GET['q'] ?? '');
if ($q !== '') { if ($q !== '') {
$stmt = $pdo->prepare( $loadSearch = static function () use ($pdo, $q): array {
"SELECT a.*, c.name AS category_name FROM kb_articles a $stmt = $pdo->prepare(
JOIN kb_categories c ON c.id = a.category_id "SELECT a.*, c.name AS category_name FROM kb_articles a
WHERE a.is_published = 1 AND (a.title LIKE ? OR a.body LIKE ?) JOIN kb_categories c ON c.id = a.category_id
ORDER BY a.views DESC" WHERE a.is_published = 1 AND (a.title LIKE ? OR a.body LIKE ?)
); ORDER BY a.views DESC"
$like = '%' . $q . '%'; );
$stmt->execute([$like, $like]); $like = '%' . $q . '%';
$articles = $stmt->fetchAll(); $stmt->execute([$like, $like]);
return $stmt->fetchAll();
};
$articles = function_exists('dhcm_cache_remember')
? dhcm_cache_remember('kb.search.' . $q, $loadSearch)
: $loadSearch();
$categories = []; $categories = [];
} else { } else {
$categories = $pdo->query('SELECT * FROM kb_categories ORDER BY sort_order, name')->fetchAll(); $loadIndex = static function () use ($pdo): array {
$artStmt = $pdo->prepare('SELECT * FROM kb_articles WHERE category_id = ? AND is_published = 1 ORDER BY title'); $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; $articles = null;
} }
@@ -54,8 +70,7 @@ require __DIR__ . '/includes/header_customer.php';
<div class="kb-category-grid"> <div class="kb-category-grid">
<?php foreach ($categories as $i => $cat): ?> <?php foreach ($categories as $i => $cat): ?>
<?php <?php
$artStmt->execute([$cat['id']]); $arts = $articlesByCategory[(int) $cat['id']] ?? [];
$arts = $artStmt->fetchAll();
if (!$arts) continue; if (!$arts) continue;
$anyArticles = true; $anyArticles = true;
?> ?>
+12 -7
View File
@@ -9,13 +9,18 @@ if (get_setting('kb_enabled', '1') !== '1') {
$pdo = get_db(); $pdo = get_db();
$slug = $_GET['slug'] ?? ''; $slug = $_GET['slug'] ?? '';
$stmt = $pdo->prepare( $loadArticle = static function () use ($pdo, $slug): ?array {
'SELECT a.*, c.name AS category_name FROM kb_articles a $stmt = $pdo->prepare(
JOIN kb_categories c ON c.id = a.category_id 'SELECT a.*, c.name AS category_name FROM kb_articles a
WHERE a.slug = ? AND a.is_published = 1' JOIN kb_categories c ON c.id = a.category_id
); WHERE a.slug = ? AND a.is_published = 1'
$stmt->execute([$slug]); );
$article = $stmt->fetch(); $stmt->execute([$slug]);
return $stmt->fetch() ?: null;
};
$article = function_exists('dhcm_cache_remember')
? dhcm_cache_remember('kb.article.' . $slug, $loadArticle)
: $loadArticle();
if (!$article) { if (!$article) {
flash('error', 'Dieser Artikel wurde nicht gefunden.'); flash('error', 'Dieser Artikel wurde nicht gefunden.');
+45 -3
View File
@@ -24,6 +24,8 @@ foreach ($components as &$component) {
} }
unset($component); unset($component);
$incidents = status_incidents($pdo, 14); $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')); $open = array_values(array_filter($incidents, fn($i) => $i['status'] !== 'resolved'));
$past = 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(); $incStates = status_incident_states();
@@ -33,10 +35,12 @@ if (($_GET['live'] ?? '') === '1') {
header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0'); header('Cache-Control: no-store, no-cache, must-revalidate, max-age=0');
$liveComponents = []; $liveComponents = [];
foreach ($components as $component) { 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[] = [ $liveComponents[] = [
'id' => (int) $component['id'], 'id' => (int) $component['id'],
'status' => (string) $component['status'], 'status' => $componentStatus,
'statusLabel' => (string) $componentState[0], 'statusLabel' => (string) $componentState[0],
'color' => (string) $componentState[1], 'color' => (string) $componentState[1],
'uptime' => $component['timeline']['uptime'], 'uptime' => $component['timeline']['uptime'],
@@ -64,11 +68,21 @@ if (($_GET['live'] ?? '') === '1') {
], $incident['updates']), ], $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([ echo json_encode([
'updatedAt' => date('d.m.Y H:i:s'), 'updatedAt' => date('d.m.Y H:i:s'),
'overall' => $overall, 'overall' => $overall,
'components' => $liveComponents, 'components' => $liveComponents,
'incidents' => $liveIncidents, 'incidents' => $liveIncidents,
'maintenanceWindows' => $liveMaintenance,
], JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT); ], JSON_UNESCAPED_UNICODE | JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT);
exit; exit;
} }
@@ -83,6 +97,17 @@ require __DIR__ . '/includes/header_customer.php';
<div id="status-live-updated-at" class="muted" style="font-size:.82rem;margin-top:2px">Stand: <?= e(date("d.m.Y H:i:s")) ?> Uhr &middot; Live-Aktualisierung aktiv</div> <div id="status-live-updated-at" class="muted" style="font-size:.82rem;margin-top:2px">Stand: <?= e(date("d.m.Y H:i:s")) ?> Uhr &middot; Live-Aktualisierung aktiv</div>
</div> </div>
<div id="status-live-maintenance">
<?php foreach ($maintenanceWindows as $window): $isActiveWindow = strtotime((string) $window['starts_at']) <= time() && strtotime((string) $window['ends_at']) >= time(); ?>
<div class="card" style="padding:14px 18px;margin:0 0 12px;border-left:6px solid #2563eb">
<strong><?= $isActiveWindow ? 'Laufende Wartung' : 'Geplante Wartung' ?>: <?= e($window['title']) ?></strong>
<div class="muted" style="font-size:.85rem;margin-top:4px"><?= $isActiveWindow ? 'Läuft seit ' : 'Geplant von ' ?><?= e(format_dt((string) $window['starts_at'])) ?> bis <?= e(format_dt((string) $window['ends_at'])) ?></div>
<?php if (!empty($window['component_name'])): ?><div class="muted" style="font-size:.82rem">Betrifft: <?= e($window['component_name']) ?></div><?php else: ?><div class="muted" style="font-size:.82rem">Betrifft: gesamte Plattform</div><?php endif; ?>
<?php if (trim((string) ($window['body'] ?? '')) !== ''): ?><div style="margin-top:6px;white-space:pre-line"><?= e((string) $window['body']) ?></div><?php endif; ?>
</div>
<?php endforeach; ?>
</div>
<div id="status-live-incidents"> <div id="status-live-incidents">
<?php foreach ($open as $i): ?> <?php foreach ($open as $i): ?>
<div class="card" style="padding:16px 20px;margin-bottom:14px;border-left:6px solid #dc2626"> <div class="card" style="padding:16px 20px;margin-bottom:14px;border-left:6px solid #dc2626">
@@ -104,7 +129,7 @@ require __DIR__ . '/includes/header_customer.php';
<?php foreach ($groups as $category => $group): ?> <?php foreach ($groups as $category => $group): ?>
<h2 style="font-size:1.05rem;margin:22px 0 8px"><?= e($category) ?></h2> <h2 style="font-size:1.05rem;margin:22px 0 8px"><?= e($category) ?></h2>
<section class="card" style="padding:0 20px;margin:0 0 16px"> <section class="card" style="padding:0 20px;margin:0 0 16px">
<?php foreach ($group as $groupIndex => $c): $st = $states[$c['status']] ?? $states['operational']; ?> <?php foreach ($group as $groupIndex => $c): $componentStatus = (string) $c['status']; if ($componentStatus === 'operational' && (isset($activeMaintenance[0]) || isset($activeMaintenance[(int) $c['id']]))) $componentStatus = 'maintenance'; $st = $states[$componentStatus] ?? $states['operational']; ?>
<div data-status-component="<?= (int) $c['id'] ?>" style="padding:13px 0;<?= $groupIndex < count($group) - 1 ? 'border-bottom:1px solid var(--border,#e3e9ee)' : '' ?>"> <div data-status-component="<?= (int) $c['id'] ?>" style="padding:13px 0;<?= $groupIndex < count($group) - 1 ? 'border-bottom:1px solid var(--border,#e3e9ee)' : '' ?>">
<div style="display:flex;justify-content:space-between;align-items:center;gap:12px"> <div style="display:flex;justify-content:space-between;align-items:center;gap:12px">
<div><strong><?= e($c['name']) ?></strong><?php if (!empty($c['description'])): ?><div class="muted" style="font-size:.82rem"><?= e($c['description']) ?></div><?php endif; ?></div> <div><strong><?= e($c['name']) ?></strong><?php if (!empty($c['description'])): ?><div class="muted" style="font-size:.82rem"><?= e($c['description']) ?></div><?php endif; ?></div>
@@ -150,6 +175,7 @@ require __DIR__ . '/includes/header_customer.php';
const overallLabel = document.getElementById('status-live-overall-label'); const overallLabel = document.getElementById('status-live-overall-label');
const updatedAt = document.getElementById('status-live-updated-at'); const updatedAt = document.getElementById('status-live-updated-at');
const incidentList = document.getElementById('status-live-incidents'); const incidentList = document.getElementById('status-live-incidents');
const maintenanceList = document.getElementById('status-live-maintenance');
let polling = false; let polling = false;
function addText(parent, tag, text, className, style) { 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() { async function refreshStatus() {
if (polling || document.visibilityState === 'hidden') return; if (polling || document.visibilityState === 'hidden') return;
polling = true; 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}) + ' %'; if (uptime) uptime.textContent = component.uptime === null ? 'Noch keine Pr\u00fcfungen' : Number(component.uptime).toLocaleString('de-DE', {minimumFractionDigits: 2, maximumFractionDigits: 2}) + ' %';
} }
renderIncidents(data.incidents); renderIncidents(data.incidents);
renderMaintenance(data.maintenanceWindows);
} catch (_) { } catch (_) {
// Bei einem voruebergehenden Netzfehler bleibt die zuletzt geladene Anzeige sichtbar. // Bei einem voruebergehenden Netzfehler bleibt die zuletzt geladene Anzeige sichtbar.
} finally { } finally {
+2 -1
View File
@@ -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" // 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']; $newStatus = $wasReopened ? 'open' : $ticket['status'];
$stmt = $pdo->prepare('UPDATE tickets SET status = ?, updated_at = ? WHERE id = ?'); $stmt = $pdo->prepare('UPDATE tickets SET status = ?, updated_at = ? WHERE id = ?');
$stmt->execute([$newStatus, $now, $ticket['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'); log_activity($pdo, $ticket['id'], null, 'Kunde hat geantwortet');
if ($wasReopened) { if ($wasReopened) {
@@ -228,6 +228,32 @@ app.whenReady().then(async () => {
await sleep(1400); await sleep(1400);
await hide(); 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 ── // ── 4. Windows-Programm ──
chapter('windows'); chapter('windows');
await go('', { file: MOCK }); await go('', { file: MOCK });
@@ -252,6 +278,10 @@ app.whenReady().then(async () => {
evt('whoosh'); evt('whoosh');
await win.loadURL(card('<h2>Android-App</h2><ul><li>Neues, helles Design mit schwebender Navigation</li><li>Wiedervorlage, Beobachten, „Dazu passend“</li><li>Digitalen Assistenten komplett verwalten</li><li>Widget in drei Größen, hell und dunkel</li></ul>')); await win.loadURL(card('<h2>Android-App</h2><ul><li>Neues, helles Design mit schwebender Navigation</li><li>Wiedervorlage, Beobachten, „Dazu passend“</li><li>Digitalen Assistenten komplett verwalten</li><li>Widget in drei Größen, hell und dunkel</li></ul>'));
await sleep(3700); await sleep(3700);
chapter('extras');
evt('whoosh');
await win.loadURL(card('<h2>Und noch mehr</h2><ul><li>Chrome-Erweiterung: Zähler, Suche, Ticket anlegen</li><li>Minecraft-Plugin für Paper, Spigot und BungeeCord</li><li>Windows-Programm auch für Linux und Mac</li><li>Plugins und Sprachpakete, zum Beispiel Englisch</li><li>Updates der Apps direkt aus DeskHub</li></ul>'));
await sleep(3700);
chapter('outro'); chapter('outro');
evt('whoosh'); evt('whoosh');
await win.loadURL(card('<div class="logo" style="font-size:84px">Desk<span>Hub</span></div><div class="sub">Kundenportal · Weboberfläche · Windows · Android</div><div class="small">Mehr in der README</div>')); await win.loadURL(card('<div class="logo" style="font-size:84px">Desk<span>Hub</span></div><div class="sub">Kundenportal · Weboberfläche · Windows · Android</div><div class="small">Mehr in der README</div>'));