Files
deskhub/DeskHub-Webseite/api/staff/v1/live_chat.php
T
2026-10-16 19:12:13 +00:00

186 lines
9.5 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/staff/v1/live_chat.php
*
* Mobile-Pendant zu staff/live_chat.php (Web-Postfach) – eigener Endpunkt statt
* Wiederverwendung von ticket.php/ticket_message.php, weil die App eine dedizierte,
* auf schnelles Chatten zugeschnittene Ansicht bekommt (eigener Tab, siehe
* mobile/.../ui/DeskHubApp.kt LiveChatScreen), analog zum separaten Web-Postfach.
* Teilt sich die Sperr-Logik (live_chat_locked_by_other()) und den Transkript-Mailversand
* (notify_customer_chat_transcript()) mit dem Web-Backend – siehe includes/functions.php
* bzw. includes/mailer.php.
*/
api_require_method('GET', 'POST');
$staff = api_require_staff();
$pdo = get_db();
[$deptFilterSql, $deptFilterParams] = dept_filter_sql(staff_department_ids($staff), 't.department_id');
function lc_api_find_ticket(PDO $pdo, string $number, string $deptFilterSql, array $deptFilterParams): ?array
{
$stmt = $pdo->prepare(
'SELECT t.*, d.name AS department_name FROM tickets t JOIN departments d ON d.id = t.department_id
WHERE t.ticket_number = ? AND t.source = \'chat\''
. ($deptFilterSql !== '' ? ' AND ' . substr($deptFilterSql, 5) : '')
);
$stmt->execute(array_merge([$number], $deptFilterParams));
return $stmt->fetch() ?: null;
}
function lc_api_messages(PDO $pdo, int $ticketId, int $since): array
{
$stmt = $pdo->prepare(
'SELECT tt.id, tt.author_type AS authorType, tt.message, tt.created_at AS createdAt, s.name AS staffName
FROM ticket_threads tt LEFT JOIN staff s ON s.id = tt.staff_id
WHERE tt.ticket_id = ? AND tt.is_internal_note = 0 AND tt.id > ?
ORDER BY tt.created_at ASC, tt.id ASC'
);
$stmt->execute([$ticketId, $since]);
$attStmt = $pdo->prepare('SELECT id, original_name AS name, filesize AS size FROM ticket_attachments WHERE thread_id = ? ORDER BY id');
return array_map(function (array $m) use ($attStmt) {
$attStmt->execute([$m['id']]);
return [
'id' => (int) $m['id'],
'authorType' => $m['authorType'],
'authorName' => $m['authorType'] === 'staff' ? ($m['staffName'] ?: 'Support') : null,
'message' => api_plain_text($m['message']),
'createdAt' => $m['createdAt'],
'attachments' => array_map(fn(array $a) => ['id' => (int) $a['id'], 'name' => $a['name'], 'size' => (int) $a['size']], $attStmt->fetchAll()),
];
}, $stmt->fetchAll());
}
if ($_SERVER['REQUEST_METHOD'] === 'GET') {
$action = $_GET['action'] ?? '';
if ($action === 'list') {
$listStmt = $pdo->prepare(
"SELECT t.ticket_number AS number, t.requester_name AS name, t.updated_at AS updatedAt,
(SELECT tt.author_type FROM ticket_threads tt WHERE tt.ticket_id = t.id ORDER BY tt.created_at DESC, tt.id DESC LIMIT 1) AS lastAuthor,
(SELECT tt.message FROM ticket_threads tt WHERE tt.ticket_id = t.id ORDER BY tt.created_at DESC, tt.id DESC LIMIT 1) AS lastMessage
FROM tickets t
WHERE t.source = 'chat' AND t.status != 'closed'" . ($deptFilterSql !== '' ? " AND $deptFilterSql" : '') . "
ORDER BY t.updated_at DESC LIMIT 100"
);
$listStmt->execute($deptFilterParams);
api_json(['items' => array_map(fn(array $c) => [
'number' => $c['number'],
'name' => $c['name'],
'preview' => mb_substr(strip_tags((string) $c['lastMessage']), 0, 60),
'needsReply' => $c['lastAuthor'] === 'customer',
'updatedAt' => $c['updatedAt'],
], $listStmt->fetchAll())]);
}
if ($action === 'messages') {
$number = trim((string) ($_GET['number'] ?? ''));
if ($number === '') api_error(422, 'TICKET_NUMBER_REQUIRED', 'Eine Ticketnummer ist erforderlich.');
$ticket = lc_api_find_ticket($pdo, $number, $deptFilterSql, $deptFilterParams);
if (!$ticket) api_error(404, 'TICKET_NOT_FOUND', 'Chat nicht gefunden oder kein Zugriff.');
$since = (int) ($_GET['since'] ?? 0);
// Sperre übernehmen/auffrischen bzw. feststellen, dass gerade jemand anders aktiv
// dran ist – wie im Web-Postfach (siehe staff/live_chat.php). Lesen bleibt für
// alle möglich, nur das Schreiben wird unten (Aktion "send") verwehrt.
$lockedByOtherName = null;
if ($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);
}
}
$customerTyping = !empty($ticket['typing_customer_until']) && strtotime($ticket['typing_customer_until']) > time();
api_json([
'status' => $ticket['status'],
'requesterName' => $ticket['requester_name'],
'requesterEmail' => $ticket['requester_email'],
'departmentName' => $ticket['department_name'],
'lockedByOtherName' => $lockedByOtherName,
'customerTyping' => $customerTyping,
'messages' => lc_api_messages($pdo, (int) $ticket['id'], $since),
]);
}
api_error(422, 'INVALID_ACTION', 'Unbekannte Aktion.');
}
// ── POST: send / end / typing ───────────────────────────────────────────────
$body = api_request_body();
$action = $body['action'] ?? '';
$number = trim((string) ($body['number'] ?? ''));
if ($number === '') api_error(422, 'TICKET_NUMBER_REQUIRED', 'Eine Ticketnummer ist erforderlich.');
$ticket = lc_api_find_ticket($pdo, $number, $deptFilterSql, $deptFilterParams);
if (!$ticket) api_error(404, 'TICKET_NOT_FOUND', 'Chat nicht gefunden oder kein Zugriff.');
if ($action === 'typing') {
if ($ticket['status'] !== 'closed' && !live_chat_locked_by_other($ticket, (int) $staff['id'], $pdo)) {
$pdo->prepare('UPDATE tickets SET typing_staff_until = ? WHERE id = ?')
->execute([date('Y-m-d H:i:s', time() + 6), $ticket['id']]);
}
api_json(['ok' => true]);
}
if ($action === 'send') {
api_require_permission($staff, 'reply_ticket', (int) $ticket['department_id']);
if ($ticket['status'] === 'closed') api_error(409, 'CHAT_CLOSED', 'Dieser Chat wurde bereits beendet.');
if ($lockedByOtherName = live_chat_locked_by_other($ticket, (int) $staff['id'], $pdo)) {
api_error(409, 'CHAT_LOCKED', "Dieser Chat wird gerade von \"$lockedByOtherName\" bearbeitet. Sie können nur zuschauen.");
}
$message = sanitize_staff_html(trim((string) ($body['message'] ?? '')));
if (trim(strip_tags($message)) === '') api_error(422, 'MESSAGE_REQUIRED', 'Bitte eine Nachricht eingeben.');
if (strlen($message) > 100000) api_error(422, 'MESSAGE_TOO_LONG', 'Die Nachricht ist zu lang.');
$now = db_now();
$pdo->prepare(
'INSERT INTO ticket_threads (ticket_id, author_type, staff_id, message, is_internal_note, created_at) VALUES (?,?,?,?,0,?)'
)->execute([$ticket['id'], 'staff', $staff['id'], $message, $now]);
$threadId = (int) $pdo->lastInsertId();
$autoAssign = ($ticket['assigned_staff_id'] === null || $ticket['assigned_staff_id'] === '');
$updateFirstResponse = $ticket['first_responded_at'] === null;
$sql = 'UPDATE tickets SET status = ?, updated_at = ?'
. ($updateFirstResponse ? ', first_responded_at = ?' : '')
. ($autoAssign ? ', assigned_staff_id = ?' : '')
. ' WHERE id = ?';
$args = ['answered', $now];
if ($updateFirstResponse) $args[] = $now;
if ($autoAssign) $args[] = $staff['id'];
$args[] = $ticket['id'];
$pdo->prepare($sql)->execute($args);
if ($autoAssign) log_activity($pdo, (int) $ticket['id'], (int) $staff['id'], 'Automatisch zugewiesen (Live-Chat, Mobile App)');
save_mentions($pdo, (int) $ticket['id'], $threadId, (int) $staff['id'], $message, $ticket);
log_activity($pdo, (int) $ticket['id'], (int) $staff['id'], 'Live-Chat-Antwort gesendet (Mobile App)');
finalize_work_session($pdo, (int) $ticket['id'], (int) $staff['id']);
api_json(['id' => $threadId, 'createdAt' => $now, 'status' => 'answered']);
}
if ($action === 'end') {
api_require_permission($staff, 'close_ticket', (int) $ticket['department_id']);
if ($lockedByOtherName = live_chat_locked_by_other($ticket, (int) $staff['id'], $pdo)) {
api_error(409, 'CHAT_LOCKED', "Dieser Chat wird gerade von \"$lockedByOtherName\" bearbeitet. Sie können nur zuschauen.");
}
$now = db_now();
$pdo->prepare(
"UPDATE tickets SET status='closed', closed_at=COALESCE(closed_at, ?), updated_at=?, locked_by=NULL, locked_at=NULL WHERE id=?"
)->execute([$now, $now, $ticket['id']]);
log_activity($pdo, (int) $ticket['id'], (int) $staff['id'], 'Live-Chat beendet (Mobile App)');
finalize_work_session($pdo, (int) $ticket['id'], (int) $staff['id']);
if ($ticket['requester_email']) {
try {
notify_customer_chat_transcript($ticket, lc_api_messages($pdo, (int) $ticket['id'], 0));
} catch (Throwable $e) {
error_log('live_chat.php (Mobile App) transcript mail: ' . $e->getMessage());
}
}
api_json(['ok' => true]);
}
api_error(422, 'INVALID_ACTION', 'Unbekannte Aktion.');