Add leaderboard period functionality to currency tab

- Implemented a tabbed interface for the currency leaderboard, allowing users to switch between weekly, monthly, quarterly, and yearly views.
- Enhanced the rendering of leaderboard data based on the selected period, improving user experience and data visibility.
- Added JavaScript functionality to dynamically update the leaderboard and URL parameters based on user selection.
This commit is contained in:
Louis Whittington 2026-03-31 12:47:11 -05:00
parent 36ce29f7bb
commit 98c1649a32
2 changed files with 154 additions and 6 deletions

View File

@ -1157,6 +1157,53 @@
window.alert(err.message || 'Could not reverse transaction');
});
});
var periodDataEl = document.getElementById('currencyLeaderboardPeriodData');
var periodBodyEl = document.getElementById('currencyLeaderboardPeriodBody');
var periodTabsEl = document.getElementById('currencyLeaderboardPeriodTabs');
if (periodDataEl && periodBodyEl && periodTabsEl) {
var periodData = {};
try {
periodData = JSON.parse(periodDataEl.textContent || '{}');
} catch (e) {
periodData = {};
}
function renderLeaderboardPeriod(periodKey) {
var rows = Array.isArray(periodData[periodKey]) ? periodData[periodKey] : [];
var sym = periodBodyEl.getAttribute('data-currency-symbol') || '';
var html = '';
rows.forEach(function (row, idx) {
var total = Number(row.period_total || 0);
var isSelf = Boolean(row.is_self);
html += '<tr class="' + (isSelf ? 'table-primary' : '') + '">';
html += '<td class="text-muted">' + String(idx + 1) + '</td>';
html += '<td>' + escapeHtml(row.name || '') + (isSelf ? ' <span class="badge text-bg-info">You</span>' : '') + '</td>';
html += '<td class="text-end text-nowrap"><strong>' + escapeHtml(total.toFixed(2)) + '</strong> ' + escapeHtml(sym) + '</td>';
html += '</tr>';
});
periodBodyEl.innerHTML = html;
}
periodTabsEl.querySelectorAll('[data-lb-period]').forEach(function (btn) {
btn.addEventListener('click', function () {
var next = btn.getAttribute('data-lb-period') || 'weekly';
periodTabsEl.querySelectorAll('[data-lb-period]').forEach(function (b) {
b.classList.remove('active');
b.setAttribute('aria-selected', 'false');
});
btn.classList.add('active');
btn.setAttribute('aria-selected', 'true');
renderLeaderboardPeriod(next);
var params = new URLSearchParams(window.location.search);
params.set('tab', 'currency');
params.set('lb_period', next);
var nextUrl = window.location.pathname + '?' + params.toString();
window.history.replaceState({}, '', nextUrl);
});
});
}
}
function bindPeopleTable() {

View File

@ -14,6 +14,7 @@ $bankingMode = bankingEnabled($familySettings);
$canRecordExpense = $activePerson !== null
&& ($activePerson['role'] ?? '') === ROLE_HEAD
&& isHohVerified();
$activePersonId = (string) ($activePerson['id'] ?? '');
$people = migrateAllPeople($people);
if ($bankingMode) {
@ -32,6 +33,12 @@ usort($leaderboard, static function ($a, $b) {
return strcmp((string) ($a['name'] ?? ''), (string) ($b['name'] ?? ''));
});
$leaderboardPeriod = trim((string) ($_GET['lb_period'] ?? 'weekly'));
$allowedLeaderboardPeriods = ['weekly', 'monthly', 'quarterly', 'yearly'];
if (!in_array($leaderboardPeriod, $allowedLeaderboardPeriods, true)) {
$leaderboardPeriod = 'weekly';
}
$nameById = [];
foreach ($people as $p) {
if (!empty($p['id'])) {
@ -60,6 +67,90 @@ usort($bankRows, static function ($a, $b) {
return strcmp((string) ($b['created_at'] ?? ''), (string) ($a['created_at'] ?? ''));
});
$recentBank = array_slice($bankRows, 0, 80);
$leaderboardRowsByPeriod = [];
foreach ($allowedLeaderboardPeriods as $periodKey) {
$periodStart = new DateTimeImmutable('today');
if ($periodKey === 'monthly') {
$periodStart = $periodStart->modify('first day of this month');
} elseif ($periodKey === 'quarterly') {
$month = (int) $periodStart->format('n');
$quarterStartMonth = ((int) floor(($month - 1) / 3) * 3) + 1;
$periodStart = $periodStart->setDate((int) $periodStart->format('Y'), $quarterStartMonth, 1);
} elseif ($periodKey === 'yearly') {
$periodStart = $periodStart->setDate((int) $periodStart->format('Y'), 1, 1);
} else {
$periodStart = $periodStart->modify('monday this week');
}
$totalsByPerson = [];
foreach ($people as $p) {
$pid = (string) ($p['id'] ?? '');
if ($pid !== '') {
$totalsByPerson[$pid] = 0.0;
}
}
foreach ($bankRows as $row) {
if (!is_array($row)) {
continue;
}
$pid = (string) ($row['person_id'] ?? '');
if ($pid === '' || !array_key_exists($pid, $totalsByPerson)) {
continue;
}
$createdAt = trim((string) ($row['created_at'] ?? ''));
if ($createdAt === '') {
continue;
}
try {
$rowDate = new DateTimeImmutable($createdAt);
} catch (Exception $e) {
continue;
}
if ($rowDate < $periodStart) {
continue;
}
$amount = (float) ($row['amount'] ?? 0);
$type = (string) ($row['type'] ?? '');
if ($type === 'manual_credit' || $type === 'income_chore' || $type === 'reversal') {
$totalsByPerson[$pid] += $amount;
continue;
}
if ($type === 'manual_debit' || $type === 'charity_outflow') {
$totalsByPerson[$pid] -= $amount;
continue;
}
if ($type === 'transfer') {
$from = (string) ($row['from_account'] ?? '');
$to = (string) ($row['to_account'] ?? '');
if ($from === 'checking') {
$totalsByPerson[$pid] -= $amount;
} elseif ($to === 'checking') {
$totalsByPerson[$pid] += $amount;
}
}
}
$rows = [];
foreach ($people as $p) {
$pid = (string) ($p['id'] ?? '');
if ($pid === '') {
continue;
}
$rows[] = [
'id' => $pid,
'name' => (string) ($p['name'] ?? ''),
'period_total' => bankingRoundMoney((float) ($totalsByPerson[$pid] ?? 0)),
'is_self' => $activePerson && ($activePerson['id'] ?? '') === $pid,
];
}
usort($rows, static function ($a, $b) {
$cmp = ((float) ($b['period_total'] ?? 0)) <=> ((float) ($a['period_total'] ?? 0));
if ($cmp !== 0) {
return $cmp;
}
return strcmp((string) ($a['name'] ?? ''), (string) ($b['name'] ?? ''));
});
$leaderboardRowsByPeriod[$periodKey] = $rows;
}
$leaderboardByPeriodRows = $leaderboardRowsByPeriod[$leaderboardPeriod] ?? [];
$donatedByPersonThisMonth = [];
foreach ($bankRows as $row) {
if (!is_array($row)) {
@ -78,7 +169,6 @@ foreach ($bankRows as $row) {
}
$donatedByPersonThisMonth[$pid] = ($donatedByPersonThisMonth[$pid] ?? 0) + (float) ($row['amount'] ?? 0);
}
$activePersonId = (string) ($activePerson['id'] ?? '');
$activeBankPerson = null;
foreach ($people as $p) {
if (($p['id'] ?? '') === $activePersonId) {
@ -111,22 +201,33 @@ $activeCharityDonated = is_numeric($activeBankPerson['charity_donated_total'] ??
<div class="tab-content">
<div class="tab-pane fade show active" id="currencyLeaderboardPane" role="tabpanel" tabindex="0">
<div class="card mb-4">
<div class="card-header"><i class="fa fa-trophy me-1"></i>Total stars leaderboard</div>
<div class="card-header">
<div class="d-flex flex-wrap justify-content-between align-items-center gap-2">
<span><i class="fa fa-trophy me-1"></i>Leaderboard</span>
<div class="nav nav-pills nav-sm gap-1" id="currencyLeaderboardPeriodTabs" role="tablist" aria-label="Leaderboard period">
<button type="button" class="nav-link py-1 px-2 <?= $leaderboardPeriod === 'weekly' ? 'active' : '' ?>" data-lb-period="weekly">Weekly</button>
<button type="button" class="nav-link py-1 px-2 <?= $leaderboardPeriod === 'monthly' ? 'active' : '' ?>" data-lb-period="monthly">Monthly</button>
<button type="button" class="nav-link py-1 px-2 <?= $leaderboardPeriod === 'quarterly' ? 'active' : '' ?>" data-lb-period="quarterly">Quarterly</button>
<button type="button" class="nav-link py-1 px-2 <?= $leaderboardPeriod === 'yearly' ? 'active' : '' ?>" data-lb-period="yearly">Yearly</button>
</div>
</div>
</div>
<div class="card-body p-0">
<script type="application/json" id="currencyLeaderboardPeriodData"><?= json_encode($leaderboardRowsByPeriod, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT) ?></script>
<div class="table-responsive">
<table class="table table-striped mb-0 leaderboard-table">
<thead>
<tr>
<th scope="col" class="text-muted">#</th>
<th scope="col">Name</th>
<th scope="col" class="text-end">Total <?= htmlspecialchars($name, ENT_QUOTES, 'UTF-8') ?></th>
<th scope="col" class="text-end">Net <?= htmlspecialchars($name, ENT_QUOTES, 'UTF-8') ?></th>
</tr>
</thead>
<tbody>
<?php foreach ($leaderboard as $rank => $p): ?>
<tbody id="currencyLeaderboardPeriodBody" data-currency-symbol="<?= htmlspecialchars($sym, ENT_QUOTES, 'UTF-8') ?>">
<?php foreach ($leaderboardByPeriodRows as $rank => $p): ?>
<?php
$pid = (string) ($p['id'] ?? '');
$total = is_numeric($p['checking_balance'] ?? null) ? (float) $p['checking_balance'] : (is_numeric($p['currency_balance'] ?? null) ? (float) $p['currency_balance'] : 0.0);
$total = (float) ($p['period_total'] ?? 0);
$isSelf = $activePerson && ($activePerson['id'] ?? '') === $pid;
?>
<tr class="<?= $isSelf ? 'table-primary' : '' ?>">