$ba; if ($cmp !== 0) { return $cmp; } 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'])) { $nameById[(string) $p['id']] = (string) ($p['name'] ?? ''); } } $expenses = normalizeExpensesList(readJsonFile('expenses.json')); usort($expenses, static function ($a, $b) { $da = (string) ($a['date'] ?? ''); $db = (string) ($b['date'] ?? ''); if ($db !== $da) { return strcmp($db, $da); } return strcmp((string) ($b['created_at'] ?? ''), (string) ($a['created_at'] ?? '')); }); $recentExpenses = array_slice($expenses, 0, 50); $today = gmdate('Y-m-d'); $currentMonth = gmdate('Y-m'); $bankRows = readJsonFile('bank_transactions.json'); if (!is_array($bankRows)) { $bankRows = []; } 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)) { continue; } if ((string) ($row['type'] ?? '') !== 'charity_outflow') { continue; } $dateYmd = bankingYmd((string) ($row['created_at'] ?? '')); if (strpos($dateYmd, $currentMonth . '-') !== 0) { continue; } $pid = (string) ($row['person_id'] ?? ''); if ($pid === '') { continue; } $donatedByPersonThisMonth[$pid] = ($donatedByPersonThisMonth[$pid] ?? 0) + (float) ($row['amount'] ?? 0); } $activeBankPerson = null; foreach ($people as $p) { if (($p['id'] ?? '') === $activePersonId) { $activeBankPerson = $p; break; } } $activeChecking = is_numeric($activeBankPerson['checking_balance'] ?? null) ? (float) $activeBankPerson['checking_balance'] : 0.0; $activeSavings = is_numeric($activeBankPerson['savings_balance'] ?? null) ? (float) $activeBankPerson['savings_balance'] : 0.0; $activeCharityPending = is_numeric($activeBankPerson['charity_pending_balance'] ?? null) ? (float) $activeBankPerson['charity_pending_balance'] : 0.0; $activeCharityDonated = is_numeric($activeBankPerson['charity_donated_total'] ?? null) ? (float) $activeBankPerson['charity_donated_total'] : 0.0; ?>

Leaderboard and expenses use the family currency from Family settings.

Leaderboard
$p): ?>
# Name Net
You
Leaderboard

Add people in Family settings to see balances.

$p): ?> 0 ? (int) max(0, min(100, round(($donatedThisMonth / $goal) * 100))) : 0; $isSelf = $activePerson && ($activePerson['id'] ?? '') === $pid; ?>
# Name Checking Savings Charity pending Donated total Donation goal progress Balance
You 0): ?>
/
No goal set

Account overview for

Track your accounts and move money with one tap.

Checking
Savings
Charity pending
Donated total
Recent bank transactions

No bank transactions recorded yet.

DatePersonTypeCategoryNoteAmount

Monthly statements

Download your monthly statement as JSON or CSV.

Record expense

Deducts from one person’s balance (e.g. spent allowance). Requires enough balance.

0): ?>

Switch to a verified Head of household to record expenses.

Recent expenses

No expenses recorded yet.

Date Title To Amount