| |
| |
| |
|
|
| let allTickets = []; |
| let filteredTickets = []; |
| let activeTicketId = null; |
|
|
| async function updateTicketBadges() { |
| try { |
| const res = await fetch('/api/admin/tickets'); |
| const tickets = await res.json(); |
| const openCount = tickets.filter(t => t.status === 'open').length; |
| |
| const badge = document.getElementById('open-tkt-badge'); |
| const navBadge = document.getElementById('open-ticket-count'); |
| |
| if (badge) { |
| badge.innerText = `${openCount} Open`; |
| badge.style.display = 'inline-block'; |
| } |
| if (navBadge) { |
| navBadge.innerText = openCount; |
| navBadge.style.display = openCount > 0 ? 'inline-block' : 'none'; |
| } |
| } catch (e) {} |
| } |
|
|
| async function loadTickets() { |
| const container = document.getElementById('ticket-items'); |
| if (!container) return; |
| |
| container.innerHTML = ` |
| <div class="loading-state" style="padding:40px;"> |
| <i class="fa-solid fa-spinner fa-spin"></i> |
| <span>Loading Tickets...</span> |
| </div>`; |
| |
| try { |
| const res = await fetch('/api/admin/tickets'); |
| allTickets = await res.json(); |
| applyTicketSearch(); |
| updateTicketBadges(); |
| } catch (err) { |
| container.innerHTML = '<div class="error-state">Error loading tickets.</div>'; |
| } |
| } |
|
|
| function applyTicketSearch() { |
| const searchInput = document.getElementById('tkt-search'); |
| const searchTerm = searchInput ? searchInput.value.toLowerCase().trim() : ''; |
| |
| filteredTickets = allTickets.filter(t => { |
| const searchable = `${t.ticket_number} ${t.user_name} ${t.subject} ${t.status}`.toLowerCase(); |
| return searchable.includes(searchTerm); |
| }); |
|
|
| renderTicketList(filteredTickets); |
| } |
|
|
| function filterTkts(status, btn) { |
| |
| document.querySelectorAll('.s-filter-btn').forEach(b => b.classList.remove('active')); |
| btn.classList.add('active'); |
| |
| |
| if (status === 'all') { |
| filteredTickets = allTickets; |
| } else { |
| filteredTickets = allTickets.filter(t => t.status === status); |
| } |
| |
| renderTicketList(filteredTickets); |
| } |
|
|
| function renderTicketList(tickets) { |
| const container = document.getElementById('ticket-items'); |
| if(!container) return; |
|
|
| if (tickets.length === 0) { |
| container.innerHTML = ` |
| <div class="empty-state" style="padding:40px; opacity:0.6;"> |
| <i class="fa-solid fa-inbox fa-2x"></i> |
| <p>No tickets found</p> |
| </div>`; |
| return; |
| } |
| |
| container.innerHTML = tickets.map(t => { |
| const isActive = activeTicketId === t.id ? 'active' : ''; |
| const initial = (t.user_name || 'U').charAt(0).toUpperCase(); |
| const priorityColor = t.priority === 'high' ? '#ef4444' : (t.priority === 'medium' ? '#f59e0b' : '#3b82f6'); |
| |
| return ` |
| <div class="tkt-card ${isActive}" onclick="selectTicket('${t.id}')"> |
| <div class="tkt-meta-top"> |
| <span class="tkt-id">#${escapeHtml(t.ticket_number)}</span> |
| <span class="tkt-date">${escapeHtml(t.date.split(',')[0])}</span> |
| </div> |
| <div class="tkt-user"> |
| <div class="tkt-avatar" style="background:${priorityColor}">${initial}</div> |
| <div class="tkt-info"> |
| <div class="tkt-username">${escapeHtml(t.user_name)}</div> |
| <div class="tkt-subject">${escapeHtml(t.subject)}</div> |
| </div> |
| </div> |
| <div class="tkt-badges"> |
| <span class="status-pill status-${t.status.toLowerCase()}">${t.status.toUpperCase()}</span> |
| <span class="status-pill" style="background:rgba(0,0,0,0.05); color:var(--text-muted); font-size:0.65rem;">${t.category.toUpperCase()}</span> |
| </div> |
| </div> |
| `; |
| }).join(''); |
| } |
|
|
| async function selectTicket(id) { |
| activeTicketId = id; |
| renderTicketList(filteredTickets); |
| |
| const workspace = document.getElementById('ticket-detail-col'); |
| if(!workspace) return; |
|
|
| workspace.innerHTML = ` |
| <div class="workspace-loading" style="height:100%; display:flex; align-items:center; justify-content:center; color:var(--text-muted);"> |
| <i class="fa-solid fa-spinner fa-spin fa-2x"></i> |
| </div>`; |
| |
| try { |
| const res = await fetch(`/api/admin/tickets/${id}`); |
| const t = await res.json(); |
| |
| workspace.innerHTML = ` |
| <!-- Premium Profile Strip --> |
| <div class="chat-profile-strip"> |
| <div class="chat-user-info"> |
| <h3>${escapeHtml(t.subject)}</h3> |
| <div class="chat-user-meta"> |
| <span><i class="fa-solid fa-user"></i> ${escapeHtml(t.user_name)}</span> |
| <span><i class="fa-solid fa-phone"></i> ${escapeHtml(t.user_phone)}</span> |
| <span><i class="fa-solid fa-hashtag"></i> ${escapeHtml(t.ticket_number)}</span> |
| </div> |
| </div> |
| <div class="chat-header-actions"> |
| <span class="status-pill status-${t.status.toLowerCase()}">${t.status.toUpperCase()}</span> |
| </div> |
| </div> |
| |
| <!-- Modern Chat Window --> |
| <div class="chat-window" id="chat-window"> |
| ${t.messages.map(m => ` |
| <div class="message-bubble ${m.sender === 'admin' ? 'admin' : 'user'}"> |
| <div class="bubble-content">${escapeHtml(m.message)}</div> |
| <div class="bubble-time">${escapeHtml(m.time)}</div> |
| </div> |
| `).join('')} |
| </div> |
| |
| <!-- Premium Reply Footer --> |
| <div class="chat-footer"> |
| <div class="reply-container"> |
| <textarea class="reply-textarea" id="admin-reply-text" rows="3" placeholder="Type your reply here..."></textarea> |
| <div class="reply-actions-row"> |
| <div class="left-actions"> |
| <button class="status-btn resolve" onclick="updateStatus('${t.id}', 'resolved')" title="Mark as Resolved"> |
| <i class="fa-solid fa-check-circle"></i> Resolve |
| </button> |
| <button class="status-btn pending" onclick="updateStatus('${t.id}', 'pending')" title="Mark as Pending"> |
| <i class="fa-solid fa-clock"></i> Wait |
| </button> |
| <button class="status-btn close" onclick="updateStatus('${t.id}', 'closed')" title="Close Ticket"> |
| <i class="fa-solid fa-times-circle"></i> Close |
| </button> |
| </div> |
| <button class="send-reply-btn" onclick="sendReply('${t.id}')"> |
| <i class="fa-solid fa-paper-plane"></i> Send Reply |
| </button> |
| </div> |
| </div> |
| </div> |
| `; |
| |
| |
| const chat = document.getElementById('chat-window'); |
| if(chat) chat.scrollTop = chat.scrollHeight; |
| |
| } catch (err) { |
| workspace.innerHTML = '<div class="workspace-error">Failed to load ticket details.</div>'; |
| } |
| } |
|
|
| async function sendReply(id) { |
| const textInput = document.getElementById('admin-reply-text'); |
| if(!textInput) return; |
| const text = textInput.value.trim(); |
| if (!text) return; |
| |
| const sendBtn = document.querySelector('.send-reply-btn'); |
| if (sendBtn) { |
| sendBtn.innerHTML = '<i class="fa-solid fa-spinner fa-spin"></i> Sending...'; |
| sendBtn.disabled = true; |
| } |
|
|
| try { |
| const res = await fetch(`/api/admin/tickets/${id}/reply`, { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify({ message: text }) |
| }); |
| |
| if (res.ok) { |
| selectTicket(id); |
| } |
| } catch (e) { |
| alert('Error sending reply.'); |
| } |
| } |
|
|
| async function updateStatus(id, status) { |
| try { |
| const res = await fetch(`/api/admin/tickets/${id}/status`, { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify({ status: status }) |
| }); |
| |
| if (res.ok) { |
| loadTickets(); |
| selectTicket(id); |
| } |
| } catch (e) {} |
| } |
|
|
| function escapeHtml(value) { |
| return String(value) |
| .replaceAll('&', '&') |
| .replaceAll('<', '<') |
| .replaceAll('>', '>') |
| .replaceAll('"', '"') |
| .replaceAll("'", '''); |
| } |