Spaces:
Sleeping
Sleeping
| /** | |
| * Admin Panel Logic - Payment Verification + Complaint Tickets | |
| * Aadhaar Pro | |
| */ | |
| // ββ State ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| let _allTickets = []; | |
| let _activeFilter = 'all'; | |
| let _activeTicketId = null; | |
| // ββ Init βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| document.addEventListener('DOMContentLoaded', () => { | |
| loadTransactions(); | |
| loadTickets(); // preload so badge count shows on page load | |
| // If URL hash is #support, switch tab automatically | |
| if (window.location.hash === '#support') switchTab('support'); | |
| }); | |
| // ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // TAB SWITCHING | |
| // ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function switchTab(tab) { | |
| // Update tab buttons | |
| document.querySelectorAll('.admin-tab-btn').forEach(b => b.classList.remove('active')); | |
| document.getElementById(`tab-btn-${tab}`)?.classList.add('active'); | |
| // Show/hide sections | |
| document.querySelectorAll('.admin-section').forEach(s => s.classList.remove('active')); | |
| document.getElementById(`section-${tab}`)?.classList.add('active'); | |
| // Update breadcrumb | |
| const labels = { payments: 'Payment Queue', support: 'Complaints' }; | |
| const bc = document.getElementById('breadcrumb-active'); | |
| if (bc) bc.textContent = labels[tab] || tab; | |
| // Load data for the activated tab | |
| if (tab === 'support') loadTickets(); | |
| if (tab === 'payments') loadTransactions(); | |
| } | |
| // ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // PAYMENT VERIFICATION (existing logic preserved) | |
| // ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| 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; | |
| // ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| // COMPLAINT / TICKET MANAGEMENT | |
| // ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| 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(); | |
| // Reset detail panel | |
| 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(); // highlight active row | |
| 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} | |
| `; | |
| // Scroll chat to bottom | |
| 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.'); | |
| } | |
| } | |
| // ββ Helpers ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| 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; | |