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:
Ty Clifford
2026-06-15 10:33:55 -04:00
parent b7eaa81501
commit 51ee4ecb5a
18 changed files with 1568 additions and 559 deletions
+65 -37
View File
@@ -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>