/** * 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 = '
Loading user wallets...
'; 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 = '
Error loading user wallets. Please refresh.
'; } } 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 = '
No matching users found.
'; 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 `
${initial}
${bal}
${shortId}... ${safeDate}
`; }).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 = ' 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 = ' View Details'; }); // Expand this card card.classList.add('expanded'); if (toggleBtn) toggleBtn.innerHTML = ' Collapse'; expandedArea.innerHTML = `
Loading history for ${escapeHtml(userName)}...
`; try { const res = await fetch(`/api/admin/user/${encodeURIComponent(userId)}/details`); const data = await res.json(); if (!res.ok) { expandedArea.innerHTML = `
${escapeHtml(data.error || 'Failed to load details')}
`; 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 `${escapeHtml((s || 'N/A').toUpperCase())}`; }; // ── Topup History Table ────────────────────── const topupsHtml = topups.length === 0 ? '
No topup records found.
' : ` ${topups.map(tx => ` `).join('')}
Date & Time Amount UTR Number Status Remark
${escapeHtml((tx.date || '').split('.')[0])} +${formatInr(tx.amount)} ${escapeHtml(tx.utr || '-')} ${statusPill(tx.status)} ${escapeHtml(tx.reason || '-')}
`; // ── Print History Table ────────────────────── const printsHtml = prints.length === 0 ? '
No print records found.
' : ` ${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 ` `; }).join('')}
Date & Time Aadhaar No. Name DOB Status Reason / Remark
${escapeHtml(ts || '-')} ${escapeHtml(maskedAadhaar)} ${escapeHtml(p.name || '-')} ${escapeHtml(p.dob || '-')} ${statusPill(p.status)} ${escapeHtml(p.reason || '-')}
`; // ── Download History Table ────────────────────── const downloadsHtml = downloads.length === 0 ? '
No download records found.
' : ` ${downloads.map(d => { const ts = (d.downloaded_at || '').replace('T', ' ').split('.')[0]; const typeLabel = d.is_masked ? 'Masked' : 'Normal'; return ` `; }).join('')}
Date & Time Aadhaar No. EID Type IP Address
${escapeHtml(ts || '-')} ${escapeHtml(d.aadhaar_number)} ${escapeHtml(d.eid || '-')} ${typeLabel} ${escapeHtml(d.ip_address || '-')}
`; // ── 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 ? '
No activity records found.
' : ` ${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' ? `${act.subType === 'credit' ? '+' : '-'}${formatInr(act.amount)}
Bal: ${formatInr(act.balance_after)}` : '-'; return ` `; }).join('')}
Date & Time Activity Aadhaar No. Details Amount/Balance
${escapeHtml(act.date || '-')} ${typeLabel} ${escapeHtml(act.aadhaar)} ${escapeHtml(act.description)} ${amountDisplay}
`; expandedArea.innerHTML = `
Name ${escapeHtml(u.name || '-')}
Phone ${escapeHtml(u.phone || '-')}
Email ${escapeHtml(u.email || '-')}
Joined ${escapeHtml((u.created_at || '').split(' ')[0])}
Role ${escapeHtml(u.role || 'user')}
Balance ${formatInr(u.balance)}
${topupsHtml}
`; card.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); } catch (e) { expandedArea.innerHTML = '
Network error loading details.
'; } } 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); });