130 lines
6.4 KiB
PHP
130 lines
6.4 KiB
PHP
<?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' => []];
|
||
}
|
||
api_json(['ticket' => $summary]);
|