First pass of features

This commit is contained in:
2026-03-30 14:53:13 -05:00
parent 1a65e69d25
commit 6353f9c9b4
54 changed files with 5409 additions and 128 deletions
+178
View File
@@ -0,0 +1,178 @@
<?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';
$sym = trim((string) ($familySettings['currency_symbol'] ?? '★'));
$name = trim((string) ($familySettings['currency_name'] ?? ''));
$tabTitle = currencyTabLabel($familySettings);
$canRecordExpense = $activePerson !== null
&& ($activePerson['role'] ?? '') === ROLE_HEAD
&& isHohVerified();
$leaderboard = $people;
usort($leaderboard, static function ($a, $b) {
$ba = is_numeric($a['currency_balance'] ?? null) ? (float) $a['currency_balance'] : 0.0;
$bb = 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'] ?? ''));
});
$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');
?>
<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>
<div class="row g-4">
<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>
<th scope="col" class="text-end">Balance</th>
</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;
$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($bal, 2, '.', ''), ENT_QUOTES, 'UTF-8') ?></strong> <?= htmlspecialchars($sym, ENT_QUOTES, 'UTF-8') ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
<?php endif; ?>
</div>
</div>
</div>
<div class="col-lg-7">
<?php if ($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 persons 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; ?>
<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>
</div>
</div>
</div>