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.
117 lines
6.0 KiB
PHP
117 lines
6.0 KiB
PHP
<?php
|
||
$currentRoute = $route ?? (string) ($_GET['route'] ?? 'dashboard');
|
||
$currentMonth = $month ?? date('Y-m');
|
||
$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) ?>">
|
||
<head>
|
||
<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?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"><i></i><b>N</b></span>
|
||
<span class="brand-copy"><strong><?= h($appName) ?></strong><small>Personal finance planner</small></span>
|
||
</a>
|
||
|
||
<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">
|
||
<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"><?= 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>
|
||
<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>
|
||
</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>
|
||
<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?v=<?= (int) filemtime(APP_ROOT . '/public/assets/app.js') ?>"></script>
|
||
</body>
|
||
</html>
|