Files
2026-10-26 21:19:21 +00:00

289 lines
13 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
require_once __DIR__ . '/includes/bootstrap.php';
$pdo = get_db();
$number = $_GET['n'] ?? '';
$stmt = $pdo->prepare(
'SELECT t.*, p.name AS priority_name, p.color AS priority_color, d.name AS department_name
FROM tickets t
JOIN priorities p ON p.id = t.priority_id
JOIN departments d ON d.id = t.department_id
WHERE t.ticket_number = ?'
);
$stmt->execute([$number]);
$ticket = $stmt->fetch();
if (!$ticket) {
flash('error', 'Dieses Ticket wurde nicht gefunden.');
redirect('/home.php');
}
$user = current_user();
if (verify_ticket_access_token((int) $ticket['id'], (string) ($_GET['token'] ?? ''))) {
if ($user) {
claim_ticket_for_customer($pdo, (int) $ticket['id'], $user);
}
$_SESSION['guest_ticket_access'][$ticket['ticket_number']] = true;
session_write_close();
redirect('/ticket_view.php?n=' . urlencode($ticket['ticket_number']));
}
$allowed = ($user && $ticket['user_id'] == $user['id'])
|| !empty($_SESSION['guest_ticket_access'][$ticket['ticket_number']]);
if (!$allowed) {
flash('error', 'Bitte melden Sie sich an oder bestätigen Sie Ihre E-Mail-Adresse, um dieses Ticket zu sehen.');
redirect('/ticket_lookup.php?n=' . urlencode($number));
}
$errors = [];
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
// Ticket-Bewertung (auch bei geschlossenem Ticket erlaubt)
if (($_POST['action'] ?? '') === 'rate_ticket') {
verify_csrf();
$rating = (int)($_POST['rating'] ?? 0);
$comment = trim($_POST['rating_comment'] ?? '');
if ($rating >= 1 && $rating <= 5 && get_setting('ticket_ratings_enabled', '1') === '1') {
$ratingSql = $pdo->getAttribute(PDO::ATTR_DRIVER_NAME) === 'sqlite'
? 'INSERT INTO ticket_ratings (ticket_id, user_id, rating, comment, created_at) VALUES (?,?,?,?,?)
ON CONFLICT(ticket_id) DO UPDATE SET
user_id=excluded.user_id, rating=excluded.rating,
comment=excluded.comment, created_at=excluded.created_at'
: 'INSERT INTO ticket_ratings (ticket_id, user_id, rating, comment, created_at) VALUES (?,?,?,?,?)
ON DUPLICATE KEY UPDATE
user_id=VALUES(user_id), rating=VALUES(rating),
comment=VALUES(comment), created_at=VALUES(created_at)';
$pdo->prepare($ratingSql)
->execute([$ticket['id'], $user['id'] ?? null, $rating, $comment ?: null, db_now()]);
flash('success', 'Vielen Dank für Ihre Bewertung!');
}
redirect('/ticket_view.php?n=' . urlencode($ticket['ticket_number']));
}
}
if ($_SERVER['REQUEST_METHOD'] === 'POST' && !in_array($ticket['status'], ['closed'])) {
verify_csrf();
$message = trim($_POST['message'] ?? '');
if ($message === '') {
$errors[] = 'Bitte geben Sie eine Nachricht ein.';
} else {
$now = db_now();
$stmt = $pdo->prepare(
'INSERT INTO ticket_threads (ticket_id, author_type, user_id, message, is_internal_note, created_at)
VALUES (?,?,?,?,0,?)'
);
$stmt->execute([$ticket['id'], 'customer', $user['id'] ?? null, $message, $now]);
$threadId = (int) $pdo->lastInsertId();
if (!empty($_FILES['attachments']['name'][0])) {
$uploadResults = handle_ticket_uploads_multi($_FILES['attachments']);
foreach ($uploadResults as $up) {
if (isset($up['error'])) {
$errors[] = $up['error'];
} else {
$pdo->prepare('INSERT INTO ticket_attachments (thread_id, original_name, stored_name, filesize, created_at) VALUES (?,?,?,?,?)')
->execute([$threadId, $up['original_name'], $up['stored_name'], $up['filesize'], $now]);
}
}
}
// Ein Kunden-Reply auf ein beantwortetes Ticket setzt den Status zurück auf "Offen"
$wasReopened = in_array($ticket['status'], ['answered', 'resolved', 'pending']);
$newStatus = $wasReopened ? 'open' : $ticket['status'];
$stmt = $pdo->prepare('UPDATE tickets SET status = ?, updated_at = ? WHERE id = ?');
$stmt->execute([$newStatus, $now, $ticket['id']]);
ticket_sla_status_transition($pdo, (int) $ticket['id'], (string) $ticket['status'], $newStatus);
log_activity($pdo, $ticket['id'], null, 'Kunde hat geantwortet');
if ($wasReopened) {
log_activity($pdo, $ticket['id'], null, 'Ticket wieder geöffnet');
}
notify_new_reply_channels($ticket, $message);
webhook_dispatch($pdo, 'ticket.replied', (int) $ticket['id'], ['author' => 'customer']);
if ($newStatus !== $ticket['status']) webhook_dispatch($pdo, 'ticket.status_changed', (int) $ticket['id'], ['previousStatus' => $ticket['status']]);
flash('success', 'Ihre Antwort wurde übermittelt.');
redirect('/ticket_view.php?n=' . urlencode($number));
}
}
// Thread laden
$stmt = $pdo->prepare(
"SELECT tt.*, s.name AS staff_name, u.name AS user_name
FROM ticket_threads tt
LEFT JOIN staff s ON s.id = tt.staff_id
LEFT JOIN users u ON u.id = tt.user_id
WHERE tt.ticket_id = ? AND tt.is_internal_note = 0
ORDER BY tt.created_at ASC"
);
$stmt->execute([$ticket['id']]);
$thread = $stmt->fetchAll();
// Anhänge je Thread-Eintrag
$attStmt = $pdo->prepare('SELECT * FROM ticket_attachments WHERE thread_id = ?');
// Individuelle Felder, die bei der Ticket-Erstellung ausgefüllt wurden
$fieldStmt = $pdo->prepare(
"SELECT cf.label, cf.field_type, v.value FROM ticket_field_values v
JOIN custom_fields cf ON cf.id = v.custom_field_id
WHERE v.ticket_id = ? ORDER BY cf.sort_order, cf.label"
);
$fieldStmt->execute([$ticket['id']]);
$customFieldValues = $fieldStmt->fetchAll();
$pageTitle = "Ticket #{$ticket['ticket_number']}";
$activePage = 'dashboard';
require __DIR__ . '/includes/header_customer.php';
[$statusLabel, $statusColor] = status_label($ticket['status']);
?>
<div class="topbar">
<div>
<h1><?= e($ticket['subject']) ?></h1>
<span class="ticket-no">#<?= e($ticket['ticket_number']) ?></span>
<span class="badge" style="background:<?= $statusColor ?>"><?= e($statusLabel) ?></span>
<span class="badge" style="background:<?= e($ticket['priority_color']) ?>"><?= e($ticket['priority_name']) ?></span>
</div>
</div>
<p class="muted"><?= e($ticket['department_name']) ?> · erstellt am <?= format_dt($ticket['created_at']) ?></p>
<?php if (!$user): ?>
<div class="card">
<strong>Ticket dauerhaft in Ihrem Konto speichern?</strong>
<p class="muted">Melden Sie sich an oder erstellen Sie ein Konto mit derselben E-Mail-Adresse. Dieses Ticket bleibt in diesem Browser freigeschaltet und kann danach in „Meine Tickets“ hinzugefügt werden.</p>
<a href="/login.php">Anmelden</a> · <a href="/register.php">Konto erstellen</a>
</div>
<?php endif; ?>
<?php if ($customFieldValues): ?>
<div class="card" style="font-size:.88rem">
<?php foreach ($customFieldValues as $fv): ?>
<div style="padding:4px 0"><strong><?= e($fv['label']) ?>:</strong>
<?= $fv['field_type'] === 'checkbox' ? ($fv['value'] ? 'Ja' : 'Nein') : e($fv['value']) ?></div>
<?php endforeach; ?>
</div>
<?php endif; ?>
<?php foreach ($errors as $err): ?>
<div class="flash error"><?= e($err) ?></div>
<?php endforeach; ?>
<?php foreach ($thread as $msg):
$attStmt->execute([$msg['id']]);
$attachments = $attStmt->fetchAll();
$isStaff = $msg['author_type'] === 'staff';
?>
<div class="thread-msg <?= $isStaff ? 'from-staff' : '' ?>">
<div class="meta">
<span><strong><?= e($isStaff ? ($msg['staff_name'] ?? 'Support-Team') : ($msg['user_name'] ?? $ticket['requester_name'])) ?></strong> <?= $isStaff ? '(Support)' : '' ?></span>
<span><?= format_dt($msg['created_at']) ?></span>
</div>
<div class="body"><?= $isStaff ? $msg['message'] : nl2br(e($msg['message'])) ?></div>
<?php if ($attachments): ?>
<div class="body" style="padding-top:0">
<?php foreach ($attachments as $a): ?>
<a href="/download.php?id=<?= $a['id'] ?>">📎 <?= e($a['original_name']) ?></a><br>
<?php endforeach; ?>
</div>
<?php endif; ?>
</div>
<?php endforeach; ?>
<div style="display:flex; justify-content:flex-end; margin-bottom:12px">
<a href="/ticket_print.php?n=<?= urlencode($ticket['ticket_number']) ?>" target="_blank" style="background:none; border:1px solid var(--border); border-radius:var(--radius); padding:7px 14px; font-size:.82rem; cursor:pointer; color:var(--muted); display:flex; align-items:center; gap:6px; text-decoration:none">🖨️ Ticket drucken</a>
</div>
<?php if ($ticket['status'] !== 'closed'): ?>
<div class="card" id="reply-form">
<h3>Antworten</h3>
<form method="post" enctype="multipart/form-data">
<?= csrf_field() ?>
<textarea name="message" placeholder="Ihre Nachricht..." required></textarea>
<label for="attachments">Anhänge (optional, mehrere möglich)</label>
<input type="file" id="attachments" name="attachments[]" multiple>
<div class="btn-row">
<button type="submit" class="btn">Antwort senden</button>
</div>
</form>
</div>
<?php else: ?>
<p class="muted">Dieses Ticket ist geschlossen. Bei weiteren Fragen erstellen Sie bitte ein <a href="/ticket_new.php">neues Ticket</a>.</p>
<?php
$ratingsEnabled = get_setting('ticket_ratings_enabled', '1') === '1';
$existingRating = null;
if ($ratingsEnabled) {
$rs = get_db()->prepare('SELECT * FROM ticket_ratings WHERE ticket_id=?');
$rs->execute([$ticket['id']]);
$existingRating = $rs->fetch();
}
if ($ratingsEnabled):
?>
<div class="card" style="margin-top:16px; text-align:center; padding:28px">
<?php if ($existingRating): ?>
<div style="font-size:2rem; margin-bottom:8px"><?= str_repeat('⭐', $existingRating['rating']) ?><?= str_repeat('☆', 5 - $existingRating['rating']) ?></div>
<div style="font-weight:600; color:var(--ink)">Danke für Ihre Bewertung!</div>
<?php if ($existingRating['comment']): ?>
<p class="muted" style="font-size:.85rem; margin-top:6px"><?= e($existingRating['comment']) ?></p>
<?php endif; ?>
<?php else: ?>
<div style="font-size:1rem; font-weight:600; margin-bottom:16px; color:var(--ink)">Wie zufrieden waren Sie mit der Bearbeitung?</div>
<form method="post" id="rating-form">
<?= csrf_field() ?>
<input type="hidden" name="action" value="rate_ticket">
<input type="hidden" name="rating" id="rating-value" value="0">
<div class="star-rating" style="display:flex; justify-content:center; gap:8px; margin-bottom:16px">
<?php for ($i = 1; $i <= 5; $i++): ?>
<button type="button" onclick="setRating(<?= $i ?>)" id="star-<?= $i ?>"
style="background:none; border:none; font-size:2.2rem; cursor:pointer; transition:transform .1s; line-height:1">☆</button>
<?php endfor; ?>
</div>
<textarea name="rating_comment" placeholder="Kommentar (optional)" rows="2"
style="width:100%; max-width:400px; margin-bottom:12px; font-size:.85rem"></textarea><br>
<button type="submit" class="btn" id="rating-submit" disabled>Bewertung abschicken</button>
</form>
<script>
function setRating(val) {
document.getElementById('rating-value').value = val;
document.getElementById('rating-submit').disabled = false;
for (var i = 1; i <= 5; i++) {
document.getElementById('star-' + i).textContent = i <= val ? '⭐' : '☆';
document.getElementById('star-' + i).style.transform = i === val ? 'scale(1.2)' : 'scale(1)';
}
}
</script>
<?php endif; ?>
</div>
<?php endif; ?>
<?php endif; ?>
<script>
(function() {
var lastCount = <?= count($thread) ?>;
var ticketNumber = <?= json_encode($ticket['ticket_number']) ?>;
function checkUpdates() {
fetch('/ticket_poll.php?n=' + encodeURIComponent(ticketNumber) + '&count=' + lastCount)
.then(function(r){ return r.json(); })
.then(function(data){
if (data.new_count > lastCount) {
// Neue Nachrichten vorhanden – Seite sanft neu laden
window.location.reload();
}
})
.catch(function(){});
}
setInterval(checkUpdates, 15000);
})();
</script>
<style>
@media print {
.site-header, .site-header *, footer.public-footer, #reply-form, .topbar nav, .btn, .btn-row { display: none !important; }
.thread-msg { break-inside: avoid; border: 1px solid #ccc !important; margin-bottom: 12px !important; }
body { background: white !important; }
}
</style>
<?php require __DIR__ . '/includes/footer_customer.php'; ?>