379 lines
19 KiB
PHP
379 lines
19 KiB
PHP
<?php
|
||
require_once __DIR__ . '/includes/bootstrap.php';
|
||
|
||
$user = current_user();
|
||
$pdo = get_db();
|
||
$departments = departments_for_select($pdo);
|
||
$priorities = $pdo->query('SELECT * FROM priorities ORDER BY sort_order')->fetchAll();
|
||
$helpTopics = help_topic_options($pdo, true);
|
||
$useHelpTopics = count($helpTopics) > 0;
|
||
// Ohne Hilfe-Themen gibt es keine Themenauswahl auf dem Formular, daher nur themenunabhängige Felder anzeigen.
|
||
$customFields = $useHelpTopics
|
||
? $pdo->query('SELECT * FROM custom_fields WHERE is_active = 1 ORDER BY sort_order, label')->fetchAll()
|
||
: $pdo->query('SELECT * FROM custom_fields WHERE is_active = 1 AND help_topic_id IS NULL ORDER BY sort_order, label')->fetchAll();
|
||
|
||
$errors = [];
|
||
|
||
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
|
||
verify_csrf();
|
||
|
||
// Honeypot: für Menschen unsichtbares Feld, das leer bleiben muss. Bots füllen
|
||
// häufig alle Formularfelder aus, daher dient dies als einfache Spam-Bremse.
|
||
if (!empty($_POST['website'])) {
|
||
flash('error', 'Es ist ein Fehler aufgetreten. Bitte versuchen Sie es erneut.');
|
||
redirect('/ticket_new.php');
|
||
}
|
||
if (!$user && !rate_limit_check('ticket_create', client_ip(), 8, 60)) {
|
||
$errors[] = 'Sie haben in kurzer Zeit zu viele Tickets erstellt. Bitte versuchen Sie es später erneut.';
|
||
}
|
||
|
||
$name = trim($_POST['name'] ?? ($user['name'] ?? ''));
|
||
$email = trim($_POST['email'] ?? ($user['email'] ?? ''));
|
||
$subject = trim($_POST['subject'] ?? '');
|
||
$message = trim($_POST['message'] ?? '');
|
||
|
||
// Gesperrte Absender: Ticket wird wie beim Honeypot kommentarlos abgewiesen,
|
||
// damit nicht erkennbar ist, dass die Adresse gezielt blockiert wurde.
|
||
if ($email !== '' && is_banned($email)) {
|
||
flash('error', 'Es ist ein Fehler aufgetreten. Bitte versuchen Sie es erneut.');
|
||
redirect('/ticket_new.php');
|
||
}
|
||
|
||
$helpTopicId = null;
|
||
$deptId = null;
|
||
$prioId = null;
|
||
|
||
if ($useHelpTopics) {
|
||
$helpTopicId = (int) ($_POST['help_topic_id'] ?? 0);
|
||
$topic = null;
|
||
foreach ($helpTopics as $t) {
|
||
if ($t['id'] == $helpTopicId) { $topic = $t; break; }
|
||
}
|
||
if (!$topic) {
|
||
$errors[] = 'Bitte wählen Sie ein Anliegen aus.';
|
||
} else {
|
||
$deptId = (int) $topic['department_id'];
|
||
$prioId = (int) $topic['priority_id'];
|
||
}
|
||
} else {
|
||
$deptId = (int) ($_POST['department_id'] ?? 0);
|
||
$prioId = (int) ($_POST['priority_id'] ?? 0);
|
||
if (!in_array($deptId, array_column($departments, 'id'))) $errors[] = 'Bitte wählen Sie eine Abteilung.';
|
||
if (!in_array($prioId, array_column($priorities, 'id'))) $errors[] = 'Bitte wählen Sie eine Priorität.';
|
||
}
|
||
|
||
if ($name === '') $errors[] = 'Bitte geben Sie Ihren Namen an.';
|
||
if (!filter_var($email, FILTER_VALIDATE_EMAIL)) $errors[] = 'Bitte geben Sie eine gültige E-Mail-Adresse an.';
|
||
if ($subject === '') $errors[] = 'Bitte geben Sie einen Betreff an.';
|
||
if ($message === '') $errors[] = 'Bitte beschreiben Sie Ihr Anliegen.';
|
||
|
||
// Individuelle Felder validieren (nur Felder, die zum gewählten Hilfe-Thema gehören oder themenunabhängig sind)
|
||
$customValues = []; // field id => submitted value
|
||
foreach ($customFields as $f) {
|
||
if ($f['help_topic_id'] !== null && (int) $f['help_topic_id'] !== (int) $helpTopicId) continue;
|
||
$fieldName = 'custom_' . $f['id'];
|
||
$val = $f['field_type'] === 'checkbox' ? (!empty($_POST[$fieldName]) ? '1' : '0') : trim($_POST[$fieldName] ?? '');
|
||
if ($f['is_required'] && $f['field_type'] !== 'checkbox' && $val === '') {
|
||
$errors[] = 'Bitte füllen Sie das Feld „' . $f['label'] . '“ aus.';
|
||
}
|
||
$customValues[$f['id']] = $val;
|
||
}
|
||
|
||
// Datei-Upload (optional, ein Anhang)
|
||
$uploadInfo = null;
|
||
if (!$errors && !empty($_FILES['attachment']['name'])) {
|
||
$uploadInfo = handle_ticket_upload($_FILES['attachment']);
|
||
if (isset($uploadInfo['error'])) {
|
||
$errors[] = $uploadInfo['error'];
|
||
}
|
||
}
|
||
|
||
if (!$errors) {
|
||
// Ticket-Regeln auswerten: koennen Abteilung/Prioritaet/Hilfe-Thema vor dem Anlegen
|
||
// ueberschreiben, das Ticket abweisen, oder nach dem Anlegen einen Mitarbeiter zuweisen/Tags setzen.
|
||
$ruleResult = evaluate_ticket_rules($pdo, [
|
||
'subject' => $subject,
|
||
'message' => $message,
|
||
'requester_email' => $email,
|
||
'requester_name' => $name,
|
||
'source' => 'web',
|
||
]);
|
||
if ($ruleResult['reject']) {
|
||
flash('error', 'Es ist ein Fehler aufgetreten. Bitte versuchen Sie es erneut.');
|
||
redirect('/ticket_new.php');
|
||
}
|
||
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'];
|
||
|
||
$priority = null;
|
||
foreach ($priorities as $p) {
|
||
if ($p['id'] == $prioId) { $priority = $p; break; }
|
||
}
|
||
$sla = calc_sla_dates($priority, null, (int) $deptId, !empty($helpTopicId) ? (int) $helpTopicId : null);
|
||
|
||
$stmt = $pdo->prepare(
|
||
'INSERT INTO tickets (ticket_number, subject, user_id, requester_name, requester_email, department_id,
|
||
help_topic_id, priority_id, status, source, requester_ip, first_response_due_at, resolution_due_at, created_at, updated_at)
|
||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)'
|
||
);
|
||
$ticketNumber = generate_ticket_number($pdo);
|
||
$now = db_now();
|
||
// IP kürzen wenn Anonymisierung aktiv
|
||
$clientIp = client_ip();
|
||
if (get_setting('ip_anonymize', '0') === '1') {
|
||
$clientIp = preg_replace('/\.(\d+)$/', '.0', $clientIp);
|
||
$clientIp = preg_replace('/:[0-9a-f]{1,4}$/', ':0', $clientIp);
|
||
}
|
||
$stmt->execute([
|
||
$ticketNumber, $subject, $user['id'] ?? null, $name, $email, $deptId,
|
||
$helpTopicId, $prioId, 'open', 'web', $clientIp, $sla['first_response_due_at'], $sla['resolution_due_at'], $now, $now,
|
||
]);
|
||
$ticketId = (int) $pdo->lastInsertId();
|
||
|
||
$stmt = $pdo->prepare(
|
||
'INSERT INTO ticket_threads (ticket_id, author_type, user_id, message, is_internal_note, created_at)
|
||
VALUES (?,?,?,?,0,?)'
|
||
);
|
||
$stmt->execute([$ticketId, 'customer', $user['id'] ?? null, $message, $now]);
|
||
$threadId = (int) $pdo->lastInsertId();
|
||
|
||
if ($uploadInfo) {
|
||
$stmt = $pdo->prepare(
|
||
'INSERT INTO ticket_attachments (thread_id, original_name, stored_name, filesize, created_at) VALUES (?,?,?,?,?)'
|
||
);
|
||
$stmt->execute([$threadId, $uploadInfo['original_name'], $uploadInfo['stored_name'], $uploadInfo['filesize'], $now]);
|
||
}
|
||
|
||
if ($customValues) {
|
||
$cfStmt = $pdo->prepare('INSERT INTO ticket_field_values (ticket_id, custom_field_id, value) VALUES (?,?,?)');
|
||
foreach ($customValues as $fieldId => $val) {
|
||
if ($val === '' ) continue;
|
||
$cfStmt->execute([$ticketId, $fieldId, $val]);
|
||
}
|
||
}
|
||
|
||
log_activity($pdo, $ticketId, null, 'Ticket erstellt');
|
||
|
||
// Verbleibende Regel-Aktionen, die eine vorhandene Ticket-ID benötigen
|
||
$assignTo = $ruleResult['assign_staff'];
|
||
if ($assignTo === null) {
|
||
// Auto-Zuweisung: war der Kunde schon mal einem Mitarbeiter zugewiesen?
|
||
$prevStmt = $pdo->prepare(
|
||
"SELECT assigned_staff_id FROM tickets
|
||
WHERE LOWER(requester_email) = LOWER(?)
|
||
AND assigned_staff_id IS NOT NULL
|
||
AND status NOT IN ('closed','resolved')
|
||
AND id != ?
|
||
ORDER BY updated_at DESC LIMIT 1"
|
||
);
|
||
$prevStmt->execute([$email, $ticketId]);
|
||
$prevId = $prevStmt->fetchColumn();
|
||
if ($prevId) {
|
||
$assignTo = (int) $prevId;
|
||
log_activity($pdo, $ticketId, null, 'Automatisch zugewiesen (letzter Bearbeiter)');
|
||
}
|
||
} else {
|
||
log_activity($pdo, $ticketId, null, 'Automatisch zugewiesen (Ticket-Regel)');
|
||
}
|
||
if ($assignTo !== null) {
|
||
$pdo->prepare('UPDATE tickets SET assigned_staff_id = ? WHERE id = ?')->execute([$assignTo, $ticketId]);
|
||
}
|
||
foreach ($ruleResult['add_tags'] as $tagName) {
|
||
apply_tag_to_ticket($pdo, $ticketId, $tagName);
|
||
}
|
||
if ($ruleResult['matched_rules']) {
|
||
log_activity($pdo, $ticketId, null, 'Ticket-Regel angewendet: ' . implode(', ', $ruleResult['matched_rules']));
|
||
}
|
||
apply_auto_reply($pdo, $ticketId, $ruleResult['auto_reply']);
|
||
|
||
$deptStmt = $pdo->prepare('SELECT name, email FROM departments WHERE id = ?');
|
||
$deptStmt->execute([$deptId]);
|
||
$dept = $deptStmt->fetch();
|
||
$ticketForNotify = [
|
||
'ticket_number' => $ticketNumber,
|
||
'subject' => $subject,
|
||
'requester_name' => $name,
|
||
'requester_email' => $email,
|
||
'department_id' => $deptId,
|
||
'department_name' => $dept['name'] ?? null,
|
||
];
|
||
notify_new_ticket_channels($ticketForNotify, $dept['email'] ?? null);
|
||
notify_staff_new_ticket($ticketForNotify);
|
||
webhook_dispatch($pdo, 'ticket.created', $ticketId);
|
||
if (file_exists(__DIR__ . '/includes/push.php')) {
|
||
require_once __DIR__ . '/includes/push.php';
|
||
push_new_ticket($ticketForNotify);
|
||
}
|
||
notify_customer_ticket_created($ticketForNotify);
|
||
|
||
$_SESSION['guest_ticket_access'][$ticketNumber] = true;
|
||
flash('success', "Ihr Ticket #{$ticketNumber} wurde erstellt. Wir melden uns so schnell wie möglich.");
|
||
redirect('/ticket_view.php?n=' . urlencode($ticketNumber));
|
||
}
|
||
}
|
||
|
||
$pageTitle = 'Neues Ticket';
|
||
$activePage = 'new';
|
||
require __DIR__ . '/includes/header_customer.php';
|
||
?>
|
||
|
||
<div class="ticket-create-page">
|
||
<div class="topbar ticket-create-header">
|
||
<div>
|
||
<h1>Neues Ticket erstellen</h1>
|
||
<p class="muted" style="margin:4px 0 0">Beschreiben Sie Ihr Anliegen so genau wie möglich – das hilft uns, schneller zu helfen.</p>
|
||
</div>
|
||
<a href="/dashboard.php" class="btn secondary" style="white-space:nowrap">Meine Tickets</a>
|
||
</div>
|
||
|
||
<?php foreach ($errors as $err): ?>
|
||
<div class="flash error"><?= e($err) ?></div>
|
||
<?php endforeach; ?>
|
||
|
||
<div class="card ticket-create-card">
|
||
<form method="post" enctype="multipart/form-data" class="ticket-create-form">
|
||
<?= csrf_field() ?>
|
||
<!-- Honeypot -->
|
||
<div style="position:absolute;left:-9999px;top:-9999px" aria-hidden="true">
|
||
<input type="text" name="website" tabindex="-1" autocomplete="off">
|
||
</div>
|
||
|
||
<?php if (!$user): ?>
|
||
<!-- Schritt 1: Kontaktdaten -->
|
||
<div class="ticket-form-section">
|
||
<p class="ticket-section-label"><span class="ticket-section-number">1</span>Ihre Kontaktdaten</p>
|
||
<div class="field-row">
|
||
<div>
|
||
<label for="name">Name <span style="color:var(--danger)">*</span></label>
|
||
<input type="text" id="name" name="name" value="<?= e($_POST['name'] ?? '') ?>" placeholder="Max Mustermann" required autofocus>
|
||
</div>
|
||
<div>
|
||
<label for="email">E-Mail-Adresse <span style="color:var(--danger)">*</span></label>
|
||
<input type="email" id="email" name="email" value="<?= e($_POST['email'] ?? '') ?>" placeholder="max@beispiel.de" required>
|
||
</div>
|
||
</div>
|
||
<p class="ticket-form-hint">
|
||
<a href="/register.php">Kostenloses Konto erstellen</a> um alle Ihre Tickets im Überblick zu behalten.
|
||
</p>
|
||
</div>
|
||
<?php endif; ?>
|
||
|
||
<!-- Schritt 2: Anliegen -->
|
||
<div class="ticket-form-section">
|
||
<p class="ticket-section-label"><span class="ticket-section-number">2</span>Ihr Anliegen</p>
|
||
|
||
<?php if ($useHelpTopics): ?>
|
||
<label for="help_topic_id">Kategorie <span style="color:var(--danger)">*</span></label>
|
||
<select id="help_topic_id" name="help_topic_id" required onchange="updateTopicFieldVisibility()" style="margin-bottom:14px">
|
||
<option value="">– bitte wählen –</option>
|
||
<?php foreach ($helpTopics as $t): ?>
|
||
<option value="<?= $t['id'] ?>" <?= (($_POST['help_topic_id'] ?? '') == $t['id']) ? 'selected' : '' ?>><?= e($t['path']) ?></option>
|
||
<?php endforeach; ?>
|
||
</select>
|
||
<?php else: ?>
|
||
<div class="field-row" style="margin-bottom:14px">
|
||
<div>
|
||
<label for="department_id">Abteilung <span style="color:var(--danger)">*</span></label>
|
||
<select id="department_id" name="department_id" required>
|
||
<option value="">– bitte wählen –</option>
|
||
<?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 <span style="color:var(--danger)">*</span></label>
|
||
<select id="priority_id" name="priority_id" required>
|
||
<?php foreach ($priorities as $p): ?>
|
||
<option value="<?= $p['id'] ?>" <?= (($_POST['priority_id'] ?? '') == $p['id'] || (!isset($_POST['priority_id']) && $p['name']==='Normal')) ? 'selected' : '' ?>><?= e($p['name']) ?></option>
|
||
<?php endforeach; ?>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
<?php endif; ?>
|
||
|
||
<label for="subject">Betreff <span style="color:var(--danger)">*</span></label>
|
||
<input type="text" id="subject" name="subject" value="<?= e($_POST['subject'] ?? '') ?>" placeholder="Kurze Zusammenfassung Ihres Problems" required style="margin-bottom:14px">
|
||
|
||
<label for="message">Beschreibung <span style="color:var(--danger)">*</span></label>
|
||
<textarea id="message" name="message" required placeholder="Beschreiben Sie Ihr Anliegen so detailliert wie möglich…" style="min-height:140px"><?= e($_POST['message'] ?? '') ?></textarea>
|
||
</div>
|
||
|
||
<?php if ($customFields): ?>
|
||
<div class="ticket-form-section">
|
||
<p class="ticket-section-label"><span class="ticket-section-number">3</span>Zusätzliche Angaben</p>
|
||
<?php foreach ($customFields as $f):
|
||
$fieldName = 'custom_' . $f['id'];
|
||
$posted = $_POST[$fieldName] ?? '';
|
||
?>
|
||
<div class="custom-field-group" data-topic="<?= $f['help_topic_id'] !== null ? (int) $f['help_topic_id'] : '' ?>">
|
||
<label for="<?= $fieldName ?>"><?= e($f['label']) ?><?= $f['is_required'] ? ' <span style="color:var(--danger)">*</span>' : '' ?></label>
|
||
<?php if ($f['field_type'] === 'textarea'): ?>
|
||
<textarea id="<?= $fieldName ?>" name="<?= $fieldName ?>" <?= $f['is_required'] ? 'required' : '' ?>><?= e($posted) ?></textarea>
|
||
<?php elseif ($f['field_type'] === 'select'): ?>
|
||
<select id="<?= $fieldName ?>" name="<?= $fieldName ?>" <?= $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'] === 'checkbox'): ?>
|
||
<label style="display:flex;align-items:center;gap:8px;font-weight:400;margin-top:4px">
|
||
<input type="checkbox" id="<?= $fieldName ?>" name="<?= $fieldName ?>" value="1" style="width:auto" <?= !empty($posted) ? 'checked' : '' ?>>
|
||
<span><?= e($f['label']) ?></span>
|
||
</label>
|
||
<?php elseif ($f['field_type'] === 'number'): ?>
|
||
<input type="number" id="<?= $fieldName ?>" name="<?= $fieldName ?>" value="<?= e($posted) ?>" <?= $f['is_required'] ? 'required' : '' ?>>
|
||
<?php elseif ($f['field_type'] === 'date'): ?>
|
||
<input type="date" id="<?= $fieldName ?>" name="<?= $fieldName ?>" value="<?= e($posted) ?>" <?= $f['is_required'] ? 'required' : '' ?>>
|
||
<?php else: ?>
|
||
<input type="text" id="<?= $fieldName ?>" name="<?= $fieldName ?>" value="<?= e($posted) ?>" <?= $f['is_required'] ? 'required' : '' ?>>
|
||
<?php endif; ?>
|
||
</div>
|
||
<?php endforeach; ?>
|
||
</div>
|
||
<?php endif; ?>
|
||
|
||
<!-- Anhang -->
|
||
<div class="ticket-form-section ticket-attachment-section">
|
||
<p class="ticket-section-label"><span class="ticket-section-number"><?= $customFields ? '4' : '3' ?></span>Anhang <span class="ticket-section-optional">(optional)</span></p>
|
||
<label for="attachment" style="display:inline-flex;align-items:center;gap:8px;padding:8px 16px;border:1.5px dashed var(--border);border-radius:var(--radius);cursor:pointer;font-size:.85rem;color:var(--muted);font-weight:400;transition:border-color .15s" onmouseover="this.style.borderColor='var(--primary)'" onmouseout="this.style.borderColor='var(--border)'">
|
||
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21.44 11.05l-9.19 9.19a6 6 0 0 1-8.49-8.49l9.19-9.19a4 4 0 0 1 5.66 5.66l-9.2 9.19a2 2 0 0 1-2.83-2.83l8.49-8.48"/></svg>
|
||
<span id="att-label">Datei auswählen</span>
|
||
</label>
|
||
<input type="file" id="attachment" name="attachment" style="display:none" onchange="document.getElementById('att-label').textContent=this.files[0]?this.files[0].name:'Datei auswählen'">
|
||
</div>
|
||
|
||
<div class="ticket-form-actions">
|
||
<p class="ticket-form-hint">Pflichtfelder sind mit <span class="ticket-required">*</span> gekennzeichnet.</p>
|
||
<button type="submit" class="btn ticket-submit">Ticket erstellen</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
|
||
<?php if ($useHelpTopics): ?>
|
||
<script>
|
||
function updateTopicFieldVisibility() {
|
||
var select = document.getElementById('help_topic_id');
|
||
var selectedTopic = select ? select.value : '';
|
||
document.querySelectorAll('.custom-field-group').forEach(function (group) {
|
||
var groupTopic = group.getAttribute('data-topic');
|
||
var matches = groupTopic === '' || groupTopic === selectedTopic;
|
||
group.style.display = matches ? '' : 'none';
|
||
group.querySelectorAll('input, select, textarea').forEach(function (el) {
|
||
if (!matches) {
|
||
if (el.required) { el.dataset.wasRequired = '1'; el.required = false; }
|
||
} else if (el.dataset.wasRequired === '1') {
|
||
el.required = true;
|
||
}
|
||
});
|
||
});
|
||
}
|
||
document.addEventListener('DOMContentLoaded', updateTopicFieldVisibility);
|
||
</script>
|
||
<?php endif; ?>
|
||
|
||
<?php require __DIR__ . '/includes/footer_customer.php'; ?>
|