339 lines
16 KiB
PHP
339 lines
16 KiB
PHP
<?php
|
||
require_once __DIR__ . '/../includes/bootstrap.php';
|
||
$staff = require_admin_or_permission('manage_escalations');
|
||
$pdo = get_db();
|
||
|
||
$priorities = $pdo->query('SELECT * FROM priorities ORDER BY sort_order')->fetchAll();
|
||
$staffList = $pdo->query("SELECT id, name FROM staff WHERE is_active = 1 ORDER BY name")->fetchAll();
|
||
$depts = $pdo->query('SELECT id, name FROM departments ORDER BY name')->fetchAll();
|
||
|
||
$triggerTypes = [
|
||
'no_reply' => 'Keine Mitarbeiter-Antwort seit X Stunden',
|
||
'no_update' => 'Kein Update seit X Stunden',
|
||
'overdue' => 'Ticket ist überfällig (SLA überschritten)',
|
||
'open_too_long' => 'Ticket offen seit X Stunden',
|
||
];
|
||
|
||
$errors = [];
|
||
|
||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||
verify_csrf();
|
||
$action = $_POST['action'] ?? '';
|
||
|
||
if ($action === 'save') {
|
||
$id = (int)($_POST['id'] ?? 0);
|
||
$name = trim($_POST['name'] ?? '');
|
||
$isActive = !empty($_POST['is_active']) ? 1 : 0;
|
||
$triggerType = $_POST['trigger_type'] ?? 'no_reply';
|
||
$triggerHours = max(1, (int)($_POST['trigger_hours'] ?? 24));
|
||
$condStatus = trim($_POST['condition_status'] ?? '');
|
||
$condPrioId = ($_POST['condition_priority_id'] ?? '') !== '' ? (int)$_POST['condition_priority_id'] : null;
|
||
$condDeptId = ($_POST['condition_dept_id'] ?? '') !== '' ? (int)$_POST['condition_dept_id'] : null;
|
||
$actPrioId = ($_POST['action_set_priority_id'] ?? '') !== '' ? (int)$_POST['action_set_priority_id'] : null;
|
||
$actStaffId = ($_POST['action_assign_staff_id'] ?? '') !== '' ? (int)$_POST['action_assign_staff_id'] : null;
|
||
$actStatus = $_POST['action_set_status'] ?? '';
|
||
$actNotAdmin = !empty($_POST['action_notify_admin']) ? 1 : 0;
|
||
$actNotStaff = !empty($_POST['action_notify_staff']) ? 1 : 0;
|
||
$sortOrder = (int)($_POST['sort_order'] ?? 0);
|
||
|
||
if ($name === '') $errors[] = 'Bitte einen Namen angeben.';
|
||
if (!array_key_exists($triggerType, $triggerTypes)) $errors[] = 'Ungültiger Trigger.';
|
||
|
||
if (!$errors) {
|
||
$data = [$name, $isActive, $triggerType, $triggerHours, $condStatus ?: null,
|
||
$condPrioId, $condDeptId, $actPrioId, $actStaffId,
|
||
$actStatus ?: null, $actNotAdmin, $actNotStaff, $sortOrder];
|
||
if ($id) {
|
||
$pdo->prepare(
|
||
'UPDATE escalation_rules SET name=?,is_active=?,trigger_type=?,trigger_hours=?,
|
||
condition_status=?,condition_priority_id=?,condition_dept_id=?,
|
||
action_set_priority_id=?,action_assign_staff_id=?,action_set_status=?,
|
||
action_notify_admin=?,action_notify_staff=?,sort_order=? WHERE id=?'
|
||
)->execute(array_merge($data, [$id]));
|
||
audit_log($staff['id'], "Eskalationsregel geändert: $name");
|
||
} else {
|
||
$pdo->prepare(
|
||
'INSERT INTO escalation_rules (name,is_active,trigger_type,trigger_hours,
|
||
condition_status,condition_priority_id,condition_dept_id,
|
||
action_set_priority_id,action_assign_staff_id,action_set_status,
|
||
action_notify_admin,action_notify_staff,sort_order,created_at)
|
||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?)'
|
||
)->execute(array_merge($data, [db_now()]));
|
||
audit_log($staff['id'], "Eskalationsregel erstellt: $name");
|
||
}
|
||
flash('success', 'Eskalationsregel gespeichert.');
|
||
redirect('/staff/escalation_rules.php');
|
||
}
|
||
}
|
||
|
||
if ($action === 'delete') {
|
||
$id = (int)($_POST['id'] ?? 0);
|
||
$pdo->prepare('DELETE FROM escalation_rules WHERE id = ?')->execute([$id]);
|
||
$pdo->prepare('DELETE FROM escalation_log WHERE rule_id = ?')->execute([$id]);
|
||
audit_log($staff['id'], "Eskalationsregel gelöscht: #$id");
|
||
flash('success', 'Regel gelöscht.');
|
||
redirect('/staff/escalation_rules.php');
|
||
}
|
||
|
||
if ($action === 'toggle') {
|
||
$id = (int)($_POST['id'] ?? 0);
|
||
$pdo->prepare('UPDATE escalation_rules SET is_active = 1 - is_active WHERE id = ?')->execute([$id]);
|
||
redirect('/staff/escalation_rules.php');
|
||
}
|
||
}
|
||
|
||
try {
|
||
$rules = $pdo->query(
|
||
'SELECT er.*,
|
||
p.name AS prio_name,
|
||
s.name AS staff_name,
|
||
d.name AS dept_name,
|
||
cp.name AS cond_prio_name,
|
||
(SELECT COUNT(*) FROM escalation_log el WHERE el.rule_id = er.id) AS applied_count
|
||
FROM escalation_rules er
|
||
LEFT JOIN priorities p ON p.id = er.action_set_priority_id
|
||
LEFT JOIN staff s ON s.id = er.action_assign_staff_id
|
||
LEFT JOIN departments d ON d.id = er.condition_dept_id
|
||
LEFT JOIN priorities cp ON cp.id = er.condition_priority_id
|
||
ORDER BY er.sort_order, er.id'
|
||
)->fetchAll();
|
||
} catch (Throwable $e) {
|
||
$rules = [];
|
||
}
|
||
|
||
$editId = (int)($_GET['edit'] ?? 0);
|
||
$editRule = null;
|
||
if ($editId) {
|
||
foreach ($rules as $r) { if ($r['id'] === $editId) { $editRule = $r; break; } }
|
||
}
|
||
|
||
$pageTitle = 'Eskalationsregeln';
|
||
$activePage = 'ticket_rules';
|
||
require __DIR__ . '/_header.php';
|
||
?>
|
||
|
||
<div class="topbar">
|
||
<h1>Eskalationsregeln</h1>
|
||
<a href="?new=1" class="btn">+ Neue Regel</a>
|
||
</div>
|
||
|
||
<p class="muted" style="margin-bottom:20px;font-size:.85rem">
|
||
Eskalationsregeln werden stündlich per Cron geprüft (<code>cron/check_escalations.php</code>).
|
||
Jede Regel wird pro Ticket nur einmal angewendet.
|
||
</p>
|
||
|
||
<?php foreach ($errors as $e): ?><div class="flash error"><?= e($e) ?></div><?php endforeach; ?>
|
||
|
||
<!-- ── Formular ──────────────────────────────────────────────────────────────── -->
|
||
<?php if (isset($_GET['new']) || $editRule): ?>
|
||
<?php $r = $editRule; ?>
|
||
<div class="card" style="margin-bottom:20px">
|
||
<h3><?= $r ? 'Regel bearbeiten' : 'Neue Eskalationsregel' ?></h3>
|
||
<form method="post">
|
||
<?= csrf_field() ?>
|
||
<input type="hidden" name="action" value="save">
|
||
<?php if ($r): ?><input type="hidden" name="id" value="<?= $r['id'] ?>"><?php endif; ?>
|
||
|
||
<div style="display:grid;grid-template-columns:1fr 1fr;gap:16px">
|
||
<div>
|
||
<label>Name <span style="color:var(--danger)">*</span></label>
|
||
<input type="text" name="name" value="<?= e($r['name'] ?? '') ?>" required placeholder="z.B. Hohe Priorität nach 8h unbeantwortet">
|
||
</div>
|
||
<div style="display:flex;align-items:flex-end;gap:16px">
|
||
<div>
|
||
<label>Sortierung</label>
|
||
<input type="number" name="sort_order" value="<?= (int)($r['sort_order'] ?? 0) ?>" style="width:80px">
|
||
</div>
|
||
<label style="display:flex;align-items:center;gap:8px;font-weight:400;cursor:pointer;margin-bottom:1px">
|
||
<input type="checkbox" name="is_active" value="1" style="width:auto" <?= ($r['is_active'] ?? 1) ? 'checked' : '' ?>>
|
||
Aktiv
|
||
</label>
|
||
</div>
|
||
</div>
|
||
|
||
<div style="margin-top:20px;padding-top:16px;border-top:1px solid var(--border-soft)">
|
||
<div style="font-size:.72rem;font-weight:700;text-transform:uppercase;letter-spacing:.06em;color:var(--muted);margin-bottom:12px">Auslöser</div>
|
||
<div style="display:grid;grid-template-columns:2fr 1fr;gap:12px">
|
||
<div>
|
||
<label>Trigger</label>
|
||
<select name="trigger_type" id="trigger-type" onchange="updateTriggerHoursVisibility()">
|
||
<?php foreach ($triggerTypes as $tv => $tl): ?>
|
||
<option value="<?= $tv ?>" <?= ($r['trigger_type'] ?? '') === $tv ? 'selected' : '' ?>><?= e($tl) ?></option>
|
||
<?php endforeach; ?>
|
||
</select>
|
||
</div>
|
||
<div id="trigger-hours-wrap">
|
||
<label>Stunden</label>
|
||
<input type="number" name="trigger_hours" value="<?= (int)($r['trigger_hours'] ?? 24) ?>" min="1" max="8760">
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div style="margin-top:20px;padding-top:16px;border-top:1px solid var(--border-soft)">
|
||
<div style="font-size:.72rem;font-weight:700;text-transform:uppercase;letter-spacing:.06em;color:var(--muted);margin-bottom:12px">Bedingungen <span style="font-weight:400;text-transform:none;letter-spacing:0">(optional – leer = alle Tickets)</span></div>
|
||
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:12px">
|
||
<div>
|
||
<label>Nur Status</label>
|
||
<input type="text" name="condition_status" value="<?= e($r['condition_status'] ?? '') ?>" placeholder="z.B. open,pending">
|
||
<small class="muted">Kommagetrennt</small>
|
||
</div>
|
||
<div>
|
||
<label>Nur Priorität</label>
|
||
<select name="condition_priority_id">
|
||
<option value="">Alle</option>
|
||
<?php foreach ($priorities as $p): ?>
|
||
<option value="<?= $p['id'] ?>" <?= ($r['condition_priority_id'] ?? '') == $p['id'] ? 'selected' : '' ?>><?= e($p['name']) ?></option>
|
||
<?php endforeach; ?>
|
||
</select>
|
||
</div>
|
||
<div>
|
||
<label>Nur Abteilung</label>
|
||
<select name="condition_dept_id">
|
||
<option value="">Alle</option>
|
||
<?php foreach ($depts as $d): ?>
|
||
<option value="<?= $d['id'] ?>" <?= ($r['condition_dept_id'] ?? '') == $d['id'] ? 'selected' : '' ?>><?= e($d['name']) ?></option>
|
||
<?php endforeach; ?>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div style="margin-top:20px;padding-top:16px;border-top:1px solid var(--border-soft)">
|
||
<div style="font-size:.72rem;font-weight:700;text-transform:uppercase;letter-spacing:.06em;color:var(--muted);margin-bottom:12px">Aktionen</div>
|
||
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:12px;margin-bottom:12px">
|
||
<div>
|
||
<label>Priorität setzen</label>
|
||
<select name="action_set_priority_id">
|
||
<option value="">– keine Änderung –</option>
|
||
<?php foreach ($priorities as $p): ?>
|
||
<option value="<?= $p['id'] ?>" <?= ($r['action_set_priority_id'] ?? '') == $p['id'] ? 'selected' : '' ?>><?= e($p['name']) ?></option>
|
||
<?php endforeach; ?>
|
||
</select>
|
||
</div>
|
||
<div>
|
||
<label>Mitarbeiter zuweisen</label>
|
||
<select name="action_assign_staff_id">
|
||
<option value="">– keine Änderung –</option>
|
||
<?php foreach ($staffList as $s): ?>
|
||
<option value="<?= $s['id'] ?>" <?= ($r['action_assign_staff_id'] ?? '') == $s['id'] ? 'selected' : '' ?>><?= e($s['name']) ?></option>
|
||
<?php endforeach; ?>
|
||
</select>
|
||
</div>
|
||
<div>
|
||
<label>Status setzen</label>
|
||
<select name="action_set_status">
|
||
<option value="">– keine Änderung –</option>
|
||
<?php foreach (['open'=>'Offen','pending'=>'Wartet auf Kunde','on_hold'=>'Pausiert'] as $sv => $sl): ?>
|
||
<option value="<?= $sv ?>" <?= ($r['action_set_status'] ?? '') === $sv ? 'selected' : '' ?>><?= $sl ?></option>
|
||
<?php endforeach; ?>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
<div style="display:flex;gap:20px;flex-wrap:wrap">
|
||
<label style="display:flex;align-items:center;gap:8px;font-weight:400;cursor:pointer">
|
||
<input type="checkbox" name="action_notify_admin" value="1" style="width:auto" <?= ($r['action_notify_admin'] ?? 1) ? 'checked' : '' ?>>
|
||
Admin benachrichtigen
|
||
</label>
|
||
<label style="display:flex;align-items:center;gap:8px;font-weight:400;cursor:pointer">
|
||
<input type="checkbox" name="action_notify_staff" value="1" style="width:auto" <?= !empty($r['action_notify_staff']) ? 'checked' : '' ?>>
|
||
Zugewiesenen Mitarbeiter benachrichtigen
|
||
</label>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="btn-row" style="margin-top:16px">
|
||
<button type="submit" class="btn">Speichern</button>
|
||
<a href="/staff/escalation_rules.php" class="btn secondary">Abbrechen</a>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
<?php endif; ?>
|
||
|
||
<!-- ── Regeln-Tabelle ─────────────────────────────────────────────────────────── -->
|
||
<div class="card">
|
||
<?php if (!$rules): ?>
|
||
<div class="empty-state">Noch keine Eskalationsregeln. Erstellen Sie eine neue Regel mit dem Button oben.</div>
|
||
<?php else: ?>
|
||
<table>
|
||
<thead>
|
||
<tr>
|
||
<th>Name</th>
|
||
<th>Auslöser</th>
|
||
<th>Aktionen</th>
|
||
<th style="text-align:right">Angewendet</th>
|
||
<th>Status</th>
|
||
<th></th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<?php foreach ($rules as $r): ?>
|
||
<tr>
|
||
<td style="font-weight:500"><?= e($r['name']) ?></td>
|
||
<td style="font-size:.82rem;color:var(--ink-soft)">
|
||
<?= e($triggerTypes[$r['trigger_type']] ?? $r['trigger_type']) ?>
|
||
<?php if ($r['trigger_type'] !== 'overdue'): ?>
|
||
<span class="muted">(<?= (int)$r['trigger_hours'] ?> Std)</span>
|
||
<?php endif; ?>
|
||
<?php if ($r['cond_prio_name'] || $r['dept_name']): ?>
|
||
<div style="font-size:.76rem;color:var(--muted)">
|
||
<?= $r['cond_prio_name'] ? '· Priorität: '.e($r['cond_prio_name']) : '' ?>
|
||
<?= $r['dept_name'] ? '· Abteilung: '.e($r['dept_name']) : '' ?>
|
||
</div>
|
||
<?php endif; ?>
|
||
</td>
|
||
<td style="font-size:.82rem">
|
||
<?php $acts = []; ?>
|
||
<?php if ($r['prio_name']): $acts[] = '↑ Priorität: '.e($r['prio_name']); endif; ?>
|
||
<?php if ($r['staff_name']): $acts[] = '→ '.e($r['staff_name']); endif; ?>
|
||
<?php if ($r['action_set_status']): $acts[] = '⚑ '.e($r['action_set_status']); endif; ?>
|
||
<?php if ($r['action_notify_admin']): $acts[] = '📧 Admin'; endif; ?>
|
||
<?php if ($r['action_notify_staff']): $acts[] = '📧 Mitarbeiter'; endif; ?>
|
||
<?= implode(', ', $acts) ?: '<span class="muted">Nur benachrichtigen</span>' ?>
|
||
</td>
|
||
<td style="text-align:right;color:var(--muted);font-size:.82rem"><?= (int)$r['applied_count'] ?>×</td>
|
||
<td>
|
||
<span style="display:inline-flex;align-items:center;gap:4px;font-size:.78rem;font-weight:600;color:<?= $r['is_active'] ? '#22c55e' : '#94a3b8' ?>">
|
||
<span style="width:7px;height:7px;border-radius:50%;background:currentColor"></span>
|
||
<?= $r['is_active'] ? 'Aktiv' : 'Inaktiv' ?>
|
||
</span>
|
||
</td>
|
||
<td style="white-space:nowrap">
|
||
<a href="?edit=<?= $r['id'] ?>" style="font-size:.82rem">Bearbeiten</a> ·
|
||
<form method="post" style="display:inline">
|
||
<?= csrf_field() ?>
|
||
<input type="hidden" name="action" value="toggle">
|
||
<input type="hidden" name="id" value="<?= $r['id'] ?>">
|
||
<button type="submit" style="background:none;border:none;cursor:pointer;color:var(--primary);font-size:.82rem;padding:0">
|
||
<?= $r['is_active'] ? 'Deaktivieren' : 'Aktivieren' ?>
|
||
</button>
|
||
</form> ·
|
||
<form method="post" style="display:inline" onsubmit="return confirm('Regel wirklich löschen?')">
|
||
<?= csrf_field() ?>
|
||
<input type="hidden" name="action" value="delete">
|
||
<input type="hidden" name="id" value="<?= $r['id'] ?>">
|
||
<button type="submit" style="background:none;border:none;cursor:pointer;color:var(--danger);font-size:.82rem;padding:0">Löschen</button>
|
||
</form>
|
||
</td>
|
||
</tr>
|
||
<?php endforeach; ?>
|
||
</tbody>
|
||
</table>
|
||
<?php endif; ?>
|
||
</div>
|
||
|
||
<div class="card" style="margin-top:20px">
|
||
<h3>Crontab-Eintrag</h3>
|
||
<pre style="background:var(--bg-soft);border:1px solid var(--border);border-radius:6px;padding:12px;font-size:.78rem;margin:0">0 * * * * php /pfad/zu/deskhub/cron/check_escalations.php</pre>
|
||
<p class="muted" style="font-size:.79rem;margin:8px 0 0">Stündliche Prüfung. Letzte Ausführung: <?= e(get_setting('cron_check_escalations_last_run', 'noch nie')) ?></p>
|
||
</div>
|
||
|
||
<script>
|
||
function updateTriggerHoursVisibility() {
|
||
var t = document.getElementById('trigger-type').value;
|
||
document.getElementById('trigger-hours-wrap').style.display = t === 'overdue' ? 'none' : 'block';
|
||
}
|
||
updateTriggerHoursVisibility();
|
||
</script>
|
||
|
||
<?php require __DIR__ . '/_footer.php'; ?>
|