Gift Card Reports & Analytics

Back to Staff Portal
0
Total Gift Cards
+0 this month
$0
Outstanding Balance
Available funds
$0
This Month's Sales
+0% vs last month
$0
This Month's Redemptions
0 transactions

Sales Trend

Gift Card Usage

Recent Transactions

Loading transactions...

// GCP Billing API base const _gcrBilBase = (window.BEWELL_CONFIG && window.BEWELL_CONFIG.BILLING_API_URL) || 'https://wellbe-billing-api-375785209979.us-central1.run.app'; function _gcrAuthHeaders() { const t = sessionStorage.getItem('wb_token') || localStorage.getItem('auth_token') || localStorage.getItem('bewell_auth_token'); return { 'Content-Type': 'application/json', ...(t ? { 'Authorization': 'Bearer ' + t } : {}) }; } async function _gcrFetchReport(from, to) { const url = `${_gcrBilBase}/api/billing/gift-cards/report?from=${encodeURIComponent(from)}&to=${encodeURIComponent(to)}`; const res = await fetch(url, { headers: _gcrAuthHeaders() }); if (!res.ok) throw new Error('Gift cards report API error: ' + res.status); return await res.json(); } let salesChart, usageChart; let currentFilters = { dateRange: 30, cardType: '' }; // Load all data on page load async function loadDashboard() { await Promise.all([ loadSummaryData(), loadChartData(), loadRecentTransactions() ]); } // Compute from/to ISO strings from currentFilters.dateRange function _gcrDateRange() { const to = new Date(); const from = new Date(); from.setDate(from.getDate() - (currentFilters.dateRange || 30)); return { from: from.toISOString().split('T')[0], to: to.toISOString().split('T')[0] }; } // Load summary statistics async function loadSummaryData() { try { const now = new Date(); const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1); const startOfLastMonth = new Date(now.getFullYear(), now.getMonth() - 1, 1); const endOfLastMonth = new Date(now.getFullYear(), now.getMonth(), 0); // Fetch current month report const monthReport = await _gcrFetchReport( startOfMonth.toISOString().split('T')[0], now.toISOString().split('T')[0] ); // Fetch last month report for comparison const lastMonthReport = await _gcrFetchReport( startOfLastMonth.toISOString().split('T')[0], endOfLastMonth.toISOString().split('T')[0] ).catch(() => ({})); const totalCards = monthReport.total_cards || 0; const cardsThisMonth = monthReport.new_cards_this_period || 0; const totalBalance = parseFloat(monthReport.total_outstanding_balance || 0); const monthRevenue = parseFloat(monthReport.period_sales || 0); const lastMonthTotal = parseFloat(lastMonthReport.period_sales || 0); const revenueChange = lastMonthTotal > 0 ? ((monthRevenue - lastMonthTotal) / lastMonthTotal * 100).toFixed(1) : 0; const monthRedemptions = parseFloat(monthReport.period_redemptions || 0); const redemptionCount = monthReport.redemption_count || 0; // Update UI document.getElementById('totalCards').textContent = totalCards; document.getElementById('cardsChange').textContent = `+${cardsThisMonth} this month`; document.getElementById('totalBalance').textContent = `$${totalBalance.toFixed(2)}`; document.getElementById('monthRevenue').textContent = `$${monthRevenue.toFixed(2)}`; document.getElementById('revenueChange').textContent = `${revenueChange >= 0 ? '+' : ''}${revenueChange}% vs last month`; document.getElementById('monthRedemptions').textContent = `$${monthRedemptions.toFixed(2)}`; document.getElementById('redemptionCount').textContent = `${redemptionCount} transactions`; // Update change indicators if (revenueChange < 0) { document.querySelector('.summary-card.warning .summary-change').classList.remove('positive'); document.querySelector('.summary-card.warning .summary-change').classList.add('negative'); document.querySelector('.summary-card.warning .summary-change i').classList.remove('fa-arrow-up'); document.querySelector('.summary-card.warning .summary-change i').classList.add('fa-arrow-down'); } } catch (error) { console.error('Error loading summary data:', error); } } // Load chart data async function loadChartData() { const { from, to } = _gcrDateRange(); try { const report = await _gcrFetchReport(from, to); // daily_sales: array of { date, amount } or object keyed by date const dailySalesRaw = report.daily_sales || {}; const dailySales = Array.isArray(dailySalesRaw) ? Object.fromEntries(dailySalesRaw.map(d => [d.date, parseFloat(d.amount || 0)])) : dailySalesRaw; // Create sales chart const ctx1 = document.getElementById('salesChart').getContext('2d'); if (salesChart) salesChart.destroy(); salesChart = new Chart(ctx1, { type: 'line', data: { labels: Object.keys(dailySales), datasets: [{ label: 'Daily Sales', data: Object.values(dailySales), borderColor: '#3498db', backgroundColor: 'rgba(52, 152, 219, 0.1)', tension: 0.4 }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: false } }, scales: { y: { beginAtZero: true, ticks: { callback: value => '$' + value.toFixed(0) } } } } }); // Purchases vs redemptions doughnut const purchases = parseFloat(report.period_sales || 0); const redemptions = parseFloat(report.period_redemptions || 0); const ctx2 = document.getElementById('usageChart').getContext('2d'); if (usageChart) usageChart.destroy(); usageChart = new Chart(ctx2, { type: 'doughnut', data: { labels: ['Purchases', 'Redemptions'], datasets: [{ data: [purchases, redemptions], backgroundColor: ['#27ae60', '#e74c3c'], borderWidth: 0 }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { position: 'bottom' } } } }); } catch (error) { console.error('Error loading chart data:', error); } } // Load recent transactions async function loadRecentTransactions() { const loading = document.getElementById('activityLoading'); const table = document.getElementById('activityTable'); const tbody = document.getElementById('activityTableBody'); loading.style.display = 'block'; table.style.display = 'none'; try { const { from, to } = _gcrDateRange(); const report = await _gcrFetchReport(from, to); const transactions = Array.isArray(report.recent_transactions) ? report.recent_transactions : (report.transactions || []); // Apply card type filter client-side if set const filtered = currentFilters.cardType ? transactions.filter(tx => (tx.card_type || '') === currentFilters.cardType) : transactions; tbody.innerHTML = filtered.slice(0, 50).map(tx => { const date = new Date(tx.created_at).toLocaleString(); const typeClass = (tx.transaction_type || '').toLowerCase(); const amount = tx.transaction_type === 'redemption' ? `-$${Math.abs(parseFloat(tx.amount || 0)).toFixed(2)}` : `+$${parseFloat(tx.amount || 0).toFixed(2)}`; return ` ${date} ${formatCardNumber(tx.card_number || 'N/A')} ${tx.transaction_type} ${amount} $${parseFloat(tx.balance_after || 0).toFixed(2)} ${tx.notes || '-'} `; }).join(''); loading.style.display = 'none'; table.style.display = 'table'; } catch (error) { console.error('Error loading transactions:', error); loading.innerHTML = '

