Upload via GUI (364 Dateien)

This commit is contained in:
2026-11-01 11:15:52 +00:00
parent aeae758b4d
commit c1a170563d
7 changed files with 781 additions and 448 deletions
+32 -32
View File
@@ -167,46 +167,46 @@ if ('serviceWorker' in navigator) {
</nav>
<div class="sidebar-footer">
<?php if ($staff): ?>
<div style="display:flex; align-items:center; gap:10px; margin-bottom:8px">
<?php
$__unreadMentions = 0;
$__unreadChat = 0;
$__ms = get_db()->prepare('SELECT COUNT(*) FROM staff_mentions WHERE mentioned_staff_id=? AND is_read=0');
$__ms->execute([$staff['id']]);
$__unreadMentions = (int)$__ms->fetchColumn();
$__cs = get_db()->prepare('SELECT COUNT(*) FROM staff_chat_messages WHERE to_staff_id=? AND is_read=0');
$__cs->execute([$staff['id']]);
$__unreadChat = (int)$__cs->fetchColumn();
$__onVacation = !empty($staff['is_on_vacation']);
?>
<div class="sf-user">
<?php if (!empty($staff['avatar_path'])): ?>
<img src="<?= e($staff['avatar_path']) ?>" alt="" style="width:32px; height:32px; border-radius:50%; object-fit:cover; flex-shrink:0; border:1px solid #2a3744">
<img class="sf-avatar" src="<?= e($staff['avatar_path']) ?>" alt="">
<?php else: ?>
<div style="width:32px; height:32px; border-radius:50%; background:rgba(15,111,106,.3); display:flex; align-items:center; justify-content:center; font-size:.85rem; font-weight:700; color:#8bc4c2; flex-shrink:0">
<?= mb_strtoupper(mb_substr($staff['name'], 0, 1)) ?>
</div>
<div class="sf-avatar sf-avatar-letter"><?= mb_strtoupper(mb_substr($staff['name'], 0, 1)) ?></div>
<?php endif; ?>
<div style="min-width:0">
<div style="font-size:.82rem; font-weight:600; color:#fff; overflow:hidden; text-overflow:ellipsis; white-space:nowrap"><?= e($staff['name']) ?></div>
<div style="font-size:.72rem; color:#4e6278"><?= $staff['role']==='admin'?'Administrator':'Mitarbeiter' ?></div>
<?php if (!empty($staff['is_on_vacation'])): ?>
<div style="font-size:.7rem; color:var(--accent)">🌴 Im Urlaub</div>
<?php endif; ?>
<div class="sf-who">
<div class="sf-name"><?= e($staff['name']) ?></div>
<div class="sf-role"><?= $staff['role']==='admin'?'Administrator':'Mitarbeiter' ?></div>
<?php if ($__onVacation): ?><span class="sf-vac">🌴 Im Urlaub</span><?php endif; ?>
</div>
</div>
<div class="lang-row" style="margin-bottom:10px"><?= i18n_switcher_html('dark') ?></div>
<div style="display:flex; gap:14px; align-items:center">
<?php
$__unreadMentions = 0;
$__unreadChat = 0;
if ($staff) {
$__ms = get_db()->prepare('SELECT COUNT(*) FROM staff_mentions WHERE mentioned_staff_id=? AND is_read=0');
$__ms->execute([$staff['id']]);
$__unreadMentions = (int)$__ms->fetchColumn();
$__cs = get_db()->prepare('SELECT COUNT(*) FROM staff_chat_messages WHERE to_staff_id=? AND is_read=0');
$__cs->execute([$staff['id']]);
$__unreadChat = (int)$__cs->fetchColumn();
}
?>
<a href="/staff/chat.php" title="Interner Chat" style="position:relative; color:#8b97a3; font-size:.78rem; text-decoration:none">
💬<?php if ($__unreadChat > 0): ?><span style="position:absolute;top:-6px;right:-8px;background:#dc2626;color:#fff;border-radius:50%;font-size:.6rem;font-weight:700;width:15px;height:15px;display:flex;align-items:center;justify-content:center;line-height:1"><?= $__unreadChat > 9 ? '9+' : $__unreadChat ?></span><?php endif; ?>
<div class="sf-actions">
<a class="sf-btn" href="/staff/chat.php" title="Interner Chat" aria-label="Interner Chat">
<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 12a8 8 0 0 1-11.6 7.1L3 21l1.9-5.4A8 8 0 1 1 21 12z"/></svg>
<?php if ($__unreadChat > 0): ?><span class="sf-badge"><?= $__unreadChat > 9 ? '9+' : $__unreadChat ?></span><?php endif; ?>
</a>
<a href="/staff/mentions.php" title="Erwähnungen" style="position:relative; color:#8b97a3; font-size:.78rem; text-decoration:none">
🔔<?php if ($__unreadMentions > 0): ?><span style="position:absolute;top:-6px;right:-8px;background:#dc2626;color:#fff;border-radius:50%;font-size:.6rem;font-weight:700;width:15px;height:15px;display:flex;align-items:center;justify-content:center;line-height:1"><?= $__unreadMentions > 9 ? '9+' : $__unreadMentions ?></span><?php endif; ?>
<a class="sf-btn" href="/staff/mentions.php" title="Erwähnungen" aria-label="Erwähnungen">
<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M18 8a6 6 0 0 0-12 0c0 7-3 9-3 9h18s-3-2-3-9M13.7 21a2 2 0 0 1-3.4 0"/></svg>
<?php if ($__unreadMentions > 0): ?><span class="sf-badge"><?= $__unreadMentions > 9 ? '9+' : $__unreadMentions ?></span><?php endif; ?>
</a>
<a class="sf-btn" href="/staff/profile.php" title="Profil" aria-label="Profil">
<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M20 21a8 8 0 0 0-16 0M12 3a4 4 0 1 0 0 8 4 4 0 0 0 0-8z"/></svg>
</a>
<a class="sf-btn sf-btn-out" href="/staff/logout.php" title="Abmelden" aria-label="Abmelden">
<svg viewBox="0 0 24 24" width="18" height="18" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4M16 17l5-5-5-5M21 12H9"/></svg>
</a>
<a href="/staff/profile.php" style="color:#8b97a3; font-size:.78rem">Profil</a>
<a href="/staff/logout.php" style="color:#8b97a3; font-size:.78rem">Abmelden</a>
</div>
<div class="lang-row"><?= i18n_switcher_html('dark') ?></div>
<?php endif; ?>
</div>
</aside>
@@ -34,7 +34,7 @@
<table>
<thead><tr>
<th style="width:24px"><input type="checkbox" onclick="document.querySelectorAll('.ticket-check').forEach(c=>c.checked=this.checked)"></th>
<th>Ticket</th><th>Betreff</th><th>Abteilung</th><th>Priorität</th><th>Status</th><th>Zugewiesen</th><th>Eingang</th><th>Fällig</th>
<th>Ticket</th><th>Betreff</th><th>Abteilung</th><th>Priorität</th><th>Status</th><th>Zugewiesen</th><th>Letzte Aktivität</th><th>Fällig</th>
</tr></thead>
<tbody>
<?php foreach ($tickets as $t):
@@ -60,7 +60,7 @@
<td><span class="badge" style="background:<?= e($t['priority_color']) ?>"><?= e($t['priority_name']) ?></span></td>
<td><span class="badge" style="background:<?= $color ?>"><?= e($label) ?></span></td>
<td><?= $t['staff_name'] ? e($t['staff_name']) : '<span class="muted">–</span>' ?></td>
<td class="muted" style="white-space:nowrap" title="Ticket eingegangen"><?= e(format_dt($t['created_at'] ?? null)) ?></td>
<td class="muted" style="white-space:nowrap" title="Eingegangen: <?= e(format_dt($t['created_at'] ?? null)) ?>"><?= e(format_dt($t['updated_at'] ?? $t['created_at'] ?? null)) ?></td>
<td class="muted"><?= $isPaused ? 'Pausiert' : time_until($t['resolution_due_at']) ?></td>
</tr>
<?php endforeach; ?>
+50 -32
View File
@@ -44,7 +44,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
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);
if (!ticket_problem_unlink($pdo, $problemId, $ticketId)) throw new InvalidArgumentException('Dieses Ticket ist mit dem Problem nicht verknüpft.');
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.');
@@ -65,13 +65,18 @@ $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>
<div class="pm-head">
<div>
<h1 style="margin:0">Problemmanagement</h1>
<p class="muted" style="margin:4px 0 0">Bündeln Sie Tickets mit derselben Ursache. Ursache, Workaround und dauerhafte Lösung werden zentral dokumentiert.</p>
</div>
<?php if ($activeTab === 'create' || $problem): ?>
<a class="btn secondary" href="/staff/problems.php">← Alle Probleme</a>
<?php else: ?>
<a class="btn" href="/staff/problems.php?tab=create">+ Neues Problem</a>
<?php endif; ?>
</div>
<?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>
@@ -86,22 +91,36 @@ require __DIR__ . '/_header.php';
</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>
<?php if (!$problem): ?>
<?php $openCount = 0; foreach ($problems as $item) { if ($item['status'] !== 'resolved') $openCount++; } ?>
<section class="card" style="padding:0;overflow:hidden">
<?php if (!$problems): ?>
<div class="empty-state" style="padding:32px">Noch keine Probleme erfasst. <a href="/staff/problems.php?tab=create">Erstes Problem anlegen</a></div>
<?php else: ?>
<div class="muted" style="padding:14px 20px;border-bottom:1px solid var(--border,#e3e9ee)"><?= count($problems) ?> Probleme · <?= $openCount ?> offen</div>
<table>
<thead><tr><th>Nr.</th><th>Titel</th><th>Status</th><th>Tickets</th><th>Zuletzt geändert</th></tr></thead>
<tbody>
<?php foreach ($problems as $item):
$pColor = ['investigating' => '#dc2626', 'workaround' => '#d97706', 'monitoring' => '#2563eb', 'resolved' => '#16a34a'][$item['status']] ?? '#64748b';
$pUrl = '/staff/problems.php?id=' . (int) $item['id'];
?>
<tr>
<td style="white-space:nowrap"><a href="<?= $pUrl ?>" class="ticket-no"><?= e(ticket_problem_number((int) $item['id'])) ?></a></td>
<td><a href="<?= $pUrl ?>"><strong><?= e($item['title']) ?></strong></a>
<?php if (!empty($item['description'])): ?><div class="muted" style="font-size:.8rem;max-width:560px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap"><?= e($item['description']) ?></div><?php endif; ?></td>
<td><span class="badge" style="background:<?= $pColor ?>"><?= e($problemStatuses[$item['status']] ?? $item['status']) ?></span></td>
<td><?= (int) $item['ticket_count'] ?></td>
<td class="muted" style="white-space:nowrap"><?= e(format_dt((string) ($item['updated_at'] ?? $item['created_at']))) ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>
</section>
<?php else: ?>
<div class="pm-detail">
<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>
@@ -116,8 +135,9 @@ require __DIR__ . '/_header.php';
<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">
</main>
<aside>
<section class="card">
<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; ?>
@@ -128,15 +148,13 @@ require __DIR__ . '/_header.php';
</div>
<?php endforeach; ?>
</section>
<?php endif; ?>
</main>
</div>
</aside>
</div>
<?php endif; ?>
<?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}}
.pm-head{display:flex;justify-content:space-between;align-items:flex-start;gap:16px;flex-wrap:wrap;margin-bottom:18px}
.pm-detail{display:grid;grid-template-columns:minmax(0,1fr) minmax(280px,400px);gap:16px;align-items:start}
@media(max-width:900px){.pm-detail{grid-template-columns:1fr}}
</style>
<?php require __DIR__ . '/_footer.php'; ?>
File diff suppressed because it is too large Load Diff
+7 -2
View File
@@ -25,6 +25,10 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$enabled = !empty($_POST['enabled']);
if (!status_set_component_enabled($pdo, (int) ($_POST['id'] ?? 0), $enabled)) $errors[] = 'Dienst konnte nicht gefunden werden.';
else flash('success', $enabled ? 'Dienst aktiviert und wieder zur Prüfung freigegeben.' : 'Dienst deaktiviert, ausgeblendet und von Prüfungen ausgenommen.');
} elseif ($action === 'component_internal') {
$internal = !empty($_POST['internal']);
if (!status_set_component_internal($pdo, (int) ($_POST['id'] ?? 0), $internal)) $errors[] = 'Nicht möglich: Bitte zuerst das Datenbank-Update einspielen (System-Update).';
else flash('success', $internal ? 'Dienst ist jetzt intern: nur Mitarbeiter sehen ihn, geprüft wird er weiter.' : 'Dienst ist jetzt öffentlich sichtbar.');
} elseif ($action === 'component_maintenance') {
if (!status_toggle_component_maintenance($pdo, (int) ($_POST['id'] ?? 0))) $errors[] = 'Dienst konnte nicht gefunden werden.';
else flash('success', 'Wartungsstatus geändert.');
@@ -182,15 +186,16 @@ require __DIR__ . '/_header.php';
$isMinecraftComponent = str_contains(mb_strtolower((string) $c['name']), 'minecraft');
$monitorTargetValue = trim((string) ($c['monitor_target'] ?? ''));
if ($monitorTargetValue === '' && $checkInfo && $checkInfo['type'] === 'http') $monitorTargetValue = (string) $checkInfo['target'];
$showsDetectedTargets = $checkInfo && in_array($checkInfo['type'], ['database', 'mail', 'fcm', 'android_app', 'windows_app'], true);
$showsDetectedTargets = $checkInfo && in_array($checkInfo['type'], ['database', 'mail', 'fcm', 'android_app', 'windows_app', 'plugin_site'], true);
$detectedTargets = $showsDetectedTargets ? status_component_detected_targets($pdo, $checkInfo) : [];
?>
<div style="flex:<?= str_contains(mb_strtolower((string) $c['name']), 'minecraft') ? '0 0 249px' : '1' ?>;min-width:180px"><strong><?= e($c['name']) ?></strong><?php if ($c['description']): ?><div class="muted" style="font-size:.8rem"><?= e($c['description']) ?></div><?php endif; ?><?php if (empty($c['is_enabled'])): ?><div class="muted" style="font-size:.75rem">Deaktiviert – nicht öffentlich sichtbar und wird nicht geprüft</div><?php endif; ?>
<div style="flex:<?= str_contains(mb_strtolower((string) $c['name']), 'minecraft') ? '0 0 249px' : '1' ?>;min-width:180px"><strong><?= e($c['name']) ?></strong><?php if ($c['description']): ?><div class="muted" style="font-size:.8rem"><?= e($c['description']) ?></div><?php endif; ?><?php if (empty($c['is_enabled'])): ?><div class="muted" style="font-size:.75rem">Deaktiviert – nicht öffentlich sichtbar und wird nicht geprüft</div><?php elseif (!empty($c['is_internal'])): ?><div class="muted" style="font-size:.75rem">Intern – nur für Mitarbeiter sichtbar, wird geprüft</div><?php endif; ?>
<div class="muted" style="font-size:.76rem;margin-top:3px"><?php if ($checkInfo): ?>Automatische Prüfung: <?= e($checkInfo['label']) ?> · <?= $c['last_checked_at'] ? 'zuletzt ' . e(format_dt((string) $c['last_checked_at'])) : 'wartet auf ersten Lauf' ?> · alle <?= max(1, (int) ($c['check_interval_minutes'] ?? 1)) ?> Min.<?php elseif (($c['monitor_type'] ?? 'auto') === 'manual'): ?>Manuell verwaltet<?php else: ?>Prüfziel fehlt – wartet auf Zuordnung<?php endif; ?><?php if (!empty($c['last_check_ms'])): ?> · <?= (int) $c['last_check_ms'] ?> ms<?php endif; ?></div>
<?php if (!empty($c['last_check_error'])): ?><div style="color:#b91c1c;font-size:.76rem"><?= e((string) $c['last_check_error']) ?></div><?php endif; ?>
</div>
<form method="post" style="display:flex;gap:5px;align-items:center"><?= csrf_field() ?><input type="hidden" name="action" value="component_category"><input type="hidden" name="id" value="<?= (int) $c['id'] ?>"><select name="category_id" aria-label="Kategorie für <?= e($c['name']) ?>" style="max-width:170px;padding:5px"><option value="">Ohne Kategorie</option><?php foreach ($categories as $category): ?><option value="<?= (int) $category['id'] ?>" <?= (int) ($c['category_id'] ?? 0) === (int) $category['id'] ? 'selected' : '' ?>><?= e($category['name']) ?></option><?php endforeach; ?></select><input type="number" name="sort_order" value="<?= (int) $c['sort_order'] ?>" title="Reihenfolge" aria-label="Reihenfolge für <?= e($c['name']) ?>" style="width:58px;padding:4px"><button class="btn secondary" type="submit" style="padding:4px 8px;font-size:.78rem">Speichern</button></form>
<form method="post"><?= csrf_field() ?><input type="hidden" name="action" value="component_enabled"><input type="hidden" name="id" value="<?= (int) $c['id'] ?>"><input type="hidden" name="enabled" value="<?= empty($c['is_enabled']) ? '1' : '0' ?>"><button class="btn secondary" type="submit" style="padding:4px 8px;font-size:.78rem"><?= empty($c['is_enabled']) ? 'Aktivieren' : 'Deaktivieren' ?></button></form>
<form method="post"><?= csrf_field() ?><input type="hidden" name="action" value="component_internal"><input type="hidden" name="id" value="<?= (int) $c['id'] ?>"><input type="hidden" name="internal" value="<?= empty($c['is_internal']) ? '1' : '0' ?>"><button class="btn secondary" type="submit" style="padding:4px 8px;font-size:.78rem" title="Intern: wird geprüft, steht aber nicht auf der öffentlichen Statusseite"><?= empty($c['is_internal']) ? 'Intern machen' : 'Öffentlich machen' ?></button></form>
<?php if ($checkInfo): ?>
<div style="display:flex;gap:8px;align-items:center;white-space:nowrap">
<span style="color:<?= e($st[1]) ?>;font-weight:600"><?= e($st[0]) ?></span>
+60 -25
View File
@@ -309,7 +309,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
if (!$errors) {
// Sperre freigeben: die aktive Bearbeitung ist abgeschlossen, ein anderer
// Mitarbeiter kann das Ticket sofort uebernehmen, ohne auf das Timeout zu warten.
$pdo->prepare('UPDATE tickets SET locked_by = NULL, locked_at = NULL WHERE id = ?')->execute([$ticket['id']]);
$pdo->prepare('UPDATE tickets SET locked_by = NULL, locked_at = NULL, updated_at = updated_at WHERE id = ?')->execute([$ticket['id']]);
finalize_work_session($pdo, (int) $ticket['id'], (int) $staff['id']);
webhook_dispatch($pdo, 'ticket.replied', (int) $ticket['id'], ['author' => 'staff', 'internal' => (bool) $isNote]);
flash('success', $isNote ? 'Notiz hinzugefügt.' : 'Antwort gesendet.');
@@ -415,7 +415,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
}
if ($action === 'release_lock') {
$pdo->prepare('UPDATE tickets SET locked_by = NULL, locked_at = NULL WHERE id = ?')->execute([$ticket['id']]);
$pdo->prepare('UPDATE tickets SET locked_by = NULL, locked_at = NULL, updated_at = updated_at WHERE id = ?')->execute([$ticket['id']]);
finalize_work_session($pdo, (int) $ticket['id'], (int) $staff['id']);
log_activity($pdo, $ticket['id'], $staff['id'], 'Sperre manuell aufgehoben');
flash('success', 'Sperre aufgehoben.');
@@ -432,7 +432,7 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
redirect('/staff/tickets.php');
}
$unlockedFromName = $_POST['locked_by_name'] ?? 'einem anderen Mitarbeiter';
$pdo->prepare('UPDATE tickets SET locked_by = NULL, locked_at = NULL WHERE id = ?')->execute([$ticket['id']]);
$pdo->prepare('UPDATE tickets SET locked_by = NULL, locked_at = NULL, updated_at = updated_at WHERE id = ?')->execute([$ticket['id']]);
log_activity($pdo, $ticket['id'], $staff['id'], "Sperre von \"{$unlockedFromName}\" durch Admin aufgehoben");
flash('success', "Sperre aufgehoben – Sie können das Ticket jetzt öffnen.");
redirect('/staff/ticket_view.php?n=' . urlencode($number));
@@ -1049,14 +1049,15 @@ $sourceLabel = $sourceLabels[$ticket['source']] ?? $ticket['source'];
<div class="thread-comments" style="border-top:1px solid var(--border-soft);background:var(--bg-soft)">
<!-- Vorhandene Kommentare -->
<?php foreach ($threadComments as $co): ?>
<div style="display:flex;gap:8px;padding:8px 16px;border-bottom:1px solid var(--border-soft)">
<div style="width:26px;height:26px;border-radius:50%;background:#6366f1;color:#fff;display:flex;align-items:center;justify-content:center;font-size:.72rem;font-weight:700;flex-shrink:0;margin-top:1px">
<div style="display:flex;gap:12px;margin:12px 16px;padding:12px 16px;background:#eef2ff;border:1px solid #c7d2fe;border-left:4px solid #6366f1;border-radius:10px">
<div style="width:34px;height:34px;border-radius:50%;background:#6366f1;color:#fff;display:flex;align-items:center;justify-content:center;font-size:.85rem;font-weight:700;flex-shrink:0">
<?= mb_strtoupper(mb_substr($co['staff_name'], 0, 1)) ?>
</div>
<div style="flex:1;min-width:0">
<div style="display:flex;align-items:baseline;gap:8px;margin-bottom:3px">
<span style="font-size:.8rem;font-weight:600;color:var(--ink)"><?= e($co['staff_name']) ?></span>
<span style="font-size:.72rem;color:var(--muted)"><?= format_dt($co['created_at']) ?></span>
<div style="display:flex;align-items:center;gap:8px;margin-bottom:6px;flex-wrap:wrap">
<span style="font-size:.9rem;font-weight:700;color:#1e1b4b"><?= e($co['staff_name']) ?></span>
<span style="font-size:.66rem;font-weight:700;letter-spacing:.04em;text-transform:uppercase;color:#4338ca;background:#e0e7ff;border-radius:6px;padding:2px 7px">Kommentar</span>
<span style="font-size:.75rem;color:var(--muted)"><?= format_dt($co['created_at']) ?></span>
<?php if ($co['staff_id'] === $staff['id'] || $staff['role'] === 'admin'): ?>
<form method="post" style="margin-left:auto">
<?= csrf_field() ?>
@@ -1066,13 +1067,13 @@ $sourceLabel = $sourceLabels[$ticket['source']] ?? $ticket['source'];
</form>
<?php endif; ?>
</div>
<div style="font-size:.83rem;color:var(--ink-soft);line-height:1.5"><?= nl2br(e($co['message'])) ?></div>
<div style="font-size:.92rem;color:var(--ink);line-height:1.55"><?= nl2br(e($co['message'])) ?></div>
</div>
</div>
<?php endforeach; ?>
<!-- Kommentar hinzufügen -->
<div class="comment-input-wrap" id="comment-wrap-<?= $msg['id'] ?>" style="display:none;padding:8px 16px">
<form method="post" style="display:flex;gap:8px;align-items:flex-start">
<form method="post" style="display:flex;gap:8px;align-items:flex-start;flex:1 1 100%;width:100%;min-width:0">
<?= csrf_field() ?>
<input type="hidden" name="action" value="add_comment">
<input type="hidden" name="thread_id" value="<?= $msg['id'] ?>">
@@ -1080,7 +1081,7 @@ $sourceLabel = $sourceLabels[$ticket['source']] ?? $ticket['source'];
<?= mb_strtoupper(mb_substr($staff['name'], 0, 1)) ?>
</div>
<textarea name="comment_message" placeholder="Interner Kommentar…" required
style="flex:1;min-height:60px;resize:vertical;font-size:.83rem;padding:6px 10px"
rows="3" style="flex:1 1 auto;width:100%;min-width:0;min-height:90px;resize:vertical;font-size:.88rem;padding:8px 10px"
onkeydown="if(event.ctrlKey&&event.key==='Enter'){this.closest('form').submit()}"></textarea>
<div style="display:flex;flex-direction:column;gap:4px">
<button type="submit" class="btn" style="font-size:.78rem;padding:5px 10px;white-space:nowrap">Senden</button>
@@ -1196,6 +1197,7 @@ $sourceLabel = $sourceLabels[$ticket['source']] ?? $ticket['source'];
<option value="answered">Beantwortet</option>
<option value="pending">Wartet auf Kunde</option>
<option value="resolved">Gelöst</option>
<option value="closed">Geschlossen</option>
</select>
</div>
</div>
@@ -1219,17 +1221,23 @@ $sourceLabel = $sourceLabels[$ticket['source']] ?? $ticket['source'];
<div class="card">
<h3>Aktivität</h3>
<?php foreach ($activity as $log): ?>
<div class="muted" style="font-size:.82rem;padding:4px 0"><?= format_dt($log['created_at']) ?> — <?= e($log['staff_name'] ?? 'System') ?>: <?= e($log['action']) ?></div>
<div class="muted" style="font-size:.82rem;padding:4px 0"><?= format_dt($log['created_at']) ?> — <?= e($log['staff_name'] ?? 'System') ?>: <span><?= e($log['action']) ?></span></div>
<?php endforeach; ?>
</div>
<?php endif; ?>
</div>
<div style="margin-top:20px;display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:16px;align-items:start">
<div style="margin-top:20px;display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:16px;align-items:start">
<nav class="tv-tabs" id="tv-tabs" role="tablist" aria-label="Ticket-Details">
<button type="button" role="tab" class="tv-tab active" data-tab="details">Details</button>
<button type="button" role="tab" class="tv-tab" data-tab="manage">Organisation</button>
<button type="button" role="tab" class="tv-tab" data-tab="more">Mehr</button>
</nav>
<!-- Ticket-Eigenschaften als horizontale Leiste -->
<div class="card" style="padding:14px 20px">
<div class="card" data-ptab="details" style="padding:14px 20px">
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:12px; padding-bottom:10px; border-bottom:1px solid var(--border-soft)">
<span style="font-size:.82rem; font-weight:700; color:var(--ink)">Eigenschaften</span>
<?php if (!$lockedByOther): ?>
@@ -1306,7 +1314,7 @@ $sourceLabel = $sourceLabels[$ticket['source']] ?? $ticket['source'];
</script>
</div>
<div class="card" style="padding:14px 20px">
<div class="card" data-ptab="manage" style="padding:14px 20px">
<div style="display:flex;justify-content:space-between;align-items:center;gap:8px"><span style="font-size:.71rem;font-weight:700;text-transform:uppercase;letter-spacing:.06em;color:var(--muted)">Zugehörige Probleme</span><?php if ($staff['role'] === 'admin'): ?><a class="btn secondary" style="padding:3px 8px;font-size:.75rem" href="/staff/problems.php">Verwalten</a><?php endif; ?></div>
<?php if ($ticketProblems): foreach ($ticketProblems as $problemItem): ?>
<div style="padding:8px 0;border-top:1px solid var(--border-soft);margin-top:8px"><a href="/staff/problems.php?id=<?= (int) $problemItem['id'] ?>"><strong><?= e(ticket_problem_number((int) $problemItem['id'])) ?> · <?= e($problemItem['title']) ?></strong></a><div class="muted" style="font-size:.78rem;margin-top:3px"><?= e(ticket_problem_statuses()[$problemItem['status']] ?? $problemItem['status']) ?></div><?php if (!empty($problemItem['workaround'])): ?><div style="font-size:.82rem;margin-top:5px"><strong>Workaround:</strong> <?= nl2br(e((string) $problemItem['workaround'])) ?></div><?php endif; ?><?php if ($staff['role'] === 'admin'): ?><form method="post" style="margin-top:6px"><?= csrf_field() ?><input type="hidden" name="action" value="problem_unlink"><input type="hidden" name="problem_id" value="<?= (int) $problemItem['id'] ?>"><button class="btn secondary" type="submit" style="padding:2px 8px;font-size:.75rem">Trennen</button></form><?php endif; ?></div>
@@ -1315,7 +1323,7 @@ $sourceLabel = $sourceLabels[$ticket['source']] ?? $ticket['source'];
</div>
<!-- SLA + Individuelle Felder -->
<div class="card" style="padding:16px 20px">
<div class="card" data-ptab="details" style="padding:16px 20px">
<div style="font-size:.71rem; font-weight:700; text-transform:uppercase; letter-spacing:.06em; color:var(--muted); margin-bottom:10px">SLA-Fristen</div>
<?php if ($slaPaused): ?><div style="padding:8px 10px;margin-bottom:12px;border-radius:7px;background:#f1f5f9;color:#475569;font-size:.82rem"><strong>SLA pausiert</strong> seit <?= e(format_dt((string) $ticket['sla_paused_at'])) ?> – die Fristen laufen bei „<?= e($statusLabel) ?>“ nicht weiter.</div><?php endif; ?>
<div style="display:grid; grid-template-columns:1fr 1fr; gap:12px">
@@ -1359,7 +1367,7 @@ $sourceLabel = $sourceLabels[$ticket['source']] ?? $ticket['source'];
<?php $csatRow = null; try { $csatRow = csat_for_ticket($pdo, (int) $ticket['id']); } catch (Throwable $e) {} ?>
<?php if ($csatRow): ?>
<!-- Zufriedenheit des Kunden -->
<div class="card" style="padding:14px 20px">
<div class="card" data-ptab="details" style="padding:14px 20px">
<span style="font-size:.71rem; font-weight:700; text-transform:uppercase; letter-spacing:.06em; color:var(--muted)">Kundenzufriedenheit</span>
<?php if ($csatRow['rating'] !== null): ?>
<div style="font-size:1.25rem;color:#f5b301;margin-top:4px"><?= e(csat_stars((int) $csatRow['rating'])) ?> <span style="font-size:.85rem;color:var(--muted)"><?= (int) $csatRow['rating'] ?> von 5</span></div>
@@ -1372,7 +1380,7 @@ $sourceLabel = $sourceLabels[$ticket['source']] ?? $ticket['source'];
<?php endif; ?>
<!-- Wiedervorlage und Beobachter -->
<div class="card" style="padding:16px 20px">
<div class="card" data-ptab="manage" style="padding:16px 20px">
<span style="font-size:.71rem; font-weight:700; text-transform:uppercase; letter-spacing:.06em; color:var(--muted)">Wiedervorlage</span>
<?php if ($snoozedUntil): ?>
<p style="font-size:.84rem;margin:8px 0">💤 Bis <strong><?= e(date('d.m.Y H:i', (int) strtotime($snoozedUntil))) ?> Uhr</strong> ausgeblendet, dann kommt es zurück.</p>
@@ -1402,7 +1410,7 @@ $sourceLabel = $sourceLabels[$ticket['source']] ?? $ticket['source'];
<?php if ($similar['articles'] || $similar['tickets']): ?>
<!-- Ähnliche Artikel und Tickets -->
<div class="card" style="padding:16px 20px">
<div class="card" data-ptab="more" style="padding:16px 20px">
<span style="font-size:.71rem; font-weight:700; text-transform:uppercase; letter-spacing:.06em; color:var(--muted)">Dazu passend</span>
<?php if ($similar['articles']): ?>
<div style="font-size:.76rem;color:var(--muted);margin:8px 0 2px">Wissensdatenbank</div>
@@ -1420,7 +1428,7 @@ $sourceLabel = $sourceLabels[$ticket['source']] ?? $ticket['source'];
<?php endif; ?>
<!-- Kollaboratoren -->
<div class="card" style="padding:16px 20px">
<div class="card" data-ptab="manage" style="padding:16px 20px">
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:10px">
<span style="font-size:.71rem; font-weight:700; text-transform:uppercase; letter-spacing:.06em; color:var(--muted)">Kollaboratoren</span>
<button type="button" id="collab-toggle" onclick="var f=document.getElementById('collab-form');f.style.display=f.style.display==='none'?'':'none'" style="background:none;border:1px solid var(--border);border-radius:5px;padding:2px 8px;font-size:.73rem;cursor:pointer;color:var(--muted)">+ hinzufügen</button>
@@ -1453,7 +1461,7 @@ $sourceLabel = $sourceLabels[$ticket['source']] ?? $ticket['source'];
</div>
<!-- Selten: Zusammenführen + Löschen -->
<details class="card" style="padding:0; overflow:hidden">
<details class="card" data-ptab="more" style="padding:0; overflow:hidden">
<summary style="padding:12px 16px; cursor:pointer; font-size:.79rem; font-weight:600; color:var(--muted); display:flex; align-items:center; justify-content:space-between; user-select:none; list-style:none">
<span>Weitere Aktionen</span><span style="font-size:.68rem; transition:transform .2s">▾</span>
</summary>
@@ -1508,7 +1516,7 @@ $sourceLabel = $sourceLabels[$ticket['source']] ?? $ticket['source'];
</details>
<!-- Zeiterfassung -->
<div class="card" style="padding:16px 20px">
<div class="card" data-ptab="manage" style="padding:16px 20px">
<div style="font-size:.71rem; font-weight:700; text-transform:uppercase; letter-spacing:.06em; color:var(--muted); margin-bottom:10px">
&#9201; Zeiterfassung
<?php if ($totalMinutes > 0): ?>
@@ -1560,9 +1568,9 @@ $sourceLabel = $sourceLabels[$ticket['source']] ?? $ticket['source'];
$alreadyBanned = (bool)$bs->fetchColumn();
}
?>
<?= plugin_filter('ticket.view.sidebar', '', ['ticketId' => (int) $ticket['id'], 'ticket' => $ticket, 'staff' => $staff]) /* Plugins liefern fertiges HTML (vertrauenswürdiger Code) */ ?>
<div data-ptab="more"><?= plugin_filter('ticket.view.sidebar', '', ['ticketId' => (int) $ticket['id'], 'ticket' => $ticket, 'staff' => $staff]) /* Plugins liefern fertiges HTML (vertrauenswürdiger Code) */ ?></div>
<?php if ($requesterEmail): ?>
<div class="card" style="margin-top:16px;padding:14px 16px">
<div class="card" data-ptab="details" style="margin-top:16px;padding:14px 16px">
<div style="font-size:.72rem;font-weight:700;text-transform:uppercase;letter-spacing:.06em;color:var(--muted);margin-bottom:10px">Absender</div>
<div style="font-size:.83rem;margin-bottom:8px;word-break:break-all"><?= e($requesterEmail) ?></div>
<?php if ($alreadyBanned): ?>
@@ -1591,7 +1599,7 @@ $sourceLabel = $sourceLabels[$ticket['source']] ?? $ticket['source'];
<!-- Kundenbewertung -->
<?php if (get_setting('ticket_ratings_enabled','1')==='1' && in_array($ticket['status'],['resolved','closed'])): ?>
<div class="card" style="margin-top:16px; padding:18px 20px">
<div class="card" data-ptab="details" style="margin-top:16px; padding:18px 20px">
<div style="font-size:.72rem; font-weight:700; text-transform:uppercase; letter-spacing:.06em; color:var(--muted); margin-bottom:12px">Kundenbewertung</div>
<?php if ($ticketRating): ?>
<div style="display:flex; align-items:center; gap:10px; margin-bottom:8px">
@@ -1879,4 +1887,31 @@ function toggleRaw(id) {
})();
</script>
<style>
.tv-tabs { grid-column:1 / -1; display:flex; gap:2px; margin:0; border-bottom:1px solid var(--border,#e2e8f0); overflow-x:auto; overflow-y:hidden; }
.tv-tab { background:none; border:0; border-bottom:3px solid transparent; padding:9px 14px; margin-bottom:-1px; cursor:pointer; font:inherit; font-weight:600; color:var(--muted,#64748b); border-radius:8px 8px 0 0; white-space:nowrap; }
.tv-tab:hover { color:var(--text,#0f172a); background:var(--surface,#fff); }
.tv-tab.active { color:var(--primary); border-bottom-color:var(--primary); background:var(--surface,#fff); }
[data-ptab][hidden] { display:none !important; }
</style>
<script>
// Reiter unter dem Verlauf: zeigt nur die Karten der gewählten Gruppe. Die Wahl bleibt beim Neuladen (z. B. nach dem Speichern) erhalten.
(function () {
var bar = document.getElementById('tv-tabs');
if (!bar) return;
var tabs = bar.querySelectorAll('.tv-tab');
function show(name) {
var found = false;
tabs.forEach(function (b) { var on = b.dataset.tab === name; if (on) found = true; b.classList.toggle('active', on); b.setAttribute('aria-selected', on ? 'true' : 'false'); });
if (!found) return show('details');
document.querySelectorAll('[data-ptab]').forEach(function (el) { el.hidden = el.dataset.ptab !== name; });
try { sessionStorage.setItem('tv_tab', name); } catch (e) {}
}
tabs.forEach(function (b) { b.addEventListener('click', function () { show(b.dataset.tab); }); });
var saved = 'details';
try { saved = sessionStorage.getItem('tv_tab') || 'details'; } catch (e) {}
show(saved);
})();
</script>
<?php require __DIR__ . '/_footer.php'; ?>
+1 -1
View File
@@ -304,7 +304,7 @@ $countStmt->execute($params);
$totalCount = (int)$countStmt->fetchColumn();
$totalPages = max(1, (int)ceil($totalCount / $perPage));
$sql .= ' ORDER BY t.created_at DESC LIMIT ' . $perPage . ' OFFSET ' . $offset;
$sql .= ' ORDER BY t.updated_at DESC, t.created_at DESC LIMIT ' . $perPage . ' OFFSET ' . $offset;
$stmt = $pdo->prepare($sql);
$stmt->execute($params);