Files
deskhub/DeskHub-Webseite/api/staff/v1/ticket.php
T
2026-10-24 20:14:48 +00:00

139 lines
7.1 KiB
PHP
Raw 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
declare(strict_types=1);
require_once __DIR__ . '/common.php';
api_require_method('GET');
$staff = api_require_staff();
$number = trim((string) ($_GET['number'] ?? ''));
if ($number === '') {
api_error(422, 'TICKET_NUMBER_REQUIRED', 'Eine Ticketnummer ist erforderlich.');
}
$pdo = get_db();
$ticket = api_find_ticket($pdo, $staff, $number);
// Generelle Ticket-Sperre (nicht Live-Chat-spezifisch, siehe ticket_locked_by_other()
// in includes/functions.php): wird von einem anderen Mitarbeiter (Web oder App) gerade
// frisch bearbeitet, wird das Ticket hier – anders als beim milderen Live-Chat-Lock
// weiter unten – gar nicht erst geladen, genau wie im Web-Backend (ticket_view.php).
// Administratoren dürfen ein von anderen bearbeitetes Ticket trotzdem lesen und bearbeiten; die Sperre des anderen bleibt dabei
// bestehen, bis sie aufgehoben wird (api/staff/v1/ticket_unlock.php). Alle anderen bekommen wie bisher einen Hinweis.
$ticketLockedBy = null;
if ($ticket['source'] !== 'chat') {
$lockedByName = ticket_locked_by_other($ticket, (int) $staff['id'], $pdo);
if ($lockedByName) {
if ($staff['role'] !== 'admin') {
api_error(409, 'TICKET_LOCKED', "Dieses Ticket wird gerade von \"$lockedByName\" bearbeitet und kann nicht gleichzeitig geöffnet werden.");
}
$ticketLockedBy = $lockedByName;
} else {
ticket_acquire_lock((int) $ticket['id'], (int) $staff['id'], $pdo);
ticket_assign_on_open($pdo, $ticket, $staff); // unzugewiesenes Ticket wird dem öffnenden Mitarbeiter zugewiesen
}
}
$threadStmt = $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 = ?
ORDER BY tt.created_at ASC, tt.id ASC'
);
$threadStmt->execute([$ticket['id']]);
$attachmentStmt = $pdo->prepare(
'SELECT id, original_name, filesize FROM ticket_attachments WHERE thread_id = ? ORDER BY id'
);
$commentStmt = $pdo->prepare(
'SELECT tc.id, tc.message, tc.created_at, s.id AS staff_id, s.name AS staff_name
FROM ticket_thread_comments tc
JOIN staff s ON s.id = tc.staff_id
WHERE tc.thread_id = ? ORDER BY tc.created_at, tc.id'
);
$threads = [];
foreach ($threadStmt->fetchAll() as $row) {
$attachmentStmt->execute([$row['id']]);
$commentStmt->execute([$row['id']]);
$threads[] = [
'id' => (int) $row['id'],
'authorType' => $row['author_type'],
'authorName' => $row['staff_name'] ?: $row['user_name'] ?: $ticket['requester_name'],
'message' => api_plain_text($row['message']),
'isInternalNote' => (bool) $row['is_internal_note'],
'createdAt' => $row['created_at'],
'attachments' => array_map(fn(array $item) => [
'id' => (int) $item['id'],
'name' => $item['original_name'],
'size' => (int) $item['filesize'],
], $attachmentStmt->fetchAll()),
'comments' => array_map(fn(array $item) => [
'id' => (int) $item['id'],
'message' => api_plain_text($item['message']),
'staff' => ['id' => (int) $item['staff_id'], 'name' => $item['staff_name']],
'createdAt' => $item['created_at'],
], $commentStmt->fetchAll()),
];
}
// Live-Chat-Sperre: wie im Web-Backend (staff/live_chat.php) "besitzt" der Mitarbeiter,
// der den Chat gerade ansieht, ihn für ein paar Minuten – verhindert, dass App und Web
// gleichzeitig unbemerkt im selben Chat antworten. Nur für Live-Chat-Tickets relevant,
// normale Tickets bleiben unangetastet. Lesen bleibt für alle möglich, nur das
// Schreiben wird in api_add_ticket_message() serverseitig blockiert.
$lockedByOtherName = null;
if ($ticket['source'] === 'chat' && $ticket['status'] !== 'closed') {
$lockedByOtherName = live_chat_locked_by_other($ticket, (int) $staff['id'], $pdo);
if (!$lockedByOtherName) {
live_chat_acquire_lock((int) $ticket['id'], (int) $staff['id'], $pdo);
ticket_assign_on_open($pdo, $ticket, $staff);
}
}
$summary = api_ticket_summary($ticket);
$summary['source'] = $ticket['source'];
$summary['firstResponseDueAt'] = $ticket['first_response_due_at'];
$summary['resolutionDueAt'] = $ticket['resolution_due_at'];
$summary['lockedByOtherName'] = $lockedByOtherName;
$summary['lockedBy'] = $ticketLockedBy; // Name, wenn ein Administrator ein fremd gesperrtes Ticket geöffnet hat
$summary['canUnlock'] = $staff['role'] === 'admin'; // darf die Sperre eines anderen aufheben
$summary['threads'] = $threads;
// Rechte für GENAU dieses Ticket: gelten pro Abteilung (Rolle des Mitarbeiters in der Abteilung des Tickets)
$summary['permissions'] = [];
foreach (['reply_ticket', 'post_note', 'close_ticket', 'assign_ticket', 'transfer_ticket', 'delete_ticket'] as $__perm) {
$summary['permissions'][$__perm] = has_permission($staff, $__perm, (int) $ticket['department_id']);
}
// Signaturen zur Auswahl beim Antworten (eigene / Abteilung / keine) als Klartext mit Hinweis auf Bilder
$sigOptions = reply_signature_options($pdo, $staff, (int) $ticket['department_id']);
$sigEntry = fn(string $html) => ['available' => $html !== '', 'text' => api_plain_text($html), 'html' => $html, 'hasImage' => stripos($html, '<img') !== false, 'imageUrl' => api_first_image_url($html)];
$summary['replySignatures'] = [
'default' => $sigOptions['default'],
'own' => $sigEntry($sigOptions['own']),
'department' => $sigEntry($sigOptions['dept']) + ['name' => $sigOptions['deptName']],
];
// Zusatzfelder des Tickets (vom Kunden im Formular ausgefüllt), in der Reihenfolge des Formulars
$cfStmt = $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'
);
$cfStmt->execute([(int) $ticket['id']]);
$summary['customFields'] = array_map(fn($r) => [
'label' => (string) $r['label'],
'type' => (string) $r['field_type'],
'value' => $r['field_type'] === 'checkbox' ? ($r['value'] ? 'Ja' : 'Nein') : (string) $r['value'],
], $cfStmt->fetchAll());
try {
$summary['similar'] = ticket_similar($pdo, $ticket, staff_department_ids($staff));
} catch (Throwable $e) {
$summary['similar'] = ['articles' => [], 'tickets' => []];
}
// Cc-Empfänger, Beobachter und Bewertung des Kunden
try {
$summary['collaborators'] = api_ticket_collaborators($pdo, (int) $ticket['id']);
$summary['followers'] = array_map(fn($f) => ['id' => (int) $f['id'], 'name' => (string) $f['name']], ticket_followers($pdo, (int) $ticket['id']));
$__csat = csat_for_ticket($pdo, (int) $ticket['id']);
$summary['csat'] = $__csat ? ['rating' => $__csat['rating'] !== null ? (int) $__csat['rating'] : null, 'comment' => (string) ($__csat['comment'] ?? ''), 'sentAt' => $__csat['sent_at'], 'answeredAt' => $__csat['answered_at']] : null;
} catch (Throwable $e) {
$summary += ['collaborators' => [], 'followers' => [], 'csat' => null];
}
api_json(['ticket' => $summary]);