PHP Classes

File: templates/admin/economy.twig

Recommend this page to a friend!
  Packages of Matthew Asham   Binkterm PHP   templates/admin/economy.twig   Download  
File: templates/admin/economy.twig
Role: Auxiliary data
Content type: text/plain
Description: Auxiliary data
Class: Binkterm PHP
Bulletin board system based on the Web
Author: By
Last change:
Date: 5 days ago
Size: 22,387 bytes
 

Contents

Class file image Download
{% extends "base.twig" %} {% block title %}{{ t('ui.admin.economy.page_title', {}, locale, ['common']) }}{% endblock %} {% block content %} <div class="container-fluid"> <div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom"> <h1 class="h2">{{ t('ui.admin.economy.heading', {}, locale, ['common']) }}</h1> <div class="d-flex align-items-center gap-2"> <label class="me-1 mb-0 text-muted small" for="period-select">{{ t('ui.admin.economy.period', {}, locale, ['common']) }}</label> <select id="period-select" class="form-select form-select-sm" style="width:auto"> <option value="7d">{{ t('ui.admin.economy.period_7d', {}, locale, ['common']) }}</option> <option value="30d" selected>{{ t('ui.admin.economy.period_30d', {}, locale, ['common']) }}</option> <option value="90d">{{ t('ui.admin.economy.period_90d', {}, locale, ['common']) }}</option> <option value="all">{{ t('ui.admin.economy.period_all', {}, locale, ['common']) }}</option> </select> <a href="/admin" class="btn btn-sm btn-outline-secondary"> <i class="fas fa-arrow-left me-1"></i>{{ t('ui.admin.economy.dashboard', {}, locale, ['common']) }} </a> </div> </div> <div id="economy-alert"></div> <div id="loading-indicator" class="text-center py-5"> <div class="spinner-border text-primary" role="status"> <span class="visually-hidden">{{ t('ui.common.loading', {}, locale, ['common']) }}</span> </div> <p class="mt-2 text-muted">{{ t('ui.admin.economy.loading_statistics', {}, locale, ['common']) }}</p> </div> <div id="economy-content" style="display:none"> <div class="row mb-4" id="summary-cards"></div> <div class="row mb-4"> <div class="col-lg-6 mb-4"> <div class="card shadow-sm h-100"> <div class="card-header"><strong>{{ t('ui.admin.economy.period_snapshot', {}, locale, ['common']) }}</strong></div> <div class="card-body"> <div class="row gy-3"> <div class="col-sm-6"> <div class="text-muted small text-uppercase">{{ t('ui.admin.economy.transactions', {}, locale, ['common']) }}</div> <div class="h4 mb-0" id="snapshot-transactions">0</div> </div> <div class="col-sm-6"> <div class="text-muted small text-uppercase">{{ t('ui.admin.economy.active_users', {}, locale, ['common']) }}</div> <div class="h4 mb-0" id="snapshot-users">0</div> </div> <div class="col-sm-6"> <div class="text-muted small text-uppercase">{{ t('ui.admin.economy.credits_earned', {}, locale, ['common']) }}</div> <div class="h4 mb-0 text-success" id="snapshot-earned">0</div> </div> <div class="col-sm-6"> <div class="text-muted small text-uppercase">{{ t('ui.admin.economy.credits_spent', {}, locale, ['common']) }}</div> <div class="h4 mb-0 text-danger" id="snapshot-spent">0</div> </div> <div class="col-12"> <div class="text-muted small text-uppercase">{{ t('ui.admin.economy.net_flow', {}, locale, ['common']) }}</div> <div class="h4 mb-0" id="snapshot-net">0</div> </div> </div> </div> </div> </div> <div class="col-lg-6 mb-4"> <div class="card shadow-sm h-100"> <div class="card-header"><strong>{{ t('ui.admin.economy.current_distribution', {}, locale, ['common']) }}</strong></div> <div class="card-body"> <div class="row gy-3"> <div class="col-sm-6"> <div class="text-muted small text-uppercase">{{ t('ui.admin.economy.funded_users', {}, locale, ['common']) }}</div> <div class="h4 mb-0" id="distribution-funded">0</div> </div> <div class="col-sm-6"> <div class="text-muted small text-uppercase">{{ t('ui.admin.economy.average_balance', {}, locale, ['common']) }}</div> <div class="h4 mb-0" id="distribution-average">0</div> </div> <div class="col-sm-6"> <div class="text-muted small text-uppercase">{{ t('ui.admin.economy.median_balance', {}, locale, ['common']) }}</div> <div class="h4 mb-0" id="distribution-median">0</div> </div> <div class="col-sm-6"> <div class="text-muted small text-uppercase">{{ t('ui.admin.economy.largest_balance', {}, locale, ['common']) }}</div> <div class="h4 mb-0" id="distribution-largest">0</div> </div> <div class="col-12"> <div class="text-muted small text-uppercase">{{ t('ui.admin.economy.richest_user', {}, locale, ['common']) }}</div> <div class="h5 mb-0" id="distribution-richest-user">{{ t('ui.common.not_applicable', {}, locale, ['common']) }}</div> </div> </div> </div> </div> </div> </div> <div class="row mb-4"> <div class="col-xl-4 mb-4"> <div class="card shadow-sm h-100"> <div class="card-header"><strong>{{ t('ui.admin.economy.transaction_types', {}, locale, ['common']) }}</strong></div> <div class="card-body p-0"> <div class="table-responsive"> <table class="table table-sm table-hover mb-0"> <thead> <tr> <th>{{ t('ui.admin.economy.type', {}, locale, ['common']) }}</th> <th class="text-end">{{ t('ui.admin.economy.count', {}, locale, ['common']) }}</th> <th class="text-end">{{ t('ui.admin.economy.net', {}, locale, ['common']) }}</th> </tr> </thead> <tbody id="type-rows"></tbody> </table> </div> </div> </div> </div> <div class="col-xl-4 mb-4"> <div class="card shadow-sm h-100"> <div class="card-header"><strong>{{ t('ui.admin.economy.top_earners', {}, locale, ['common']) }}</strong></div> <div class="card-body p-0"> <div class="table-responsive"> <table class="table table-sm table-hover mb-0"> <thead> <tr> <th>{{ t('ui.admin.economy.user', {}, locale, ['common']) }}</th> <th class="text-end">{{ t('ui.admin.economy.earned', {}, locale, ['common']) }}</th> <th class="text-end">{{ t('ui.admin.economy.tx_short', {}, locale, ['common']) }}</th> </tr> </thead> <tbody id="earner-rows"></tbody> </table> </div> </div> </div> </div> <div class="col-xl-4 mb-4"> <div class="card shadow-sm h-100"> <div class="card-header"><strong>{{ t('ui.admin.economy.top_spenders', {}, locale, ['common']) }}</strong></div> <div class="card-body p-0"> <div class="table-responsive"> <table class="table table-sm table-hover mb-0"> <thead> <tr> <th>{{ t('ui.admin.economy.user', {}, locale, ['common']) }}</th> <th class="text-end">{{ t('ui.admin.economy.spent', {}, locale, ['common']) }}</th> <th class="text-end">{{ t('ui.admin.economy.tx_short', {}, locale, ['common']) }}</th> </tr> </thead> <tbody id="spender-rows"></tbody> </table> </div> </div> </div> </div> </div> <div class="row mb-4"> <div class="col-lg-6 mb-4"> <div class="card shadow-sm h-100"> <div class="card-header"><strong>{{ t('ui.admin.economy.richest_accounts', {}, locale, ['common']) }}</strong></div> <div class="card-body p-0"> <div class="table-responsive"> <table class="table table-sm table-hover mb-0"> <thead> <tr> <th>{{ t('ui.admin.economy.user', {}, locale, ['common']) }}</th> <th class="text-end">{{ t('ui.admin.economy.balance', {}, locale, ['common']) }}</th> </tr> </thead> <tbody id="richest-rows"></tbody> </table> </div> </div> </div> </div> <div class="col-lg-6 mb-4"> <div class="card shadow-sm h-100"> <div class="card-header"><strong>{{ t('ui.admin.economy.recent_transactions', {}, locale, ['common']) }}</strong></div> <div class="card-body p-0"> <div class="table-responsive"> <table class="table table-sm table-hover mb-0"> <thead> <tr> <th>{{ t('ui.admin.economy.user', {}, locale, ['common']) }}</th> <th>{{ t('ui.common.description', {}, locale, ['common']) }}</th> <th class="text-end">{{ t('ui.admin.economy.amount', {}, locale, ['common']) }}</th> </tr> </thead> <tbody id="recent-rows"></tbody> </table> </div> </div> </div> </div> </div> </div> </div> <script> (function() { function uiT(key, fallback, params = {}) { if (window.t) { return window.t(key, params, fallback); } return fallback; } const periodSelect = document.getElementById('period-select'); const loadingIndicator = document.getElementById('loading-indicator'); const economyContent = document.getElementById('economy-content'); const economyAlert = document.getElementById('economy-alert'); let creditsSymbol = '$'; function escapeHtml(value) { return String(value ?? '') .replace(/&/g, '&amp;') .replace(/</g, '&lt;') .replace(/>/g, '&gt;') .replace(/"/g, '&quot;') .replace(/'/g, '&#39;'); } function formatNumber(value) { return new Intl.NumberFormat().format(Number(value || 0)); } function formatAmount(value) { return `${creditsSymbol}${formatNumber(value)}`; } function formatSignedAmount(value) { const numericValue = Number(value || 0); const prefix = numericValue > 0 ? '+' : ''; return `${prefix}${creditsSymbol}${formatNumber(numericValue)}`; } function formatUser(user) { if (!user) { return uiT('ui.common.not_applicable', 'n/a'); } const username = user.username ? escapeHtml(user.username) : uiT('ui.common.unknown', 'unknown'); const profileUrl = user.username ? `/profile/${encodeURIComponent(user.username)}` : null; const usernameHtml = profileUrl ? `<a href="${profileUrl}" class="text-decoration-none">${username}</a>` : username; const realName = user.real_name ? ` <span class="text-muted">(${escapeHtml(user.real_name)})</span>` : ''; return `${usernameHtml}${realName}`; } function setAlert(message, type) { economyAlert.innerHTML = `<div class="alert alert-${type}">${escapeHtml(message)}</div>`; } function clearAlert() { economyAlert.innerHTML = ''; } function renderSummaryCards(data) { const summary = data.summary || {}; const cards = [ { label: uiT('ui.admin.economy.credits_in_circulation', 'Credits In Circulation'), value: formatAmount(summary.total_credits), accent: 'success', icon: 'fa-coins' }, { label: uiT('ui.admin.economy.funded_wallets', 'Funded Wallets'), value: formatNumber(summary.funded_users), accent: 'primary', icon: 'fa-wallet' }, { label: uiT('ui.admin.economy.average_balance', 'Average Balance'), value: formatAmount(Math.round(summary.avg_balance || 0)), accent: 'info', icon: 'fa-chart-line' }, { label: uiT('ui.admin.economy.median_balance', 'Median Balance'), value: formatAmount(Math.round(summary.median_balance || 0)), accent: 'warning', icon: 'fa-scale-balanced' } ]; document.getElementById('summary-cards').innerHTML = cards.map(card => ` <div class="col-xl-3 col-md-6 mb-4"> <div class="card border-start border-4 border-${card.accent} shadow-sm h-100"> <div class="card-body"> <div class="d-flex justify-content-between align-items-center"> <div> <div class="text-muted small text-uppercase mb-1">${escapeHtml(card.label)}</div> <div class="h4 mb-0">${escapeHtml(card.value)}</div> </div> <i class="fas ${card.icon} fa-2x text-muted opacity-50"></i> </div> </div> </div> </div> `).join(''); } function renderRows(tbodyId, rows, renderFn, emptyMessage, colspan) { const tbody = document.getElementById(tbodyId); if (!tbody) { return; } if (!rows || rows.length === 0) { tbody.innerHTML = `<tr><td colspan="${colspan}" class="text-center text-muted py-3">${escapeHtml(emptyMessage)}</td></tr>`; return; } tbody.innerHTML = rows.map(renderFn).join(''); } function renderData(data) { creditsSymbol = data.credits_symbol || '$'; clearAlert(); if (!data.credits_enabled) { setAlert(uiT('ui.admin.economy.credits_disabled_notice', 'Credits are currently disabled. Historical balances and ledger data are still shown.'), 'warning'); } renderSummaryCards(data); const summary = data.summary || {}; const periodSummary = data.period_summary || {}; const richestUser = data.richest_user || null; document.getElementById('snapshot-transactions').textContent = formatNumber(periodSummary.transaction_count); document.getElementById('snapshot-users').textContent = formatNumber(periodSummary.active_users); document.getElementById('snapshot-earned').textContent = formatAmount(periodSummary.credits_earned); document.getElementById('snapshot-spent').textContent = formatAmount(periodSummary.credits_spent); document.getElementById('snapshot-net').textContent = formatSignedAmount(periodSummary.net_flow); document.getElementById('snapshot-net').className = `h4 mb-0 ${periodSummary.net_flow > 0 ? 'text-success' : (periodSummary.net_flow < 0 ? 'text-danger' : 'text-muted')}`; document.getElementById('distribution-funded').textContent = `${formatNumber(summary.funded_users)} / ${formatNumber(summary.total_users)}`; document.getElementById('distribution-average').textContent = formatAmount(Math.round(summary.avg_balance || 0)); document.getElementById('distribution-median').textContent = formatAmount(Math.round(summary.median_balance || 0)); document.getElementById('distribution-largest').textContent = formatAmount(summary.max_balance); document.getElementById('distribution-richest-user').innerHTML = richestUser ? `${formatUser(richestUser)} <span class="text-success ms-1">${escapeHtml(formatAmount(richestUser.credit_balance))}</span>` : uiT('ui.common.not_applicable', 'n/a'); renderRows( 'type-rows', data.transaction_types, row => ` <tr> <td> <div class="fw-semibold">${escapeHtml(row.transaction_type)}</div> <div class="small text-muted">${formatNumber(row.unique_users)} ${uiT('ui.admin.economy.users', 'users')}</div> </td> <td class="text-end">${formatNumber(row.transaction_count)}</td> <td class="text-end ${row.net_amount > 0 ? 'text-success' : (row.net_amount < 0 ? 'text-danger' : 'text-muted')}">${escapeHtml(formatSignedAmount(row.net_amount))}</td> </tr> `, uiT('ui.admin.economy.no_transactions_period', 'No transactions for this period'), 3 ); renderRows( 'earner-rows', data.top_earners, row => ` <tr> <td>${formatUser(row)}</td> <td class="text-end text-success">${escapeHtml(formatAmount(row.total_earned))}</td> <td class="text-end">${formatNumber(row.transaction_count)}</td> </tr> `, uiT('ui.admin.economy.no_earning_activity_period', 'No earning activity for this period'), 3 ); renderRows( 'spender-rows', data.top_spenders, row => ` <tr> <td>${formatUser(row)}</td> <td class="text-end text-danger">${escapeHtml(formatAmount(row.total_spent))}</td> <td class="text-end">${formatNumber(row.transaction_count)}</td> </tr> `, uiT('ui.admin.economy.no_spending_activity_period', 'No spending activity for this period'), 3 ); renderRows( 'richest-rows', data.richest_users, row => ` <tr> <td>${formatUser(row)}</td> <td class="text-end fw-semibold">${escapeHtml(formatAmount(row.credit_balance))}</td> </tr> `, uiT('ui.admin.economy.no_funded_users_yet', 'No funded users yet'), 2 ); renderRows( 'recent-rows', data.recent_transactions, row => ` <tr> <td> <div>${formatUser(row)}</div> <div class="small text-muted">${escapeHtml(row.transaction_type)}</div> </td> <td> <div>${escapeHtml(row.description || uiT('ui.admin.economy.no_description', 'No description'))}</div> <div class="small text-muted">${escapeHtml(new Date(row.created_at).toLocaleString())}</div> </td> <td class="text-end ${row.amount > 0 ? 'text-success' : 'text-danger'}"> <div>${escapeHtml(formatSignedAmount(row.amount))}</div> <div class="small text-muted">${uiT('ui.admin.economy.bal_short', 'Bal')} ${escapeHtml(formatAmount(row.balance_after))}</div> </td> </tr> `, uiT('ui.admin.economy.no_recent_transactions_period', 'No recent transactions in this period'), 3 ); } function loadEconomy() { loadingIndicator.style.display = ''; economyContent.style.display = 'none'; fetch(`/admin/api/economy?period=${encodeURIComponent(periodSelect.value)}`) .then(response => { if (!response.ok) { throw new Error(uiT('ui.admin.economy.load_failed', 'Failed to load economy statistics')); } return response.json(); }) .then(data => { renderData(data); loadingIndicator.style.display = 'none'; economyContent.style.display = ''; }) .catch(error => { loadingIndicator.style.display = 'none'; economyContent.style.display = 'none'; setAlert(error.message || uiT('ui.admin.economy.load_failed', 'Failed to load economy statistics'), 'danger'); }); } periodSelect.addEventListener('change', loadEconomy); loadI18nNamespaces(['common', 'errors']).then(function() { loadEconomy(); }); })(); </script> {% endblock %}