Upload via GUI (364 Dateien)
This commit is contained in:
@@ -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; ?>
|
||||
|
||||
@@ -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 ? '&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'; ?>
|
||||
|
||||
+629
-354
File diff suppressed because it is too large
Load Diff
@@ -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>
|
||||
|
||||
@@ -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">
|
||||
⏱ 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'; ?>
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user