Spaces:
Sleeping
Sleeping
| /** | |
| * Admin Panel - Wallet Logic (Expanding Card Edition) | |
| */ | |
| let allWalletUsers = []; | |
| let filteredWalletUsers = []; | |
| let walletTransactions = []; | |
| let activeWalletUser = null; | |
| async function loadUserWallets() { | |
| const cardsGrid = document.getElementById('wallet-cards-grid'); | |
| if (!cardsGrid) return; | |
| cardsGrid.innerHTML = '<div class="loading-state"><i class="fa-solid fa-spinner fa-spin"></i><span>Loading user wallets...</span></div>'; | |
| try { | |
| const walletRes = await fetch('/api/admin/wallets'); | |
| allWalletUsers = await walletRes.json(); | |
| try { | |
| const txRes = await fetch('/api/admin/transactions'); | |
| walletTransactions = txRes.ok ? await txRes.json() : []; | |
| } catch (e) { | |
| walletTransactions = []; | |
| } | |
| updateWalletOverview(allWalletUsers, walletTransactions); | |
| applyWalletFilters(); | |
| } catch (err) { | |
| cardsGrid.innerHTML = '<div class="error-state"><i class="fa-solid fa-circle-exclamation"></i><span>Error loading user wallets. Please refresh.</span></div>'; | |
| } | |
| } | |
| function applyWalletFilters() { | |
| const searchInput = document.getElementById('wallet-search'); | |
| const balanceFilter = document.getElementById('wallet-balance-filter'); | |
| const sortSelect = document.getElementById('wallet-sort'); | |
| const searchTerm = searchInput ? searchInput.value.trim().toLowerCase() : ''; | |
| const filterType = balanceFilter ? balanceFilter.value : 'all'; | |
| const sortType = sortSelect ? sortSelect.value : 'balance-desc'; | |
| filteredWalletUsers = [...allWalletUsers].filter(user => { | |
| const balance = parseFloat(user.balance) || 0; | |
| const searchable = `${user.id || ''} ${user.name || ''} ${user.phone || ''}`.toLowerCase(); | |
| if (searchTerm && !searchable.includes(searchTerm)) return false; | |
| if (filterType === 'low' && balance >= 100) return false; | |
| if (filterType === 'medium' && (balance < 100 || balance > 1000)) return false; | |
| if (filterType === 'high' && balance < 1000) return false; | |
| return true; | |
| }); | |
| filteredWalletUsers.sort((a, b) => { | |
| const balanceA = parseFloat(a.balance) || 0; | |
| const balanceB = parseFloat(b.balance) || 0; | |
| const recentA = Date.parse(a.last_tx_date) || 0; | |
| const recentB = Date.parse(b.last_tx_date) || 0; | |
| if (sortType === 'balance-asc') return balanceA - balanceB; | |
| if (sortType === 'name-asc') return (a.name || '').localeCompare(b.name || ''); | |
| if (sortType === 'recent') return recentB - recentA; | |
| return balanceB - balanceA; | |
| }); | |
| renderWalletUsers(filteredWalletUsers); | |
| } | |
| function renderWalletUsers(wallets) { | |
| const cardsGrid = document.getElementById('wallet-cards-grid'); | |
| const resultCount = document.getElementById('wallet-result-count'); | |
| if (!cardsGrid) return; | |
| if (resultCount) resultCount.innerText = `${wallets.length} users`; | |
| if (wallets.length === 0) { | |
| cardsGrid.innerHTML = '<div class="empty-state"><i class="fa-solid fa-users-slash"></i><span>No matching users found.</span></div>'; | |
| return; | |
| } | |
| const balanceLevelClass = (b) => { | |
| const n = parseFloat(b) || 0; | |
| if (n < 100) return 'balance-low'; | |
| if (n < 1000) return 'balance-medium'; | |
| return 'balance-high'; | |
| }; | |
| cardsGrid.innerHTML = wallets.map(w => { | |
| const safeId = escapeHtml(w.id || ''); | |
| const safeName = escapeHtml(w.name || 'Unknown'); | |
| const safePhone = escapeHtml(w.phone || '-'); | |
| const safeDate = escapeHtml(w.last_tx_date || 'N/A'); | |
| const shortId = escapeHtml((w.id || '').slice(0, 10)); | |
| const bal = formatInr(w.balance); | |
| const balClass = balanceLevelClass(w.balance); | |
| const initial = (w.name || 'U').charAt(0).toUpperCase(); | |
| return ` | |
| <div class="wallet-user-card" id="wallet-card-${safeId}"> | |
| <div class="wallet-card-header"> | |
| <div class="wallet-user-avatar">${initial}</div> | |
| <div class="wallet-user-info"> | |
| <div class="wallet-user-name">${safeName}</div> | |
| <div class="wallet-user-phone"><i class="fa-solid fa-mobile-screen"></i> ${safePhone}</div> | |
| </div> | |
| <div class="wallet-balance-badge ${balClass}">${bal}</div> | |
| </div> | |
| <div class="wallet-card-meta"> | |
| <span><i class="fa-solid fa-fingerprint"></i> ${shortId}...</span> | |
| <span><i class="fa-solid fa-clock-rotate-left"></i> ${safeDate}</span> | |
| </div> | |
| <div class="wallet-card-actions"> | |
| <button class="wallet-action-btn" id="wallet-toggle-btn-${safeId}" | |
| onclick="toggleWalletUserDetails('${safeId}', '${safeName}')"> | |
| <i class="fa-solid fa-chevron-down"></i> View Details | |
| </button> | |
| <button class="wallet-action-btn primary" | |
| onclick="showWalletAdjustModal('${safeId}', '${safeName}', ${parseFloat(w.balance) || 0})"> | |
| <i class="fa-solid fa-wallet"></i> Manage | |
| </button> | |
| </div> | |
| <div class="wallet-expanded-area" id="wallet-expanded-${safeId}"></div> | |
| </div>`; | |
| }).join(''); | |
| } | |
| async function toggleWalletUserDetails(userId, userName) { | |
| const card = document.getElementById(`wallet-card-${userId}`); | |
| const expandedArea = document.getElementById(`wallet-expanded-${userId}`); | |
| const toggleBtn = document.getElementById(`wallet-toggle-btn-${userId}`); | |
| if (!card || !expandedArea) return; | |
| // Collapse if already expanded | |
| if (card.classList.contains('expanded')) { | |
| card.classList.remove('expanded'); | |
| expandedArea.innerHTML = ''; | |
| if (toggleBtn) toggleBtn.innerHTML = '<i class="fa-solid fa-chevron-down"></i> View Details'; | |
| return; | |
| } | |
| // Collapse all other expanded cards | |
| document.querySelectorAll('.wallet-user-card.expanded').forEach(c => { | |
| c.classList.remove('expanded'); | |
| const ea = c.querySelector('.wallet-expanded-area'); | |
| if (ea) ea.innerHTML = ''; | |
| const tb = c.querySelector('[id^="wallet-toggle-btn-"]'); | |
| if (tb) tb.innerHTML = '<i class="fa-solid fa-chevron-down"></i> View Details'; | |
| }); | |
| // Expand this card | |
| card.classList.add('expanded'); | |
| if (toggleBtn) toggleBtn.innerHTML = '<i class="fa-solid fa-chevron-up"></i> Collapse'; | |
| expandedArea.innerHTML = `<div class="wallet-detail-loading"><i class="fa-solid fa-spinner fa-spin"></i> Loading history for ${escapeHtml(userName)}...</div>`; | |
| try { | |
| const res = await fetch(`/api/admin/user/${encodeURIComponent(userId)}/details`); | |
| const data = await res.json(); | |
| if (!res.ok) { | |
| expandedArea.innerHTML = `<div class="error-state">${escapeHtml(data.error || 'Failed to load details')}</div>`; | |
| return; | |
| } | |
| const u = data.user || {}; | |
| const topups = data.topup_history || []; | |
| const prints = data.print_history || []; | |
| const downloads = data.download_history || []; | |
| const ledger = data.wallet_ledger || []; | |
| const statusPill = (s) => { | |
| const cls = (s || '').toLowerCase(); | |
| return `<span class="status-pill status-${cls}">${escapeHtml((s || 'N/A').toUpperCase())}</span>`; | |
| }; | |
| // ── Topup History Table ────────────────────── | |
| const topupsHtml = topups.length === 0 | |
| ? '<div class="wh-empty"><i class="fa-solid fa-inbox"></i> No topup records found.</div>' | |
| : `<table class="wh-table"> | |
| <thead> | |
| <tr> | |
| <th>Date & Time</th> | |
| <th>Amount</th> | |
| <th>UTR Number</th> | |
| <th>Status</th> | |
| <th>Remark</th> | |
| </tr> | |
| </thead> | |
| <tbody> | |
| ${topups.map(tx => ` | |
| <tr> | |
| <td style="white-space:nowrap;">${escapeHtml((tx.date || '').split('.')[0])}</td> | |
| <td class="wh-amount-credit">+${formatInr(tx.amount)}</td> | |
| <td class="wh-utr">${escapeHtml(tx.utr || '-')}</td> | |
| <td>${statusPill(tx.status)}</td> | |
| <td style="color:var(--text-muted); font-size:0.82rem;">${escapeHtml(tx.reason || '-')}</td> | |
| </tr>`).join('')} | |
| </tbody> | |
| </table>`; | |
| // ── Print History Table ────────────────────── | |
| const printsHtml = prints.length === 0 | |
| ? '<div class="wh-empty"><i class="fa-solid fa-inbox"></i> No print records found.</div>' | |
| : `<table class="wh-table"> | |
| <thead> | |
| <tr> | |
| <th>Date & Time</th> | |
| <th>Aadhaar No.</th> | |
| <th>Name</th> | |
| <th>DOB</th> | |
| <th>Status</th> | |
| <th>Reason / Remark</th> | |
| </tr> | |
| </thead> | |
| <tbody> | |
| ${prints.map(p => { | |
| const rawAadhaar = p.aadhaar || ''; | |
| const maskedAadhaar = rawAadhaar.length >= 8 | |
| ? 'XXXX XXXX ' + rawAadhaar.slice(-4) | |
| : (rawAadhaar || '-'); | |
| const ts = (p.timestamp || '').replace('T', ' ').split('.')[0]; | |
| return ` | |
| <tr> | |
| <td style="white-space:nowrap;">${escapeHtml(ts || '-')}</td> | |
| <td class="wh-utr">${escapeHtml(maskedAadhaar)}</td> | |
| <td style="font-weight:600;">${escapeHtml(p.name || '-')}</td> | |
| <td>${escapeHtml(p.dob || '-')}</td> | |
| <td>${statusPill(p.status)}</td> | |
| <td style="max-width:220px; color:var(--text-muted); font-size:0.82rem;">${escapeHtml(p.reason || '-')}</td> | |
| </tr>`; | |
| }).join('')} | |
| </tbody> | |
| </table>`; | |
| // ── Download History Table ────────────────────── | |
| const downloadsHtml = downloads.length === 0 | |
| ? '<div class="wh-empty"><i class="fa-solid fa-inbox"></i> No download records found.</div>' | |
| : `<table class="wh-table"> | |
| <thead> | |
| <tr> | |
| <th>Date & Time</th> | |
| <th>Aadhaar No.</th> | |
| <th>EID</th> | |
| <th>Type</th> | |
| <th>IP Address</th> | |
| </tr> | |
| </thead> | |
| <tbody> | |
| ${downloads.map(d => { | |
| const ts = (d.downloaded_at || '').replace('T', ' ').split('.')[0]; | |
| const typeLabel = d.is_masked | |
| ? '<span class="status-pill status-masked">Masked</span>' | |
| : '<span class="status-pill status-normal">Normal</span>'; | |
| return ` | |
| <tr> | |
| <td style="white-space:nowrap;">${escapeHtml(ts || '-')}</td> | |
| <td class="wh-utr">${escapeHtml(d.aadhaar_number)}</td> | |
| <td>${escapeHtml(d.eid || '-')}</td> | |
| <td>${typeLabel}</td> | |
| <td style="font-size:0.82rem; color:var(--text-muted);">${escapeHtml(d.ip_address || '-')}</td> | |
| </tr>`; | |
| }).join('')} | |
| </tbody> | |
| </table>`; | |
| // ── Combined All Activity Table ───────────────────── | |
| // Combine ledger, prints, and downloads into one activity stream | |
| let allActivities = []; | |
| // Add wallet transactions | |
| ledger.forEach(l => { | |
| allActivities.push({ | |
| type: 'wallet', | |
| subType: l.type, | |
| date: l.date, | |
| description: l.description || 'Wallet adjustment', | |
| amount: l.amount, | |
| balance_after: l.balance_after, | |
| aadhaar: '-', | |
| status: l.type | |
| }); | |
| }); | |
| // Add print/generate activities | |
| prints.forEach(p => { | |
| const rawAadhaar = p.aadhaar || ''; | |
| const maskedAadhaar = rawAadhaar.length >= 8 | |
| ? 'XXXX XXXX ' + rawAadhaar.slice(-4) | |
| : (rawAadhaar || '-'); | |
| allActivities.push({ | |
| type: 'print', | |
| subType: 'generate', | |
| date: (p.timestamp || '').replace('T', ' ').split('.')[0], | |
| description: `Generated Aadhaar for ${p.name || 'User'}`, | |
| amount: null, | |
| balance_after: null, | |
| aadhaar: maskedAadhaar, | |
| status: 'completed' | |
| }); | |
| }); | |
| // Add download activities | |
| downloads.forEach(d => { | |
| allActivities.push({ | |
| type: 'download', | |
| subType: d.is_masked ? 'masked' : 'normal', | |
| date: (d.downloaded_at || '').replace('T', ' ').split('.')[0], | |
| description: `Downloaded Aadhaar ${d.is_masked ? '(Masked)' : ''}`, | |
| amount: null, | |
| balance_after: null, | |
| aadhaar: d.aadhaar_number, | |
| status: d.is_masked ? 'masked' : 'normal' | |
| }); | |
| }); | |
| // Sort by date descending | |
| allActivities.sort((a, b) => new Date(b.date) - new Date(a.date)); | |
| const activityHtml = allActivities.length === 0 | |
| ? '<div class="wh-empty"><i class="fa-solid fa-inbox"></i> No activity records found.</div>' | |
| : `<table class="wh-table"> | |
| <thead> | |
| <tr> | |
| <th>Date & Time</th> | |
| <th>Activity</th> | |
| <th>Aadhaar No.</th> | |
| <th>Details</th> | |
| <th>Amount/Balance</th> | |
| </tr> | |
| </thead> | |
| <tbody> | |
| ${allActivities.map(act => { | |
| let typeLabel, typeIcon, typeClass; | |
| if (act.type === 'wallet') { | |
| typeLabel = act.subType === 'credit' ? 'Wallet Credit' : 'Wallet Debit'; | |
| typeIcon = 'fa-wallet'; | |
| typeClass = act.subType; | |
| } else if (act.type === 'print') { | |
| typeLabel = 'Generate Aadhaar'; | |
| typeIcon = 'fa-id-card'; | |
| typeClass = 'success'; | |
| } else if (act.type === 'download') { | |
| typeLabel = act.subType === 'masked' ? 'Download (Masked)' : 'Download'; | |
| typeIcon = 'fa-download'; | |
| typeClass = act.subType === 'masked' ? 'masked' : 'normal'; | |
| } | |
| const amountDisplay = act.type === 'wallet' | |
| ? `<span class="${act.subType === 'credit' ? 'wh-amount-credit' : 'wh-amount-debit'}">${act.subType === 'credit' ? '+' : '-'}${formatInr(act.amount)}</span><br><small>Bal: ${formatInr(act.balance_after)}</small>` | |
| : '-'; | |
| return ` | |
| <tr> | |
| <td style="white-space:nowrap;">${escapeHtml(act.date || '-')}</td> | |
| <td><span class="status-pill status-${typeClass}"><i class="fa-solid ${typeIcon}"></i> ${typeLabel}</span></td> | |
| <td class="wh-utr">${escapeHtml(act.aadhaar)}</td> | |
| <td style="font-size:0.85rem;">${escapeHtml(act.description)}</td> | |
| <td>${amountDisplay}</td> | |
| </tr>`; | |
| }).join('')} | |
| </tbody> | |
| </table>`; | |
| expandedArea.innerHTML = ` | |
| <div class="wallet-detail-panel"> | |
| <!-- User Profile Strip --> | |
| <div class="wdp-profile"> | |
| <div class="wdp-profile-grid"> | |
| <div class="wdp-item"> | |
| <span class="wdp-label"><i class="fa-solid fa-user"></i> Name</span> | |
| <span class="wdp-value">${escapeHtml(u.name || '-')}</span> | |
| </div> | |
| <div class="wdp-item"> | |
| <span class="wdp-label"><i class="fa-solid fa-mobile-screen"></i> Phone</span> | |
| <span class="wdp-value">${escapeHtml(u.phone || '-')}</span> | |
| </div> | |
| <div class="wdp-item"> | |
| <span class="wdp-label"><i class="fa-solid fa-envelope"></i> Email</span> | |
| <span class="wdp-value">${escapeHtml(u.email || '-')}</span> | |
| </div> | |
| <div class="wdp-item"> | |
| <span class="wdp-label"><i class="fa-solid fa-calendar-plus"></i> Joined</span> | |
| <span class="wdp-value">${escapeHtml((u.created_at || '').split(' ')[0])}</span> | |
| </div> | |
| <div class="wdp-item"> | |
| <span class="wdp-label"><i class="fa-solid fa-shield-halved"></i> Role</span> | |
| <span class="wdp-value">${escapeHtml(u.role || 'user')}</span> | |
| </div> | |
| <div class="wdp-item"> | |
| <span class="wdp-label"><i class="fa-solid fa-wallet"></i> Balance</span> | |
| <span class="wdp-value wdp-balance">${formatInr(u.balance)}</span> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- History Tabs --> | |
| <div class="wdp-tabs"> | |
| <button class="wdp-tab active" onclick="switchWalletTab(this, 'wdp-topups-${userId}')"> | |
| <i class="fa-solid fa-arrow-up-to-dotted-line"></i> | |
| Topups | |
| <span class="wdp-tab-badge">${topups.length}</span> | |
| </button> | |
| <button class="wdp-tab" onclick="switchWalletTab(this, 'wdp-prints-${userId}')"> | |
| <i class="fa-solid fa-file-invoice"></i> | |
| Prints | |
| <span class="wdp-tab-badge">${prints.length}</span> | |
| </button> | |
| <button class="wdp-tab" onclick="switchWalletTab(this, 'wdp-downloads-${userId}')"> | |
| <i class="fa-solid fa-download"></i> | |
| Downloads | |
| <span class="wdp-tab-badge">${downloads.length}</span> | |
| </button> | |
| <button class="wdp-tab" onclick="switchWalletTab(this, 'wdp-ledger-${userId}')"> | |
| <i class="fa-solid fa-list-ul"></i> | |
| All Activity | |
| <span class="wdp-tab-badge">${allActivities.length}</span> | |
| </button> | |
| </div> | |
| <div class="wdp-content active" id="wdp-topups-${userId}">${topupsHtml}</div> | |
| <div class="wdp-content" id="wdp-prints-${userId}" style="display:none;">${printsHtml}</div> | |
| <div class="wdp-content" id="wdp-downloads-${userId}" style="display:none;">${downloadsHtml}</div> | |
| <div class="wdp-content" id="wdp-ledger-${userId}" style="display:none;">${activityHtml}</div> | |
| </div>`; | |
| card.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); | |
| } catch (e) { | |
| expandedArea.innerHTML = '<div class="error-state"><i class="fa-solid fa-triangle-exclamation"></i> Network error loading details.</div>'; | |
| } | |
| } | |
| function switchWalletTab(btn, targetId) { | |
| const panel = btn.closest('.wallet-detail-panel'); | |
| panel.querySelectorAll('.wdp-tab').forEach(t => t.classList.remove('active')); | |
| panel.querySelectorAll('.wdp-content').forEach(c => c.style.display = 'none'); | |
| btn.classList.add('active'); | |
| const target = document.getElementById(targetId); | |
| if (target) target.style.display = 'block'; | |
| } | |
| function updateWalletOverview(users, transactions) { | |
| const totalUsersEl = document.getElementById('wallet-total-users'); | |
| const totalBalanceEl = document.getElementById('wallet-total-balance'); | |
| const recentActivityEl = document.getElementById('wallet-recent-activity'); | |
| const lowBalanceEl = document.getElementById('wallet-low-balance'); | |
| const totalUsers = users.length; | |
| const totalBalance = users.reduce((sum, user) => sum + (parseFloat(user.balance) || 0), 0); | |
| const lowBalanceCount = users.filter(user => (parseFloat(user.balance) || 0) < 100).length; | |
| const dayAgo = Date.now() - (24 * 60 * 60 * 1000); | |
| const recentCount = transactions.filter(tx => new Date(tx.date).getTime() >= dayAgo).length; | |
| if (totalUsersEl) totalUsersEl.innerText = String(totalUsers); | |
| if (totalBalanceEl) totalBalanceEl.innerText = formatInr(totalBalance); | |
| if (recentActivityEl) recentActivityEl.innerText = String(recentCount); | |
| if (lowBalanceEl) lowBalanceEl.innerText = String(lowBalanceCount); | |
| } | |
| function formatInr(value) { | |
| const amount = Number(value) || 0; | |
| return `₹${amount.toLocaleString('en-IN', { maximumFractionDigits: 2 })}`; | |
| } | |
| function escapeHtml(value) { | |
| return String(value) | |
| .replaceAll('&', '&') | |
| .replaceAll('<', '<') | |
| .replaceAll('>', '>') | |
| .replaceAll('"', '"') | |
| .replaceAll("'", '''); | |
| } | |
| function showWalletAdjustModal(userId, userName, currentBalance) { | |
| if (typeof openWalletManageModal === 'function') { | |
| openWalletManageModal(userId, userName, currentBalance); | |
| } | |
| } | |
| function openWalletManageModal(userId, userName, currentBalance) { | |
| activeWalletUser = userId; | |
| const modal = document.getElementById('wallet-manage-modal'); | |
| const userEl = document.getElementById('wallet-manage-user'); | |
| const balanceEl = document.getElementById('wallet-manage-current-balance'); | |
| const amountInput = document.getElementById('wallet-manage-amount'); | |
| const reasonInput = document.getElementById('wallet-manage-reason'); | |
| const feedback = document.getElementById('wallet-manage-feedback'); | |
| if (!modal) return; | |
| if (userEl) userEl.innerText = `Managing: ${userName}`; | |
| if (balanceEl) balanceEl.innerText = `Current Balance: ${formatInr(currentBalance)}`; | |
| if (amountInput) amountInput.value = ''; | |
| if (reasonInput) reasonInput.value = ''; | |
| if (feedback) feedback.innerText = ''; | |
| modal.style.display = 'flex'; | |
| } | |
| function closeWalletManageModal() { | |
| activeWalletUser = null; | |
| const modal = document.getElementById('wallet-manage-modal'); | |
| if (modal) modal.style.display = 'none'; | |
| } | |
| async function submitWalletManage() { | |
| if (!activeWalletUser) return; | |
| const action = document.getElementById('wallet-manage-action').value; | |
| const amount = parseFloat(document.getElementById('wallet-manage-amount').value); | |
| const reason = document.getElementById('wallet-manage-reason').value.trim(); | |
| const feedback = document.getElementById('wallet-manage-feedback'); | |
| if (!amount || amount <= 0) { | |
| if (feedback) feedback.innerText = 'Please enter a valid amount greater than 0.'; | |
| return; | |
| } | |
| if (!reason) { | |
| if (feedback) feedback.innerText = 'Please provide a reason for this adjustment.'; | |
| return; | |
| } | |
| if (feedback) feedback.innerText = 'Processing request...'; | |
| try { | |
| const res = await fetch(`/api/admin/user/${encodeURIComponent(activeWalletUser)}/wallet`, { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ action, amount, reason }) | |
| }); | |
| const data = await res.json(); | |
| if (res.ok) { | |
| closeWalletManageModal(); | |
| if (typeof showToast === 'function') { | |
| showToast('success', 'Wallet Updated', `Successfully ${action}ed ${formatInr(amount)}`); | |
| } | |
| loadUserWallets(); | |
| } else { | |
| if (feedback) feedback.innerText = data.error || 'Failed to update wallet.'; | |
| } | |
| } catch (e) { | |
| if (feedback) feedback.innerText = 'Network error occurred.'; | |
| } | |
| } | |
| // Event Listeners | |
| document.addEventListener('DOMContentLoaded', () => { | |
| const walletSearch = document.getElementById('wallet-search'); | |
| const walletBalanceFilter = document.getElementById('wallet-balance-filter'); | |
| const walletSort = document.getElementById('wallet-sort'); | |
| if (walletSearch) walletSearch.addEventListener('input', applyWalletFilters); | |
| if (walletBalanceFilter) walletBalanceFilter.addEventListener('change', applyWalletFilters); | |
| if (walletSort) walletSort.addEventListener('change', applyWalletFilters); | |
| }); |