Upload via GUI (340 Dateien)
This commit is contained in:
+28
-13
@@ -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';
|
||||
<div class="kb-category-grid">
|
||||
<?php foreach ($categories as $i => $cat): ?>
|
||||
<?php
|
||||
$artStmt->execute([$cat['id']]);
|
||||
$arts = $artStmt->fetchAll();
|
||||
$arts = $articlesByCategory[(int) $cat['id']] ?? [];
|
||||
if (!$arts) continue;
|
||||
$anyArticles = true;
|
||||
?>
|
||||
|
||||
@@ -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.');
|
||||
|
||||
@@ -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';
|
||||
<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 · Live-Aktualisierung aktiv</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">
|
||||
<?php foreach ($open as $i): ?>
|
||||
<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): ?>
|
||||
<h2 style="font-size:1.05rem;margin:22px 0 8px"><?= e($category) ?></h2>
|
||||
<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 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>
|
||||
@@ -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 {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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('<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);
|
||||
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');
|
||||
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>'));
|
||||
|
||||
Reference in New Issue
Block a user