Error loading transactions

'; } } // Format card number function formatCardNumber(cardNumber) { if (!cardNumber) return 'N/A'; return cardNumber.replace(/(.{3})(.{4})(.{4})(.{4})/, '$1-$2-$3-$4'); } // Handle date range change function handleDateRangeChange() { const dateRange = document.getElementById('dateRange').value; const customStart = document.getElementById('customDateStart'); const customEnd = document.getElementById('customDateEnd'); if (dateRange === 'custom') { customStart.style.display = 'block'; customEnd.style.display = 'block'; } else { customStart.style.display = 'none'; customEnd.style.display = 'none'; currentFilters.dateRange = parseInt(dateRange); } } // Apply filters function applyFilters(event) { event.preventDefault(); const dateRange = document.getElementById('dateRange').value; const cardType = document.getElementById('cardType').value; if (dateRange === 'custom') { // Handle custom date range const startDate = document.getElementById('startDate').value; const endDate = document.getElementById('endDate').value; if (!startDate || !endDate) { alert('Please select both start and end dates'); return; } // Calculate days difference const start = new Date(startDate); const end = new Date(endDate); const days = Math.ceil((end - start) / (1000 * 60 * 60 * 24)); currentFilters.dateRange = days; } else { currentFilters.dateRange = parseInt(dateRange); } currentFilters.cardType = cardType; // Reload data with new filters loadDashboard(); } // Export chart function exportChart(chartId) { const chart = chartId === 'salesChart' ? salesChart : usageChart; const url = chart.toBase64Image(); const a = document.createElement('a'); a.href = url; a.download = `${chartId}_${new Date().toISOString().split('T')[0]}.png`; document.body.appendChild(a); a.click(); document.body.removeChild(a); } // Export transactions to CSV async function exportTransactions() { try { // Fetch transactions via GCP report endpoint (all time) const today = new Date().toISOString().split('T')[0]; const report = await _gcrFetchReport('2000-01-01', today); const transactions = Array.isArray(report.recent_transactions) ? report.recent_transactions : (report.transactions || []); // Create CSV const headers = ['Date', 'Card Number', 'Type', 'Amount', 'Balance After', 'Notes']; const rows = transactions.map(tx => [ new Date(tx.created_at).toLocaleString(), tx.card_number || 'N/A', tx.transaction_type, parseFloat(tx.amount || 0).toFixed(2), parseFloat(tx.balance_after || 0).toFixed(2), tx.notes || '' ]); const csvContent = [ headers.join(','), ...rows.map(row => row.map(cell => `"${cell}"`).join(',')) ].join('\n'); // Download CSV const blob = new Blob([csvContent], { type: 'text/csv' }); const url = window.URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `gift_card_transactions_${new Date().toISOString().split('T')[0]}.csv`; document.body.appendChild(a); a.click(); document.body.removeChild(a); window.URL.revokeObjectURL(url); } catch (error) { console.error('Error exporting transactions:', error); alert('Failed to export transactions. Please try again.'); } } // Initialize dashboard loadDashboard(); // Add Font Awesome icons if (!document.querySelector('link[href*="font-awesome"]')) { const link = document.createElement('link'); link.rel = 'stylesheet'; link.href = 'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css'; document.head.appendChild(link); }