Files
budget/views/dashboard.php
T
Ty Clifford 51ee4ecb5a -
Finished the planner end to end.
Corrected rollover and yearly accounting, recurring targets, savings 
rules, and targeted dormant restorations.
Added editing/deletion for income, expenses, targets, and categories.
Rebuilt dashboard, analytics, calendar, settings, exports, 
authentication, and responsive mobile UI.
Added original artwork at 
[financial-flow.png](/Users/tyemeclifford/Documents/GH/budget/public/assets/images/financial-flow.png), 
generated with built-in Imagegen using an abstract financial-momentum 
landscape prompt.
Added 
[self-test.php](/Users/tyemeclifford/Documents/GH/budget/scripts/self-test.php).
Verification passed: automated accounting/export tests, full browser 
workflow, responsive 390px layout, light/dark themes, and no console 
errors. Tested with FrankenPHP v1.12.4. Live Mailgun delivery was not 
attempted without credentials.
2026-06-15 10:33:55 -04:00

275 lines
20 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
$monthLabel = date('F Y', strtotime($month . '-01'));
$daysInMonth = (int) date('t', strtotime($month . '-01'));
$currentMonth = date('Y-m');
$elapsedDays = $month < $currentMonth ? $daysInMonth : ($month > $currentMonth ? 0 : min($daysInMonth, (int) date('j')));
$forecast = $elapsedDays > 0 ? $summary['expenses'] / $elapsedDays * $daysInMonth : 0;
$spentPercent = $summary['gross_resources'] > 0 ? min(100, max(0, $summary['expenses'] / $summary['gross_resources'] * 100)) : 0;
$remainingPercent = $summary['gross_resources'] > 0 ? min(100, max(0, $summary['remaining'] / $summary['gross_resources'] * 100)) : 0;
$remainingDelta = $summary['remaining'] - $previousSummary['remaining'];
$incomeRule = (string) $settings->get('dead.income_mode', 'percent');
$incomeRuleValue = (float) $settings->get('dead.income_value', 0);
$transactionRule = (string) $settings->get('dead.transaction_mode', 'fixed');
$transactionRuleValue = (float) $settings->get('dead.transaction_value', 0);
?>
<section class="overview-hero card">
<div class="hero-art" aria-hidden="true"></div>
<div class="hero-copy">
<div class="status-line">
<span class="live-dot"></span>
<span><?= h($monthLabel) ?> plan</span>
<span class="status-divider"></span>
<span><?= $plan['rolling_enabled'] ? 'Rolling budget on' : 'Manual rollover' ?></span>
</div>
<p class="hero-label">Spendable balance</p>
<div class="hero-value <?= $summary['remaining'] >= 0 ? '' : 'negative' ?>"><?= h(Support::money($summary['remaining'], $symbol)) ?></div>
<p class="hero-summary">
<?= h(Support::money($summary['gross_resources'], $symbol)) ?> available
<span>·</span>
<?= number_format($remainingPercent, 0) ?>% remains
</p>
<div class="hero-actions">
<a class="button primary" href="<?= h(url('transactions', ['month' => $month, 'open' => 'expense-dialog'])) ?>"><?= icon('plus') ?> Record expense</a>
<button class="button glass" type="button" data-dialog-open="plan-dialog"><?= icon('wallet') ?> Edit monthly plan</button>
</div>
</div>
<div class="hero-gauge">
<div class="gauge-ring" style="--progress:<?= h((string) $remainingPercent) ?>">
<span><?= number_format($remainingPercent, 0) ?>%</span>
<small>remaining</small>
</div>
<div class="gauge-note">
<span>Month forecast</span>
<strong><?= h(Support::money($forecast, $symbol)) ?></strong>
<small><?= $elapsedDays === 0 ? 'Begins when spending is recorded' : 'At the current daily pace' ?></small>
</div>
</div>
</section>
<section class="metric-grid">
<article class="metric-card card">
<div class="metric-top"><span class="metric-icon green"><?= icon('wallet') ?></span><span class="trend-tag">Fresh funds</span></div>
<p>Income this month</p>
<strong><?= h(Support::money($summary['fresh_income'], $symbol)) ?></strong>
<small><?= h(Support::money($summary['starting_income'], $symbol)) ?> starting + <?= h(Support::money($summary['additional_income'], $symbol)) ?> added</small>
</article>
<article class="metric-card card">
<div class="metric-top"><span class="metric-icon pink"><?= icon('expense') ?></span><span class="trend-tag <?= $spentPercent > 80 ? 'warning' : '' ?>"><?= number_format($spentPercent, 0) ?>% used</span></div>
<p>Total spent</p>
<strong><?= h(Support::money($summary['expenses'], $symbol)) ?></strong>
<small><?= count($items) ?> target<?= count($items) === 1 ? '' : 's' ?> · <?= h(Support::money($summary['planned'], $symbol)) ?> planned</small>
</article>
<article class="metric-card card">
<div class="metric-top"><span class="metric-icon purple"><?= icon('vault') ?></span><span class="trend-tag"><?= number_format($summary['savings_rate'], 1) ?>% rate</span></div>
<p>Moved dormant</p>
<strong><?= h(Support::money($summary['dead_deposits'], $symbol)) ?></strong>
<small><?= h(Support::money($summary['dead_restorations'], $symbol)) ?> restored this month</small>
</article>
<article class="metric-card card">
<div class="metric-top"><span class="metric-icon blue"><?= icon('data') ?></span><span class="trend-tag <?= $remainingDelta < 0 ? 'negative' : '' ?>"><?= $remainingDelta >= 0 ? '+' : '' ?><?= h(Support::money($remainingDelta, $symbol)) ?></span></div>
<p>Previous remainder</p>
<strong><?= h(Support::money($summary['rollover_in'], $symbol)) ?></strong>
<small>Compared with <?= h(date('F', strtotime($month . '-01 -1 month'))) ?>s closing balance</small>
</article>
</section>
<section class="dashboard-layout">
<div class="dashboard-main stack">
<article class="card panel">
<div class="card-heading">
<div><p class="eyebrow">Progressional plan</p><h2>Budget targets</h2><p class="heading-note">Payments reduce the amount still owing in real time.</p></div>
<button class="button small primary" type="button" data-dialog-open="item-dialog"><?= icon('plus') ?> New target</button>
</div>
<?php if ($items === []): ?>
<div class="empty-state"><span class="empty-icon"><?= icon('chart') ?></span><strong>Build your first target</strong><p>Add rent, internet, groceries, or any amount you want to track.</p><button class="button small" type="button" data-dialog-open="item-dialog">Add a budget target</button></div>
<?php else: ?>
<div class="budget-list">
<?php foreach ($items as $item): ?>
<div class="budget-row">
<div class="budget-identity">
<span class="category-mark" style="--dot:<?= h($item['category_color'] ?: '#718096') ?>"><?= h(strtoupper(substr($item['name'], 0, 1))) ?></span>
<div><strong><?= h($item['name']) ?></strong><span><?= h($item['category_name'] ?: 'Uncategorized') ?><?php if ($item['due_date']): ?> · due <?= h(date('M j', strtotime($item['due_date']))) ?><?php endif; ?></span></div>
</div>
<div class="budget-progress">
<div class="progress-labels"><span><?= number_format((float) $item['progress'], 0) ?>% paid</span><strong><?= h(Support::money($item['remaining_amount'], $symbol)) ?> left</strong></div>
<div class="progress-track"><span style="width:<?= h((string) $item['progress']) ?>%;--bar:<?= h($item['category_color'] ?: '#7c5cff') ?>"></span></div>
<small><?= h(Support::money($item['paid_amount'], $symbol)) ?> of <?= h(Support::money($item['planned_amount'], $symbol)) ?></small>
</div>
<div class="row-actions">
<button class="icon-button" type="button" data-dialog-open="item-edit-<?= (int) $item['id'] ?>" aria-label="Edit <?= h($item['name']) ?>"><?= icon('edit') ?></button>
<form method="post" data-confirm="Remove this budget target? Existing expenses will remain.">
<?= csrf_field() ?><input type="hidden" name="action" value="item.delete"><input type="hidden" name="month" value="<?= h($month) ?>"><input type="hidden" name="id" value="<?= (int) $item['id'] ?>">
<button class="icon-button danger" type="submit" aria-label="Delete <?= h($item['name']) ?>"><?= icon('trash') ?></button>
</form>
</div>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
</article>
<article class="card panel chart-panel">
<div class="card-heading">
<div><p class="eyebrow">Comparative movement</p><h2>Six-month cash flow</h2><p class="heading-note">Resources include rollover for each individual month.</p></div>
<a class="text-link" href="<?= h(url('comparisons', ['month' => $month])) ?>">Open analytics <span>→</span></a>
</div>
<canvas class="trend-chart" data-trend-chart='<?= h(json_encode($comparison)) ?>' data-symbol="<?= h($symbol) ?>" height="255"></canvas>
</article>
<article class="card panel">
<div class="card-heading">
<div><p class="eyebrow">Cash activity</p><h2>Recent movement</h2></div>
<a class="text-link" href="<?= h(url('transactions', ['month' => $month])) ?>">All transactions <span>→</span></a>
</div>
<?php if ($recentActivity === []): ?>
<div class="empty-state compact"><strong>No movement yet</strong><p>Income, expenses, and dormant transfers will collect here.</p></div>
<?php else: ?>
<div class="activity-list">
<?php foreach ($recentActivity as $activity): ?>
<div class="activity-row">
<span class="activity-icon <?= h($activity['type']) ?>"><?= icon($activity['type'] === 'expense' ? 'expense' : ($activity['type'] === 'saved' ? 'vault' : 'wallet')) ?></span>
<div><strong><?= h($activity['title']) ?></strong><small><?= h($activity['detail']) ?> · <?= h(date('M j', strtotime($activity['date']))) ?></small></div>
<b class="<?= $activity['amount'] >= 0 ? 'green' : '' ?>"><?= $activity['amount'] >= 0 ? '+' : '-' ?><?= h(Support::money(abs($activity['amount']), $symbol)) ?></b>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
</article>
</div>
<aside class="dashboard-side stack">
<article class="card panel plan-card">
<div class="card-heading">
<div><p class="eyebrow">Plan health</p><h2><?= h($monthLabel) ?></h2></div>
<button class="icon-button" type="button" data-dialog-open="plan-dialog" aria-label="Edit monthly plan"><?= icon('edit') ?></button>
</div>
<div class="health-score">
<span style="--score:<?= h((string) min(100, max(0, $summary['budget_coverage']))) ?>"><?= number_format($summary['budget_coverage'], 0) ?>%</span>
<div><strong>Target coverage</strong><small>Spendable funds compared with planned targets</small></div>
</div>
<dl class="plan-facts">
<div><dt>Starting point</dt><dd><?= h(Support::money($summary['starting_income'], $symbol)) ?></dd></div>
<div><dt>Rollover field</dt><dd><?= h(Support::money($summary['rollover_in'], $symbol)) ?></dd></div>
<div><dt>Still unassigned</dt><dd class="<?= $summary['unallocated'] < 0 ? 'red' : '' ?>"><?= h(Support::money($summary['unallocated'], $symbol)) ?></dd></div>
</dl>
<form method="post" class="rollover-refresh">
<?= csrf_field() ?><input type="hidden" name="action" value="plan.refresh_rollover"><input type="hidden" name="month" value="<?= h($month) ?>">
<button class="text-link" type="submit">Refresh from <?= h(date('F', strtotime($month . '-01 -1 month'))) ?> <span>↻</span></button>
</form>
<div class="rule-summary">
<span><?= icon('vault') ?></span>
<p><strong>Automatic set-aside</strong><small><?= h((string) $incomeRuleValue) ?><?= $incomeRule === 'percent' ? '% of income' : ' per income' ?> · <?= h((string) $transactionRuleValue) ?><?= $transactionRule === 'percent' ? '% per expense' : ' per expense' ?></small></p>
</div>
</article>
<article class="card panel">
<div class="card-heading">
<div><p class="eyebrow">Coming up</p><h2>Next bills</h2></div>
<a class="text-link" href="<?= h(url('calendar', ['month' => $month])) ?>">Calendar <span>→</span></a>
</div>
<?php if ($upcomingBills === []): ?>
<div class="empty-state compact"><strong>Nothing due</strong><p>Add due dates to targets to build the bill queue.</p></div>
<?php else: ?>
<div class="due-list">
<?php foreach ($upcomingBills as $bill): ?>
<?php $days = (int) floor((strtotime($bill['due_date']) - strtotime(date('Y-m-d'))) / 86400); ?>
<div>
<span class="date-tile"><b><?= h(date('d', strtotime($bill['due_date']))) ?></b><small><?= h(strtoupper(date('M', strtotime($bill['due_date'])))) ?></small></span>
<p><strong><?= h($bill['name']) ?></strong><small><?= h($bill['category_name'] ?: 'Uncategorized') ?> · <?= $days < 0 ? abs($days) . 'd overdue' : ($days === 0 ? 'due today' : 'in ' . $days . 'd') ?></small></p>
<b><?= h(Support::money($bill['remaining_amount'], $symbol)) ?></b>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
</article>
<?php if ($settings->module('income')): ?>
<article class="card panel">
<div class="card-heading">
<div><p class="eyebrow">Income stream</p><h2>Additional income</h2></div>
<button class="button small" type="button" data-dialog-open="income-dialog"><?= icon('plus') ?> Add</button>
</div>
<?php if ($incomes === []): ?>
<div class="empty-state compact"><strong>No additional income</strong><p>Your manual starting point still counts as the base income.</p></div>
<?php else: ?>
<div class="income-list">
<?php foreach (array_slice($incomes, 0, 5) as $income): ?>
<div>
<span class="activity-icon income"><?= icon('wallet') ?></span>
<p><strong><?= h($income['label']) ?></strong><small><?= h(date('M j', strtotime($income['received_on']))) ?> · <?= h(Support::money($income['withholding_amount'], $symbol)) ?> set aside</small></p>
<b>+<?= h(Support::money($income['amount'], $symbol)) ?></b>
<button class="micro-button" type="button" data-dialog-open="income-edit-<?= (int) $income['id'] ?>" aria-label="Edit <?= h($income['label']) ?>"><?= icon('edit') ?></button>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
</article>
<?php endif; ?>
<article class="usage-card card">
<p class="eyebrow">Private usage stats</p>
<div><span><strong><?= (int) $visitorStats['today'] ?></strong><small>today</small></span><span><strong><?= (int) $visitorStats['thirty_days'] ?></strong><small>30-day visitors</small></span><span><strong><?= (int) $visitorStats['views'] ?></strong><small>page views</small></span></div>
</article>
</aside>
</section>
<dialog id="plan-dialog">
<form method="post" class="dialog-card">
<?= csrf_field() ?><input type="hidden" name="action" value="plan.save"><input type="hidden" name="month" value="<?= h($month) ?>">
<div class="dialog-heading"><div><p class="eyebrow">Monthly foundation</p><h2>Edit <?= h($monthLabel) ?> plan</h2></div><button class="icon-button" type="button" data-dialog-close><?= icon('close') ?></button></div>
<div class="form-grid">
<label>Manual starting income<div class="money-input"><span><?= h($symbol) ?></span><input type="number" name="starting_income" min="0" step="0.01" value="<?= h((string) $plan['starting_income']) ?>"></div></label>
<label>Previous-month remainder<div class="money-input"><span><?= h($symbol) ?></span><input type="number" name="rollover_in" min="0" step="0.01" value="<?= h((string) $plan['rollover_in']) ?>"></div></label>
<label class="check-row span-2"><input type="checkbox" name="rolling_enabled" value="1" <?= $plan['rolling_enabled'] ? 'checked' : '' ?>><span><strong>Carry positive remainder forward</strong><small>The next month can use any money left at close.</small></span></label>
<label class="span-2">Monthly note<textarea name="notes" rows="3" placeholder="Intent, constraints, or focus for this month"><?= h($plan['notes']) ?></textarea></label>
</div>
<div class="dialog-actions"><button class="button" type="button" data-dialog-close>Cancel</button><button class="button primary" type="submit">Save monthly plan</button></div>
</form>
</dialog>
<dialog id="item-dialog">
<form method="post" class="dialog-card">
<?= csrf_field() ?><input type="hidden" name="action" value="item.add"><input type="hidden" name="month" value="<?= h($month) ?>">
<div class="dialog-heading"><div><p class="eyebrow">Progressional target</p><h2>Add budget item</h2></div><button class="icon-button" type="button" data-dialog-close><?= icon('close') ?></button></div>
<?php require APP_ROOT . '/views/partials/item-fields.php'; ?>
<div class="dialog-actions"><button class="button" type="button" data-dialog-close>Cancel</button><button class="button primary" type="submit">Add target</button></div>
</form>
</dialog>
<?php foreach ($items as $item): ?>
<dialog id="item-edit-<?= (int) $item['id'] ?>">
<form method="post" class="dialog-card">
<?= csrf_field() ?><input type="hidden" name="action" value="item.update"><input type="hidden" name="month" value="<?= h($month) ?>"><input type="hidden" name="id" value="<?= (int) $item['id'] ?>">
<div class="dialog-heading"><div><p class="eyebrow">Maintain target</p><h2>Edit <?= h($item['name']) ?></h2></div><button class="icon-button" type="button" data-dialog-close><?= icon('close') ?></button></div>
<?php $editingItem = $item; require APP_ROOT . '/views/partials/item-fields.php'; unset($editingItem); ?>
<p class="form-note">Changes apply to this month. Future recurring copies use the latest saved version.</p>
<div class="dialog-actions"><button class="button" type="button" data-dialog-close>Cancel</button><button class="button primary" type="submit">Save target</button></div>
</form>
</dialog>
<?php endforeach; ?>
<dialog id="income-dialog">
<form method="post" class="dialog-card">
<?= csrf_field() ?><input type="hidden" name="action" value="income.add"><input type="hidden" name="month" value="<?= h($month) ?>">
<div class="dialog-heading"><div><p class="eyebrow">Beyond the starting point</p><h2>Add income</h2></div><button class="icon-button" type="button" data-dialog-close><?= icon('close') ?></button></div>
<?php require APP_ROOT . '/views/partials/income-fields.php'; ?>
<p class="form-note">The configured withholding rule is applied automatically.</p>
<div class="dialog-actions"><button class="button" type="button" data-dialog-close>Cancel</button><button class="button primary" type="submit">Record income</button></div>
</form>
</dialog>
<?php foreach ($incomes as $income): ?>
<dialog id="income-edit-<?= (int) $income['id'] ?>">
<form method="post" class="dialog-card">
<?= csrf_field() ?><input type="hidden" name="action" value="income.update"><input type="hidden" name="month" value="<?= h($month) ?>"><input type="hidden" name="id" value="<?= (int) $income['id'] ?>">
<div class="dialog-heading"><div><p class="eyebrow">Maintain income</p><h2>Edit <?= h($income['label']) ?></h2></div><button class="icon-button" type="button" data-dialog-close><?= icon('close') ?></button></div>
<?php $editingIncome = $income; require APP_ROOT . '/views/partials/income-fields.php'; unset($editingIncome); ?>
<div class="dialog-actions">
<button class="text-link danger-link push-left" type="submit" name="action" value="income.delete" formnovalidate data-confirm-button="Delete this income entry and reverse its automatic withholding?"><?= icon('trash') ?> Delete</button>
<button class="button" type="button" data-dialog-close>Cancel</button>
<button class="button primary" type="submit">Save income</button>
</div>
</form>
</dialog>
<?php endforeach; ?>