<?php  if (!defined('DASHBOARD_ROOT')) { http_response_code(403); exit; } function renderThread(int $threadId): string { $ticket = Database::fetchOne("SELECT id, customer_id as user_id, subject FROM support_tickets WHERE id = :tid", [':tid' => $threadId]); $uid = $ticket ? (int)$ticket['user_id'] : 0; $subj = $ticket ? ($ticket['subject'] ?? '') : ''; $msgs = Database::fetchAll( "SELECT id, ticket_id, message, sender_type as is_admin, sender_name, created_at FROM support_replies WHERE ticket_id = :tid ORDER BY created_at", [":tid" => $threadId] ); if (!$msgs && $ticket) { $msgs = [['id' => 0, 'ticket_id' => $threadId, 'message' => $subj, 'is_admin' => 0, 'sender_name' => 'Customer', 'created_at' => $ticket['created_at'] ?? '']]; } Events::record(Events::CAT_READ, 'support.thread_viewed', 'user', $uid); $html = '<div class="thread-viewer">'; if (empty($msgs)) { $html .= '<div class="empty-state"><span>No messages in this thread.</span></div>'; } foreach ($msgs as $m) { $body = $m['message'] ?? ''; if ($body !== '') { try { $body = Encryption::render($body); } catch (\Throwable $e) { } } $sender = $m['sender_name'] ?? ($m['is_admin'] ? 'Admin' : 'Customer'); $cls = ($m['is_admin'] === 'admin' || $m['is_admin'] == 1) ? 'msg-admin' : 'msg-customer'; $html .= '<div class="thread-msg ' . $cls . '">'; $html .= '<div class="msg-label">' . UI::e($sender) . '</div>'; $html .= '<div class="msg-body">' . UI::e($body) . '</div>'; $html .= '<div class="msg-meta">' . UI::e($m['created_at']) . '</div>'; $html .= '</div>'; } $html .= '</div>'; return $html; } function renderThreadList(): string { $threads = Database::fetchAll( "SELECT st.id as ticket_id, st.customer_id as user_id, st.subject, st.status, st.priority, st.created_at as last_msg, st.updated_at FROM support_tickets st ORDER BY st.updated_at DESC, st.created_at DESC LIMIT 100" ); $userIds = array_column($threads, 'user_id'); $userMap = []; if (!empty($userIds)) { $ph = implode(',', array_fill(0, count($userIds), '?')); $users = Database::fetchAll("SELECT id, first_name, last_name, email FROM users WHERE id IN ($ph)", array_values($userIds)); foreach ($users as $u) { $u = Encryption::decryptFields($u, ['first_name', 'last_name', 'email']); $userMap[(int)$u['id']] = $u; } } $html = '<div class="thread-list-inner">'; foreach ($threads as $t) { $uid = (int)$t['user_id']; $u = $userMap[$uid] ?? null; $name = $u ? trim(($u['first_name'] ?? '') . ' ' . ($u['last_name'] ?? '')) : ''; if (!$name) $name = 'User #' . $uid; $unread = (int)$t['unread']; $badge = $unread > 0 ? ' <span class="badge badge-danger">' . $unread . '</span>' : ''; $status = $t['status'] ?? 'open'; $priority = $t['priority'] ?? 'medium'; $subj = ''; try { $subj = Encryption::render($t['subject'] ?? ''); } catch (\Throwable $e) { } $statusBadge = ' <span class="badge badge-' . $status . '">' . $status . '</span>'; $html .= '<div class="thread-entry" data-action="openThread" data-tid="' . (int)$t['ticket_id'] . '">'; $html .= '<div class="thread-entry-top"><span class="thread-entry-name">' . UI::e($name) . '</span>' . $statusBadge . '</div>'; $html .= '<div class="thread-entry-subj">' . UI::e($subj) . '</div>'; $html .= '<div class="thread-entry-meta">' . UI::e($priority) . ' | ' . UI::e($t['last_msg']) . '</div>'; $html .= '</div>'; } if (empty($threads)) { $html .= '<div class="empty-state"><span>No support threads.</span></div>'; } $html .= '</div>'; return $html; } 