Files
deskhub/DeskHub-Webseite/staff/escalation_rules.php
T
2026-10-16 19:12:21 +00:00

339 lines
16 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?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'; ?>