/**
* 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 `
${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.
'
: `
| Date & Time |
Amount |
UTR Number |
Status |
Remark |
${topups.map(tx => `
| ${escapeHtml((tx.date || '').split('.')[0])} |
+${formatInr(tx.amount)} |
${escapeHtml(tx.utr || '-')} |
${statusPill(tx.status)} |
${escapeHtml(tx.reason || '-')} |
`).join('')}
`;
// ── Print History Table ──────────────────────
const printsHtml = prints.length === 0
? ' No print records found.
'
: `
| Date & Time |
Aadhaar No. |
Name |
DOB |
Status |
Reason / Remark |
${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 `
| ${escapeHtml(ts || '-')} |
${escapeHtml(maskedAadhaar)} |
${escapeHtml(p.name || '-')} |
${escapeHtml(p.dob || '-')} |
${statusPill(p.status)} |
${escapeHtml(p.reason || '-')} |
`;
}).join('')}
`;
// ── Download History Table ──────────────────────
const downloadsHtml = downloads.length === 0
? ' No download records found.
'
: `
| Date & Time |
Aadhaar No. |
EID |
Type |
IP Address |
${downloads.map(d => {
const ts = (d.downloaded_at || '').replace('T', ' ').split('.')[0];
const typeLabel = d.is_masked
? 'Masked'
: 'Normal';
return `
| ${escapeHtml(ts || '-')} |
${escapeHtml(d.aadhaar_number)} |
${escapeHtml(d.eid || '-')} |
${typeLabel} |
${escapeHtml(d.ip_address || '-')} |
`;
}).join('')}
`;
// ── 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.
'
: `
| Date & Time |
Activity |
Aadhaar No. |
Details |
Amount/Balance |
${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 `
| ${escapeHtml(act.date || '-')} |
${typeLabel} |
${escapeHtml(act.aadhaar)} |
${escapeHtml(act.description)} |
${amountDisplay} |
`;
}).join('')}
`;
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}
${printsHtml}
${downloadsHtml}
${activityHtml}
`;
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);
});