-
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.
This commit is contained in:
@@ -6,7 +6,7 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="color-scheme" content="dark">
|
||||
<title><?= h($title ?? 'Welcome') ?> · <?= h($appName) ?></title>
|
||||
<link rel="stylesheet" href="/assets/app.css">
|
||||
<link rel="stylesheet" href="/assets/app.css?v=<?= (int) filemtime(APP_ROOT . '/public/assets/app.css') ?>">
|
||||
</head>
|
||||
<body class="auth-body">
|
||||
<main class="auth-shell">
|
||||
|
||||
+18
-9
@@ -2,16 +2,24 @@
|
||||
$totals = $yearly['totals'];
|
||||
$bestMonth = null;
|
||||
foreach ($yearly['months'] as $row) {
|
||||
$hasActivity = (float) $row['fresh_income'] !== 0.0
|
||||
|| (float) $row['rollover_in'] !== 0.0
|
||||
|| (float) $row['expenses'] !== 0.0
|
||||
|| (float) $row['planned'] !== 0.0
|
||||
|| (float) $row['dead_movement'] !== 0.0;
|
||||
if (!$hasActivity) {
|
||||
continue;
|
||||
}
|
||||
if ($bestMonth === null || $row['remaining'] > $bestMonth['remaining']) {
|
||||
$bestMonth = $row;
|
||||
}
|
||||
}
|
||||
?>
|
||||
<section class="hero-grid comparison-metrics">
|
||||
<article class="metric-card card"><div class="metric-icon blue">Σ</div><p><?= (int) $yearly['year'] ?> resources</p><strong><?= h(Support::money($totals['gross_resources'], $symbol)) ?></strong><small>Across all twelve months</small></article>
|
||||
<article class="metric-card card"><div class="metric-icon pink">↗</div><p><?= (int) $yearly['year'] ?> expenses</p><strong><?= h(Support::money($totals['expenses'], $symbol)) ?></strong><small><?= $totals['gross_resources'] > 0 ? number_format($totals['expenses'] / $totals['gross_resources'] * 100, 1) : '0.0' ?>% of resources</small></article>
|
||||
<article class="metric-card card"><div class="metric-icon orange">◇</div><p>Moved dormant</p><strong><?= h(Support::money($totals['dead_movement'], $symbol)) ?></strong><small>Net dormant deposits</small></article>
|
||||
<article class="metric-card card"><div class="metric-icon green">★</div><p>Strongest remainder</p><strong><?= h(Support::money($bestMonth['remaining'] ?? 0, $symbol)) ?></strong><small><?= $bestMonth ? h(date('F', strtotime($bestMonth['month'] . '-01'))) : 'No data' ?></small></article>
|
||||
<article class="metric-card card"><div class="metric-icon blue"><?= icon('wallet') ?></div><p><?= (int) $yearly['year'] ?> fresh income</p><strong><?= h(Support::money($totals['fresh_income'], $symbol)) ?></strong><small>Rollover excluded to avoid double-counting</small></article>
|
||||
<article class="metric-card card"><div class="metric-icon pink"><?= icon('expense') ?></div><p><?= (int) $yearly['year'] ?> expenses</p><strong><?= h(Support::money($totals['expenses'], $symbol)) ?></strong><small><?= $totals['fresh_income'] > 0 ? number_format($totals['expenses'] / $totals['fresh_income'] * 100, 1) : '0.0' ?>% of fresh income</small></article>
|
||||
<article class="metric-card card"><div class="metric-icon orange"><?= icon('vault') ?></div><p>Moved dormant</p><strong><?= h(Support::money($totals['dead_deposits'], $symbol)) ?></strong><small><?= h(Support::money($totals['dead_restorations'], $symbol)) ?> restored</small></article>
|
||||
<article class="metric-card card"><div class="metric-icon green"><?= icon('chart') ?></div><p>Strongest remainder</p><strong><?= h(Support::money($bestMonth['remaining'] ?? 0, $symbol)) ?></strong><small><?= $bestMonth ? h(date('F', strtotime($bestMonth['month'] . '-01'))) : 'No data' ?></small></article>
|
||||
</section>
|
||||
|
||||
<section class="content-grid comparison-grid">
|
||||
@@ -30,7 +38,7 @@ foreach ($yearly['months'] as $row) {
|
||||
<div>
|
||||
<p><span><i style="--dot:<?= h($category['color']) ?>"></i><?= h($category['category_name']) ?></span><strong><?= h(Support::money($category['spent'], $symbol)) ?></strong></p>
|
||||
<div class="progress-track"><span style="width:<?= h((string) $width) ?>%;--bar:<?= h($category['color']) ?>"></span></div>
|
||||
<?php if ((float) $category['monthly_limit'] > 0): ?><small><?= number_format($category['spent'] / $category['monthly_limit'] * 100, 0) ?>% of <?= h(Support::money($category['monthly_limit'], $symbol)) ?> category limit</small><?php endif; ?>
|
||||
<small><?php if ((float) $category['planned'] > 0): ?><?= number_format($category['spent'] / $category['planned'] * 100, 0) ?>% of <?= h(Support::money($category['planned'], $symbol)) ?> planned<?php elseif ((float) $category['monthly_limit'] > 0): ?><?= number_format($category['spent'] / $category['monthly_limit'] * 100, 0) ?>% of <?= h(Support::money($category['monthly_limit'], $symbol)) ?> category limit<?php else: ?>No target defined<?php endif; ?></small>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
@@ -41,19 +49,20 @@ foreach ($yearly['months'] as $row) {
|
||||
<article class="card">
|
||||
<div class="card-heading">
|
||||
<div><p class="eyebrow">Statistical table</p><h2><?= (int) $yearly['year'] ?> monthly breakdown</h2></div>
|
||||
<label class="search-box"><span>⌕</span><input type="search" placeholder="Find a month…" data-table-search="year-table"></label>
|
||||
<label class="search-box"><?= icon('search') ?><input type="search" placeholder="Find a month…" data-table-search="year-table"></label>
|
||||
</div>
|
||||
<div class="table-wrap">
|
||||
<table id="year-table">
|
||||
<thead><tr><th>Month</th><th>Resources</th><th>Planned</th><th>Expenses</th><th>Dormant</th><th>Remainder</th></tr></thead>
|
||||
<thead><tr><th>Month</th><th>Fresh income</th><th>Rollover</th><th>Planned</th><th>Expenses</th><th>Saved</th><th>Remainder</th></tr></thead>
|
||||
<tbody>
|
||||
<?php foreach ($yearly['months'] as $row): ?>
|
||||
<tr>
|
||||
<td><a class="text-link" href="<?= h(url('comparisons', ['month' => $row['month']])) ?>"><?= h(date('F Y', strtotime($row['month'] . '-01'))) ?></a></td>
|
||||
<td><?= h(Support::money($row['gross_resources'], $symbol)) ?></td>
|
||||
<td><?= h(Support::money($row['fresh_income'], $symbol)) ?></td>
|
||||
<td><?= h(Support::money($row['rollover_in'], $symbol)) ?></td>
|
||||
<td><?= h(Support::money($row['planned'], $symbol)) ?></td>
|
||||
<td><?= h(Support::money($row['expenses'], $symbol)) ?></td>
|
||||
<td><?= h(Support::money($row['dead_movement'], $symbol)) ?></td>
|
||||
<td><?= h(Support::money($row['dead_deposits'], $symbol)) ?></td>
|
||||
<td class="<?= $row['remaining'] >= 0 ? 'green' : 'red' ?>"><strong><?= h(Support::money($row['remaining'], $symbol)) ?></strong></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
|
||||
+222
-92
@@ -1,144 +1,274 @@
|
||||
<?php
|
||||
$remainingTone = $summary['remaining'] >= 0 ? 'green' : 'red';
|
||||
$progress = $summary['gross_resources'] > 0 ? min(100, max(0, $summary['expenses'] / $summary['gross_resources'] * 100)) : 0;
|
||||
$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="hero-grid">
|
||||
<article class="hero-balance card glow-purple">
|
||||
<div>
|
||||
<p class="eyebrow"><?= h(date('F Y', strtotime($month . '-01'))) ?> remaining</p>
|
||||
<div class="hero-amount <?= h($remainingTone) ?>"><?= h(Support::money($summary['remaining'], $symbol)) ?></div>
|
||||
<p class="muted"><?= number_format($progress, 1) ?>% of available resources spent</p>
|
||||
<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>
|
||||
<div class="balance-ring" style="--progress:<?= h((string) $progress) ?>">
|
||||
<span><?= number_format(100 - $progress, 0) ?>%</span><small>left</small>
|
||||
<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-icon blue">↙</div><p>Gross resources</p><strong><?= h(Support::money($summary['gross_resources'], $symbol)) ?></strong>
|
||||
<small><?= h(Support::money($summary['starting_income'], $symbol)) ?> start + <?= h(Support::money($summary['rollover_in'], $symbol)) ?> rollover</small>
|
||||
<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-icon pink">↗</div><p>Expenses</p><strong><?= h(Support::money($summary['expenses'], $symbol)) ?></strong>
|
||||
<small><?= count($items) ?> defined budget item<?= count($items) === 1 ? '' : 's' ?></small>
|
||||
<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-icon orange">◇</div><p>Dormant balance</p>
|
||||
<strong><?= h(Support::money($deadBalance, $symbol)) ?></strong>
|
||||
<small><?= number_format($summary['savings_rate'], 1) ?>% of this month routed away</small>
|
||||
<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="content-grid dashboard-grid">
|
||||
<div class="span-2 stack">
|
||||
<article class="card">
|
||||
<section class="dashboard-layout">
|
||||
<div class="dashboard-main stack">
|
||||
<article class="card panel">
|
||||
<div class="card-heading">
|
||||
<div><p class="eyebrow">Progressional tracking</p><h2>Budget targets</h2></div>
|
||||
<button class="button small" type="button" data-dialog-open="item-dialog">+ Add item</button>
|
||||
<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"><strong>No targets yet</strong><p>Add a bill or category target to begin progress tracking.</p></div>
|
||||
<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">
|
||||
<span class="category-dot" style="--dot:<?= h($item['category_color'] ?: '#718096') ?>"></span>
|
||||
<div class="budget-main">
|
||||
<div class="budget-title">
|
||||
<strong><?= h($item['name']) ?></strong>
|
||||
<span><?= h($item['category_name'] ?: 'Uncategorized') ?><?= $item['due_date'] ? ' · due ' . h(date('M j', strtotime($item['due_date']))) : '' ?></span>
|
||||
</div>
|
||||
<div class="progress-track"><span style="width:<?= h((string) $item['progress']) ?>%;--bar:<?= h($item['category_color'] ?: '#7c5cff') ?>"></span></div>
|
||||
<div class="budget-meta">
|
||||
<span><?= h(Support::money($item['paid_amount'], $symbol)) ?> paid</span>
|
||||
<span><?= h(Support::money($item['remaining_amount'], $symbol)) ?> owing</span>
|
||||
</div>
|
||||
<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>
|
||||
<strong class="target"><?= h(Support::money($item['planned_amount'], $symbol)) ?></strong>
|
||||
<form method="post" data-confirm="Remove this budget item? 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" title="Delete item" type="submit">×</button>
|
||||
</form>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</article>
|
||||
|
||||
<article class="card">
|
||||
<article class="card panel chart-panel">
|
||||
<div class="card-heading">
|
||||
<div><p class="eyebrow">Comparative view</p><h2>Six-month movement</h2></div>
|
||||
<a class="text-link" href="<?= h(url('comparisons', ['month' => $month])) ?>">Full analysis →</a>
|
||||
<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="220"></canvas>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<div class="stack">
|
||||
<article class="card accent-card">
|
||||
<div class="card-heading"><div><p class="eyebrow">Monthly inputs</p><h2>Starting point</h2></div></div>
|
||||
<form method="post" class="stack-form compact">
|
||||
<?= csrf_field() ?><input type="hidden" name="action" value="plan.save"><input type="hidden" name="month" value="<?= h($month) ?>">
|
||||
<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 slim"><input type="checkbox" name="rolling_enabled" value="1" <?= $plan['rolling_enabled'] ? 'checked' : '' ?>><span>Carry positive remainder forward</span></label>
|
||||
<label>Monthly note<textarea name="notes" rows="2" placeholder="What matters this month?"><?= h($plan['notes']) ?></textarea></label>
|
||||
<button class="button primary" type="submit">Save starting point</button>
|
||||
</form>
|
||||
<form method="post" class="inline-form top-gap">
|
||||
<?= 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">↻ Recalculate from <?= h(date('F', strtotime($month . '-01 -1 month'))) ?></button>
|
||||
</form>
|
||||
<canvas class="trend-chart" data-trend-chart='<?= h(json_encode($comparison)) ?>' data-symbol="<?= h($symbol) ?>" height="255"></canvas>
|
||||
</article>
|
||||
|
||||
<?php if ($settings->module('income')): ?>
|
||||
<article class="card">
|
||||
<div class="card-heading"><div><p class="eyebrow">Income stream</p><h2>Additional income</h2></div><button class="button small secondary" type="button" data-dialog-open="income-dialog">+ Add</button></div>
|
||||
<?php if ($incomes === []): ?>
|
||||
<p class="muted">No additional income recorded.</p>
|
||||
<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="mini-list">
|
||||
<?php foreach (array_slice($incomes, 0, 5) as $income): ?>
|
||||
<div><span><strong><?= h($income['label']) ?></strong><small><?= h(date('M j', strtotime($income['received_on']))) ?> · <?= h(Support::money($income['withholding_amount'], $symbol)) ?> withheld</small></span><strong class="green">+<?= h(Support::money($income['amount'], $symbol)) ?></strong></div>
|
||||
<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="card stats-card">
|
||||
<div class="card-heading"><div><p class="eyebrow">Usage stats</p><h2>Planner activity</h2></div></div>
|
||||
<div class="stat-trio"><div><strong><?= (int) $visitorStats['today'] ?></strong><span>today</span></div><div><strong><?= (int) $visitorStats['thirty_days'] ?></strong><span>30-day visitors</span></div><div><strong><?= (int) $visitorStats['views'] ?></strong><span>page views</span></div></div>
|
||||
<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>
|
||||
</div>
|
||||
</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="card-heading"><div><p class="eyebrow">Define the target</p><h2>Add budget item</h2></div><button class="icon-button" type="button" data-dialog-close>×</button></div>
|
||||
<div class="form-grid">
|
||||
<label class="span-2">Name<input type="text" name="name" required placeholder="Internet, rent, groceries…"></label>
|
||||
<label>Target amount<div class="money-input"><span><?= h($symbol) ?></span><input type="number" name="planned_amount" min="0" step="0.01" required></div></label>
|
||||
<label>Category<select name="category_id"><option value="">Uncategorized</option><?php foreach ($categories as $category): ?><option value="<?= (int) $category['id'] ?>"><?= h($category['name']) ?></option><?php endforeach; ?></select></label>
|
||||
<label>Due date<input type="date" name="due_date" value="<?= h($month . '-01') ?>"></label>
|
||||
<label>Repeat<select name="recurrence"><option value="none">One time</option><option value="monthly">Monthly</option><option value="yearly">Yearly</option></select></label>
|
||||
<label class="span-2">Notes<textarea name="notes" rows="2"></textarea></label>
|
||||
</div>
|
||||
<button class="button primary full" type="submit">Add budget item</button>
|
||||
<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="card-heading"><div><p class="eyebrow">Beyond the starting point</p><h2>Add income</h2></div><button class="icon-button" type="button" data-dialog-close>×</button></div>
|
||||
<div class="form-grid">
|
||||
<label class="span-2">Label<input type="text" name="label" required placeholder="Paycheck, freelance, refund…"></label>
|
||||
<label>Amount<div class="money-input"><span><?= h($symbol) ?></span><input type="number" name="amount" min="0.01" step="0.01" required></div></label>
|
||||
<label>Received<input type="date" name="received_on" value="<?= h(date('Y-m-d')) ?>" required></label>
|
||||
<label class="span-2">Notes<textarea name="notes" rows="2"></textarea></label>
|
||||
</div>
|
||||
<p class="form-note">The configured income withholding will move automatically to the dormant account.</p>
|
||||
<button class="button primary full" type="submit">Record income</button>
|
||||
<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; ?>
|
||||
|
||||
+14
-9
@@ -1,8 +1,9 @@
|
||||
<section class="dead-hero card glow-blue">
|
||||
<section class="dead-hero card">
|
||||
<div>
|
||||
<span class="metric-icon purple"><?= icon('vault') ?></span>
|
||||
<p class="eyebrow">Dormant and disregarded</p>
|
||||
<h2>Dead account balance</h2>
|
||||
<p>Money here is excluded from usable monthly funds until you explicitly restore it.</p>
|
||||
<h2>Your out-of-use reserve</h2>
|
||||
<p>Money here is subtracted from spendable funds, remains fully traceable, and can later be restored to monthly income or earmarked for a target.</p>
|
||||
</div>
|
||||
<?php if ($hideBalance): ?>
|
||||
<button class="concealed-balance" type="button" data-concealed-balance data-value="<?= h(Support::money($balance, $symbol)) ?>">
|
||||
@@ -16,10 +17,14 @@
|
||||
<section class="content-grid dead-grid">
|
||||
<article class="card">
|
||||
<div class="card-heading"><div><p class="eyebrow">Manual movement</p><h2>Deposit or restore</h2></div></div>
|
||||
<form method="post" class="stack-form">
|
||||
<form method="post" class="stack-form" data-dead-transfer-form>
|
||||
<?= csrf_field() ?><input type="hidden" name="action" value="dead.move"><input type="hidden" name="month" value="<?= h($month) ?>">
|
||||
<label>Direction<select name="direction"><option value="deposit">Move into dormant account</option><option value="restore">Restore to this month’s usable budget</option></select></label>
|
||||
<label>Amount<div class="money-input"><span><?= h($symbol) ?></span><input type="number" name="amount" min="0.01" step="0.01" required></div></label>
|
||||
<div class="form-grid" data-restore-fields hidden>
|
||||
<label>Restore to<select name="destination_type" data-restore-type><option value="income">Monthly income</option><option value="item">Specific budget target</option></select></label>
|
||||
<label data-restore-item hidden>Budget target<select name="destination_id"><option value="">Choose a target</option><?php foreach ($items as $item): ?><option value="<?= (int) $item['id'] ?>"><?= h($item['name']) ?> · <?= h(Support::money($item['remaining_amount'], $symbol)) ?> left</option><?php endforeach; ?></select></label>
|
||||
</div>
|
||||
<label>Reason<textarea name="notes" rows="3" placeholder="Emergency reserve, return to groceries…"></textarea></label>
|
||||
<button class="button primary" type="submit">Record transfer</button>
|
||||
</form>
|
||||
@@ -27,9 +32,9 @@
|
||||
<article class="card">
|
||||
<div class="card-heading"><div><p class="eyebrow">Automatic rules</p><h2>How funds arrive here</h2></div><a class="text-link" href="<?= h(url('settings')) ?>">Configure →</a></div>
|
||||
<div class="rule-list">
|
||||
<div><span class="metric-icon blue">↙</span><p><strong>Income withholding</strong><small><?= h((string) $settings->get('dead.income_value', 0)) ?><?= $settings->get('dead.income_mode') === 'percent' ? '%' : ' fixed' ?> from each income source</small></p></div>
|
||||
<div><span class="metric-icon pink">↗</span><p><strong>Transaction pull</strong><small><?= h((string) $settings->get('dead.transaction_value', 0)) ?><?= $settings->get('dead.transaction_mode') === 'percent' ? '%' : ' fixed' ?> whenever an expense is recorded</small></p></div>
|
||||
<div><span class="metric-icon orange">◇</span><p><strong>Restorations</strong><small>Return to the selected month without changing historical expense records</small></p></div>
|
||||
<div><span class="metric-icon blue"><?= icon('wallet') ?></span><p><strong>Income withholding</strong><small><?= h((string) $settings->get('dead.income_value', 0)) ?><?= $settings->get('dead.income_mode') === 'percent' ? '%' : ' fixed' ?> from each income source</small></p></div>
|
||||
<div><span class="metric-icon pink"><?= icon('expense') ?></span><p><strong>Transaction pull</strong><small><?= h((string) $settings->get('dead.transaction_value', 0)) ?><?= $settings->get('dead.transaction_mode') === 'percent' ? '%' : ' fixed' ?> whenever an expense is recorded</small></p></div>
|
||||
<div><span class="metric-icon orange"><?= icon('data') ?></span><p><strong>Restorations</strong><small>Return to monthly income or identify a specific target without rewriting history</small></p></div>
|
||||
</div>
|
||||
</article>
|
||||
</section>
|
||||
@@ -37,7 +42,7 @@
|
||||
<article class="card">
|
||||
<div class="card-heading">
|
||||
<div><p class="eyebrow">Permanent paper trail</p><h2>Dormant ledger</h2></div>
|
||||
<label class="search-box"><span>⌕</span><input type="search" placeholder="Search movements…" data-table-search="dead-table"></label>
|
||||
<label class="search-box"><?= icon('search') ?><input type="search" placeholder="Search movements…" data-table-search="dead-table"></label>
|
||||
</div>
|
||||
<?php if ($entries === []): ?>
|
||||
<div class="empty-state"><strong>The account is empty</strong><p>Automatic or manual transfers will appear here.</p></div>
|
||||
@@ -51,7 +56,7 @@
|
||||
<td><?= h(date('M j, Y g:i A', strtotime($entry['created_at']))) ?></td>
|
||||
<td><a class="text-link" href="<?= h(url('dead-account', ['month' => $entry['month']])) ?>"><?= h($entry['month']) ?></a></td>
|
||||
<td><strong><?= h(ucwords(str_replace('_', ' ', $entry['kind']))) ?></strong><?php if ($entry['notes']): ?><small><?= h($entry['notes']) ?></small><?php endif; ?></td>
|
||||
<td><?= h(ucfirst((string) ($entry['source_type'] ?: 'manual'))) ?></td>
|
||||
<td><?php if ($entry['source_type'] === 'restore_item'): ?>Target: <?= h($entry['destination_name'] ?: 'Archived target') ?><?php elseif ($entry['source_type'] === 'restore_income'): ?>Monthly income<?php else: ?><?= h(ucwords(str_replace('_', ' ', (string) ($entry['source_type'] ?: 'manual')))) ?><?php endif; ?></td>
|
||||
<td class="<?= (float) $entry['amount'] >= 0 ? 'green' : 'orange' ?>"><strong><?= (float) $entry['amount'] >= 0 ? '+' : '' ?><?= h(Support::money($entry['amount'], $symbol)) ?></strong></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
|
||||
+65
-37
@@ -5,6 +5,14 @@ $previousMonth = date('Y-m', strtotime($currentMonth . '-01 -1 month'));
|
||||
$nextMonth = date('Y-m', strtotime($currentMonth . '-01 +1 month'));
|
||||
$appName = (string) $settings->get('app.name', 'Neon Ledger');
|
||||
$defaultTheme = (string) $settings->get('theme.default', 'dark');
|
||||
$user = $auth->user();
|
||||
$navItems = [
|
||||
['dashboard', 'home', 'Overview', true],
|
||||
['transactions', 'expense', 'Transactions', true],
|
||||
['calendar', 'calendar', 'Bill calendar', $settings->module('calendar')],
|
||||
['comparisons', 'chart', 'Analytics', $settings->module('comparisons')],
|
||||
['dead-account', 'vault', 'Dormant account', $settings->module('dead_account')],
|
||||
];
|
||||
?>
|
||||
<!doctype html>
|
||||
<html lang="en" data-theme="<?= h($defaultTheme) ?>">
|
||||
@@ -12,77 +20,97 @@ $defaultTheme = (string) $settings->get('theme.default', 'dark');
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="color-scheme" content="dark light">
|
||||
<meta name="theme-color" content="#090b12">
|
||||
<title><?= h($title ?? 'Planner') ?> · <?= h($appName) ?></title>
|
||||
<link rel="stylesheet" href="/assets/app.css">
|
||||
<link rel="stylesheet" href="/assets/app.css?v=<?= (int) filemtime(APP_ROOT . '/public/assets/app.css') ?>">
|
||||
<script>document.documentElement.dataset.theme=localStorage.getItem('budget-theme')||<?= json_encode($defaultTheme) ?>;</script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="app-shell">
|
||||
<aside class="sidebar" id="sidebar">
|
||||
<a class="brand" href="<?= h(url('dashboard', ['month' => $currentMonth])) ?>">
|
||||
<span class="brand-mark">NL</span>
|
||||
<span><strong><?= h($appName) ?></strong><small>Financial command center</small></span>
|
||||
<span class="brand-mark"><i></i><b>N</b></span>
|
||||
<span class="brand-copy"><strong><?= h($appName) ?></strong><small>Personal finance planner</small></span>
|
||||
</a>
|
||||
<nav class="nav">
|
||||
<a class="<?= $currentRoute === 'dashboard' ? 'active' : '' ?>" href="<?= h(url('dashboard', ['month' => $currentMonth])) ?>"><span>⌂</span> Command center</a>
|
||||
<a class="<?= $currentRoute === 'transactions' ? 'active' : '' ?>" href="<?= h(url('transactions', ['month' => $currentMonth])) ?>"><span>↗</span> Expenses</a>
|
||||
<?php if ($settings->module('calendar')): ?>
|
||||
<a class="<?= $currentRoute === 'calendar' ? 'active' : '' ?>" href="<?= h(url('calendar', ['month' => $currentMonth])) ?>"><span>□</span> Bill calendar</a>
|
||||
<?php endif; ?>
|
||||
<?php if ($settings->module('comparisons')): ?>
|
||||
<a class="<?= $currentRoute === 'comparisons' ? 'active' : '' ?>" href="<?= h(url('comparisons', ['month' => $currentMonth])) ?>"><span>⌁</span> Comparisons</a>
|
||||
<?php endif; ?>
|
||||
<?php if ($settings->module('dead_account')): ?>
|
||||
<a class="<?= $currentRoute === 'dead-account' ? 'active' : '' ?>" href="<?= h(url('dead-account', ['month' => $currentMonth])) ?>"><span>◇</span> Dormant account</a>
|
||||
<?php endif; ?>
|
||||
</nav>
|
||||
<div class="nav-label">Administration</div>
|
||||
<nav class="nav">
|
||||
<a class="<?= $currentRoute === 'settings' ? 'active' : '' ?>" href="<?= h(url('settings')) ?>"><span>⚙</span> Settings</a>
|
||||
<?php if ($settings->module('import_export')): ?>
|
||||
<a class="<?= $currentRoute === 'data' ? 'active' : '' ?>" href="<?= h(url('data', ['month' => $currentMonth])) ?>"><span>⇄</span> Import & export</a>
|
||||
<?php endif; ?>
|
||||
</nav>
|
||||
|
||||
<div class="sidebar-section">
|
||||
<p class="nav-label">Workspace</p>
|
||||
<nav class="nav" aria-label="Planner">
|
||||
<?php foreach ($navItems as [$navRoute, $navIcon, $navLabel, $enabled]): ?>
|
||||
<?php if ($enabled): ?>
|
||||
<a class="<?= $currentRoute === $navRoute ? 'active' : '' ?>" href="<?= h(url($navRoute, ['month' => $currentMonth])) ?>">
|
||||
<?= icon($navIcon) ?><span><?= h($navLabel) ?></span>
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
<?php endforeach; ?>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<div class="sidebar-section admin-section">
|
||||
<p class="nav-label">Administration</p>
|
||||
<nav class="nav" aria-label="Administration">
|
||||
<a class="<?= $currentRoute === 'settings' ? 'active' : '' ?>" href="<?= h(url('settings')) ?>"><?= icon('settings') ?><span>Configuration</span></a>
|
||||
<?php if ($settings->module('import_export')): ?>
|
||||
<a class="<?= $currentRoute === 'data' ? 'active' : '' ?>" href="<?= h(url('data', ['month' => $currentMonth])) ?>"><?= icon('data') ?><span>Import & export</span></a>
|
||||
<?php endif; ?>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<div class="sidebar-insight">
|
||||
<span class="insight-orb"></span>
|
||||
<p>Monthly planning</p>
|
||||
<strong><?= h(date('F Y', strtotime($currentMonth . '-01'))) ?></strong>
|
||||
<small>Progress is measured against income, targets, and money moved out of use.</small>
|
||||
</div>
|
||||
|
||||
<div class="sidebar-footer">
|
||||
<button class="theme-toggle" type="button" data-theme-toggle aria-label="Toggle light and dark mode"><span>◐</span> <span data-theme-label>Light mode</span></button>
|
||||
<form method="post">
|
||||
<?= csrf_field() ?>
|
||||
<input type="hidden" name="action" value="logout">
|
||||
<button class="text-button" type="submit">Sign out</button>
|
||||
</form>
|
||||
<div class="admin-card">
|
||||
<span class="avatar"><?= h(strtoupper(substr((string) ($user['email'] ?? 'A'), 0, 1))) ?></span>
|
||||
<span><strong>Administrator</strong><small><?= h($user['email'] ?? '') ?></small></span>
|
||||
</div>
|
||||
<div class="sidebar-actions">
|
||||
<button class="icon-control" type="button" data-theme-toggle aria-label="Toggle light and dark mode"><?= icon('sun') ?></button>
|
||||
<form method="post">
|
||||
<?= csrf_field() ?><input type="hidden" name="action" value="logout">
|
||||
<button class="icon-control" type="submit" aria-label="Sign out"><?= icon('logout') ?></button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
<button class="sidebar-scrim" type="button" data-menu-close aria-label="Close navigation"></button>
|
||||
|
||||
<main class="main">
|
||||
<header class="topbar">
|
||||
<button class="menu-button" type="button" data-menu-toggle aria-label="Open navigation">☰</button>
|
||||
<div>
|
||||
<p class="eyebrow"><?= h(date('l, F j')) ?></p>
|
||||
<button class="menu-button" type="button" data-menu-toggle aria-label="Open navigation"><?= icon('menu') ?></button>
|
||||
<div class="page-title">
|
||||
<p class="eyebrow"><?= h(date('l, F j, Y')) ?></p>
|
||||
<h1><?= h($title ?? 'Planner') ?></h1>
|
||||
</div>
|
||||
<?php if (!in_array($currentRoute, ['settings', 'data'], true)): ?>
|
||||
<div class="month-switcher">
|
||||
<a aria-label="Previous month" href="<?= h(url($currentRoute, ['month' => $previousMonth])) ?>">←</a>
|
||||
<a aria-label="Previous month" href="<?= h(url($currentRoute, ['month' => $previousMonth])) ?>">‹</a>
|
||||
<label>
|
||||
<span class="sr-only">Budget month</span>
|
||||
<input type="month" value="<?= h($currentMonth) ?>" data-month-picker data-route="<?= h($currentRoute) ?>">
|
||||
</label>
|
||||
<a aria-label="Next month" href="<?= h(url($currentRoute, ['month' => $nextMonth])) ?>">→</a>
|
||||
<a aria-label="Next month" href="<?= h(url($currentRoute, ['month' => $nextMonth])) ?>">›</a>
|
||||
</div>
|
||||
<a class="button primary top-action" href="<?= h(url('transactions', ['month' => $currentMonth, 'open' => 'expense-dialog'])) ?>"><?= icon('plus') ?> Add expense</a>
|
||||
<?php endif; ?>
|
||||
</header>
|
||||
|
||||
<?php foreach ($flashes as $flash): ?>
|
||||
<div class="flash <?= h($flash['type']) ?>" role="status">
|
||||
<span><?= $flash['type'] === 'success' ? '✓' : '!' ?></span>
|
||||
<?= h($flash['message']) ?>
|
||||
<button type="button" aria-label="Dismiss">×</button>
|
||||
<p><?= h($flash['message']) ?></p>
|
||||
<button type="button" aria-label="Dismiss"><?= icon('close') ?></button>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
|
||||
<div class="page-content"><?= $content ?></div>
|
||||
<footer class="app-footer"><span><?= h($appName) ?></span><span>Private by default · SQLite powered</span></footer>
|
||||
</main>
|
||||
</div>
|
||||
<script src="/assets/app.js"></script>
|
||||
<script src="/assets/app.js?v=<?= (int) filemtime(APP_ROOT . '/public/assets/app.js') ?>"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
<?php $fieldIncome = $editingIncome ?? []; $defaultIncomeDate = $month === date('Y-m') ? date('Y-m-d') : $month . '-01'; ?>
|
||||
<div class="form-grid">
|
||||
<label class="span-2">Label<input type="text" name="label" required placeholder="Paycheck, freelance, refund…" value="<?= h($fieldIncome['label'] ?? '') ?>"></label>
|
||||
<label>Amount<div class="money-input"><span><?= h($symbol) ?></span><input type="number" name="amount" min="0.01" step="0.01" required value="<?= h((string) ($fieldIncome['amount'] ?? '')) ?>"></div></label>
|
||||
<label>Received<input type="date" name="received_on" value="<?= h((string) ($fieldIncome['received_on'] ?? $defaultIncomeDate)) ?>" min="<?= h($month . '-01') ?>" max="<?= h($month . '-' . date('t', strtotime($month . '-01'))) ?>" required></label>
|
||||
<label class="span-2">Notes<textarea name="notes" rows="2" placeholder="Optional details"><?= h($fieldIncome['notes'] ?? '') ?></textarea></label>
|
||||
</div>
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php $fieldItem = $editingItem ?? []; ?>
|
||||
<div class="form-grid">
|
||||
<label class="span-2">Name<input type="text" name="name" required placeholder="Internet, rent, groceries…" value="<?= h($fieldItem['name'] ?? '') ?>"></label>
|
||||
<label>Target amount<div class="money-input"><span><?= h($symbol) ?></span><input type="number" name="planned_amount" min="0" step="0.01" required value="<?= h((string) ($fieldItem['planned_amount'] ?? '')) ?>"></div></label>
|
||||
<label>Category<select name="category_id"><option value="">Uncategorized</option><?php foreach ($categories as $category): ?><option value="<?= (int) $category['id'] ?>" <?= (int) ($fieldItem['category_id'] ?? 0) === (int) $category['id'] ? 'selected' : '' ?>><?= h($category['name']) ?></option><?php endforeach; ?></select></label>
|
||||
<label>Due date<input type="date" name="due_date" value="<?= h((string) ($fieldItem['due_date'] ?? $month . '-01')) ?>"></label>
|
||||
<label>Repeat<select name="recurrence"><option value="none" <?= ($fieldItem['recurrence'] ?? 'none') === 'none' ? 'selected' : '' ?>>One time</option><option value="monthly" <?= ($fieldItem['recurrence'] ?? '') === 'monthly' ? 'selected' : '' ?>>Monthly</option><option value="yearly" <?= ($fieldItem['recurrence'] ?? '') === 'yearly' ? 'selected' : '' ?>>Yearly</option></select></label>
|
||||
<label class="span-2">Notes<textarea name="notes" rows="2" placeholder="Optional details"><?= h($fieldItem['notes'] ?? '') ?></textarea></label>
|
||||
</div>
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php $fieldTransaction = $editingTransaction ?? []; $defaultTransactionDate = $month === date('Y-m') ? date('Y-m-d') : $month . '-01'; ?>
|
||||
<div class="form-grid">
|
||||
<label class="span-2">Description<input type="text" name="merchant" required placeholder="Payment or merchant" value="<?= h($fieldTransaction['merchant'] ?? '') ?>"></label>
|
||||
<label>Amount<div class="money-input"><span><?= h($symbol) ?></span><input type="number" name="amount" min="0.01" step="0.01" required value="<?= h((string) ($fieldTransaction['amount'] ?? '')) ?>"></div></label>
|
||||
<label>Date<input type="date" name="transacted_on" value="<?= h((string) ($fieldTransaction['transacted_on'] ?? $defaultTransactionDate)) ?>" min="<?= h($month . '-01') ?>" max="<?= h($month . '-' . date('t', strtotime($month . '-01'))) ?>" required></label>
|
||||
<label>Budget target<select name="budget_item_id"><option value="">General expense</option><?php foreach ($items as $item): ?><option value="<?= (int) $item['id'] ?>" <?= (int) ($fieldTransaction['budget_item_id'] ?? 0) === (int) $item['id'] ? 'selected' : '' ?>><?= h($item['name']) ?> · <?= h(Support::money($item['remaining_amount'], $symbol)) ?> left</option><?php endforeach; ?></select></label>
|
||||
<label>Category<select name="category_id"><option value="">Use target / uncategorized</option><?php foreach ($categories as $category): ?><option value="<?= (int) $category['id'] ?>" <?= (int) ($fieldTransaction['category_id'] ?? 0) === (int) $category['id'] ? 'selected' : '' ?>><?= h($category['name']) ?></option><?php endforeach; ?></select></label>
|
||||
<label class="span-2">Notes<textarea name="notes" rows="2" placeholder="Optional details"><?= h($fieldTransaction['notes'] ?? '') ?></textarea></label>
|
||||
</div>
|
||||
+17
-2
@@ -96,12 +96,27 @@
|
||||
</section>
|
||||
|
||||
<article class="card">
|
||||
<div class="card-heading"><div><p class="eyebrow">Organize spending</p><h2>Add an expense category</h2></div></div>
|
||||
<div class="card-heading"><div><p class="eyebrow">Organize spending</p><h2>Expense categories</h2><p class="heading-note">Searchable records use these names, colors, and optional monthly guardrails.</p></div></div>
|
||||
<form method="post" class="inline-fields">
|
||||
<?= csrf_field() ?><input type="hidden" name="action" value="category.add">
|
||||
<?= csrf_field() ?><input type="hidden" name="action" value="category.add"><input type="hidden" name="return_route" value="settings">
|
||||
<label>Name<input type="text" name="name" required placeholder="Utilities"></label>
|
||||
<label>Color<input type="color" name="color" value="#7c5cff"></label>
|
||||
<label>Optional monthly limit<input type="number" name="monthly_limit" min="0" step="0.01" value="0"></label>
|
||||
<button class="button primary" type="submit">Add category</button>
|
||||
</form>
|
||||
<?php if ($categories !== []): ?>
|
||||
<div class="category-admin-list">
|
||||
<?php foreach ($categories as $category): ?>
|
||||
<form method="post">
|
||||
<?= csrf_field() ?><input type="hidden" name="action" value="category.update"><input type="hidden" name="id" value="<?= (int) $category['id'] ?>">
|
||||
<span class="category-swatch" style="--dot:<?= h($category['color']) ?>"></span>
|
||||
<input aria-label="Category name" type="text" name="name" value="<?= h($category['name']) ?>" required>
|
||||
<input aria-label="Category color" type="color" name="color" value="<?= h($category['color']) ?>">
|
||||
<div class="money-input"><span><?= h((string) $s['app.currency_symbol']) ?></span><input aria-label="Monthly limit" type="number" name="monthly_limit" min="0" step="0.01" value="<?= h((string) $category['monthly_limit']) ?>"></div>
|
||||
<button class="icon-button" type="submit" aria-label="Save <?= h($category['name']) ?>"><?= icon('edit') ?></button>
|
||||
<button class="icon-button danger" type="submit" name="action" value="category.archive" formnovalidate data-confirm-button="Archive this category? Historical records will keep their category data." aria-label="Archive <?= h($category['name']) ?>"><?= icon('trash') ?></button>
|
||||
</form>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</article>
|
||||
|
||||
+37
-27
@@ -1,33 +1,39 @@
|
||||
<section class="summary-strip">
|
||||
<div><span>Available this month</span><strong><?= h(Support::money($summary['gross_resources'], $symbol)) ?></strong></div>
|
||||
<div><span>Total spent</span><strong class="pink"><?= h(Support::money($summary['expenses'], $symbol)) ?></strong></div>
|
||||
<div><span>Remaining</span><strong class="<?= $summary['remaining'] >= 0 ? 'green' : 'red' ?>"><?= h(Support::money($summary['remaining'], $symbol)) ?></strong></div>
|
||||
<button class="button primary" type="button" data-dialog-open="expense-dialog">+ Record expense</button>
|
||||
<section class="page-intro">
|
||||
<div><p class="eyebrow">Searchable expense ledger</p><h2>Every payment, one trail</h2><p>Link a payment to a target and its owing balance moves immediately.</p></div>
|
||||
<button class="button primary" type="button" data-dialog-open="expense-dialog"><?= icon('plus') ?> Record expense</button>
|
||||
</section>
|
||||
|
||||
<article class="card">
|
||||
<section class="summary-grid">
|
||||
<article class="summary-card card"><span class="metric-icon blue"><?= icon('wallet') ?></span><div><small>Available</small><strong><?= h(Support::money($summary['gross_resources'], $symbol)) ?></strong></div></article>
|
||||
<article class="summary-card card"><span class="metric-icon pink"><?= icon('expense') ?></span><div><small>Spent</small><strong><?= h(Support::money($summary['expenses'], $symbol)) ?></strong></div></article>
|
||||
<article class="summary-card card"><span class="metric-icon purple"><?= icon('vault') ?></span><div><small>Set aside</small><strong><?= h(Support::money($summary['dead_deposits'], $symbol)) ?></strong></div></article>
|
||||
<article class="summary-card card"><span class="metric-icon green"><?= icon('chart') ?></span><div><small>Remaining</small><strong class="<?= $summary['remaining'] < 0 ? 'red' : '' ?>"><?= h(Support::money($summary['remaining'], $symbol)) ?></strong></div></article>
|
||||
</section>
|
||||
|
||||
<article class="card panel">
|
||||
<div class="card-heading">
|
||||
<div><p class="eyebrow">Searchable ledger</p><h2>Expense history</h2></div>
|
||||
<label class="search-box"><span>⌕</span><input type="search" placeholder="Search expenses…" data-table-search="expense-table"></label>
|
||||
<div><p class="eyebrow"><?= h(date('F Y', strtotime($month . '-01'))) ?></p><h2>Transaction history</h2><p class="heading-note"><?= count($transactions) ?> recorded expense<?= count($transactions) === 1 ? '' : 's' ?></p></div>
|
||||
<label class="search-box"><?= icon('search') ?><input type="search" placeholder="Search description, item, category…" data-table-search="expense-table"></label>
|
||||
</div>
|
||||
<?php if ($transactions === []): ?>
|
||||
<div class="empty-state"><strong>No expenses recorded</strong><p>Record a payment to move a bill’s progress forward.</p></div>
|
||||
<div class="empty-state"><span class="empty-icon"><?= icon('expense') ?></span><strong>No expenses recorded</strong><p>Record a payment to start progressing this month’s targets.</p><button class="button small" type="button" data-dialog-open="expense-dialog">Record the first expense</button></div>
|
||||
<?php else: ?>
|
||||
<div class="table-wrap">
|
||||
<table id="expense-table">
|
||||
<thead><tr><th>Date</th><th>Description</th><th>Category / item</th><th>Amount</th><th>Dormant pull</th><th></th></tr></thead>
|
||||
<table id="expense-table" class="data-table">
|
||||
<thead><tr><th>Date</th><th>Description</th><th>Category / target</th><th>Amount</th><th>Dormant pull</th><th><span class="sr-only">Actions</span></th></tr></thead>
|
||||
<tbody>
|
||||
<?php foreach ($transactions as $transaction): ?>
|
||||
<tr>
|
||||
<td data-label="Date"><?= h(date('M j, Y', strtotime($transaction['transacted_on']))) ?></td>
|
||||
<td data-label="Date"><span class="table-date"><?= h(date('M j', strtotime($transaction['transacted_on']))) ?><small><?= h(date('Y', strtotime($transaction['transacted_on']))) ?></small></span></td>
|
||||
<td data-label="Description"><strong><?= h($transaction['merchant']) ?></strong><?php if ($transaction['notes']): ?><small><?= h($transaction['notes']) ?></small><?php endif; ?></td>
|
||||
<td data-label="Category"><span class="pill"><i style="--dot:<?= h($transaction['category_color'] ?: '#718096') ?>"></i><?= h($transaction['category_name'] ?: 'Uncategorized') ?></span><?php if ($transaction['item_name']): ?><small><?= h($transaction['item_name']) ?></small><?php endif; ?></td>
|
||||
<td data-label="Amount"><strong><?= h(Support::money($transaction['amount'], $symbol)) ?></strong></td>
|
||||
<td data-label="Category / target"><span class="pill"><i style="--dot:<?= h($transaction['category_color'] ?: '#718096') ?>"></i><?= h($transaction['category_name'] ?: 'Uncategorized') ?></span><?php if ($transaction['item_name']): ?><small><?= h($transaction['item_name']) ?></small><?php endif; ?></td>
|
||||
<td data-label="Amount"><strong class="amount-negative">-<?= h(Support::money($transaction['amount'], $symbol)) ?></strong></td>
|
||||
<td data-label="Dormant pull"><?= h(Support::money($transaction['dead_pull_amount'], $symbol)) ?></td>
|
||||
<td>
|
||||
<td class="table-actions">
|
||||
<button class="icon-button" type="button" data-dialog-open="expense-edit-<?= (int) $transaction['id'] ?>" aria-label="Edit <?= h($transaction['merchant']) ?>"><?= icon('edit') ?></button>
|
||||
<form method="post" data-confirm="Delete this expense and reverse its dormant transfer?">
|
||||
<?= csrf_field() ?><input type="hidden" name="action" value="transaction.delete"><input type="hidden" name="month" value="<?= h($month) ?>"><input type="hidden" name="id" value="<?= (int) $transaction['id'] ?>">
|
||||
<button class="icon-button danger" type="submit" title="Delete">×</button>
|
||||
<button class="icon-button danger" type="submit" aria-label="Delete <?= h($transaction['merchant']) ?>"><?= icon('trash') ?></button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -41,16 +47,20 @@
|
||||
<dialog id="expense-dialog">
|
||||
<form method="post" class="dialog-card">
|
||||
<?= csrf_field() ?><input type="hidden" name="action" value="transaction.add"><input type="hidden" name="month" value="<?= h($month) ?>">
|
||||
<div class="card-heading"><div><p class="eyebrow">Progress a target</p><h2>Record expense</h2></div><button class="icon-button" type="button" data-dialog-close>×</button></div>
|
||||
<div class="form-grid">
|
||||
<label class="span-2">Description<input type="text" name="merchant" required placeholder="Payment or merchant"></label>
|
||||
<label>Amount<div class="money-input"><span><?= h($symbol) ?></span><input type="number" name="amount" min="0.01" step="0.01" required></div></label>
|
||||
<label>Date<input type="date" name="transacted_on" value="<?= h(date('Y-m-d')) ?>" required></label>
|
||||
<label>Budget item<select name="budget_item_id"><option value="">General expense</option><?php foreach ($items as $item): ?><option value="<?= (int) $item['id'] ?>"><?= h($item['name']) ?> (<?= h(Support::money($item['remaining_amount'], $symbol)) ?> owing)</option><?php endforeach; ?></select></label>
|
||||
<label>Category<select name="category_id"><option value="">Use item / uncategorized</option><?php foreach ($categories as $category): ?><option value="<?= (int) $category['id'] ?>"><?= h($category['name']) ?></option><?php endforeach; ?></select></label>
|
||||
<label class="span-2">Notes<textarea name="notes" rows="2"></textarea></label>
|
||||
</div>
|
||||
<p class="form-note">A configured per-transaction amount or percentage will also be moved to the dormant account.</p>
|
||||
<button class="button primary full" type="submit">Record expense</button>
|
||||
<div class="dialog-heading"><div><p class="eyebrow">Progress a target</p><h2>Record expense</h2></div><button class="icon-button" type="button" data-dialog-close><?= icon('close') ?></button></div>
|
||||
<?php require APP_ROOT . '/views/partials/transaction-fields.php'; ?>
|
||||
<p class="form-note">Your configured per-transaction dormant 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 expense</button></div>
|
||||
</form>
|
||||
</dialog>
|
||||
|
||||
<?php foreach ($transactions as $transaction): ?>
|
||||
<dialog id="expense-edit-<?= (int) $transaction['id'] ?>">
|
||||
<form method="post" class="dialog-card">
|
||||
<?= csrf_field() ?><input type="hidden" name="action" value="transaction.update"><input type="hidden" name="month" value="<?= h($month) ?>"><input type="hidden" name="id" value="<?= (int) $transaction['id'] ?>">
|
||||
<div class="dialog-heading"><div><p class="eyebrow">Maintain transaction</p><h2>Edit <?= h($transaction['merchant']) ?></h2></div><button class="icon-button" type="button" data-dialog-close><?= icon('close') ?></button></div>
|
||||
<?php $editingTransaction = $transaction; require APP_ROOT . '/views/partials/transaction-fields.php'; unset($editingTransaction); ?>
|
||||
<div class="dialog-actions"><button class="button" type="button" data-dialog-close>Cancel</button><button class="button primary" type="submit">Save expense</button></div>
|
||||
</form>
|
||||
</dialog>
|
||||
<?php endforeach; ?>
|
||||
|
||||
Reference in New Issue
Block a user