- 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.
536 lines
38 KiB
PHP
536 lines
38 KiB
PHP
<?php
|
||
require_once __DIR__ . '/../includes/db.php';
|
||
require_once __DIR__ . '/../includes/utils.php';
|
||
require_once __DIR__ . '/../includes/persona.php';
|
||
require_once __DIR__ . '/../includes/expense_helpers.php';
|
||
require_once __DIR__ . '/../includes/family_settings.php';
|
||
require_once __DIR__ . '/../includes/banking_helpers.php';
|
||
|
||
$sym = trim((string) ($familySettings['currency_symbol'] ?? '★'));
|
||
$name = trim((string) ($familySettings['currency_name'] ?? ''));
|
||
$tabTitle = currencyTabLabel($familySettings);
|
||
$bankingMode = bankingEnabled($familySettings);
|
||
|
||
$canRecordExpense = $activePerson !== null
|
||
&& ($activePerson['role'] ?? '') === ROLE_HEAD
|
||
&& isHohVerified();
|
||
$activePersonId = (string) ($activePerson['id'] ?? '');
|
||
|
||
$people = migrateAllPeople($people);
|
||
if ($bankingMode) {
|
||
$people = bankingApplySavingsInterestToPeople($people, $familySettings);
|
||
writeJsonFile('people.json', $people);
|
||
}
|
||
|
||
$leaderboard = $people;
|
||
usort($leaderboard, static function ($a, $b) {
|
||
$ba = is_numeric($a['checking_balance'] ?? null) ? (float) $a['checking_balance'] : (is_numeric($a['currency_balance'] ?? null) ? (float) $a['currency_balance'] : 0.0);
|
||
$bb = is_numeric($b['checking_balance'] ?? null) ? (float) $b['checking_balance'] : (is_numeric($b['currency_balance'] ?? null) ? (float) $b['currency_balance'] : 0.0);
|
||
$cmp = $bb <=> $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;
|
||
?>
|
||
|
||
<div id="currency" class="tab-content">
|
||
<h2 class="mb-2"><?= htmlspecialchars($tabTitle, ENT_QUOTES, 'UTF-8') ?></h2>
|
||
<p class="text-muted small mb-4">Leaderboard and expenses use the family currency from Family settings.</p>
|
||
<?php if ($bankingMode): ?>
|
||
<ul class="nav nav-pills mb-3 gap-2" role="tablist">
|
||
<li class="nav-item" role="presentation">
|
||
<button class="nav-link active" data-bs-toggle="pill" data-bs-target="#currencyLeaderboardPane" type="button" role="tab" aria-selected="true">
|
||
<i class="fa fa-trophy me-1"></i>Leaderboard
|
||
</button>
|
||
</li>
|
||
<li class="nav-item" role="presentation">
|
||
<button class="nav-link" data-bs-toggle="pill" data-bs-target="#currencyBankingPane" type="button" role="tab" aria-selected="false">
|
||
<i class="fa fa-building-columns me-1"></i>Banking
|
||
</button>
|
||
</li>
|
||
</ul>
|
||
<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">
|
||
<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">Net <?= htmlspecialchars($name, ENT_QUOTES, 'UTF-8') ?></th>
|
||
</tr>
|
||
</thead>
|
||
<tbody id="currencyLeaderboardPeriodBody" data-currency-symbol="<?= htmlspecialchars($sym, ENT_QUOTES, 'UTF-8') ?>">
|
||
<?php foreach ($leaderboardByPeriodRows as $rank => $p): ?>
|
||
<?php
|
||
$pid = (string) ($p['id'] ?? '');
|
||
$total = (float) ($p['period_total'] ?? 0);
|
||
$isSelf = $activePerson && ($activePerson['id'] ?? '') === $pid;
|
||
?>
|
||
<tr class="<?= $isSelf ? 'table-primary' : '' ?>">
|
||
<td class="text-muted"><?= (int) $rank + 1 ?></td>
|
||
<td><?= sanitizeInput($p['name'] ?? '') ?><?php if ($isSelf): ?> <span class="badge text-bg-info">You</span><?php endif; ?></td>
|
||
<td class="text-end text-nowrap"><strong><?= htmlspecialchars(number_format($total, 2, '.', ''), ENT_QUOTES, 'UTF-8') ?></strong> <?= htmlspecialchars($sym, ENT_QUOTES, 'UTF-8') ?></td>
|
||
</tr>
|
||
<?php endforeach; ?>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="tab-pane fade" id="currencyBankingPane" role="tabpanel" tabindex="0">
|
||
<?php endif; ?>
|
||
|
||
<div class="<?= $bankingMode ? '' : 'row g-4' ?>">
|
||
<?php if (!$bankingMode): ?>
|
||
<div class="col-lg-5">
|
||
<div class="card h-100">
|
||
<div class="card-header">
|
||
<i class="fa fa-trophy me-1" aria-hidden="true"></i> Leaderboard
|
||
</div>
|
||
<div class="card-body p-0">
|
||
<?php if (count($leaderboard) === 0): ?>
|
||
<p class="p-3 text-muted mb-0">Add people in Family settings to see balances.</p>
|
||
<?php else: ?>
|
||
<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>
|
||
<?php if ($bankingMode): ?>
|
||
<th scope="col" class="text-end">Checking</th>
|
||
<th scope="col" class="text-end">Savings</th>
|
||
<th scope="col" class="text-end">Charity pending</th>
|
||
<th scope="col" class="text-end">Donated total</th>
|
||
<th scope="col">Donation goal progress</th>
|
||
<?php else: ?>
|
||
<th scope="col" class="text-end">Balance</th>
|
||
<?php endif; ?>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<?php foreach ($leaderboard as $rank => $p): ?>
|
||
<?php
|
||
$pid = (string) ($p['id'] ?? '');
|
||
$bal = is_numeric($p['currency_balance'] ?? null) ? (float) $p['currency_balance'] : 0.0;
|
||
$checkingBal = is_numeric($p['checking_balance'] ?? null) ? (float) $p['checking_balance'] : 0.0;
|
||
$savingsBal = is_numeric($p['savings_balance'] ?? null) ? (float) $p['savings_balance'] : 0.0;
|
||
$charityPending = is_numeric($p['charity_pending_balance'] ?? null) ? (float) $p['charity_pending_balance'] : 0.0;
|
||
$charityDonated = is_numeric($p['charity_donated_total'] ?? null) ? (float) $p['charity_donated_total'] : 0.0;
|
||
$goal = is_numeric($p['donation_goal_monthly'] ?? null) ? (float) $p['donation_goal_monthly'] : 0.0;
|
||
$donatedThisMonth = (float) ($donatedByPersonThisMonth[$pid] ?? 0);
|
||
$goalPct = $goal > 0 ? (int) max(0, min(100, round(($donatedThisMonth / $goal) * 100))) : 0;
|
||
$isSelf = $activePerson && ($activePerson['id'] ?? '') === $pid;
|
||
?>
|
||
<tr class="<?= $isSelf ? 'table-primary' : '' ?>">
|
||
<td class="text-muted"><?= (int) $rank + 1 ?></td>
|
||
<td><?= sanitizeInput($p['name'] ?? '') ?><?php if ($isSelf): ?> <span class="badge text-bg-info">You</span><?php endif; ?></td>
|
||
<?php if ($bankingMode): ?>
|
||
<td class="text-end text-nowrap"><strong><?= htmlspecialchars(number_format($checkingBal, 2, '.', ''), ENT_QUOTES, 'UTF-8') ?></strong> <?= htmlspecialchars($sym, ENT_QUOTES, 'UTF-8') ?></td>
|
||
<td class="text-end text-nowrap"><?= htmlspecialchars(number_format($savingsBal, 2, '.', ''), ENT_QUOTES, 'UTF-8') ?> <?= htmlspecialchars($sym, ENT_QUOTES, 'UTF-8') ?></td>
|
||
<td class="text-end text-nowrap"><?= htmlspecialchars(number_format($charityPending, 2, '.', ''), ENT_QUOTES, 'UTF-8') ?> <?= htmlspecialchars($sym, ENT_QUOTES, 'UTF-8') ?></td>
|
||
<td class="text-end text-nowrap"><?= htmlspecialchars(number_format($charityDonated, 2, '.', ''), ENT_QUOTES, 'UTF-8') ?> <?= htmlspecialchars($sym, ENT_QUOTES, 'UTF-8') ?></td>
|
||
<td>
|
||
<?php if ($goal > 0): ?>
|
||
<div class="progress" role="progressbar" aria-label="Donation goal progress" aria-valuemin="0" aria-valuemax="100" aria-valuenow="<?= $goalPct ?>">
|
||
<div class="progress-bar" style="width: <?= $goalPct ?>%"></div>
|
||
</div>
|
||
<div class="small text-muted mt-1"><?= htmlspecialchars(number_format($donatedThisMonth, 2, '.', ''), ENT_QUOTES, 'UTF-8') ?> / <?= htmlspecialchars(number_format($goal, 2, '.', ''), ENT_QUOTES, 'UTF-8') ?> <?= htmlspecialchars($sym, ENT_QUOTES, 'UTF-8') ?></div>
|
||
<?php else: ?>
|
||
<span class="small text-muted">No goal set</span>
|
||
<?php endif; ?>
|
||
</td>
|
||
<?php else: ?>
|
||
<td class="text-end text-nowrap"><strong><?= htmlspecialchars(number_format($bal, 2, '.', ''), ENT_QUOTES, 'UTF-8') ?></strong> <?= htmlspecialchars($sym, ENT_QUOTES, 'UTF-8') ?></td>
|
||
<?php endif; ?>
|
||
</tr>
|
||
<?php endforeach; ?>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
<?php endif; ?>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<?php endif; ?>
|
||
|
||
<div class="<?= $bankingMode ? '' : 'col-lg-7' ?>">
|
||
<?php if ($bankingMode): ?>
|
||
<div class="row g-4">
|
||
<div class="col-lg-8">
|
||
<div class="banking-hero mb-3">
|
||
<h3 class="h5 mb-1">Account overview for <?= sanitizeInput((string) ($activeBankPerson['name'] ?? 'current person')) ?></h3>
|
||
<p class="small text-muted mb-0">Track your accounts and move money with one tap.</p>
|
||
</div>
|
||
<div class="row g-3 mb-4">
|
||
<div class="col-sm-6">
|
||
<div class="card banking-overview-card banking-overview-checking"><div class="card-body"><div class="small text-muted">Checking</div><div class="h4 mb-0"><?= htmlspecialchars(number_format($activeChecking, 2, '.', ''), ENT_QUOTES, 'UTF-8') ?> <?= htmlspecialchars($sym, ENT_QUOTES, 'UTF-8') ?></div></div></div>
|
||
</div>
|
||
<div class="col-sm-6">
|
||
<div class="card banking-overview-card banking-overview-savings"><div class="card-body"><div class="small text-muted">Savings</div><div class="h4 mb-0"><?= htmlspecialchars(number_format($activeSavings, 2, '.', ''), ENT_QUOTES, 'UTF-8') ?> <?= htmlspecialchars($sym, ENT_QUOTES, 'UTF-8') ?></div></div></div>
|
||
</div>
|
||
<div class="col-sm-6">
|
||
<div class="card banking-overview-card banking-overview-charity"><div class="card-body"><div class="small text-muted">Charity pending</div><div class="h4 mb-0"><?= htmlspecialchars(number_format($activeCharityPending, 2, '.', ''), ENT_QUOTES, 'UTF-8') ?> <?= htmlspecialchars($sym, ENT_QUOTES, 'UTF-8') ?></div></div></div>
|
||
</div>
|
||
<div class="col-sm-6">
|
||
<div class="card banking-overview-card banking-overview-donated"><div class="card-body"><div class="small text-muted">Donated total</div><div class="h4 mb-0"><?= htmlspecialchars(number_format($activeCharityDonated, 2, '.', ''), ENT_QUOTES, 'UTF-8') ?> <?= htmlspecialchars($sym, ENT_QUOTES, 'UTF-8') ?></div></div></div>
|
||
</div>
|
||
</div>
|
||
<div class="card">
|
||
<div class="card-header">Recent bank transactions</div>
|
||
<div class="card-body p-0">
|
||
<?php if (count($recentBank) === 0): ?>
|
||
<p class="p-3 text-muted mb-0">No bank transactions recorded yet.</p>
|
||
<?php else: ?>
|
||
<div class="table-responsive">
|
||
<table class="table table-sm mb-0">
|
||
<thead>
|
||
<tr><th>Date</th><th>Person</th><th>Type</th><th>Category</th><th>Note</th><th class="text-end">Amount</th><?php if ($canRecordExpense): ?><th></th><?php endif; ?></tr>
|
||
</thead>
|
||
<tbody>
|
||
<?php foreach ($recentBank as $row): ?>
|
||
<?php $pid = (string) ($row['person_id'] ?? ''); $rawAmt = (float) ($row['amount'] ?? 0); $type = (string) ($row['type'] ?? ''); $amtSign = in_array($type, ['manual_debit', 'charity_outflow'], true) ? '−' : '+'; ?>
|
||
<tr>
|
||
<td class="text-nowrap"><?= sanitizeInput((string) ($row['created_at'] ?? '')) ?></td>
|
||
<td><?= sanitizeInput($nameById[$pid] ?? '') ?></td>
|
||
<td><?= sanitizeInput($type) ?></td>
|
||
<td><?= sanitizeInput((string) ($row['category'] ?? '')) ?></td>
|
||
<td><?= sanitizeInput((string) ($row['note'] ?? '')) ?></td>
|
||
<td class="text-end text-nowrap"><?= $amtSign ?><?= htmlspecialchars(number_format($rawAmt, 2, '.', ''), ENT_QUOTES, 'UTF-8') ?> <?= htmlspecialchars($sym, ENT_QUOTES, 'UTF-8') ?></td>
|
||
<?php if ($canRecordExpense): ?><td class="text-end"><?php if (empty($row['reversed_by_transaction_id']) && ($row['type'] ?? '') !== 'reversal'): ?><button type="button" class="btn btn-sm btn-outline-danger btn-bank-reverse" data-id="<?= htmlspecialchars((string) ($row['id'] ?? ''), ENT_QUOTES, 'UTF-8') ?>">Reverse</button><?php endif; ?></td><?php endif; ?>
|
||
</tr>
|
||
<?php endforeach; ?>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
<?php endif; ?>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<div class="col-lg-4">
|
||
<div class="accordion" id="bankingActionsAccordion">
|
||
<div class="accordion-item">
|
||
<h2 class="accordion-header"><button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#bankActionTransfer">Move funds</button></h2>
|
||
<div id="bankActionTransfer" class="accordion-collapse collapse" data-bs-parent="#bankingActionsAccordion"><div class="accordion-body">
|
||
<form id="bankTransferForm" class="row g-3">
|
||
<div class="col-6"><label class="form-label" for="transfer_from_account">From</label><select class="form-select" id="transfer_from_account" required><option value="checking">Checking</option><option value="savings">Savings</option></select></div>
|
||
<div class="col-6"><label class="form-label" for="transfer_to_account">To</label><select class="form-select" id="transfer_to_account" required><option value="savings">Savings</option><option value="checking">Checking</option><option value="charity">Charity (irreversible)</option></select></div>
|
||
<div class="col-12"><label class="form-label" for="transfer_amount">Amount (<?= htmlspecialchars($sym, ENT_QUOTES, 'UTF-8') ?>)</label><input type="number" step="0.01" min="0.01" class="form-control" id="transfer_amount" required></div>
|
||
<div class="col-12"><label class="form-label" for="transfer_category">Category</label><input class="form-control" id="transfer_category" maxlength="50" value="transfer"></div>
|
||
<div class="col-12"><label class="form-label" for="transfer_note">Note</label><input class="form-control" id="transfer_note" maxlength="200" placeholder="Optional note"></div>
|
||
<div class="col-12"><button type="submit" class="btn btn-primary w-100">Submit transfer</button></div>
|
||
<div class="col-12"><div class="alert d-none" id="bankTransferFeedback" role="status"></div></div>
|
||
</form>
|
||
</div></div>
|
||
</div>
|
||
<div class="accordion-item">
|
||
<h2 class="accordion-header"><button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#bankActionGoal">Donation goal</button></h2>
|
||
<div id="bankActionGoal" class="accordion-collapse collapse" data-bs-parent="#bankingActionsAccordion"><div class="accordion-body">
|
||
<form id="donationGoalForm" class="row g-3">
|
||
<?php if ($canRecordExpense): ?><div class="col-12"><label class="form-label" for="donation_goal_person">Person</label><select class="form-select" id="donation_goal_person"><?php foreach ($people as $p): ?><?php if (empty($p['id'])) { continue; } ?><option value="<?= htmlspecialchars((string) $p['id'], ENT_QUOTES, 'UTF-8') ?>" <?= (($activePerson['id'] ?? '') === ($p['id'] ?? '')) ? 'selected' : '' ?>><?= sanitizeInput($p['name'] ?? '') ?></option><?php endforeach; ?></select></div><?php else: ?><input type="hidden" id="donation_goal_person" value="<?= htmlspecialchars((string) ($activePerson['id'] ?? ''), ENT_QUOTES, 'UTF-8') ?>"><?php endif; ?>
|
||
<div class="col-12"><label class="form-label" for="donation_goal_monthly">Goal for month (<?= htmlspecialchars($sym, ENT_QUOTES, 'UTF-8') ?>)</label><input type="number" step="0.01" min="0" class="form-control" id="donation_goal_monthly" value="<?= htmlspecialchars(number_format((float) ($activePerson['donation_goal_monthly'] ?? 0), 2, '.', ''), ENT_QUOTES, 'UTF-8') ?>"></div>
|
||
<div class="col-12"><button type="submit" class="btn btn-outline-primary w-100">Save donation goal</button></div>
|
||
<div class="col-12"><div class="alert d-none" id="donationGoalFeedback" role="status"></div></div>
|
||
</form>
|
||
</div></div>
|
||
</div>
|
||
<?php if ($canRecordExpense): ?>
|
||
<div class="accordion-item">
|
||
<h2 class="accordion-header"><button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#bankActionAdjust">HoH checking credit/debit</button></h2>
|
||
<div id="bankActionAdjust" class="accordion-collapse collapse" data-bs-parent="#bankingActionsAccordion"><div class="accordion-body">
|
||
<form id="bankAdjustForm" class="row g-3">
|
||
<div class="col-12"><label class="form-label" for="bank_adjust_person">Person</label><select class="form-select" id="bank_adjust_person" required><?php foreach ($people as $p): ?><?php if (empty($p['id'])) { continue; } ?><option value="<?= htmlspecialchars((string) $p['id'], ENT_QUOTES, 'UTF-8') ?>"><?= sanitizeInput($p['name'] ?? '') ?></option><?php endforeach; ?></select></div>
|
||
<div class="col-6"><label class="form-label" for="bank_adjust_type">Type</label><select class="form-select" id="bank_adjust_type"><option value="credit">Credit</option><option value="debit">Debit</option></select></div>
|
||
<div class="col-6"><label class="form-label" for="bank_adjust_amount">Amount</label><input type="number" min="0.01" step="0.01" class="form-control" id="bank_adjust_amount" required></div>
|
||
<div class="col-12"><label class="form-label" for="bank_adjust_category">Category</label><input class="form-control" id="bank_adjust_category" maxlength="50" value="manual"></div>
|
||
<div class="col-12"><label class="form-label" for="bank_adjust_note">Note</label><input class="form-control" id="bank_adjust_note" maxlength="200"></div>
|
||
<div class="col-12"><button type="submit" class="btn btn-danger w-100">Save checking transaction</button></div>
|
||
<div class="col-12"><div class="alert d-none" id="bankAdjustFeedback" role="status"></div></div>
|
||
</form>
|
||
</div></div>
|
||
</div>
|
||
<div class="accordion-item">
|
||
<h2 class="accordion-header"><button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#bankActionCharity">HoH charity outflow</button></h2>
|
||
<div id="bankActionCharity" class="accordion-collapse collapse" data-bs-parent="#bankingActionsAccordion"><div class="accordion-body">
|
||
<form id="charityOutflowForm" class="row g-3">
|
||
<div class="col-12"><label class="form-label" for="charity_outflow_person">Person</label><select class="form-select" id="charity_outflow_person" required><?php foreach ($people as $p): ?><?php if (empty($p['id'])) { continue; } ?><option value="<?= htmlspecialchars((string) $p['id'], ENT_QUOTES, 'UTF-8') ?>"><?= sanitizeInput($p['name'] ?? '') ?></option><?php endforeach; ?></select></div>
|
||
<div class="col-6"><label class="form-label" for="charity_outflow_amount">Amount</label><input type="number" min="0.01" step="0.01" class="form-control" id="charity_outflow_amount" required></div>
|
||
<div class="col-6"><label class="form-label" for="charity_outflow_category">Category</label><input class="form-control" id="charity_outflow_category" maxlength="50" value="donation"></div>
|
||
<div class="col-12"><label class="form-label" for="charity_outflow_note">Note</label><input class="form-control" id="charity_outflow_note" maxlength="200"></div>
|
||
<div class="col-12"><button type="submit" class="btn btn-warning w-100">Log charity outflow</button></div>
|
||
<div class="col-12"><div class="alert d-none" id="charityOutflowFeedback" role="status"></div></div>
|
||
</form>
|
||
</div></div>
|
||
</div>
|
||
<?php endif; ?>
|
||
</div>
|
||
<div class="card mt-3">
|
||
<div class="card-header">Monthly statements</div>
|
||
<div class="card-body">
|
||
<p class="small text-muted mb-2">Download your monthly statement as JSON or CSV.</p>
|
||
<div class="d-flex flex-wrap gap-2">
|
||
<a class="btn btn-outline-secondary btn-sm" href="<?= htmlspecialchars($familyHubApiBase, ENT_QUOTES, 'UTF-8') ?>/bank_statement.php?month=<?= htmlspecialchars($currentMonth, ENT_QUOTES, 'UTF-8') ?>&format=json">Current month JSON</a>
|
||
<a class="btn btn-outline-secondary btn-sm" href="<?= htmlspecialchars($familyHubApiBase, ENT_QUOTES, 'UTF-8') ?>/bank_statement.php?month=<?= htmlspecialchars($currentMonth, ENT_QUOTES, 'UTF-8') ?>&format=csv">Current month CSV</a>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<?php elseif ($canRecordExpense): ?>
|
||
<div class="card border-secondary mb-4">
|
||
<div class="card-header d-flex justify-content-between align-items-center">
|
||
<span><i class="fa fa-minus-circle me-1"></i> Record expense</span>
|
||
<button class="btn btn-sm btn-outline-secondary" type="button" data-bs-toggle="collapse" data-bs-target="#expenseFormCollapse" aria-expanded="false" aria-controls="expenseFormCollapse">
|
||
Show form
|
||
</button>
|
||
</div>
|
||
<div class="collapse" id="expenseFormCollapse">
|
||
<div class="card-body">
|
||
<p class="small text-muted">Deducts from one person’s balance (e.g. spent allowance). Requires enough balance.</p>
|
||
<form id="expenseForm" class="row g-3">
|
||
<div class="col-md-6">
|
||
<label class="form-label" for="expense_title">Title</label>
|
||
<input class="form-control" id="expense_title" required maxlength="120">
|
||
</div>
|
||
<div class="col-md-6">
|
||
<label class="form-label" for="expense_value">Amount (<?= htmlspecialchars($sym, ENT_QUOTES, 'UTF-8') ?>)</label>
|
||
<input type="number" class="form-control" id="expense_value" min="0.01" step="0.01" required>
|
||
</div>
|
||
<div class="col-md-6">
|
||
<label class="form-label" for="expense_date">Date</label>
|
||
<input type="date" class="form-control" id="expense_date" value="<?= htmlspecialchars($today, ENT_QUOTES, 'UTF-8') ?>" required>
|
||
</div>
|
||
<div class="col-md-6">
|
||
<label class="form-label" for="expense_assignee">Charged to</label>
|
||
<select class="form-select" id="expense_assignee" required>
|
||
<option value="">Choose person…</option>
|
||
<?php foreach ($people as $p): ?>
|
||
<?php if (empty($p['id'])) { continue; } ?>
|
||
<option value="<?= htmlspecialchars((string) $p['id'], ENT_QUOTES, 'UTF-8') ?>"><?= sanitizeInput($p['name'] ?? '') ?></option>
|
||
<?php endforeach; ?>
|
||
</select>
|
||
</div>
|
||
<div class="col-12">
|
||
<label class="form-label" for="expense_description">Description</label>
|
||
<textarea class="form-control" id="expense_description" rows="2"></textarea>
|
||
</div>
|
||
<div class="col-12">
|
||
<button type="submit" class="btn btn-danger">Apply expense</button>
|
||
</div>
|
||
<div class="col-12">
|
||
<div class="alert d-none" id="expenseFormFeedback" role="status"></div>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<?php elseif (count($people) > 0): ?>
|
||
<p class="text-muted small">Switch to a verified Head of household to record expenses.</p>
|
||
<?php endif; ?>
|
||
|
||
<?php if (!$bankingMode): ?>
|
||
<div class="card">
|
||
<div class="card-header">Recent expenses</div>
|
||
<div class="card-body p-0">
|
||
<?php if (count($recentExpenses) === 0): ?>
|
||
<p class="p-3 text-muted mb-0">No expenses recorded yet.</p>
|
||
<?php else: ?>
|
||
<div class="table-responsive">
|
||
<table class="table table-sm mb-0">
|
||
<thead>
|
||
<tr>
|
||
<th>Date</th>
|
||
<th>Title</th>
|
||
<th>To</th>
|
||
<th class="text-end">Amount</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<?php foreach ($recentExpenses as $ex): ?>
|
||
<tr>
|
||
<td class="text-nowrap"><?= sanitizeInput((string) ($ex['date'] ?? '')) ?></td>
|
||
<td><?= sanitizeInput((string) ($ex['title'] ?? '')) ?></td>
|
||
<td><?= sanitizeInput($nameById[(string) ($ex['assignee_id'] ?? '')] ?? '') ?></td>
|
||
<td class="text-end text-nowrap">−<?= htmlspecialchars(number_format((float) ($ex['value'] ?? 0), 2, '.', ''), ENT_QUOTES, 'UTF-8') ?> <?= htmlspecialchars($sym, ENT_QUOTES, 'UTF-8') ?></td>
|
||
</tr>
|
||
<?php endforeach; ?>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
<?php endif; ?>
|
||
</div>
|
||
</div>
|
||
<?php endif; ?>
|
||
</div>
|
||
</div>
|
||
<?php if ($bankingMode): ?>
|
||
</div>
|
||
</div>
|
||
<?php endif; ?>
|
||
</div>
|