Files
deskhub/DeskHub-Webseite/staff/ticket_rules.php
T
2026-10-19 18:06:02 +00:00

429 lines
23 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';
require_once __DIR__ . '/_ui.php';
$staff = require_admin();
$pdo = get_db();
$departments = departments_for_select($pdo, array_map('intval', $pdo->query("SELECT action_value FROM ticket_rule_actions WHERE action_type = 'set_department'")->fetchAll(PDO::FETCH_COLUMN)));
$priorities = $pdo->query('SELECT * FROM priorities ORDER BY sort_order')->fetchAll();
$helpTopics = $pdo->query('SELECT * FROM help_topics ORDER BY name')->fetchAll();
$staffList = $pdo->query("SELECT id, name FROM staff WHERE is_active = 1 ORDER BY name")->fetchAll();
$cannedResponses = $pdo->query('SELECT id, title FROM canned_responses ORDER BY title')->fetchAll();
$fieldLabels = [
'subject' => 'Betreff',
'message' => 'Nachricht',
'requester_email' => 'Absender-E-Mail',
'requester_name' => 'Absender-Name',
'source' => 'Kanal (web/email)',
];
$operatorLabels = [
'contains' => 'enthält',
'not_contains' => 'enthält nicht',
'equals' => 'ist genau',
'starts_with' => 'beginnt mit',
'ends_with' => 'endet mit',
];
$actionLabels = [
'set_department' => 'Abteilung setzen',
'set_priority' => 'Priorität setzen',
'set_help_topic' => 'Hilfe-Thema setzen',
'assign_staff' => 'Mitarbeiter zuweisen',
'add_tag' => 'Tag hinzufügen',
'auto_reply' => 'Auto-Antwort senden (Textbaustein)',
'close' => 'Ticket schließen (wird angelegt, aber sofort geschlossen)',
'reject' => 'Ticket verwerfen (z.B. Spam)',
];
$errors = [];
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
verify_csrf();
$action = $_POST['action'] ?? '';
if ($action === 'create' || $action === 'update') {
$name = trim($_POST['name'] ?? '');
$matchType = ($_POST['match_type'] ?? 'all') === 'any' ? 'any' : 'all';
$isActive = ($_POST['status'] ?? 'active') === 'active' ? 1 : 0;
$notes = trim($_POST['notes'] ?? '');
$sortOrder = (int) ($_POST['sort_order'] ?? 0);
$condFields = $_POST['cond_field'] ?? [];
$condOperators = $_POST['cond_operator'] ?? [];
$condValues = $_POST['cond_value'] ?? [];
$actTypes = $_POST['action_type'] ?? [];
$actDept = $_POST['action_value_dept'] ?? [];
$actPrio = $_POST['action_value_prio'] ?? [];
$actTopic = $_POST['action_value_topic'] ?? [];
$actStaff = $_POST['action_value_staff'] ?? [];
$actTag = $_POST['action_value_tag'] ?? [];
$actCanned = $_POST['action_value_canned'] ?? [];
if ($name === '') $errors[] = 'Bitte einen Namen für die Regel angeben.';
$conditions = [];
foreach ($condFields as $i => $f) {
$val = trim($condValues[$i] ?? '');
if ($val === '') continue;
if (!isset($fieldLabels[$f]) || !isset($operatorLabels[$condOperators[$i] ?? ''])) continue;
$conditions[] = ['field' => $f, 'operator' => $condOperators[$i], 'value' => $val];
}
if (!$conditions) $errors[] = 'Bitte mindestens eine Bedingung mit einem Wert angeben.';
$actionsToSave = [];
foreach ($actTypes as $i => $t) {
if (!isset($actionLabels[$t])) continue;
$value = match ($t) {
'set_department' => $actDept[$i] ?? '',
'set_priority' => $actPrio[$i] ?? '',
'set_help_topic' => $actTopic[$i] ?? '',
'assign_staff' => $actStaff[$i] ?? '',
'add_tag' => trim($actTag[$i] ?? ''),
'auto_reply' => $actCanned[$i] ?? '',
'reject', 'close' => '1',
default => '',
};
if (!in_array($t, ['reject', 'close'], true) && $value === '') continue;
$actionsToSave[] = ['action_type' => $t, 'action_value' => $value];
}
if (!$actionsToSave) $errors[] = 'Bitte mindestens eine Aktion festlegen.';
if (!$errors) {
if ($action === 'create') {
$pdo->prepare('INSERT INTO ticket_rules (name, match_type, is_active, sort_order, notes, created_at, updated_at) VALUES (?,?,?,?,?,?,?)')
->execute([$name, $matchType, $isActive, $sortOrder, $notes ?: null, db_now(), db_now()]);
$ruleId = (int) $pdo->lastInsertId();
audit_log($staff['id'], "Ticket-Regel erstellt: {$name}");
flash('success', 'Ticket-Regel erstellt.');
} else {
$ruleId = (int) $_POST['id'];
$pdo->prepare('UPDATE ticket_rules SET name=?, match_type=?, is_active=?, sort_order=?, notes=?, updated_at=? WHERE id=?')
->execute([$name, $matchType, $isActive, $sortOrder, $notes ?: null, db_now(), $ruleId]);
$pdo->prepare('DELETE FROM ticket_rule_conditions WHERE rule_id = ?')->execute([$ruleId]);
$pdo->prepare('DELETE FROM ticket_rule_actions WHERE rule_id = ?')->execute([$ruleId]);
audit_log($staff['id'], "Ticket-Regel aktualisiert: {$name}");
flash('success', 'Ticket-Regel aktualisiert.');
}
$condStmt = $pdo->prepare('INSERT INTO ticket_rule_conditions (rule_id, field, operator, value) VALUES (?,?,?,?)');
foreach ($conditions as $c) {
$condStmt->execute([$ruleId, $c['field'], $c['operator'], $c['value']]);
}
$actStmt = $pdo->prepare('INSERT INTO ticket_rule_actions (rule_id, action_type, action_value) VALUES (?,?,?)');
foreach ($actionsToSave as $a) {
$actStmt->execute([$ruleId, $a['action_type'], $a['action_value']]);
}
redirect('/staff/ticket_rules.php');
}
}
if ($action === 'toggle_active') {
$id = (int) $_POST['id'];
$pdo->prepare('UPDATE ticket_rules SET is_active = 1 - is_active WHERE id = ?')->execute([$id]);
redirect('/staff/ticket_rules.php');
}
if ($action === 'delete') {
$id = (int) $_POST['id'];
$nameStmt = $pdo->prepare('SELECT name FROM ticket_rules WHERE id = ?');
$nameStmt->execute([$id]);
$ruleName = $nameStmt->fetchColumn();
$pdo->prepare('DELETE FROM ticket_rules WHERE id = ?')->execute([$id]);
audit_log($staff['id'], "Ticket-Regel gelöscht: {$ruleName}");
flash('success', 'Ticket-Regel gelöscht.');
redirect('/staff/ticket_rules.php');
}
if ($action === 'move' && in_array($_POST['dir'] ?? '', ['up', 'down'], true)) {
$id = (int) $_POST['id'];
$dir = $_POST['dir'];
$rules = $pdo->query('SELECT id, sort_order FROM ticket_rules ORDER BY sort_order, id')->fetchAll();
$idx = null;
foreach ($rules as $i => $r) { if ((int) $r['id'] === $id) { $idx = $i; break; } }
$swapIdx = $dir === 'up' ? $idx - 1 : $idx + 1;
if ($idx !== null && isset($rules[$swapIdx])) {
$a = $rules[$idx]; $b = $rules[$swapIdx];
$pdo->prepare('UPDATE ticket_rules SET sort_order = ? WHERE id = ?')->execute([$b['sort_order'], $a['id']]);
$pdo->prepare('UPDATE ticket_rules SET sort_order = ? WHERE id = ?')->execute([$a['sort_order'], $b['id']]);
}
redirect('/staff/ticket_rules.php');
}
}
$rules = $pdo->query('SELECT * FROM ticket_rules ORDER BY sort_order, id')->fetchAll();
foreach ($rules as &$r) {
$cs = $pdo->prepare('SELECT * FROM ticket_rule_conditions WHERE rule_id = ?');
$cs->execute([$r['id']]);
$r['conditions'] = $cs->fetchAll();
$as = $pdo->prepare('SELECT * FROM ticket_rule_actions WHERE rule_id = ?');
$as->execute([$r['id']]);
$r['actions'] = $as->fetchAll();
}
unset($r);
$editId = $_GET['edit'] ?? null;
$showForm = isset($_GET['new']) || $editId || ($errors && in_array($_POST['action'] ?? '', ['create', 'update'], true));
$editRule = null;
if ($editId) {
foreach ($rules as $r) { if ((int) $r['id'] === (int) $editId) { $editRule = $r; break; } }
}
$pageTitle = 'Ticket-Regeln';
$activePage = 'ticket_rules';
require __DIR__ . '/_header.php';
/** Rendert die Werte-Auswahl einer Aktionszeile für eine bereits gespeicherte Aktion (Bearbeiten-Modus) */
function render_action_value_selects(array $departments, array $priorities, array $helpTopics, array $staffList, array $cannedResponses, ?array $existing = null): void
{
$effectiveType = $existing['action_type'] ?? 'set_department'; // Default-Auswahl des Typ-Selects
$sel = fn($type) => ($effectiveType === $type) ? '' : 'style="display:none"';
$val = fn($type) => ($existing && $existing['action_type'] === $type) ? $existing['action_value'] : '';
?>
<select name="action_value_dept[]" class="rule-val rule-val-set_department" <?= $sel('set_department') ?>>
<option value="">– Abteilung wählen –</option>
<?php foreach ($departments as $d): ?>
<option value="<?= $d['id'] ?>" <?= $val('set_department') == $d['id'] ? 'selected' : '' ?>><?= e($d['name']) ?></option>
<?php endforeach; ?>
</select>
<select name="action_value_prio[]" class="rule-val rule-val-set_priority" <?= $sel('set_priority') ?>>
<option value="">– Priorität wählen –</option>
<?php foreach ($priorities as $p): ?>
<option value="<?= $p['id'] ?>" <?= $val('set_priority') == $p['id'] ? 'selected' : '' ?>><?= e($p['name']) ?></option>
<?php endforeach; ?>
</select>
<select name="action_value_topic[]" class="rule-val rule-val-set_help_topic" <?= $sel('set_help_topic') ?>>
<option value="">– Hilfe-Thema wählen –</option>
<?php foreach ($helpTopics as $t): ?>
<option value="<?= $t['id'] ?>" <?= $val('set_help_topic') == $t['id'] ? 'selected' : '' ?>><?= e($t['name']) ?></option>
<?php endforeach; ?>
</select>
<select name="action_value_staff[]" class="rule-val rule-val-assign_staff" <?= $sel('assign_staff') ?>>
<option value="">– Mitarbeiter wählen –</option>
<?php foreach ($staffList as $s): ?>
<option value="<?= $s['id'] ?>" <?= $val('assign_staff') == $s['id'] ? 'selected' : '' ?>><?= e($s['name']) ?></option>
<?php endforeach; ?>
</select>
<input type="text" name="action_value_tag[]" class="rule-val rule-val-add_tag" placeholder="Tag-Name" value="<?= e($val('add_tag')) ?>" <?= $sel('add_tag') ?>>
<?php if ($cannedResponses): ?>
<select name="action_value_canned[]" class="rule-val rule-val-auto_reply" <?= $sel('auto_reply') ?>>
<option value="">– Textbaustein wählen –</option>
<?php foreach ($cannedResponses as $c): ?>
<option value="<?= $c['id'] ?>" <?= $val('auto_reply') == $c['id'] ? 'selected' : '' ?>><?= e($c['title']) ?></option>
<?php endforeach; ?>
</select>
<?php else: ?>
<span class="rule-val rule-val-auto_reply muted" style="<?= $sel('auto_reply') === '' ? '' : 'display:none' ?>; align-self:center; font-size:.85rem">Noch keine Textbausteine angelegt – <a href="/staff/canned_responses.php">jetzt erstellen</a>.</span>
<?php endif; ?>
<span class="rule-val rule-val-close muted" style="<?= $sel('close') === '' ? '' : 'display:none' ?>; align-self:center; font-size:.85rem">Das Ticket wird angelegt und gleich geschlossen. Es gibt keine Benachrichtigung an Mitarbeiter oder Kunden.</span>
<span class="rule-val rule-val-reject muted" style="<?= $sel('reject') === '' ? '' : 'display:none' ?>; align-self:center; font-size:.85rem">Keine weitere Einstellung nötig.</span>
<?php
}
?>
<?php if (!$showForm): ?>
<div style="display:flex;justify-content:space-between;align-items:center;gap:12px">
<h1 style="margin:0">Ticket-Regeln (Filter)</h1>
<a href="/staff/ticket_rules.php?new=1" class="btn">Neue Regel hinzufügen</a>
</div>
<p class="muted">Automatische Aktionen für neue Tickets (Web-Formular und E-Mail-Eingang), ausgewertet in der Reihenfolge unten. Trifft eine Regel mit Aktion „Ticket verwerfen“ zu, werden keine weiteren Regeln mehr geprüft.</p>
<?php endif; ?>
<?php foreach ($errors as $err): ?>
<div class="flash error"><?= e($err) ?></div>
<?php endforeach; ?>
<?php if (!$showForm): ?>
<div class="card">
<table>
<thead><tr><th>Reihenfolge</th><th>Name</th><th>Bedingungen</th><th>Aktionen</th><th>Status</th><th>Zuletzt aktualisiert</th><th></th></tr></thead>
<tbody>
<?php foreach ($rules as $i => $r): ?>
<tr>
<td style="white-space:nowrap">
<form method="post" style="display:inline">
<?= csrf_field() ?>
<input type="hidden" name="action" value="move"><input type="hidden" name="dir" value="up"><input type="hidden" name="id" value="<?= $r['id'] ?>">
<button type="submit" style="background:none;border:none;cursor:pointer;padding:0 4px" <?= $i===0?'disabled':'' ?>>▲</button>
</form>
<form method="post" style="display:inline">
<?= csrf_field() ?>
<input type="hidden" name="action" value="move"><input type="hidden" name="dir" value="down"><input type="hidden" name="id" value="<?= $r['id'] ?>">
<button type="submit" style="background:none;border:none;cursor:pointer;padding:0 4px" <?= $i===count($rules)-1?'disabled':'' ?>>▼</button>
</form>
</td>
<td><a href="/staff/ticket_rules.php?edit=<?= $r['id'] ?>"><?= e($r['name']) ?></a></td>
<td class="muted" style="font-size:.82rem">
<?= $r['match_type']==='all' ? 'Alle:' : 'Eine von:' ?>
<?php foreach ($r['conditions'] as $c): ?>
<div><?= e($fieldLabels[$c['field']] ?? $c['field']) ?> <?= e($operatorLabels[$c['operator']] ?? $c['operator']) ?> „<?= e($c['value']) ?>“</div>
<?php endforeach; ?>
</td>
<td class="muted" style="font-size:.82rem">
<?php foreach ($r['actions'] as $a): ?>
<div><?= e($actionLabels[$a['action_type']] ?? $a['action_type']) ?><?= !in_array($a['action_type'], ['reject', 'close'], true) ? ': ' . e($a['action_value']) : '' ?></div>
<?php endforeach; ?>
</td>
<td>
<form method="post">
<?= csrf_field() ?>
<input type="hidden" name="action" value="toggle_active">
<input type="hidden" name="id" value="<?= $r['id'] ?>">
<button type="submit" class="badge" style="border:none;cursor:pointer;background:<?= $r['is_active']?'#16a34a':'#6b7280' ?>"><?= $r['is_active']?'Aktiv':'Deaktiviert' ?></button>
</form>
</td>
<td class="muted" style="font-size:.82rem"><?= e(ui_date($r['updated_at'] ?? null)) ?></td>
<td>
<a href="/staff/ticket_rules.php?edit=<?= $r['id'] ?>">Bearbeiten</a> ·
<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;color:#dc2626;cursor:pointer;padding:0;font-size:.9rem">Löschen</button>
</form>
</td>
</tr>
<?php endforeach; ?>
<?php if (!$rules): ?>
<tr><td colspan="7" class="muted">Noch keine Ticket-Regeln angelegt.</td></tr>
<?php endif; ?>
</tbody>
</table>
</div>
<?php else: ?>
<?php
$f = fn(string $k, $default = '') => $_POST[$k] ?? $default;
$curActive = $_POST ? (($_POST['status'] ?? 'active') === 'active') : (bool) ($editRule['is_active'] ?? 1);
?>
<h1>
<?= $editRule ? 'Regel aktualisieren' : 'Neue Regel' ?>
<?php if ($editRule): ?><span class="muted" style="font-size:1rem;font-weight:400">— <?= e($editRule['name']) ?></span><?php endif; ?>
</h1>
<div class="card" id="rule-form-card" style="padding:0">
<form method="post" id="rule-form">
<?= csrf_field() ?>
<input type="hidden" name="action" value="<?= $editRule ? 'update' : 'create' ?>">
<?php if ($editRule): ?><input type="hidden" name="id" value="<?= $editRule['id'] ?>"><?php endif; ?>
<?php ui_tab_bar(['rule' => 'Regel', 'conds' => 'Bedingungen', 'acts' => 'Aktionen']); ?>
<div class="tab-pane" data-pane="rule" style="padding:16px 20px">
<h3 style="margin-top:0">Regelinformationen</h3>
<label for="name">Name *</label>
<input type="text" id="name" name="name" value="<?= e($f('name', $editRule['name'] ?? '')) ?>" placeholder="z. B. Spam-Betreffzeile abweisen" required>
<?php ui_status_radios($curActive); ?>
<label for="sort_order">Reihenfolge</label>
<input type="number" id="sort_order" name="sort_order" value="<?= (int) $f('sort_order', $editRule['sort_order'] ?? (count($rules) + 1) * 10) ?>" style="max-width:140px">
<h3>Administratornotizen <span class="muted" style="font-weight:400;font-size:.85rem">– Interne Notizen, sichtbar für alle Administratoren</span></h3>
<textarea name="notes" rows="5"><?= e($f('notes', $editRule['notes'] ?? '')) ?></textarea>
</div>
<div class="tab-pane" data-pane="conds" style="padding:16px 20px;display:none">
<label for="match_type" style="margin-top:0">Verknüpfung der Bedingungen</label>
<select id="match_type" name="match_type" style="max-width:420px">
<option value="all" <?= ($editRule['match_type'] ?? 'all')==='all'?'selected':'' ?>>Alle Bedingungen müssen zutreffen (UND)</option>
<option value="any" <?= ($editRule['match_type'] ?? '')==='any'?'selected':'' ?>>Mindestens eine Bedingung muss zutreffen (ODER)</option>
</select>
<h4 style="margin:18px 0 8px">Bedingungen</h4>
<div id="cond-rows">
<?php
$condsToRender = $editRule['conditions'] ?? [['field' => 'subject', 'operator' => 'contains', 'value' => '']];
foreach ($condsToRender as $c):
?>
<div class="rule-cond-row" style="display:flex; gap:8px; margin-bottom:8px; align-items:center">
<select name="cond_field[]" style="flex:1">
<?php foreach ($fieldLabels as $key => $label): ?>
<option value="<?= $key ?>" <?= ($c['field'] ?? '')===$key?'selected':'' ?>><?= e($label) ?></option>
<?php endforeach; ?>
</select>
<select name="cond_operator[]" style="flex:1">
<?php foreach ($operatorLabels as $key => $label): ?>
<option value="<?= $key ?>" <?= ($c['operator'] ?? '')===$key?'selected':'' ?>><?= e($label) ?></option>
<?php endforeach; ?>
</select>
<input type="text" name="cond_value[]" style="flex:2" placeholder="Wert" value="<?= e($c['value'] ?? '') ?>">
<button type="button" onclick="this.closest('.rule-cond-row').remove()" style="background:none;border:none;color:#dc2626;cursor:pointer">✕</button>
</div>
<?php endforeach; ?>
</div>
<button type="button" class="btn secondary" id="add-cond-btn" style="margin-bottom:18px">+ Bedingung hinzufügen</button>
</div>
<div class="tab-pane" data-pane="acts" style="padding:16px 20px;display:none">
<h4 style="margin:0 0 8px">Aktionen</h4>
<div id="action-rows">
<?php
$actionsToRender = $editRule['actions'] ?? [['action_type' => 'set_department', 'action_value' => '']];
foreach ($actionsToRender as $a):
?>
<div class="rule-action-row" style="display:flex; gap:8px; margin-bottom:8px; align-items:center">
<select name="action_type[]" class="rule-action-type" style="flex:1" onchange="updateRuleActionRow(this)">
<?php foreach ($actionLabels as $key => $label): ?>
<option value="<?= $key ?>" <?= ($a['action_type'] ?? '')===$key?'selected':'' ?>><?= e($label) ?></option>
<?php endforeach; ?>
</select>
<div style="flex:2; display:flex">
<?php render_action_value_selects($departments, $priorities, $helpTopics, $staffList, $cannedResponses, $a); ?>
</div>
<button type="button" onclick="this.closest('.rule-action-row').remove()" style="background:none;border:none;color:#dc2626;cursor:pointer">✕</button>
</div>
<?php endforeach; ?>
</div>
<button type="button" class="btn secondary" id="add-action-btn" style="margin-bottom:18px">+ Aktion hinzufügen</button>
</div>
<div class="btn-row" style="padding:0 20px 18px">
<button type="submit" class="btn"><?= $editRule ? 'Änderungen speichern' : 'Regel erstellen' ?></button>
<a href="/staff/ticket_rules.php" class="btn secondary">Abbrechen</a>
</div>
</form>
</div>
<?php ui_tab_assets('rule'); ?>
<?php endif; ?>
<template id="cond-row-template">
<div class="rule-cond-row" style="display:flex; gap:8px; margin-bottom:8px; align-items:center">
<select name="cond_field[]" style="flex:1">
<?php foreach ($fieldLabels as $key => $label): ?><option value="<?= $key ?>"><?= e($label) ?></option><?php endforeach; ?>
</select>
<select name="cond_operator[]" style="flex:1">
<?php foreach ($operatorLabels as $key => $label): ?><option value="<?= $key ?>"><?= e($label) ?></option><?php endforeach; ?>
</select>
<input type="text" name="cond_value[]" style="flex:2" placeholder="Wert">
<button type="button" onclick="this.closest('.rule-cond-row').remove()" style="background:none;border:none;color:#dc2626;cursor:pointer">✕</button>
</div>
</template>
<template id="action-row-template">
<div class="rule-action-row" style="display:flex; gap:8px; margin-bottom:8px; align-items:center">
<select name="action_type[]" class="rule-action-type" style="flex:1" onchange="updateRuleActionRow(this)">
<?php foreach ($actionLabels as $key => $label): ?><option value="<?= $key ?>"><?= e($label) ?></option><?php endforeach; ?>
</select>
<div style="flex:2; display:flex">
<?php render_action_value_selects($departments, $priorities, $helpTopics, $staffList, $cannedResponses, null); ?>
</div>
<button type="button" onclick="this.closest('.rule-action-row').remove()" style="background:none;border:none;color:#dc2626;cursor:pointer">✕</button>
</div>
</template>
<script>
function updateRuleActionRow(select) {
var row = select.closest('.rule-action-row');
row.querySelectorAll('.rule-val').forEach(function (el) { el.style.display = 'none'; });
var match = row.querySelector('.rule-val-' + select.value);
if (match) match.style.display = '';
}
var addCond = document.getElementById('add-cond-btn');
if (addCond) addCond.addEventListener('click', function () {
var tpl = document.getElementById('cond-row-template');
document.getElementById('cond-rows').appendChild(tpl.content.cloneNode(true));
});
var addAct = document.getElementById('add-action-btn');
if (addAct) addAct.addEventListener('click', function () {
var tpl = document.getElementById('action-row-template');
document.getElementById('action-rows').appendChild(tpl.content.cloneNode(true));
});
</script>
<?php require __DIR__ . '/_footer.php'; ?>