Upload via GUI (258 Dateien)

This commit is contained in:
2026-10-16 19:12:28 +00:00
parent 5a602de484
commit 57f52d5a9f
8 changed files with 956 additions and 0 deletions
+378
View File
@@ -0,0 +1,378 @@
<?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'; ?>