Support Inbox
Admin
← Dashboard
Admin Access Required
You need to be logged in as an admin to view support tickets.
Log In
–
Open
–
In Progress
–
Resolved
–
Last 24h
–
From AI Chat
Status:
All
Open
In Progress
Resolved
Loading support tickets…
No support tickets yet — great news!
function getCookie(n){for(var c=document.cookie.split(";"),i=0;i
(function () { 'use strict'; var TOKEN_KEY = 'cloche_token'; var token = localStorage.getItem(TOKEN_KEY) || ''; var currentStatus = 'all'; var allTickets = []; // ── Auth check ────────────────────────────────────────────────────────── if (getCookie("polsia_session")) { fetch("/api/admin/check-cookie",{credentials:"include"}).then(function(r){return r.ok?r.json():{success:false}}).then(function(d){ if(d.success){token="__cookie__";loadTickets(token);}else{token=localStorage.getItem("cloche_token")||"";if(token){loadTickets(token);}else{document.getElementById("authGate").style.display="";}} }).catch(function(){token=localStorage.getItem("cloche_token")||"";if(token){loadTickets(token);}else{document.getElementById("authGate").style.display="";}}); } else if (token) { loadTickets(token); } else { document.getElementById("authGate").style.display = ""; } // ── Filter buttons ────────────────────────────────────────────────────── document.querySelectorAll('.filter-btn').forEach(function (btn) { btn.addEventListener('click', function () { document.querySelectorAll('.filter-btn').forEach(function (b) { b.classList.remove('active'); }); btn.classList.add('active'); currentStatus = btn.dataset.status; renderTickets(); }); }); // ── Load tickets ───────────────────────────────────────────────────────── function loadTickets(tok) { document.getElementById('mainContent').style.display = ''; document.getElementById('loadingMsg').style.display = ''; document.getElementById('ticketList').innerHTML = ''; document.getElementById('emptyState').style.display = 'none'; fetch("/api/admin/support-tickets?limit=200",tok==="__cookie__"?{credentials:"include"}:{headers:{"Authorization":"Bearer "+tok}}) .then(function (r) { if (r.status === 401 || r.status === 403) { document.getElementById('mainContent').style.display = 'none'; document.getElementById('authGate').style.display = ''; var err = document.getElementById('authError'); if (r.status === 403) { err.textContent = 'Your account does not have admin access.'; err.style.display = ''; } else { window.location.href = '/login?redirect=/admin-support'; } throw new Error('auth'); } if (!r.ok) throw new Error('HTTP ' + r.status); return r.json(); }) .then(function (data) { if (!data.success) throw new Error(data.message || 'Unknown error'); document.getElementById('loadingMsg').style.display = 'none'; // Populate header email fetch('/api/auth/me', { headers: { 'Authorization': 'Bearer ' + tok } }) .then(function (r) { return r.json(); }) .then(function (d) { if (d.user && d.user.email) document.getElementById('headerEmail').textContent = d.user.email; }).catch(function () {}); // Stats var s = data.stats || {}; document.getElementById('statOpen').textContent = s.open_count || '0'; document.getElementById('statInProgress').textContent = s.in_progress_count || '0'; document.getElementById('statResolved').textContent = s.resolved_count || '0'; document.getElementById('statLast24h').textContent = s.last_24h || '0'; document.getElementById('statFromAI').textContent = s.from_ai_chat || '0'; allTickets = data.tickets || []; renderTickets(); }) .catch(function (err) { if (err.message === 'auth') return; document.getElementById('loadingMsg').style.display = 'none'; document.getElementById('emptyState').style.display = ''; }); } // ── Render tickets ───────────────────────────────────────────────────── function renderTickets() { var list = document.getElementById('ticketList'); list.innerHTML = ''; var filtered = currentStatus === 'all' ? allTickets : allTickets.filter(function (t) { return t.status === currentStatus; }); if (!filtered.length) { document.getElementById('emptyState').style.display = ''; return; } document.getElementById('emptyState').style.display = 'none'; filtered.forEach(function (ticket) { list.appendChild(buildTicketCard(ticket)); }); } // ── Build ticket card ─────────────────────────────────────────────────── function esc(s) { return String(s || '').replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"'); } function timeAgo(dateStr) { var d = new Date(dateStr); var diff = Date.now() - d.getTime(); var mins = Math.floor(diff / 60000); if (mins < 1) return 'just now'; if (mins < 60) return mins + 'm ago'; var hrs = Math.floor(mins / 60); if (hrs < 24) return hrs + 'h ago'; var days = Math.floor(hrs / 24); if (days < 30) return days + 'd ago'; return d.toLocaleDateString(); } var categoryLabels = { billing: '
Billing', account: '
Account', bug: '
Bug Report', escalation_request: '
Human Requested', feature: '
Feature Request', other: '
General' }; function buildTicketCard(ticket) { var div = document.createElement('div'); div.className = 'ticket-card status-' + (ticket.status || 'open'); div.dataset.id = ticket.id; var sourceLabel = ticket.source === 'ai_chat' ? '
AI Chat' : '
Direct'; var sourcePill = ticket.source === 'ai_chat' ? 'pill-source-ai' : 'pill-source-direct'; var catLabel = categoryLabels[ticket.issue_category] || ticket.issue_category || 'General'; var tierPill = (ticket.subscription_tier && ticket.subscription_tier !== 'free') ? 'pill-tier-pro' : 'pill-tier-free'; var tierLabel = ticket.subscription_tier || 'free'; var statusPill = 'pill-status-' + (ticket.status || 'open'); var statusLabel = (ticket.status || 'open').replace('_', ' '); var displayName = ticket.user_display_name || ticket.user_name || ''; var adminNoteHtml = ticket.admin_notes ? '
' + esc(ticket.admin_notes) + '
' : ''; // Transcript var transcriptHtml = ''; var msgs = []; try { msgs = typeof ticket.conversation_transcript === 'string' ? JSON.parse(ticket.conversation_transcript) : (ticket.conversation_transcript || []); } catch(_) {} if (msgs.length > 0) { var msgHtml = msgs.map(function (m) { return '
' + esc(m.role) + '
' + esc(m.content || '') + '
'; }).join(''); transcriptHtml = '
View conversation (' + msgs.length + ' messages)
' + '
' + msgHtml + '
'; } div.innerHTML = '
' + '
' + (displayName ? '
' + esc(displayName) + '
' : '') + '
' + esc(ticket.user_email || 'Unknown user') + '
' + '
' + '
' + timeAgo(ticket.created_at) + '
' + '
' + '
' + '
' + sourceLabel + '
' + '
' + esc(catLabel) + '
' + '
' + esc(tierLabel) + '
' + '
' + esc(statusLabel) + '
' + '
' + '
' + esc(ticket.message) + '
' + transcriptHtml + adminNoteHtml + '
' + '
' + '
Open
' + '
In Progress
' + '
Resolved
' + '
' + '
' + '
Save
' + '
Delete
' + '
Saved
' + '
'; return div; } // ── Toggle transcript ─────────────────────────────────────────────────── window.toggleTranscript = function (btn) { var body = btn.nextElementSibling; if (!body) return; var isOpen = body.classList.contains('open'); body.classList.toggle('open', !isOpen); btn.textContent = isOpen ? btn.textContent.replace('Hide', 'View') : btn.textContent.replace('View', 'Hide'); }; // ── Update ticket ─────────────────────────────────────────────────────── window.updateTicket = function (btn) { var card = btn.closest('.ticket-card'); var id = card.dataset.id; var status = card.querySelector('.status-select').value; var notes = card.querySelector('.ticket-notes-input').value; var flash = card.querySelector('.saved-flash'); btn.textContent = 'Saving…'; btn.disabled = true; fetch('/api/admin/support-tickets/' + id, { method: 'PATCH', headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token }, body: JSON.stringify({ status: status, admin_notes: notes }) }) .then(function (r) { return r.json(); }) .then(function (d) { btn.textContent = 'Save'; btn.disabled = false; if (d.success) { // Update local data var t = allTickets.find(function (t) { return t.id == id; }); if (t) { t.status = status; t.admin_notes = notes; } // Update card class card.className = card.className.replace(/status-\S+/, 'status-' + status); if (flash) { flash.classList.add('show'); setTimeout(function () { flash.classList.remove('show'); }, 2000); } } }) .catch(function () { btn.textContent = 'Save'; btn.disabled = false; }); }; // ── Delete ticket ───────────────────────────────────────────────────── window.deleteTicket = function (btn) { if (!await confirmSheet('Delete this support ticket?', { danger: true, confirmLabel: 'Delete', icon: '
' })) return; var card = btn.closest('.ticket-card'); var id = card.dataset.id; fetch('/api/admin/support-tickets/' + id, { method: 'DELETE', headers: { 'Authorization': 'Bearer ' + token } }) .then(function (r) { return r.json(); }) .then(function (d) { if (d.success) { allTickets = allTickets.filter(function (t) { return t.id != id; }); card.style.opacity = '0'; card.style.transition = 'opacity 0.3s'; setTimeout(function () { card.remove(); }, 300); } }) .catch(function () {}); }; })();