Files
2026-10-25 17:57:42 +00:00

377 lines
19 KiB
PHP
Raw Permalink 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
/**
* staff/ticket_create.php
*
* Ticket-Erstellung durch Mitarbeiter (z.B. Telefonanruf, Vor-Ort-Anfrage).
* Unterschied zu ticket_new.php (Kundenportal):
* – Kein Honeypot, kein Rate-Limit
* – Mitarbeiter wählt Abteilung, Priorität und Hilfe-Thema direkt
* – Kann im Namen jedes Kunden (Name + E-Mail) erstellen
* – source = 'phone' oder 'staff'
* – Optionale erste interne Notiz
*/
require_once __DIR__ . '/../includes/bootstrap.php';
$staff = require_staff_login();
$pdo = get_db();
$myDeptIds = staff_department_ids($staff); // null = Admin (alle Abteilungen)
$departments = $pdo->query('SELECT * FROM departments WHERE is_active = 1 ORDER BY name')->fetchAll();
$priorities = $pdo->query('SELECT * FROM priorities ORDER BY sort_order')->fetchAll();
$helpTopics = help_topic_options($pdo);
$allAgents = $pdo->query('SELECT id, name FROM staff WHERE is_active = 1 ORDER BY name')->fetchAll();
$customFields = $pdo->query('SELECT * FROM custom_fields WHERE is_active = 1 AND department_id IS NULL ORDER BY sort_order, label')->fetchAll();
// Abteilungen auf zugängliche einschränken
if ($myDeptIds !== null) {
$departments = array_filter($departments, fn($d) => in_array((int) $d['id'], $myDeptIds, true));
}
$errors = [];
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
verify_csrf();
$requesterName = trim($_POST['requester_name'] ?? '');
$requesterEmail = trim($_POST['requester_email'] ?? '');
$subject = trim($_POST['subject'] ?? '');
$message = sanitize_staff_html(trim($_POST['message'] ?? ''));
$internalNote = sanitize_staff_html(trim($_POST['internal_note'] ?? ''));
$source = in_array($_POST['source'] ?? '', ['phone','staff','web']) ? $_POST['source'] : 'phone';
$deptId = (int) ($_POST['department_id'] ?? ($departments[0]['id'] ?? 0));
$prioId = (int) ($_POST['priority_id'] ?? ($priorities[0]['id'] ?? 0));
$helpTopicId = ($_POST['help_topic_id'] ?? '') !== '' ? (int) $_POST['help_topic_id'] : null;
$assignedId = ($_POST['assigned_staff_id'] ?? '') !== '' ? (int) $_POST['assigned_staff_id'] : null;
if ($requesterName === '') $errors[] = 'Bitte Name des Anfragers angeben.';
if ($requesterEmail !== '' && !filter_var($requesterEmail, FILTER_VALIDATE_EMAIL)) {
$errors[] = 'Bitte eine gültige E-Mail-Adresse angeben (oder das Feld leer lassen).';
}
if ($subject === '') $errors[] = 'Bitte einen Betreff angeben.';
if (trim(strip_tags($message)) === '') $errors[] = 'Bitte eine Beschreibung eingeben.';
if (!$errors) {
// Sperrliste prüfen (nur wenn E-Mail vorhanden)
if ($requesterEmail !== '' && is_banned($requesterEmail)) {
$errors[] = 'Diese E-Mail-Adresse steht auf der Sperrliste.';
}
}
if (!$errors) {
// Ticket-Regeln anwenden
$ruleResult = evaluate_ticket_rules($pdo, [
'subject' => $subject,
'message' => strip_tags($message),
'requester_email' => $requesterEmail,
'requester_name' => $requesterName,
'source' => $source,
]);
if ($ruleResult['set_department'] !== null) $deptId = $ruleResult['set_department'];
if ($ruleResult['set_priority'] !== null) $prioId = $ruleResult['set_priority'];
if ($ruleResult['set_help_topic'] !== null) $helpTopicId = $ruleResult['set_help_topic'];
if ($ruleResult['assign_staff'] !== null && $assignedId === null) $assignedId = $ruleResult['assign_staff'];
$prioStmt = $pdo->prepare('SELECT * FROM priorities WHERE id = ?');
$prioStmt->execute([$prioId]);
$priority = $prioStmt->fetch();
$sla = calc_sla_dates($priority, null, (int) $deptId, $helpTopicId ? (int) $helpTopicId : null);
$now = db_now();
$ticketNumber = generate_ticket_number($pdo);
// Bekannten registrierten Kunden nachschlagen
$userId = null;
if ($requesterEmail !== '') {
$userStmt = $pdo->prepare('SELECT id FROM users WHERE LOWER(email) = LOWER(?)');
$userStmt->execute([$requesterEmail]);
$userId = $userStmt->fetchColumn() ?: null;
}
$pdo->prepare(
'INSERT INTO tickets (ticket_number, subject, user_id, requester_name, requester_email, department_id,
help_topic_id, priority_id, status, source, assigned_staff_id,
first_response_due_at, resolution_due_at, created_at, updated_at)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)'
)->execute([
$ticketNumber, $subject, $userId, $requesterName, $requesterEmail, $deptId,
$helpTopicId, $prioId, 'open', $source, $assignedId,
$sla['first_response_due_at'], $sla['resolution_due_at'], $now, $now,
]);
$ticketId = (int) $pdo->lastInsertId();
// Erste Nachricht eintragen (öffentlich, vom Mitarbeiter im Namen des Kunden)
$pdo->prepare(
'INSERT INTO ticket_threads (ticket_id, author_type, staff_id, message, is_internal_note, created_at) VALUES (?,?,?,?,0,?)'
)->execute([$ticketId, 'staff', $staff['id'], $message, $now]);
// Optionale interne Notiz
if (trim(strip_tags($internalNote)) !== '') {
$pdo->prepare(
'INSERT INTO ticket_threads (ticket_id, author_type, staff_id, message, is_internal_note, created_at) VALUES (?,?,?,?,1,?)'
)->execute([$ticketId, 'staff', $staff['id'], $internalNote, $now]);
}
// Individuelle Felder
if ($customFields) {
$cfStmt = $pdo->prepare('INSERT INTO ticket_field_values (ticket_id, custom_field_id, value) VALUES (?,?,?)');
foreach ($customFields as $f) {
$fn = 'custom_' . $f['id'];
$val = $f['field_type'] === 'checkbox' ? (!empty($_POST[$fn]) ? '1' : '0') : trim($_POST[$fn] ?? '');
if ($val !== '' && $val !== '0') $cfStmt->execute([$ticketId, $f['id'], $val]);
}
}
log_activity($pdo, $ticketId, $staff['id'], "Ticket durch Mitarbeiter erstellt (Quelle: {$source})");
webhook_dispatch($pdo, 'ticket.created', $ticketId);
foreach ($ruleResult['add_tags'] as $tagName) apply_tag_to_ticket($pdo, $ticketId, $tagName);
if ($ruleResult['matched_rules']) {
log_activity($pdo, $ticketId, $staff['id'], 'Ticket-Regel: ' . implode(', ', $ruleResult['matched_rules']));
}
// Eingangsbestätigung an Kunden (nur wenn E-Mail vorhanden und nicht interne Quelle)
if ($requesterEmail !== '' && !empty($_POST['notify_customer'])) {
$deptStmt = $pdo->prepare('SELECT name, email FROM departments WHERE id = ?');
$deptStmt->execute([$deptId]);
$dept = $deptStmt->fetch();
notify_customer_ticket_created([
'ticket_number' => $ticketNumber,
'subject' => $subject,
'requester_name' => $requesterName,
'requester_email' => $requesterEmail,
'department_id' => $deptId,
'department_name' => $dept['name'] ?? null,
]);
}
flash('success', "Ticket #{$ticketNumber} erfolgreich erstellt.");
redirect('/staff/ticket_view.php?n=' . urlencode($ticketNumber));
}
}
// Ticket-Vorlagen: ?template=ID füllt das Formular vor
$ticketTemplates = [];
$activeTemplateId = 0;
try {
$ticketTemplates = ticket_templates_list($pdo);
if ($_SERVER['REQUEST_METHOD'] !== 'POST' && (int) ($_GET['template'] ?? 0) > 0) {
$tpl = ticket_template_get($pdo, (int) $_GET['template']);
if ($tpl) {
$activeTemplateId = (int) $tpl['id'];
$_POST['subject'] = (string) $tpl['subject'];
$_POST['message'] = nl2br(e((string) ($tpl['body'] ?? ''))); // der Editor erwartet HTML
if ($tpl['department_id']) $_POST['department_id'] = (string) $tpl['department_id'];
if ($tpl['help_topic_id']) $_POST['help_topic_id'] = (string) $tpl['help_topic_id'];
if ($tpl['priority_id']) $_POST['priority_id'] = (string) $tpl['priority_id'];
}
}
} catch (Throwable $e) {
$ticketTemplates = [];
}
$pageTitle = 'Ticket erstellen';
$activePage = 'tickets';
require __DIR__ . '/_header.php';
?>
<div style="max-width:800px">
<div class="topbar">
<h1>Neues Ticket erstellen</h1>
<a href="/staff/tickets.php" class="btn secondary">← Zur Ticket-Liste</a>
</div>
<p class="muted">Ticket im Namen eines Kunden anlegen – z.B. bei Telefonanrufen oder Vor-Ort-Anfragen.</p>
<?php if ($ticketTemplates): ?>
<div style="display:flex;gap:8px;flex-wrap:wrap;align-items:center;margin:0 0 14px">
<span class="muted" style="font-size:.82rem">Vorlage:</span>
<?php foreach ($ticketTemplates as $__tp): ?>
<a href="/staff/ticket_create.php?template=<?= (int) $__tp['id'] ?>" class="btn <?= $activeTemplateId === (int) $__tp['id'] ? '' : 'secondary' ?>" style="font-size:.82rem;padding:4px 12px"><?= e($__tp['name']) ?></a>
<?php endforeach; ?>
<?php if ($activeTemplateId): ?><a href="/staff/ticket_create.php" class="muted" style="font-size:.8rem">Ohne Vorlage</a><?php endif; ?>
</div>
<?php endif; ?>
<?php foreach ($errors as $err): ?>
<div class="flash error"><?= e($err) ?></div>
<?php endforeach; ?>
<form method="post">
<?= csrf_field() ?>
<div class="card">
<h3>Anfrage-Quelle</h3>
<div style="display:flex; gap:10px; flex-wrap:wrap">
<?php foreach (['phone' => '📞 Telefon', 'staff' => '🖥 Vor Ort / Intern', 'web' => '🌐 Web (im Namen)'] as $val => $lbl): ?>
<label style="display:flex; align-items:center; gap:6px; font-weight:500; cursor:pointer; padding:9px 16px; border:2px solid var(--border); border-radius:8px; background:var(--surface); transition:.1s"
onclick="this.parentNode.querySelectorAll('label').forEach(l=>l.style.borderColor='var(--border)'); this.style.borderColor='var(--primary)'">
<input type="radio" name="source" value="<?= $val ?>" style="width:auto" <?= ($val==='phone')?'checked':'' ?>
onchange="this.closest('form').querySelector('[name=source]:checked').closest('label').style.borderColor='var(--primary)'">
<?= $lbl ?>
</label>
<?php endforeach; ?>
</div>
</div>
<div class="card">
<h3>Kundendaten</h3>
<div class="field-row">
<div>
<label for="requester_name">Name <span style="color:var(--danger)">*</span></label>
<input type="text" id="requester_name" name="requester_name"
value="<?= e($_POST['requester_name'] ?? '') ?>"
placeholder="Vorname Nachname" required>
</div>
<div>
<label for="requester_email">E-Mail-Adresse <span class="muted" style="font-weight:400">(optional)</span></label>
<input type="email" id="requester_email" name="requester_email"
value="<?= e($_POST['requester_email'] ?? '') ?>"
placeholder="kunde@beispiel.de">
</div>
</div>
<label style="display:flex; align-items:center; gap:8px; margin-top:12px; font-weight:400; cursor:pointer">
<input type="checkbox" name="notify_customer" value="1" style="width:auto" checked>
<span>Eingangsbestätigung per E-Mail an Kunden senden (nur wenn E-Mail angegeben)</span>
</label>
</div>
<div class="card">
<h3>Ticket-Details</h3>
<label for="subject">Betreff <span style="color:var(--danger)">*</span></label>
<input type="text" id="subject" name="subject"
value="<?= e($_POST['subject'] ?? '') ?>"
placeholder="Worum geht es?" required>
<div class="field-row" style="margin-top:6px">
<div>
<label for="department_id">Abteilung</label>
<select name="department_id" id="department_id">
<?php foreach ($departments as $d): ?>
<option value="<?= $d['id'] ?>" <?= (($_POST['department_id'] ?? '') == $d['id']) ? 'selected' : '' ?>><?= e($d['name']) ?></option>
<?php endforeach; ?>
</select>
</div>
<div>
<label for="priority_id">Priorität</label>
<select name="priority_id" id="priority_id">
<?php foreach ($priorities as $p): ?>
<option value="<?= $p['id'] ?>" <?= (($_POST['priority_id'] ?? '') == $p['id']) ? 'selected' : '' ?>><?= e($p['name']) ?></option>
<?php endforeach; ?>
</select>
</div>
</div>
<div class="field-row">
<?php if ($helpTopics): ?>
<div>
<label for="help_topic_id">Hilfe-Thema (optional)</label>
<select name="help_topic_id" id="help_topic_id">
<option value="">– kein Thema –</option>
<?php foreach ($helpTopics as $t): ?>
<option value="<?= $t['id'] ?>" <?= (($_POST['help_topic_id'] ?? '') == $t['id']) ? 'selected' : '' ?>><?= e($t['path']) ?></option>
<?php endforeach; ?>
</select>
</div>
<?php endif; ?>
<div>
<label for="assigned_staff_id">Zuweisen an</label>
<select name="assigned_staff_id" id="assigned_staff_id">
<option value="">– nicht zugewiesen –</option>
<?php foreach ($allAgents as $a): ?>
<option value="<?= $a['id'] ?>" <?= (($_POST['assigned_staff_id'] ?? '') == $a['id'] || (!isset($_POST['assigned_staff_id']) && $a['id'] == $staff['id'])) ? 'selected' : '' ?>><?= e($a['name']) ?></option>
<?php endforeach; ?>
</select>
</div>
</div>
<label for="message">Inhalt / Zusammenfassung <span style="color:var(--danger)">*</span></label>
<div class="richtext-wrap">
<div class="richtext-toolbar">
<button type="button" onclick="document.execCommand('bold')"><strong>B</strong></button>
<button type="button" onclick="document.execCommand('italic')"><em>I</em></button>
<button type="button" onclick="document.execCommand('underline')"><u>U</u></button>
<button type="button" onclick="document.execCommand('insertUnorderedList')">•</button>
<button type="button" onclick="document.execCommand('insertOrderedList')">1.</button>
</div>
<div class="richtext-editable" id="msg-editor" contenteditable="true"><?= sanitize_staff_html((string) ($_POST['message'] ?? '')) ?></div>
</div>
<input type="hidden" name="message" id="msg-hidden">
</div>
<?php if ($customFields): ?>
<div class="card">
<h3>Individuelle Felder</h3>
<?php foreach ($customFields as $f):
$fn = 'custom_' . $f['id'];
$posted = $_POST[$fn] ?? '';
?>
<label for="<?= $fn ?>"><?= e($f['label']) ?><?= $f['is_required'] ? ' *' : '' ?></label>
<?php if ($f['field_type'] === 'textarea'): ?>
<textarea id="<?= $fn ?>" name="<?= $fn ?>" <?= $f['is_required'] ? 'required' : '' ?>><?= e($posted) ?></textarea>
<?php elseif ($f['field_type'] === 'select'): ?>
<select id="<?= $fn ?>" name="<?= $fn ?>" <?= $f['is_required'] ? 'required' : '' ?>>
<option value="">– bitte wählen –</option>
<?php foreach (preg_split('/\r?\n/', trim((string)$f['options'])) as $opt): $opt = trim($opt); if (!$opt) continue; ?>
<option value="<?= e($opt) ?>" <?= $posted === $opt ? 'selected' : '' ?>><?= e($opt) ?></option>
<?php endforeach; ?>
</select>
<?php elseif ($f['field_type'] === 'radio'): ?>
<?php foreach (preg_split('/\r?\n/', trim((string)$f['options'])) as $opt): $opt = trim($opt); if (!$opt) continue; ?>
<label style="display:flex; align-items:center; gap:8px; font-weight:400; margin:4px 0">
<input type="radio" name="<?= $fn ?>" value="<?= e($opt) ?>" style="width:auto" <?= $f['is_required'] ? 'required' : '' ?> <?= $posted === $opt ? 'checked' : '' ?>>
<span><?= e($opt) ?></span>
</label>
<?php endforeach; ?>
<?php elseif ($f['field_type'] === 'checkbox'): ?>
<label style="display:flex; align-items:center; gap:8px; font-weight:400; margin-top:4px">
<input type="checkbox" id="<?= $fn ?>" name="<?= $fn ?>" value="1" style="width:auto" <?= !empty($posted) ? 'checked' : '' ?>>
<span><?= e($f['label']) ?></span>
</label>
<?php else: ?>
<input type="<?= $f['field_type'] === 'number' ? 'number' : ($f['field_type'] === 'date' ? 'date' : 'text') ?>"
id="<?= $fn ?>" name="<?= $fn ?>" value="<?= e($posted) ?>" <?= $f['is_required'] ? 'required' : '' ?>>
<?php endif; ?>
<?php endforeach; ?>
</div>
<?php endif; ?>
<div class="card">
<h3>Interne Notiz <span class="muted" style="font-weight:400; font-size:.85rem">(optional, nur für Mitarbeiter sichtbar)</span></h3>
<div class="richtext-wrap">
<div class="richtext-toolbar">
<button type="button" onclick="document.execCommand('bold')"><strong>B</strong></button>
<button type="button" onclick="document.execCommand('italic')"><em>I</em></button>
<button type="button" onclick="document.execCommand('underline')"><u>U</u></button>
<button type="button" onclick="document.execCommand('insertUnorderedList')">•</button>
</div>
<div class="richtext-editable" id="note-editor" contenteditable="true" style="min-height:80px; border-color:var(--accent)"><?= sanitize_staff_html((string) ($_POST['internal_note'] ?? '')) ?></div>
</div>
<input type="hidden" name="internal_note" id="note-hidden">
</div>
<div class="btn-row" style="margin-bottom:40px">
<button type="submit" class="btn" id="submit-btn">Ticket erstellen</button>
<a href="/staff/tickets.php" class="btn secondary">Abbrechen</a>
</div>
</form>
</div>
<script>
document.getElementById('submit-btn').closest('form').addEventListener('submit', function () {
document.getElementById('msg-hidden').value = document.getElementById('msg-editor').innerHTML;
document.getElementById('note-hidden').value = document.getElementById('note-editor').innerHTML;
});
// Radio-Button Quell-Karten visuell updaten beim Laden
document.querySelectorAll('input[name="source"]').forEach(function (radio) {
radio.addEventListener('change', function () {
document.querySelectorAll('input[name="source"]').forEach(function (r) {
r.closest('label').style.borderColor = 'var(--border)';
});
this.closest('label').style.borderColor = 'var(--primary)';
});
if (radio.checked) radio.closest('label').style.borderColor = 'var(--primary)';
});
</script>
<?php require __DIR__ . '/_footer.php'; ?>