$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.
| # | Name | Net = htmlspecialchars($name, ENT_QUOTES, 'UTF-8') ?> |
|---|---|---|
| = (int) $rank + 1 ?> | = sanitizeInput($p['name'] ?? '') ?> You | = htmlspecialchars(number_format($total, 2, '.', ''), ENT_QUOTES, 'UTF-8') ?> = htmlspecialchars($sym, ENT_QUOTES, 'UTF-8') ?> |
Add people in Family settings to see balances.
| # | Name | Checking | Savings | Charity pending | Donated total | Donation goal progress | Balance |
|---|---|---|---|---|---|---|---|
| = (int) $rank + 1 ?> | = sanitizeInput($p['name'] ?? '') ?> You | = htmlspecialchars(number_format($checkingBal, 2, '.', ''), ENT_QUOTES, 'UTF-8') ?> = htmlspecialchars($sym, ENT_QUOTES, 'UTF-8') ?> | = htmlspecialchars(number_format($savingsBal, 2, '.', ''), ENT_QUOTES, 'UTF-8') ?> = htmlspecialchars($sym, ENT_QUOTES, 'UTF-8') ?> | = htmlspecialchars(number_format($charityPending, 2, '.', ''), ENT_QUOTES, 'UTF-8') ?> = htmlspecialchars($sym, ENT_QUOTES, 'UTF-8') ?> | = htmlspecialchars(number_format($charityDonated, 2, '.', ''), ENT_QUOTES, 'UTF-8') ?> = htmlspecialchars($sym, ENT_QUOTES, 'UTF-8') ?> |
0): ?>
= htmlspecialchars(number_format($donatedThisMonth, 2, '.', ''), ENT_QUOTES, 'UTF-8') ?> / = htmlspecialchars(number_format($goal, 2, '.', ''), ENT_QUOTES, 'UTF-8') ?> = htmlspecialchars($sym, ENT_QUOTES, 'UTF-8') ?>
No goal set
|
= htmlspecialchars(number_format($bal, 2, '.', ''), ENT_QUOTES, 'UTF-8') ?> = htmlspecialchars($sym, ENT_QUOTES, 'UTF-8') ?> |
Track your accounts and move money with one tap.
No bank transactions recorded yet.
| Date | Person | Type | Category | Note | Amount | |
|---|---|---|---|---|---|---|
| = sanitizeInput((string) ($row['created_at'] ?? '')) ?> | = sanitizeInput($nameById[$pid] ?? '') ?> | = sanitizeInput($type) ?> | = sanitizeInput((string) ($row['category'] ?? '')) ?> | = sanitizeInput((string) ($row['note'] ?? '')) ?> | = $amtSign ?>= htmlspecialchars(number_format($rawAmt, 2, '.', ''), ENT_QUOTES, 'UTF-8') ?> = htmlspecialchars($sym, ENT_QUOTES, 'UTF-8') ?> |
Download your monthly statement as JSON or CSV.
Deducts from one person’s balance (e.g. spent allowance). Requires enough balance.
Switch to a verified Head of household to record expenses.
No expenses recorded yet.
| Date | Title | To | Amount |
|---|---|---|---|
| = sanitizeInput((string) ($ex['date'] ?? '')) ?> | = sanitizeInput((string) ($ex['title'] ?? '')) ?> | = sanitizeInput($nameById[(string) ($ex['assignee_id'] ?? '')] ?? '') ?> | −= htmlspecialchars(number_format((float) ($ex['value'] ?? 0), 2, '.', ''), ENT_QUOTES, 'UTF-8') ?> = htmlspecialchars($sym, ENT_QUOTES, 'UTF-8') ?> |