| |
| |
| |
| |
|
|
| |
| let _allTickets = []; |
| let _activeFilter = 'all'; |
| let _activeTicketId = null; |
|
|
| |
| document.addEventListener('DOMContentLoaded', () => { |
| loadTransactions(); |
| loadTickets(); |
|
|
| |
| if (window.location.hash === '#support') switchTab('support'); |
| }); |
|
|
| |
| |
| |
|
|
| function switchTab(tab) { |
| |
| document.querySelectorAll('.admin-tab-btn').forEach(b => b.classList.remove('active')); |
| document.getElementById(`tab-btn-${tab}`)?.classList.add('active'); |
|
|
| |
| document.querySelectorAll('.admin-section').forEach(s => s.classList.remove('active')); |
| document.getElementById(`section-${tab}`)?.classList.add('active'); |
|
|
| |
| const labels = { payments: 'Payment Queue', support: 'Complaints' }; |
| const bc = document.getElementById('breadcrumb-active'); |
| if (bc) bc.textContent = labels[tab] || tab; |
|
|
| |
| if (tab === 'support') loadTickets(); |
| if (tab === 'payments') loadTransactions(); |
| } |
|
|
| |
| |
| |
|
|
| async function loadTransactions() { |
| const tableBody = document.getElementById('tx-table-body'); |
| if (!tableBody) return; |
|
|
| try { |
| const res = await fetch('/api/admin/transactions'); |
| const transactions = await res.json(); |
|
|
| if (transactions.length === 0) { |
| tableBody.innerHTML = '<tr><td colspan="7" class="text-center padding-30">No transactions found.</td></tr>'; |
| return; |
| } |
|
|
| tableBody.innerHTML = transactions.map(t => { |
| const statusClass = `status-${t.status.toLowerCase()}`; |
| const actions = t.status === 'pending' ? ` |
| <div class="admin-actions"> |
| <button onclick="verifyTransaction('${t.tx_id}', 'approve')" class="btn-verify btn-approve"> |
| <i class="fa-solid fa-check"></i> Approve |
| </button> |
| <button onclick="verifyTransaction('${t.tx_id}', 'reject')" class="btn-verify btn-reject"> |
| <i class="fa-solid fa-xmark"></i> Reject |
| </button> |
| </div> |
| ` : `<span class="text-muted-small"><i class="fa-solid fa-circle-check"></i> Complete</span>`; |
|
|
| return ` |
| <tr> |
| <td class="text-muted-small">${t.date}</td> |
| <td> |
| <div class="user-cell"> |
| <strong>${t.user}</strong> |
| <small>${t.phone}</small> |
| </div> |
| </td> |
| <td class="balance-amount">βΉ${t.amount}</td> |
| <td><code>${t.utr}</code></td> |
| <td> |
| ${t.screenshot |
| ? `<a href="${t.screenshot}" target="_blank" class="proof-link"><i class="fa-solid fa-image"></i> View Proof</a>` |
| : '<span class="text-muted-small">N/A</span>'} |
| </td> |
| <td><span class="status-pill ${statusClass}">${t.status.toUpperCase()}</span></td> |
| <td>${actions}</td> |
| </tr> |
| `; |
| }).join(''); |
|
|
| } catch (err) { |
| console.error('Failed to load transactions:', err); |
| tableBody.innerHTML = '<tr><td colspan="7" class="error-state">Failed to fetch transactions.</td></tr>'; |
| } |
| } |
|
|
| function showConfirmModal(action) { |
| return new Promise((resolve) => { |
| const modal = document.getElementById('custom-confirm-modal'); |
| const title = document.getElementById('modal-title'); |
| const message = document.getElementById('modal-message'); |
| const confirmBtn = document.getElementById('modal-confirm-btn'); |
| const cancelBtn = document.getElementById('modal-cancel-btn'); |
| const icon = document.getElementById('modal-icon'); |
| const iconCont = document.getElementById('modal-icon-container'); |
|
|
| if (!modal) { resolve(confirm(`Are you sure you want to ${action} this transaction?`)); return; } |
|
|
| if (action === 'approve') { |
| title.innerText = 'Approve Payment?'; |
| message.innerHTML = 'This will securely add the <br>requested funds to the user profile.'; |
| icon.className = 'fa-solid fa-check-circle'; |
| iconCont.style.color = '#00b894'; |
| confirmBtn.style.background = '#00b894'; |
| confirmBtn.style.boxShadow = '0 10px 25px rgba(0,184,148,0.3)'; |
| confirmBtn.innerText = 'Yes, Approve'; |
| } else { |
| title.innerText = 'Reject Payment?'; |
| message.innerHTML = 'This request will be permanently<br>declined and user will not get funds.'; |
| icon.className = 'fa-solid fa-triangle-exclamation'; |
| iconCont.style.color = '#ff7675'; |
| confirmBtn.style.background = '#ff7675'; |
| confirmBtn.style.boxShadow = '0 10px 25px rgba(255,118,117,0.3)'; |
| confirmBtn.innerText = 'Yes, Reject'; |
| } |
|
|
| modal.style.display = 'flex'; |
|
|
| const cleanup = () => { confirmBtn.onclick = null; cancelBtn.onclick = null; modal.style.display = 'none'; }; |
| confirmBtn.onclick = () => { cleanup(); resolve(true); }; |
| cancelBtn.onclick = () => { cleanup(); resolve(false); }; |
| }); |
| } |
|
|
| async function verifyTransaction(tx_id, action) { |
| const confirmed = await showConfirmModal(action); |
| if (!confirmed) return; |
|
|
| try { |
| const res = await fetch('/api/admin/verify', { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify({ tx_id, action }) |
| }); |
| const data = await res.json(); |
| if (data.success) loadTransactions(); |
| else alert(data.error || 'Verification failed'); |
| } catch (err) { |
| console.error('Verification error:', err); |
| alert('A network error occurred.'); |
| } |
| } |
|
|
| window.verifyTransaction = verifyTransaction; |
|
|
| |
| |
| |
|
|
| async function loadTickets() { |
| try { |
| const res = await fetch('/api/admin/tickets'); |
| _allTickets = await res.json(); |
| renderTicketList(); |
| updateTicketBadge(); |
| } catch (err) { |
| console.error('Failed to load tickets:', err); |
| document.getElementById('ticket-items').innerHTML = |
| '<p style="padding:16px;color:var(--danger,#e74c3c);">Tickets load nahi hue.</p>'; |
| } |
| } |
|
|
| function updateTicketBadge() { |
| const openCount = _allTickets.filter(t => t.status === 'open').length; |
| const badge = document.getElementById('tab-badge'); |
| const navBadge = document.getElementById('open-ticket-count'); |
| [badge, navBadge].forEach(el => { |
| if (!el) return; |
| if (openCount > 0) { el.style.display = 'inline'; el.textContent = openCount; } |
| else { el.style.display = 'none'; } |
| }); |
| } |
|
|
| function filterTkts(filter, el) { |
| _activeFilter = filter; |
| _activeTicketId = null; |
| document.querySelectorAll('.tf-tab').forEach(t => t.classList.remove('on')); |
| el.classList.add('on'); |
| renderTicketList(); |
| |
| document.getElementById('ticket-detail-col').innerHTML = |
| '<div class="detail-empty">Koi ticket select karein</div>'; |
| } |
|
|
| function renderTicketList() { |
| const container = document.getElementById('ticket-items'); |
| if (!container) return; |
|
|
| const list = _activeFilter === 'all' |
| ? _allTickets |
| : _allTickets.filter(t => t.status === _activeFilter); |
|
|
| if (list.length === 0) { |
| container.innerHTML = '<p style="padding:16px;color:var(--text-muted);font-size:0.85rem;">Koi ticket nahi mila.</p>'; |
| return; |
| } |
|
|
| container.innerHTML = list.map(t => ` |
| <div class="tkt-row${t.id === _activeTicketId ? ' active' : ''}" onclick="openTicket('${t.id}')"> |
| <div class="tkt-row-top"> |
| <span class="priority-dot p-${t.priority}"></span> |
| <span class="tkt-num">${t.ticket_number}</span> |
| <span class="tkt-name">${t.user_name}</span> |
| <span class="tkt-time">${t.date.split(',')[0]}</span> |
| </div> |
| <div class="tkt-row-bot"> |
| <span class="tkt-subject">${t.subject}</span> |
| <span class="spill spill-${t.status}">${statusLabel(t.status)}</span> |
| </div> |
| </div> |
| `).join(''); |
| } |
|
|
| async function openTicket(ticketId) { |
| _activeTicketId = ticketId; |
| renderTicketList(); |
|
|
| const col = document.getElementById('ticket-detail-col'); |
| col.innerHTML = '<div class="detail-empty"><i class="fa-solid fa-spinner fa-spin"></i></div>'; |
|
|
| try { |
| const res = await fetch(`/api/admin/tickets/${ticketId}`); |
| const ticket = await res.json(); |
| if (ticket.error) { col.innerHTML = `<div class="detail-empty">${ticket.error}</div>`; return; } |
|
|
| renderTicketDetail(ticket); |
| } catch (err) { |
| col.innerHTML = '<div class="detail-empty">Load karne mein error aaya.</div>'; |
| } |
| } |
|
|
| function renderTicketDetail(ticket) { |
| const col = document.getElementById('ticket-detail-col'); |
| const isResolved = ticket.status === 'resolved' || ticket.status === 'closed'; |
|
|
| const msgHtml = ticket.messages.map(m => ` |
| <div class="chat-msg from-${m.sender}"> |
| <div class="chat-bubble">${escapeHtml(m.message)}</div> |
| <div class="chat-meta">${m.sender === 'admin' ? 'Support Team' : ticket.user_name} Β· ${m.time}</div> |
| </div> |
| `).join(''); |
|
|
| const replySection = isResolved ? ` |
| <div class="reply-footer" style="font-size:0.85rem;color:var(--text-muted);text-align:center;padding:14px;"> |
| <i class="fa-solid fa-circle-check" style="color:#27ae60;"></i> Yeh ticket resolve ho chuka hai. |
| </div> |
| ` : ` |
| <div class="reply-footer"> |
| <textarea class="reply-textarea" id="admin-reply-input" rows="2" |
| placeholder="User ko reply likhein..."></textarea> |
| <div class="reply-actions"> |
| <button class="ra-btn primary" onclick="sendAdminReply('${ticket.id}')"> |
| <i class="fa-solid fa-paper-plane"></i> Reply |
| </button> |
| <button class="ra-btn resolve" onclick="updateTicketStatus('${ticket.id}','resolved')"> |
| <i class="fa-solid fa-circle-check"></i> Resolved |
| </button> |
| <button class="ra-btn escalate" onclick="updateTicketStatus('${ticket.id}','open','high')"> |
| <i class="fa-solid fa-circle-arrow-up"></i> Escalate |
| </button> |
| <button class="ra-btn" onclick="sendPromptAboutTicket('${ticket.id}')"> |
| Wallet adjust β |
| </button> |
| </div> |
| </div> |
| `; |
|
|
| col.innerHTML = ` |
| <div class="detail-head"> |
| <div class="detail-head-top"> |
| <span class="detail-head-num">${ticket.ticket_number}</span> |
| <span class="detail-head-name">${ticket.user_name}</span> |
| <span class="spill spill-${ticket.status}">${statusLabel(ticket.status)}</span> |
| </div> |
| <div class="detail-head-meta"> |
| <span>${ticket.subject}</span> |
| <span class="cpill cpill-${ticket.category}">${categoryLabel(ticket.category)}</span> |
| <span class="priority-dot p-${ticket.priority}" title="${ticket.priority} priority"></span> |
| <span>${ticket.priority} priority</span> |
| <span>Β·</span> |
| <span>${ticket.user_phone}</span> |
| <span>Β·</span> |
| <span>${ticket.date}</span> |
| </div> |
| </div> |
| <div class="chat-window" id="chat-window-${ticket.id}">${msgHtml}</div> |
| ${replySection} |
| `; |
|
|
| |
| const cw = document.getElementById(`chat-window-${ticket.id}`); |
| if (cw) cw.scrollTop = cw.scrollHeight; |
| } |
|
|
| async function sendAdminReply(ticketId) { |
| const input = document.getElementById('admin-reply-input'); |
| const msg = input?.value.trim(); |
| if (!msg) { alert('Reply khali nahi ho sakta.'); return; } |
|
|
| try { |
| const res = await fetch(`/api/admin/tickets/${ticketId}/reply`, { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify({ message: msg }) |
| }); |
| const data = await res.json(); |
| if (data.success) { |
| await loadTickets(); |
| openTicket(ticketId); |
| } else { |
| alert(data.error || 'Reply send nahi hua.'); |
| } |
| } catch (err) { |
| alert('Network error.'); |
| } |
| } |
|
|
| async function updateTicketStatus(ticketId, status, priority) { |
| const payload = { status }; |
| if (priority) payload.priority = priority; |
|
|
| try { |
| const res = await fetch(`/api/admin/tickets/${ticketId}/status`, { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify(payload) |
| }); |
| const data = await res.json(); |
| if (data.success) { |
| await loadTickets(); |
| openTicket(ticketId); |
| } else { |
| alert(data.error || 'Status update nahi hua.'); |
| } |
| } catch (err) { |
| alert('Network error.'); |
| } |
| } |
|
|
| |
|
|
| function statusLabel(s) { |
| return { open: 'Open', pending: 'Pending', resolved: 'Resolved', closed: 'Closed' }[s] || s; |
| } |
| function categoryLabel(c) { |
| return { payment: 'Payment', wallet: 'Wallet', refund: 'Refund', other: 'Other' }[c] || c; |
| } |
| function escapeHtml(text) { |
| return String(text) |
| .replace(/&/g, '&').replace(/</g, '<') |
| .replace(/>/g, '>').replace(/"/g, '"'); |
| } |
| function sendPromptAboutTicket(id) { |
| const t = _allTickets.find(x => x.id === id); |
| if (t) alert(`Wallet adjust ke liye: Admin > Wallet > User search karein (${t.user_name}, ${t.user_phone})`); |
| } |
|
|
| window.filterTkts = filterTkts; |
| window.openTicket = openTicket; |
| window.sendAdminReply = sendAdminReply; |
| window.updateTicketStatus = updateTicketStatus; |
| window.switchTab = switchTab; |
|
|