Spaces:
Sleeping
Sleeping
| /** | |
| * Admin Panel - Support Tickets Logic (Premium Refactor) | |
| */ | |
| 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(); // This will render the list | |
| 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) { | |
| // UI Update for Filter Buttons | |
| document.querySelectorAll('.s-filter-btn').forEach(b => b.classList.remove('active')); | |
| btn.classList.add('active'); | |
| // Filtering Logic | |
| 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); // Update active class | |
| 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> | |
| `; | |
| // Auto-scroll to bottom | |
| 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); // Reload chat | |
| } | |
| } 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(); // Refresh sidebar list | |
| selectTicket(id); // Refresh workspace view | |
| } | |
| } catch (e) {} | |
| } | |
| function escapeHtml(value) { | |
| return String(value) | |
| .replaceAll('&', '&') | |
| .replaceAll('<', '<') | |
| .replaceAll('>', '>') | |
| .replaceAll('"', '"') | |
| .replaceAll("'", '''); | |
| } |