Upload via GUI (340 Dateien)

This commit is contained in:
2026-10-26 21:19:10 +00:00
parent ecabba060c
commit a7aeec1b26
7 changed files with 160 additions and 6 deletions
+8 -2
View File
@@ -24,8 +24,11 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
->execute([$matchType, $value, $reason ?: null, $staff['id'], db_now()]);
// Tickets schließen
$col = $matchType === 'email' ? 'requester_email' : "" . sql_email_domain("requester_email") . "";
$pdo->prepare("UPDATE tickets SET status='closed',closed_at=?,updated_at=? WHERE $col=? AND status NOT IN ('closed')")
$priorStmt = $pdo->prepare("SELECT id,status FROM tickets WHERE $col=? AND status NOT IN ('closed')");
$priorStmt->execute([$value]); $priorTickets = $priorStmt->fetchAll();
$pdo->prepare("UPDATE tickets SET status='closed',resolution_code='spam',resolution_note='Im Rahmen einer Sperre geschlossen',closed_at=?,updated_at=? WHERE $col=? AND status NOT IN ('closed')")
->execute([db_now(), db_now(), $value]);
ticket_sla_transition_rows($pdo, $priorTickets, 'closed', (int) $staff['id']);
audit_log($staff['id'], "Sperrliste: $value hinzugefügt ($matchType)");
}
flash('success', "$value gesperrt.");
@@ -50,8 +53,11 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
->execute([$type, $val, $reason ?: null, $staff['id'], db_now()]);
// Tickets schließen
$col = $type === 'email' ? 'requester_email' : "" . sql_email_domain("requester_email") . "";
$pdo->prepare("UPDATE tickets SET status='closed',closed_at=?,updated_at=? WHERE $col=? AND status NOT IN ('closed')")
$priorStmt = $pdo->prepare("SELECT id,status FROM tickets WHERE $col=? AND status NOT IN ('closed')");
$priorStmt->execute([$val]); $priorTickets = $priorStmt->fetchAll();
$pdo->prepare("UPDATE tickets SET status='closed',resolution_code='spam',resolution_note='Im Rahmen einer Sperre geschlossen',closed_at=?,updated_at=? WHERE $col=? AND status NOT IN ('closed')")
->execute([db_now(), db_now(), $val]);
ticket_sla_transition_rows($pdo, $priorTickets, 'closed', (int) $staff['id']);
$added++;
}
audit_log($staff['id'], "Sperrliste Massen-Import: $added hinzugefügt, $skipped übersprungen");
+1 -1
View File
@@ -17,7 +17,7 @@ $mine->execute(array_merge([$staff['id']], $deptParams)); $mine = (int)$mine->fe
$unassigned = $pdo->prepare("SELECT COUNT(*) FROM tickets WHERE assigned_staff_id IS NULL AND status NOT IN ('resolved','closed')" . $deptFilter);
$unassigned->execute($deptParams); $unassigned = (int)$unassigned->fetchColumn();
$overdue = $pdo->prepare("SELECT COUNT(*) FROM tickets WHERE status NOT IN ('resolved','closed') AND resolution_due_at IS NOT NULL AND resolution_due_at < ?" . $deptFilter);
$overdue = $pdo->prepare("SELECT COUNT(*) FROM tickets WHERE status NOT IN ('resolved','closed') AND (is_overdue = 1 OR (sla_paused_at IS NULL AND resolution_due_at IS NOT NULL AND resolution_due_at < ?))" . $deptFilter);
$overdue->execute(array_merge([$now], $deptParams)); $overdue = (int)$overdue->fetchColumn();
$unanswered = $pdo->prepare("SELECT COUNT(*) FROM tickets WHERE status = 'open'" . $deptFilter);
+4
View File
@@ -16,6 +16,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
} else {
$slug = slugify($name);
$pdo->prepare('INSERT INTO kb_categories (name, slug, sort_order) VALUES (?,?,0)')->execute([$name, $slug]);
if (function_exists('dhcm_cache_clear')) dhcm_cache_clear();
audit_log($admin['id'], "KB-Kategorie erstellt: {$name}");
flash('success', 'Kategorie erstellt.');
redirect('/staff/kb_manage.php');
@@ -33,6 +34,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$nameStmt->execute([$id]);
$catName = $nameStmt->fetchColumn();
$pdo->prepare('DELETE FROM kb_categories WHERE id = ?')->execute([$id]);
if (function_exists('dhcm_cache_clear')) dhcm_cache_clear();
audit_log($admin['id'], "KB-Kategorie gelöscht: {$catName}");
flash('success', 'Kategorie gelöscht.');
}
@@ -69,6 +71,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
audit_log($admin['id'], "KB-Artikel aktualisiert: {$title}");
flash('success', 'Artikel aktualisiert.');
}
if (function_exists('dhcm_cache_clear')) dhcm_cache_clear();
redirect('/staff/kb_manage.php');
}
}
@@ -79,6 +82,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$titleStmt->execute([$id]);
$artTitle = $titleStmt->fetchColumn();
$pdo->prepare('DELETE FROM kb_articles WHERE id = ?')->execute([$id]);
if (function_exists('dhcm_cache_clear')) dhcm_cache_clear();
audit_log($admin['id'], "KB-Artikel gelöscht: {$artTitle}");
flash('success', 'Artikel gelöscht.');
redirect('/staff/kb_manage.php');
+3 -1
View File
@@ -164,8 +164,9 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_POST['action'] ?? '') === 'end')
$now = db_now();
$pdo->prepare(
"UPDATE tickets SET status='closed', closed_at=COALESCE(closed_at, ?), updated_at=?, locked_by=NULL, locked_at=NULL WHERE id=?"
"UPDATE tickets SET status='closed', resolution_code='other', resolution_note='Live-Chat beendet', closed_at=COALESCE(closed_at, ?), updated_at=?, locked_by=NULL, locked_at=NULL WHERE id=?"
)->execute([$now, $now, $ticket['id']]);
ticket_sla_status_transition($pdo, (int) $ticket['id'], (string) $ticket['status'], 'closed', (int) $staff['id']);
log_activity($pdo, $ticket['id'], $staff['id'], 'Live-Chat beendet');
finalize_work_session($pdo, (int) $ticket['id'], (int) $staff['id']);
@@ -231,6 +232,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if ($autoAssign) $args[] = $staff['id'];
$args[] = $ticket['id'];
$stmt->execute($args);
ticket_sla_status_transition($pdo, (int) $ticket['id'], (string) $ticket['status'], $newStatus, (int) $staff['id']);
if ($autoAssign) log_activity($pdo, $ticket['id'], $staff['id'], 'Automatisch zugewiesen (Live-Chat)');
save_mentions($pdo, $ticket['id'], $threadId, $staff['id'], $message, $ticket);
+1 -1
View File
@@ -39,7 +39,7 @@ $counts = [
'open' => $count($open),
'mine' => $count("t.assigned_staff_id = ? AND {$open}", [(int) $staff['id']]),
'unassigned' => $count("t.assigned_staff_id IS NULL AND {$open}"),
'overdue' => $count("{$open} AND t.resolution_due_at IS NOT NULL AND t.resolution_due_at < ?", [db_now()]),
'overdue' => $count("{$open} AND (t.is_overdue = 1 OR (t.sla_paused_at IS NULL AND t.resolution_due_at IS NOT NULL AND t.resolution_due_at < ?))", [db_now()]),
'unanswered' => $count("t.status = 'open'"),
];
+142
View File
@@ -0,0 +1,142 @@
<?php
require_once __DIR__ . '/../includes/bootstrap.php';
$staff = require_admin();
$pdo = get_db();
$errors = [];
$problemId = (int) ($_GET['id'] ?? $_POST['problem_id'] ?? 0);
$activeTab = (string) ($_GET['tab'] ?? 'list');
if (!in_array($activeTab, ['list', 'create'], true)) $activeTab = 'list';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
verify_csrf();
$action = (string) ($_POST['action'] ?? '');
$activeTab = $action === 'problem_create' ? 'create' : 'list';
try {
if ($action === 'problem_create') {
$pdo->beginTransaction();
$problemId = ticket_problem_create($pdo, $_POST, (int) $staff['id']);
$ticketNumber = trim((string) ($_POST['ticket_number'] ?? ''));
if ($ticketNumber !== '') {
$ticketStmt = $pdo->prepare('SELECT id FROM tickets WHERE ticket_number = ?');
$ticketStmt->execute([$ticketNumber]);
$ticketId = (int) $ticketStmt->fetchColumn();
if (!$ticketId) throw new InvalidArgumentException('Das angegebene Ticket wurde nicht gefunden.');
ticket_problem_link($pdo, $problemId, $ticketId, (int) $staff['id']);
log_activity($pdo, $ticketId, (int) $staff['id'], 'Mit ' . ticket_problem_number($problemId) . ' verknüpft');
}
$pdo->commit();
audit_log($staff['id'], 'Problem ' . ticket_problem_number($problemId) . ' erstellt');
flash('success', ticket_problem_number($problemId) . ' wurde angelegt.');
redirect('/staff/problems.php?tab=list&id=' . $problemId);
} elseif ($action === 'problem_update') {
if (!ticket_problem_update($pdo, $problemId, $_POST, (int) $staff['id'])) throw new InvalidArgumentException('Problem nicht gefunden.');
audit_log($staff['id'], 'Problem ' . ticket_problem_number($problemId) . ' aktualisiert');
flash('success', 'Problem wurde gespeichert.');
} elseif ($action === 'ticket_link') {
$ticketNumber = trim((string) ($_POST['ticket_number'] ?? ''));
$ticketStmt = $pdo->prepare('SELECT id FROM tickets WHERE ticket_number = ?');
$ticketStmt->execute([$ticketNumber]);
$ticketId = (int) $ticketStmt->fetchColumn();
if (!$ticketId) throw new InvalidArgumentException('Das angegebene Ticket wurde nicht gefunden.');
if (!ticket_problem_link($pdo, $problemId, $ticketId, (int) $staff['id'])) throw new InvalidArgumentException('Ticket oder Problem wurde nicht gefunden.');
log_activity($pdo, $ticketId, (int) $staff['id'], 'Mit ' . ticket_problem_number($problemId) . ' verknüpft');
audit_log($staff['id'], 'Ticket mit ' . ticket_problem_number($problemId) . ' verknüpft');
flash('success', 'Ticket wurde mit dem Problem verknüpft.');
} elseif ($action === 'ticket_unlink') {
$ticketId = (int) ($_POST['ticket_id'] ?? 0);
ticket_problem_unlink($pdo, $problemId, $ticketId);
log_activity($pdo, $ticketId, (int) $staff['id'], 'Verknüpfung mit ' . ticket_problem_number($problemId) . ' entfernt');
audit_log($staff['id'], 'Ticket von ' . ticket_problem_number($problemId) . ' getrennt');
flash('success', 'Ticket-Verknüpfung wurde entfernt.');
}
} catch (Throwable $e) {
if ($pdo->inTransaction()) $pdo->rollBack();
$errors[] = $e instanceof InvalidArgumentException ? $e->getMessage() : 'Änderung konnte nicht gespeichert werden.';
}
if (!$errors) redirect('/staff/problems.php' . ($problemId ? '?id=' . $problemId : ''));
}
$problems = ticket_problems($pdo);
$problem = $problemId ? ticket_problem_get($pdo, $problemId) : null;
if ($problemId && !$problem) $errors[] = 'Problem nicht gefunden.';
$linkedTickets = $problem ? ticket_problem_tickets($pdo, $problemId) : [];
$problemStatuses = ticket_problem_statuses();
$activePage = 'problems';
$pageTitle = 'Problemmanagement';
require __DIR__ . '/_header.php';
?>
<h1>Problemmanagement</h1>
<p class="muted">Bündeln Sie Tickets mit derselben Ursache. Ursache, Workaround und dauerhafte Lösung werden zentral dokumentiert.</p>
<?php foreach ($errors as $error): ?><div class="flash error"><?= e($error) ?></div><?php endforeach; ?>
<nav class="problem-tabs" role="tablist" aria-label="Problemmanagement">
<a role="tab" aria-selected="<?= $activeTab === 'list' ? 'true' : 'false' ?>" class="<?= $activeTab === 'list' ? 'active' : '' ?>" href="/staff/problems.php?tab=list<?= $problemId ? '&amp;id=' . $problemId : '' ?>">Probleme</a>
<a role="tab" aria-selected="<?= $activeTab === 'create' ? 'true' : 'false' ?>" class="<?= $activeTab === 'create' ? 'active' : '' ?>" href="/staff/problems.php?tab=create">Neues Problem</a>
</nav>
<?php if ($activeTab === 'create'): ?>
<section class="card">
<h2 style="margin-top:0">Neues Problem erfassen</h2>
<p class="muted">Titel und erste Ticketnummer eintragen. Ursache und dauerhafte L&#246;sung k&#246;nnen danach erg&#228;nzt werden.</p>
<form method="post"><?= csrf_field() ?><input type="hidden" name="action" value="problem_create">
<label for="new_problem_title">Titel</label><input id="new_problem_title" type="text" name="title" maxlength="200" required placeholder="Kurze Problembeschreibung">
<label for="new_problem_ticket" style="margin-top:8px">Ticket verkn&#252;pfen (optional)</label><input id="new_problem_ticket" type="text" name="ticket_number" placeholder="Ticketnummer">
<label for="new_problem_description" style="margin-top:8px">Beschreibung / Auswirkungen</label><textarea id="new_problem_description" name="description" rows="3"></textarea>
<label for="new_problem_cause" style="margin-top:8px">Ursache (falls bekannt)</label><textarea id="new_problem_cause" name="root_cause" rows="3"></textarea>
<label for="new_problem_workaround" style="margin-top:8px">Workaround (optional)</label><textarea id="new_problem_workaround" name="workaround" rows="3"></textarea>
<button class="btn" type="submit" style="margin-top:12px">Problem anlegen</button>
</form>
</section>
<?php else: ?>
<div style="display:grid;grid-template-columns:minmax(240px,320px) minmax(0,1fr);gap:16px;align-items:start">
<aside class="card">
<h3 style="margin-top:0">Probleme</h3>
<?php if (!$problems): ?><p class="muted">Noch keine Probleme erfasst.</p><?php endif; ?>
<?php foreach ($problems as $item): ?>
<a href="/staff/problems.php?id=<?= (int) $item['id'] ?>" style="display:block;padding:9px 4px;border-top:1px solid var(--border,#e3e9ee);text-decoration:none;<?= (int) $item['id'] === $problemId ? 'font-weight:700;color:var(--primary)' : '' ?>">
<strong><?= e(ticket_problem_number((int) $item['id'])) ?></strong> · <?= e($item['title']) ?><br>
<span class="muted" style="font-size:.78rem"><?= e($problemStatuses[$item['status']] ?? $item['status']) ?> · <?= (int) $item['ticket_count'] ?> Tickets</span>
</a>
<?php endforeach; ?>
</aside>
<main>
<?php if (!$problem): ?>
<section class="card"><h3>Problem auswählen</h3><p class="muted">Wählen Sie links ein Problem aus oder legen Sie ein neues an.</p></section>
<?php else: ?>
<section class="card">
<div style="display:flex;justify-content:space-between;align-items:start;gap:12px;flex-wrap:wrap">
<div><span class="muted"><?= e(ticket_problem_number($problemId)) ?></span><h2 style="margin:2px 0 12px"><?= e($problem['title']) ?></h2></div>
<span class="muted">Erstellt <?= e(format_dt((string) $problem['created_at'])) ?><?= $problem['creator_name'] ? ' von ' . e($problem['creator_name']) : '' ?></span>
</div>
<form method="post"><?= csrf_field() ?><input type="hidden" name="action" value="problem_update"><input type="hidden" name="problem_id" value="<?= $problemId ?>">
<label for="problem_title">Titel</label><input id="problem_title" type="text" name="title" maxlength="200" required value="<?= e($problem['title']) ?>">
<label for="problem_description" style="margin-top:8px">Beschreibung / Auswirkungen</label><textarea id="problem_description" name="description" rows="3"><?= e((string) ($problem['description'] ?? '')) ?></textarea>
<label for="problem_root_cause" style="margin-top:8px">Ursache</label><textarea id="problem_root_cause" name="root_cause" rows="3"><?= e((string) ($problem['root_cause'] ?? '')) ?></textarea>
<label for="problem_workaround" style="margin-top:8px">Workaround</label><textarea id="problem_workaround" name="workaround" rows="3"><?= e((string) ($problem['workaround'] ?? '')) ?></textarea>
<label for="problem_resolution" style="margin-top:8px">Dauerhafte Lösung</label><textarea id="problem_resolution" name="resolution" rows="3"><?= e((string) ($problem['resolution'] ?? '')) ?></textarea>
<div style="display:flex;gap:12px;align-items:end;flex-wrap:wrap;margin-top:8px"><div><label for="problem_status">Status</label><select id="problem_status" name="status"><?php foreach ($problemStatuses as $key => $label): ?><option value="<?= e($key) ?>" <?= $problem['status'] === $key ? 'selected' : '' ?>><?= e($label) ?></option><?php endforeach; ?></select></div><button class="btn" type="submit">Problem speichern</button></div>
</form>
</section>
<section class="card" style="margin-top:16px">
<h3 style="margin-top:0">Verknüpfte Tickets (<?= count($linkedTickets) ?>)</h3>
<form method="post" style="display:flex;gap:8px;flex-wrap:wrap;align-items:end;margin-bottom:12px"><?= csrf_field() ?><input type="hidden" name="action" value="ticket_link"><input type="hidden" name="problem_id" value="<?= $problemId ?>"><div style="flex:1;min-width:200px"><label for="link_ticket_number">Ticketnummer</label><input id="link_ticket_number" type="text" name="ticket_number" required placeholder="z. B. 260001"></div><button class="btn secondary" type="submit">Ticket verknüpfen</button></form>
<?php if (!$linkedTickets): ?><p class="muted">Noch keine Tickets mit diesem Problem verknüpft.</p><?php endif; ?>
<?php foreach ($linkedTickets as $linked): ?>
<div style="display:flex;justify-content:space-between;gap:10px;align-items:center;border-top:1px solid var(--border,#e3e9ee);padding:9px 0">
<div><a href="/staff/ticket_view.php?n=<?= urlencode((string) $linked['ticket_number']) ?>"><strong>#<?= e($linked['ticket_number']) ?></strong> · <?= e($linked['subject']) ?></a><div class="muted" style="font-size:.78rem"><?= e($linked['requester_name']) ?> · <?= e(status_label((string) $linked['status'])[0]) ?></div></div>
<form method="post" onsubmit="return confirm('Ticket-Verknüpfung entfernen?')"><?= csrf_field() ?><input type="hidden" name="action" value="ticket_unlink"><input type="hidden" name="problem_id" value="<?= $problemId ?>"><input type="hidden" name="ticket_id" value="<?= (int) $linked['id'] ?>"><button class="btn secondary" type="submit">Trennen</button></form>
</div>
<?php endforeach; ?>
</section>
<?php endif; ?>
</main>
</div>
<?php endif; ?>
<style>
.problem-tabs{display:flex;gap:8px;margin:18px 0 12px;border-bottom:1px solid var(--border,#d8e0e8)}
.problem-tabs a{border-bottom:3px solid transparent;border-radius:7px 7px 0 0;background:transparent;color:var(--muted,#64748b);padding:10px 16px;font-weight:700;text-decoration:none}
.problem-tabs a.active{color:var(--text,#0f172a);border-bottom-color:var(--primary,#0f766e);background:var(--surface,#fff)}
@media(max-width:760px){.card+main{min-width:0}}
</style>
<?php require __DIR__ . '/_footer.php'; ?>
+1 -1
View File
@@ -337,7 +337,7 @@ $stmt = $pdo->prepare("SELECT COUNT(*) FROM tickets WHERE assigned_staff_id IS N
$stmt->execute($deptParamsT);
$dashUnassigned = (int) $stmt->fetchColumn();
$stmt = $pdo->prepare("SELECT COUNT(*) FROM tickets WHERE status NOT IN ('resolved','closed') AND resolution_due_at IS NOT NULL AND resolution_due_at < ?{$deptFilterT}");
$stmt = $pdo->prepare("SELECT COUNT(*) FROM tickets WHERE status NOT IN ('resolved','closed') AND (is_overdue = 1 OR (sla_paused_at IS NULL AND resolution_due_at IS NOT NULL AND resolution_due_at < ?)){$deptFilterT}");
$stmt->execute(array_merge([$now], $deptParamsT));
$dashOverdue = (int) $stmt->fetchColumn();