Spaces:
Sleeping
Sleeping
| /** | |
| * Admin Panel - Payments Queue Logic | |
| */ | |
| let allTransactions = []; | |
| let currentPaymentTab = 'pending'; | |
| let filteredTransactions = []; | |
| let currentRejectionTxId = null; | |
| async function loadTransactions() { | |
| const tableBody = document.getElementById('tx-table-body'); | |
| if (!tableBody) return; | |
| try { | |
| const res = await fetch('/api/admin/transactions'); | |
| allTransactions = await res.json(); | |
| if (allTransactions.length === 0) { | |
| tableBody.innerHTML = '<tr><td colspan="7" class="text-center padding-30">No transactions found.</td></tr>'; | |
| updatePaymentCounts(); | |
| return; | |
| } | |
| filterAndDisplayTransactions(); | |
| } 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 refreshPayments() { | |
| loadTransactions(); | |
| if(typeof showToast === 'function') { | |
| showToast('success', 'Payments Refreshed', 'Payment queue has been updated'); | |
| } | |
| } | |
| function switchPaymentTab(tabName) { | |
| currentPaymentTab = tabName; | |
| // Update tab buttons | |
| document.querySelectorAll('.payment-tab-btn').forEach(btn => btn.classList.remove('active')); | |
| document.getElementById(`tab-${tabName}`).classList.add('active'); | |
| // Filter and display transactions | |
| filterAndDisplayTransactions(); | |
| } | |
| function filterAndDisplayTransactions() { | |
| // Apply tab filter | |
| filteredTransactions = allTransactions.filter(tx => { | |
| if (currentPaymentTab === 'pending') return tx.status === 'pending'; | |
| if (currentPaymentTab === 'approved') return tx.status === 'approved'; | |
| if (currentPaymentTab === 'rejected') return tx.status === 'rejected'; | |
| return true; | |
| }); | |
| // Apply search filter | |
| const searchInput = document.getElementById('payment-search'); | |
| const searchTerm = searchInput ? searchInput.value.toLowerCase() : ''; | |
| if (searchTerm) { | |
| filteredTransactions = filteredTransactions.filter(tx => | |
| (tx.user || '').toLowerCase().includes(searchTerm) || | |
| (tx.phone || '').toLowerCase().includes(searchTerm) || | |
| (tx.utr || '').toLowerCase().includes(searchTerm) | |
| ); | |
| } | |
| // Apply amount filter | |
| const filterSelect = document.getElementById('payment-filter'); | |
| const amountFilter = filterSelect ? filterSelect.value : 'all'; | |
| if (amountFilter !== 'all') { | |
| filteredTransactions = filteredTransactions.filter(tx => { | |
| const amount = parseFloat(tx.amount) || 0; | |
| switch (amountFilter) { | |
| case '0-500': return amount <= 500; | |
| case '500-1000': return amount > 500 && amount <= 1000; | |
| case '1000-5000': return amount > 1000 && amount <= 5000; | |
| case '5000+': return amount > 5000; | |
| default: return true; | |
| } | |
| }); | |
| } | |
| // Apply sorting | |
| const sortSelect = document.getElementById('payment-sort'); | |
| const sortBy = sortSelect ? sortSelect.value : 'recent'; | |
| filteredTransactions.sort((a, b) => { | |
| switch (sortBy) { | |
| case 'newest': | |
| case 'recent': return new Date(b.date) - new Date(a.date); | |
| case 'oldest': return new Date(a.date) - new Date(b.date); | |
| case 'amount-high': return parseFloat(b.amount) - parseFloat(a.amount); | |
| case 'amount-low': return parseFloat(a.amount) - parseFloat(b.amount); | |
| default: return 0; | |
| } | |
| }); | |
| // Update counts | |
| updatePaymentCounts(); | |
| // Display transactions | |
| renderTransactions(filteredTransactions); | |
| } | |
| function updatePaymentCounts() { | |
| const pendingCount = allTransactions.filter(t => t.status === 'pending').length; | |
| const approvedCount = allTransactions.filter(t => t.status === 'approved').length; | |
| const rejectedCount = allTransactions.filter(t => t.status === 'rejected').length; | |
| const pendingEl = document.getElementById('pending-count'); | |
| const approvedEl = document.getElementById('approved-count'); | |
| const rejectedEl = document.getElementById('rejected-count'); | |
| if (pendingEl) { | |
| pendingEl.textContent = pendingCount; | |
| pendingEl.style.display = pendingCount > 0 ? 'inline-block' : 'none'; | |
| } | |
| if (approvedEl) { | |
| approvedEl.textContent = approvedCount; | |
| approvedEl.style.display = approvedCount > 0 ? 'inline-block' : 'none'; | |
| } | |
| if (rejectedEl) { | |
| rejectedEl.textContent = rejectedCount; | |
| rejectedEl.style.display = rejectedCount > 0 ? 'inline-block' : 'none'; | |
| } | |
| } | |
| function renderTransactions(transactions) { | |
| const tableBody = document.getElementById('tx-table-body'); | |
| if (!tableBody) return; | |
| if (transactions.length === 0) { | |
| tableBody.innerHTML = ` | |
| <tr> | |
| <td colspan="7" class="text-center padding-30"> | |
| <i class="fa-solid fa-inbox" style="font-size:2rem; color:var(--text-muted); margin-bottom:10px;"></i> | |
| <p style="color:var(--text-muted);">No ${currentPaymentTab} transactions found</p> | |
| </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> | |
| <button onclick="showPaymentDetails('${t.tx_id}')" class="btn-verify btn-details"> | |
| <i class="fa-solid fa-eye"></i> Details | |
| </button> | |
| </div> | |
| ` : ` | |
| <div class="admin-actions"> | |
| <span class="text-muted-small"><i class="fa-solid fa-circle-check"></i> Complete</span> | |
| <button onclick="showPaymentDetails('${t.tx_id}')" class="btn-verify btn-details"> | |
| <i class="fa-solid fa-eye"></i> View | |
| </button> | |
| </div> | |
| `; | |
| 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" onclick="event.stopPropagation();"> | |
| <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(''); | |
| } | |
| async function showPaymentDetails(txId) { | |
| const transaction = allTransactions.find(t => t.tx_id === txId); | |
| if (!transaction) return; | |
| const modal = document.getElementById('payment-details-modal'); | |
| const modalTitle = document.getElementById('modal-title'); | |
| const modalBody = document.getElementById('modal-body'); | |
| const modalActions = document.getElementById('modal-actions'); | |
| if(!modal || !modalTitle || !modalBody || !modalActions) return; | |
| modalTitle.textContent = `Payment Details - ${transaction.utr}`; | |
| let rejectionCommentHtml = ''; | |
| if (transaction.status === 'rejected' && transaction.rejection_comment) { | |
| rejectionCommentHtml = ` | |
| <div style="margin-bottom:20px; padding:15px; background:rgba(255, 118, 117, 0.1); border-radius:8px; border-left:4px solid #ff7675;"> | |
| <strong style="color:#ff7675;">Rejection Reason:</strong><br> | |
| <p style="margin:8px 0 0 0; color:var(--text-color);">${transaction.rejection_comment}</p> | |
| </div> | |
| `; | |
| } | |
| modalBody.innerHTML = ` | |
| <div style="display:grid; grid-template-columns:1fr 1fr; gap:16px; margin-bottom:20px;"> | |
| <div> | |
| <strong>User:</strong><br> | |
| ${transaction.user}<br> | |
| <small style="color:var(--text-muted);">${transaction.phone}</small> | |
| </div> | |
| <div> | |
| <strong>Amount:</strong><br> | |
| <span style="font-weight:700; color:var(--primary-color);">₹${transaction.amount}</span> | |
| </div> | |
| <div> | |
| <strong>Status:</strong><br> | |
| <span class="status-pill status-${transaction.status.toLowerCase()}">${transaction.status.toUpperCase()}</span> | |
| </div> | |
| <div> | |
| <strong>Date:</strong><br> | |
| ${transaction.date} | |
| </div> | |
| </div> | |
| ${rejectionCommentHtml} | |
| <div style="margin-bottom:20px;"> | |
| <strong>UTR Number:</strong><br> | |
| <code style="background:var(--code-bg); padding:8px 12px; border-radius:6px; display:inline-block; margin-top:4px;">${transaction.utr}</code> | |
| </div> | |
| <div style="margin-bottom:20px;"> | |
| <strong>Screenshot Proof:</strong><br> | |
| ${transaction.screenshot ? ` | |
| <a href="${transaction.screenshot}" target="_blank" style="color:var(--primary-color); text-decoration:underline; margin-top:4px; display:inline-block;"> | |
| <i class="fa-solid fa-image"></i> View Payment Proof | |
| </a> | |
| ` : '<span style="color:var(--text-muted);">No screenshot provided</span>'} | |
| </div> | |
| `; | |
| if (transaction.status === 'pending') { | |
| modalActions.innerHTML = ` | |
| <button onclick="verifyTransaction('${transaction.tx_id}', 'approve')" class="btn-verify btn-approve" style="padding:8px 16px;"> | |
| <i class="fa-solid fa-check"></i> Approve | |
| </button> | |
| <button onclick="verifyTransaction('${transaction.tx_id}', 'reject')" class="btn-verify btn-reject" style="padding:8px 16px;"> | |
| <i class="fa-solid fa-xmark"></i> Reject | |
| </button> | |
| `; | |
| } else { | |
| modalActions.innerHTML = ''; | |
| } | |
| modal.style.display = 'flex'; | |
| } | |
| function closePaymentModal() { | |
| const modal = document.getElementById('payment-details-modal'); | |
| if (modal) modal.style.display = 'none'; | |
| } | |
| 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 iconContainer = 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'; | |
| iconContainer.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'; | |
| iconContainer.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); }; | |
| }); | |
| } | |
| function showRejectionCommentModal(txId) { | |
| currentRejectionTxId = txId; | |
| const modal = document.getElementById('rejection-comment-modal'); | |
| const commentInput = document.getElementById('rejection-comment'); | |
| if(modal) modal.style.display = 'flex'; | |
| if(commentInput) { | |
| commentInput.value = ''; | |
| commentInput.focus(); | |
| } | |
| } | |
| function closeRejectionModal() { | |
| const modal = document.getElementById('rejection-comment-modal'); | |
| if (modal) modal.style.display = 'none'; | |
| currentRejectionTxId = null; | |
| } | |
| async function confirmRejectionWithComment() { | |
| const commentInput = document.getElementById('rejection-comment'); | |
| const errorElement = document.getElementById('rejection-error'); | |
| if(!commentInput || !errorElement) return; | |
| const comment = commentInput.value.trim(); | |
| if (!comment) { | |
| errorElement.style.display = 'block'; | |
| commentInput.style.borderColor = '#ff7675'; | |
| commentInput.style.boxShadow = '0 0 0 3px rgba(255, 118, 117, 0.1)'; | |
| commentInput.classList.add('shake-animation'); | |
| setTimeout(() => commentInput.classList.remove('shake-animation'), 500); | |
| commentInput.focus(); | |
| return; | |
| } | |
| if (!currentRejectionTxId) { | |
| alert('No transaction selected for rejection.'); | |
| return; | |
| } | |
| errorElement.style.display = 'none'; | |
| commentInput.style.borderColor = 'var(--border-color)'; | |
| commentInput.style.boxShadow = 'none'; | |
| try { | |
| const res = await fetch('/api/admin/verify-payment', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ | |
| tx_id: currentRejectionTxId, | |
| action: 'reject', | |
| comment: comment | |
| }) | |
| }); | |
| const data = await res.json(); | |
| if (data.success) { | |
| closeRejectionModal(); | |
| loadTransactions(); | |
| if(typeof showToast === 'function') { | |
| showToast('success', 'Payment Rejected', 'Payment has been rejected with your comment.'); | |
| } | |
| } else { | |
| alert(data.error || 'Rejection failed'); | |
| } | |
| } catch (err) { | |
| console.error('Rejection error:', err); | |
| alert('A network error occurred.'); | |
| } | |
| } | |
| async function verifyTransaction(tx_id, action) { | |
| if (action === 'reject') { | |
| showRejectionCommentModal(tx_id); | |
| return; | |
| } | |
| const isConfirmed = await showConfirmModal(action); | |
| if (!isConfirmed) return; | |
| try { | |
| const res = await fetch('/api/admin/verify-payment', { | |
| 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.'); | |
| } | |
| } | |
| function debounce(func, wait) { | |
| let timeout; | |
| return function executedFunction(...args) { | |
| const later = () => { | |
| clearTimeout(timeout); | |
| func(...args); | |
| }; | |
| clearTimeout(timeout); | |
| timeout = setTimeout(later, wait); | |
| }; | |
| } | |
| document.addEventListener('DOMContentLoaded', function() { | |
| const searchInput = document.getElementById('payment-search'); | |
| const filterSelect = document.getElementById('payment-filter'); | |
| const sortSelect = document.getElementById('payment-sort'); | |
| if (searchInput) searchInput.addEventListener('input', debounce(filterAndDisplayTransactions, 300)); | |
| if (filterSelect) filterSelect.addEventListener('change', filterAndDisplayTransactions); | |
| if (sortSelect) sortSelect.addEventListener('change', filterAndDisplayTransactions); | |
| }); |