377 lines
19 KiB
PHP
377 lines
19 KiB
PHP
<?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'; ?>
|