Dilip8756's picture
Upload 100 files
58c1398 verified
/**
* 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('&', '&amp;')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;')
.replaceAll('"', '&quot;')
.replaceAll("'", '&#39;');
}
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);
});