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 AND department_id IS NULL ORDER BY sort_order, label')->fetchAll() : $pdo->query('SELECT * FROM custom_fields WHERE is_active = 1 AND department_id IS NULL AND help_topic_id IS NULL ORDER BY sort_order, label')->fetchAll(); // Abteilungen mit eigenem Formular: Texte und Felder (erscheinen nur, wenn der Kunde diese Abteilung wählt) $deptForms = []; foreach ($departments as $d) { $df = department_form($pdo, (int) $d['id']); if ($df) { $deptForms[(int) $d['id']] = $df; } } $topicDept = []; foreach ($helpTopics as $t) $topicDept[(int) $t['id']] = (int) $t['department_id']; $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.'; } // Eigenes Formular der gewählten Abteilung: ersetzt Betreff-/Feld-/Anhang-Vorgaben des Standardformulars $ownForm = $deptId ? ($deptForms[(int) $deptId] ?? null) : null; if ($ownForm && !$ownForm['subject_show']) $subject = 'Anfrage ' . $ownForm['department_name']; if ($ownForm && !$ownForm['message_show']) { // Ohne Beschreibungsfeld besteht der Ticket-Text aus den Angaben der eigenen Felder $lines = []; foreach ($ownForm['fields'] as $ff) { $raw = $ff['field_type'] === 'checkbox' ? (!empty($_POST['custom_' . $ff['id']]) ? 'Ja' : 'Nein') : trim((string) ($_POST['custom_' . $ff['id']] ?? '')); if ($raw !== '') $lines[] = $ff['label'] . ': ' . $raw; } $message = $lines ? implode("\n", $lines) : 'Anfrage ohne weitere Angaben'; } 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 ($ownForm ? $ownForm['fields'] : $customFields as $f) { if (!$ownForm && ($f['department_id'] !== null || ($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.'; } if ($f['is_required'] && $f['field_type'] === 'checkbox' && $val !== '1') { $errors[] = 'Bitte bestätigen Sie „' . $f['label'] . '“.'; } if (in_array($f['field_type'], ['select', 'radio'], true) && $val !== '' && !in_array($val, array_map('trim', preg_split('/\r?\n/', (string) $f['options'])), true)) { $errors[] = 'Bitte wählen Sie bei „' . $f['label'] . '“ eine der angebotenen Möglichkeiten.'; $val = ''; } $customValues[$f['id']] = $val; } // Datei-Upload (optional, ein Anhang) $uploadInfo = null; if (!$errors && (!$ownForm || $ownForm['attachment_show']) && !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']; // Einstellung der Abteilung ("Neue Tickets automatisch zuweisen"): Ticket-Regel hat Vorrang, dann die Abteilung, dann der letzte Bearbeiter $deptAssignee = $assignTo === null ? department_auto_assignee($pdo, (int) $deptId) : null; if ($deptAssignee !== null) { $assignTo = $deptAssignee; log_activity($pdo, $ticketId, null, 'Automatisch zugewiesen (Einstellung der Abteilung)'); } elseif ($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']); $closedByRule = apply_rule_close($pdo, $ticketId, $ruleResult); $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, ]; if (!$closedByRule) { 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)); } } // Ticket-Vorlagen: ?template=ID füllt das Formular vor (nur Vorlagen, die für Kunden sichtbar sind) $ticketTemplates = []; $activeTemplateId = 0; try { $ticketTemplates = ticket_templates_list(get_db(), true); if ($_SERVER['REQUEST_METHOD'] !== 'POST' && (int) ($_GET['template'] ?? 0) > 0) { $tpl = ticket_template_get(get_db(), (int) $_GET['template'], true); if ($tpl) { $activeTemplateId = (int) $tpl['id']; $_POST['subject'] = (string) $tpl['subject']; $_POST['message'] = (string) ($tpl['body'] ?? ''); 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 = []; // Datenbank noch nicht aktualisiert: Formular funktioniert wie bisher } $pageTitle = 'Neues Ticket'; $activePage = 'new'; require __DIR__ . '/includes/header_customer.php'; ?>

Neues Ticket erstellen

Beschreiben Sie Ihr Anliegen so genau wie möglich – das hilft uns, schneller zu helfen.

Meine Tickets
Womit können wir helfen? Eine Vorlage füllt das Formular für Sie vor:
> Ohne Vorlage
$opt): $opt = trim($opt); if ($opt === '') continue; ?>
> > >

'; foreach ($list as $ff) $renderField($ff); if ($wrap) echo '
'; }; ?>

Kostenloses Konto erstellen um alle Ihre Tickets im Überblick zu behalten.

$df): $groups = []; foreach ($df['fields'] as $ff) if (!in_array($ff['_section'], ['contact', 'contact_top', 'issue', 'issue_top', 'issue_mid', 'topic', 'topic_top'], true)) $groups[$ff['_section_title']][] = $ff; foreach ($groups as $title => $list): ?>

Pflichtfelder sind mit * gekennzeichnet.