-
This commit is contained in:
@@ -0,0 +1,33 @@
|
||||
<?php $appName = (string) $settings->get('app.name', 'Neon Ledger'); ?>
|
||||
<!doctype html>
|
||||
<html lang="en" data-theme="dark">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<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">
|
||||
</head>
|
||||
<body class="auth-body">
|
||||
<main class="auth-shell">
|
||||
<section class="auth-story">
|
||||
<div class="brand auth-brand"><span class="brand-mark">NL</span><strong><?= h($appName) ?></strong></div>
|
||||
<div>
|
||||
<p class="eyebrow">Plan forward. Compare backward.</p>
|
||||
<h1>Your money, with a memory.</h1>
|
||||
<p>Build monthly budgets, carry unused funds forward, track bill progress, and move money out of sight without losing the paper trail.</p>
|
||||
</div>
|
||||
<div class="auth-orbit" aria-hidden="true">
|
||||
<span class="orbit-one"></span><span class="orbit-two"></span><span class="orbit-three"></span>
|
||||
<strong>$</strong>
|
||||
</div>
|
||||
</section>
|
||||
<section class="auth-panel">
|
||||
<?php foreach ($flashes as $flash): ?>
|
||||
<div class="flash <?= h($flash['type']) ?>"><span>!</span><?= h($flash['message']) ?></div>
|
||||
<?php endforeach; ?>
|
||||
<?= $content ?>
|
||||
</section>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,52 @@
|
||||
<?php
|
||||
$first = new DateTimeImmutable($month . '-01');
|
||||
$daysInMonth = (int) $first->format('t');
|
||||
$startOffset = (int) $first->format('w');
|
||||
$entriesByDay = [];
|
||||
foreach ($entries as $entry) {
|
||||
$day = (int) date('j', strtotime($entry['event_date']));
|
||||
$entriesByDay[$day][] = $entry;
|
||||
}
|
||||
?>
|
||||
<section class="calendar-toolbar">
|
||||
<div><p class="eyebrow">Due dates and milestones</p><p class="muted">Bills are added automatically from budget targets.</p></div>
|
||||
<button class="button primary" type="button" data-dialog-open="event-dialog">+ Add event</button>
|
||||
</section>
|
||||
<article class="card calendar-card">
|
||||
<div class="calendar-weekdays"><?php foreach (['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'] as $weekday): ?><span><?= h($weekday) ?></span><?php endforeach; ?></div>
|
||||
<div class="calendar-grid">
|
||||
<?php for ($blank = 0; $blank < $startOffset; $blank++): ?><div class="calendar-day blank"></div><?php endfor; ?>
|
||||
<?php for ($day = 1; $day <= $daysInMonth; $day++): ?>
|
||||
<?php $isToday = date('Y-m-d') === $month . '-' . str_pad((string) $day, 2, '0', STR_PAD_LEFT); ?>
|
||||
<div class="calendar-day <?= $isToday ? 'today' : '' ?>">
|
||||
<span class="day-number"><?= $day ?></span>
|
||||
<?php foreach ($entriesByDay[$day] ?? [] as $entry): ?>
|
||||
<div class="calendar-event <?= h($entry['event_type']) ?>">
|
||||
<span><?= $entry['event_type'] === 'bill' ? '●' : '◆' ?></span>
|
||||
<div><strong><?= h($entry['title']) ?></strong><?php if ($entry['amount'] !== null): ?><small><?= h(Support::money($entry['amount'], $symbol)) ?></small><?php endif; ?></div>
|
||||
<?php if ($entry['event_type'] !== 'bill'): ?>
|
||||
<form method="post" data-confirm="Remove this calendar event?">
|
||||
<?= csrf_field() ?><input type="hidden" name="action" value="event.delete"><input type="hidden" name="month" value="<?= h($month) ?>"><input type="hidden" name="id" value="<?= (int) $entry['id'] ?>">
|
||||
<button type="submit" aria-label="Delete event">×</button>
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endfor; ?>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<dialog id="event-dialog">
|
||||
<form method="post" class="dialog-card">
|
||||
<?= csrf_field() ?><input type="hidden" name="action" value="event.add"><input type="hidden" name="month" value="<?= h($month) ?>">
|
||||
<div class="card-heading"><div><p class="eyebrow">Personal milestone</p><h2>Add calendar event</h2></div><button class="icon-button" type="button" data-dialog-close>×</button></div>
|
||||
<div class="form-grid">
|
||||
<label class="span-2">Title<input type="text" name="title" required placeholder="Payday, renewal, deadline…"></label>
|
||||
<label>Date<input type="date" name="event_date" value="<?= h($month . '-01') ?>" required></label>
|
||||
<label>Type<select name="event_type"><option value="note">Note</option><option value="payday">Payday</option><option value="deadline">Deadline</option></select></label>
|
||||
<label class="span-2">Notes<textarea name="notes" rows="2"></textarea></label>
|
||||
</div>
|
||||
<button class="button primary full" type="submit">Add to calendar</button>
|
||||
</form>
|
||||
</dialog>
|
||||
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
$totals = $yearly['totals'];
|
||||
$bestMonth = null;
|
||||
foreach ($yearly['months'] as $row) {
|
||||
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>
|
||||
</section>
|
||||
|
||||
<section class="content-grid comparison-grid">
|
||||
<article class="card span-2">
|
||||
<div class="card-heading"><div><p class="eyebrow">Month-to-month</p><h2>Resources vs. expenses</h2></div></div>
|
||||
<canvas class="trend-chart large" data-trend-chart='<?= h(json_encode($comparison)) ?>' data-symbol="<?= h($symbol) ?>" height="280"></canvas>
|
||||
</article>
|
||||
<article class="card">
|
||||
<div class="card-heading"><div><p class="eyebrow"><?= h(date('F', strtotime($month . '-01'))) ?></p><h2>Category share</h2></div></div>
|
||||
<?php if ($categoryStats === []): ?>
|
||||
<div class="empty-state compact"><strong>No category data</strong><p>Record categorized expenses to build this view.</p></div>
|
||||
<?php else: ?>
|
||||
<?php $categoryTotal = array_sum(array_column($categoryStats, 'spent')); ?>
|
||||
<div class="category-bars">
|
||||
<?php foreach ($categoryStats as $category): $width = $categoryTotal > 0 ? (float) $category['spent'] / $categoryTotal * 100 : 0; ?>
|
||||
<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; ?>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<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>
|
||||
</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>
|
||||
<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['planned'], $symbol)) ?></td>
|
||||
<td><?= h(Support::money($row['expenses'], $symbol)) ?></td>
|
||||
<td><?= h(Support::money($row['dead_movement'], $symbol)) ?></td>
|
||||
<td class="<?= $row['remaining'] >= 0 ? 'green' : 'red' ?>"><strong><?= h(Support::money($row['remaining'], $symbol)) ?></strong></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</article>
|
||||
@@ -0,0 +1,144 @@
|
||||
<?php
|
||||
$remainingTone = $summary['remaining'] >= 0 ? 'green' : 'red';
|
||||
$progress = $summary['gross_resources'] > 0 ? min(100, max(0, $summary['expenses'] / $summary['gross_resources'] * 100)) : 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>
|
||||
</div>
|
||||
<div class="balance-ring" style="--progress:<?= h((string) $progress) ?>">
|
||||
<span><?= number_format(100 - $progress, 0) ?>%</span><small>left</small>
|
||||
</div>
|
||||
</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>
|
||||
</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>
|
||||
</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>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<section class="content-grid dashboard-grid">
|
||||
<div class="span-2 stack">
|
||||
<article class="card">
|
||||
<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>
|
||||
<?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>
|
||||
<?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>
|
||||
<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">
|
||||
<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>
|
||||
<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>
|
||||
</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>
|
||||
<?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>
|
||||
<?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>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<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>
|
||||
</form>
|
||||
</dialog>
|
||||
|
||||
<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>
|
||||
</form>
|
||||
</dialog>
|
||||
@@ -0,0 +1,46 @@
|
||||
<section class="hero-grid data-actions">
|
||||
<a class="card export-card purple" href="<?= h(url('export-json')) ?>">
|
||||
<span class="export-icon">{ }</span><div><p class="eyebrow">Portable planner package</p><h2>Export JSON</h2><small>Financial records plus non-secret configuration</small></div><b>↓</b>
|
||||
</a>
|
||||
<a class="card export-card blue" href="<?= h(url('export-html', ['month' => $month])) ?>">
|
||||
<span class="export-icon"></></span><div><p class="eyebrow"><?= h($month) ?></p><h2>Export HTML</h2><small>Styled, printable monthly report</small></div><b>↓</b>
|
||||
</a>
|
||||
<a class="card export-card pink" href="<?= h(url('export-pdf', ['month' => $month])) ?>">
|
||||
<span class="export-icon">PDF</span><div><p class="eyebrow"><?= h($month) ?></p><h2>Export PDF</h2><small>Compact monthly summary and target progress</small></div><b>↓</b>
|
||||
</a>
|
||||
</section>
|
||||
|
||||
<section class="content-grid data-grid">
|
||||
<article class="card">
|
||||
<div class="card-heading"><div><p class="eyebrow">Bring a planner home</p><h2>Import package</h2></div></div>
|
||||
<form method="post" enctype="multipart/form-data" class="stack-form" data-confirm="Import this planner package? Replacing data cannot be undone without a backup.">
|
||||
<?= csrf_field() ?><input type="hidden" name="action" value="data.import">
|
||||
<label class="file-drop">Planner JSON file<input type="file" name="planner_file" accept="application/json,.json" required><span>Choose a <code>neon-ledger/budget-planner</code> package</span></label>
|
||||
<label class="check-row"><input type="checkbox" name="replace_existing" value="1"><span><strong>Replace existing financial data</strong><small>Otherwise matching record IDs are skipped and new records are merged.</small></span></label>
|
||||
<button class="button primary" type="submit">Import planner data</button>
|
||||
</form>
|
||||
</article>
|
||||
<article class="card">
|
||||
<div class="card-heading"><div><p class="eyebrow">Database mode</p><h2><?= h(strtoupper($databaseDriver)) ?> is active</h2></div><span class="status-badge success">Connected</span></div>
|
||||
<?php if ($databaseDriver === 'sqlite'): ?>
|
||||
<p class="muted">SQLite remains the zero-configuration default. Use this one-time transfer to copy every planner table to an existing MySQL or MariaDB database and switch modes.</p>
|
||||
<form method="post" class="form-grid" data-confirm="Copy all SQLite records and switch this planner to MySQL/MariaDB?">
|
||||
<?= csrf_field() ?><input type="hidden" name="action" value="database.migrate">
|
||||
<label>Host<input type="text" name="host" value="127.0.0.1" required></label>
|
||||
<label>Port<input type="number" name="port" value="3306" required></label>
|
||||
<label>Database<input type="text" name="database" required placeholder="budget_planner"></label>
|
||||
<label>Username<input type="text" name="username" required autocomplete="username"></label>
|
||||
<label class="span-2">Password<input type="password" name="password" autocomplete="current-password"></label>
|
||||
<button class="button primary span-2" type="submit">Copy data and switch database</button>
|
||||
</form>
|
||||
<?php else: ?>
|
||||
<div class="database-facts"><div><span>Host</span><strong><?= h($databaseConfig['mysql']['host'] ?? '') ?></strong></div><div><span>Database</span><strong><?= h($databaseConfig['mysql']['database'] ?? '') ?></strong></div><div><span>Charset</span><strong><?= h($databaseConfig['mysql']['charset'] ?? 'utf8mb4') ?></strong></div></div>
|
||||
<p class="form-note">The original SQLite file remains in storage as a fallback snapshot. Switching back is done by changing <code>config/database.json</code>.</p>
|
||||
<?php endif; ?>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<article class="card portability-note">
|
||||
<span class="metric-icon orange">i</span>
|
||||
<div><h2>Portable by design</h2><p>JSON exports omit administrator credentials, 2FA secrets, and the Mailgun API key. HTML and PDF exports contain only the selected month’s financial summary.</p></div>
|
||||
</article>
|
||||
@@ -0,0 +1,62 @@
|
||||
<section class="dead-hero card glow-blue">
|
||||
<div>
|
||||
<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>
|
||||
</div>
|
||||
<?php if ($hideBalance): ?>
|
||||
<button class="concealed-balance" type="button" data-concealed-balance data-value="<?= h(Support::money($balance, $symbol)) ?>">
|
||||
<span class="masked">••••••</span><span class="revealed"></span><small>Hover, then click to confirm</small>
|
||||
</button>
|
||||
<?php else: ?>
|
||||
<div class="visible-balance"><?= h(Support::money($balance, $symbol)) ?></div>
|
||||
<?php endif; ?>
|
||||
</section>
|
||||
|
||||
<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">
|
||||
<?= 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>
|
||||
<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>
|
||||
</article>
|
||||
<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>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<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>
|
||||
</div>
|
||||
<?php if ($entries === []): ?>
|
||||
<div class="empty-state"><strong>The account is empty</strong><p>Automatic or manual transfers will appear here.</p></div>
|
||||
<?php else: ?>
|
||||
<div class="table-wrap">
|
||||
<table id="dead-table">
|
||||
<thead><tr><th>Date</th><th>Budget month</th><th>Movement</th><th>Source</th><th>Amount</th></tr></thead>
|
||||
<tbody>
|
||||
<?php foreach ($entries as $entry): ?>
|
||||
<tr>
|
||||
<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 class="<?= (float) $entry['amount'] >= 0 ? 'green' : 'orange' ?>"><strong><?= (float) $entry['amount'] >= 0 ? '+' : '' ?><?= h(Support::money($entry['amount'], $symbol)) ?></strong></td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</article>
|
||||
@@ -0,0 +1,88 @@
|
||||
<?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');
|
||||
?>
|
||||
<!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">
|
||||
<title><?= h($title ?? 'Planner') ?> · <?= h($appName) ?></title>
|
||||
<link rel="stylesheet" href="/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>
|
||||
</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-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>
|
||||
</aside>
|
||||
|
||||
<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>
|
||||
<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>
|
||||
<?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>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
|
||||
<div class="page-content"><?= $content ?></div>
|
||||
</main>
|
||||
</div>
|
||||
<script src="/assets/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,12 @@
|
||||
<div class="auth-card">
|
||||
<p class="eyebrow">Administrator access</p>
|
||||
<h2>Welcome back</h2>
|
||||
<p class="muted">Sign in to continue planning.</p>
|
||||
<form method="post" class="stack-form">
|
||||
<?= csrf_field() ?>
|
||||
<input type="hidden" name="action" value="login">
|
||||
<label>Email address<input type="email" name="email" required autocomplete="email"></label>
|
||||
<label>Password<input type="password" name="password" required autocomplete="current-password"></label>
|
||||
<button class="button primary full" type="submit">Sign in</button>
|
||||
</form>
|
||||
</div>
|
||||
@@ -0,0 +1,107 @@
|
||||
<?php $s = $allSettings; ?>
|
||||
<?php if ($newSecret): ?>
|
||||
<div class="setup-secret card glow-green">
|
||||
<div><p class="eyebrow">Authenticator setup</p><h2>Store this key now</h2><p>Add the key manually in an authenticator app. It is only shown once.</p></div>
|
||||
<code><?= h($newSecret) ?></code>
|
||||
<details><summary>Authenticator URI</summary><code class="wrap-code"><?= h($totpUri) ?></code></details>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<form method="post">
|
||||
<?= csrf_field() ?><input type="hidden" name="action" value="settings.save">
|
||||
<section class="settings-grid">
|
||||
<article class="card settings-nav">
|
||||
<p class="eyebrow">Configuration</p>
|
||||
<a href="#general">General</a><a href="#modules">Modules</a><a href="#dormant">Dormant rules</a><a href="#alerts">Alerts & Mailgun</a><a href="#security">Security</a>
|
||||
</article>
|
||||
<div class="stack span-2">
|
||||
<article class="card" id="general">
|
||||
<div class="card-heading"><div><p class="eyebrow">Identity and display</p><h2>General</h2></div></div>
|
||||
<div class="form-grid">
|
||||
<label class="span-2">Planner name<input type="text" name="app.name" value="<?= h($s['app.name']) ?>" required></label>
|
||||
<label>Currency symbol<input type="text" name="app.currency_symbol" value="<?= h($s['app.currency_symbol']) ?>" maxlength="4" required></label>
|
||||
<label>Default theme<select name="theme.default"><option value="dark" <?= $s['theme.default'] === 'dark' ? 'selected' : '' ?>>Dark</option><option value="light" <?= $s['theme.default'] === 'light' ? 'selected' : '' ?>>Light</option></select></label>
|
||||
<label class="span-2">Timezone<input type="text" name="app.timezone" value="<?= h($s['app.timezone']) ?>" placeholder="America/New_York"></label>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article class="card" id="modules">
|
||||
<div class="card-heading"><div><p class="eyebrow">Modular planner</p><h2>Enabled modules</h2></div></div>
|
||||
<div class="toggle-grid">
|
||||
<?php
|
||||
$moduleLabels = [
|
||||
'income' => ['Income tracking', 'Starting points and additional income'],
|
||||
'bills' => ['Budget items', 'Targets, bills, and progress'],
|
||||
'calendar' => ['Bill calendar', 'Due dates and personal events'],
|
||||
'dead_account' => ['Dormant account', 'Withholding and restorations'],
|
||||
'comparisons' => ['Comparisons', 'Monthly and yearly analysis'],
|
||||
'alerts' => ['Email alerts', 'Mailgun bill reminders'],
|
||||
'import_export' => ['Data portability', 'JSON, HTML, and PDF'],
|
||||
];
|
||||
foreach ($moduleLabels as $key => [$label, $description]):
|
||||
?>
|
||||
<label class="toggle-card"><span><strong><?= h($label) ?></strong><small><?= h($description) ?></small></span><input type="checkbox" name="modules_<?= h($key) ?>" value="1" <?= !empty($s['modules.' . $key]) ? 'checked' : '' ?>><i></i></label>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article class="card" id="dormant">
|
||||
<div class="card-heading"><div><p class="eyebrow">Automated set-aside</p><h2>Dormant account rules</h2></div></div>
|
||||
<div class="form-grid">
|
||||
<label>Income rule<select name="dead.income_mode"><option value="percent" <?= $s['dead.income_mode'] === 'percent' ? 'selected' : '' ?>>Percentage</option><option value="fixed" <?= $s['dead.income_mode'] === 'fixed' ? 'selected' : '' ?>>Direct amount</option></select></label>
|
||||
<label>Income value<input type="number" name="dead.income_value" min="0" step="0.01" value="<?= h((string) $s['dead.income_value']) ?>"></label>
|
||||
<label>Per-transaction rule<select name="dead.transaction_mode"><option value="percent" <?= $s['dead.transaction_mode'] === 'percent' ? 'selected' : '' ?>>Percentage</option><option value="fixed" <?= $s['dead.transaction_mode'] === 'fixed' ? 'selected' : '' ?>>Direct amount</option></select></label>
|
||||
<label>Per-transaction value<input type="number" name="dead.transaction_value" min="0" step="0.01" value="<?= h((string) $s['dead.transaction_value']) ?>"></label>
|
||||
<label class="check-row span-2"><input type="checkbox" name="dead_hide_balance" value="1" <?= $s['dead.hide_balance'] ? 'checked' : '' ?>><span><strong>Conceal dormant balance</strong><small>Require a hover and confirmation click to reveal it.</small></span></label>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article class="card" id="alerts">
|
||||
<div class="card-heading"><div><p class="eyebrow">Upcoming bills</p><h2>Email alerts via Mailgun</h2></div><span class="status-badge <?= $mailgunConfigured ? 'success' : 'warning' ?>"><?= $mailgunConfigured ? 'Configured' : 'Needs setup' ?></span></div>
|
||||
<div class="form-grid">
|
||||
<label class="span-2">Alert recipient<input type="email" name="alerts.email" value="<?= h($s['alerts.email']) ?>" placeholder="alerts@example.com"></label>
|
||||
<label>Alert windows (days)<input type="text" name="alerts.windows" value="<?= h(implode(', ', (array) $s['alerts.windows'])) ?>" placeholder="7, 3, 1"></label>
|
||||
<label>Reminder times<input type="text" name="alerts.times" value="<?= h(implode(', ', (array) $s['alerts.times'])) ?>" placeholder="09:00, 17:30"></label>
|
||||
<label>Mailgun domain<input type="text" name="mailgun.domain" value="<?= h($s['mailgun.domain']) ?>" placeholder="mg.example.com"></label>
|
||||
<label>Mailgun region<select name="mailgun.region"><option value="us" <?= $s['mailgun.region'] === 'us' ? 'selected' : '' ?>>United States</option><option value="eu" <?= $s['mailgun.region'] === 'eu' ? 'selected' : '' ?>>European Union</option></select></label>
|
||||
<label class="span-2">From address<input type="text" name="mailgun.from" value="<?= h($s['mailgun.from']) ?>"></label>
|
||||
<label class="span-2">Mailgun API key<input type="password" name="mailgun.api_key" placeholder="<?= $s['mailgun.api_key'] ? 'Stored — enter a value to replace it' : 'key-…' ?>" autocomplete="off"></label>
|
||||
<?php if ($s['mailgun.api_key']): ?><label class="check-row span-2 slim"><input type="checkbox" name="clear_mailgun_key" value="1"><span>Clear the stored API key</span></label><?php endif; ?>
|
||||
</div>
|
||||
<p class="form-note">Run <code>php scripts/send-reminders.php</code> every minute with cron; it only sends at configured times.</p>
|
||||
</article>
|
||||
|
||||
<article class="card" id="security">
|
||||
<div class="card-heading"><div><p class="eyebrow">Administrator protection</p><h2>Security</h2></div></div>
|
||||
<label class="check-row"><input type="checkbox" name="security_2fa_required" value="1" <?= $s['security.2fa_required'] ? 'checked' : '' ?>><span><strong>Require two-factor authentication</strong><small>Uses any standard TOTP authenticator app.</small></span></label>
|
||||
<p class="form-note">Administrator: <?= h($user['email']) ?> · account created <?= h(date('M j, Y', strtotime($user['created_at']))) ?></p>
|
||||
</article>
|
||||
|
||||
<button class="button primary sticky-save" type="submit">Save all settings</button>
|
||||
</div>
|
||||
</section>
|
||||
</form>
|
||||
|
||||
<section class="settings-actions">
|
||||
<form method="post">
|
||||
<?= csrf_field() ?><input type="hidden" name="action" value="2fa.regenerate">
|
||||
<button class="button secondary" type="submit">Generate new 2FA key</button>
|
||||
</form>
|
||||
<?php if ($settings->module('alerts')): ?>
|
||||
<form method="post" data-confirm="Run the reminder scan now? Configured emails may be sent through Mailgun.">
|
||||
<?= csrf_field() ?><input type="hidden" name="action" value="alerts.run">
|
||||
<button class="button secondary" type="submit">Run reminders now</button>
|
||||
</form>
|
||||
<?php endif; ?>
|
||||
</section>
|
||||
|
||||
<article class="card">
|
||||
<div class="card-heading"><div><p class="eyebrow">Organize spending</p><h2>Add an expense category</h2></div></div>
|
||||
<form method="post" class="inline-fields">
|
||||
<?= csrf_field() ?><input type="hidden" name="action" value="category.add">
|
||||
<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>
|
||||
</article>
|
||||
@@ -0,0 +1,14 @@
|
||||
<div class="auth-card">
|
||||
<p class="eyebrow">First run</p>
|
||||
<h2>Create the administrator</h2>
|
||||
<p class="muted">This account controls the planner, its modules, exports, and alert settings.</p>
|
||||
<form method="post" class="stack-form">
|
||||
<?= csrf_field() ?>
|
||||
<input type="hidden" name="action" value="setup">
|
||||
<label>Email address<input type="email" name="email" required autocomplete="email" placeholder="you@example.com"></label>
|
||||
<label>Password<input type="password" name="password" required minlength="10" autocomplete="new-password" placeholder="At least 10 characters"></label>
|
||||
<label>Confirm password<input type="password" name="password_confirmation" required minlength="10" autocomplete="new-password"></label>
|
||||
<label class="check-row"><input type="checkbox" name="enable_2fa" value="1"><span><strong>Enable optional 2FA now</strong><small>You will receive a setup key after account creation.</small></span></label>
|
||||
<button class="button primary full" type="submit">Create planner</button>
|
||||
</form>
|
||||
</div>
|
||||
@@ -0,0 +1,56 @@
|
||||
<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>
|
||||
|
||||
<article class="card">
|
||||
<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>
|
||||
<?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>
|
||||
<?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>
|
||||
<tbody>
|
||||
<?php foreach ($transactions as $transaction): ?>
|
||||
<tr>
|
||||
<td data-label="Date"><?= h(date('M j, Y', strtotime($transaction['transacted_on']))) ?></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="Dormant pull"><?= h(Support::money($transaction['dead_pull_amount'], $symbol)) ?></td>
|
||||
<td>
|
||||
<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>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
<?php endforeach; ?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</article>
|
||||
|
||||
<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>
|
||||
</form>
|
||||
</dialog>
|
||||
@@ -0,0 +1,11 @@
|
||||
<div class="auth-card">
|
||||
<p class="eyebrow">Second step</p>
|
||||
<h2>Authentication code</h2>
|
||||
<p class="muted">Enter the current six-digit code from your authenticator.</p>
|
||||
<form method="post" class="stack-form">
|
||||
<?= csrf_field() ?>
|
||||
<input type="hidden" name="action" value="verify_2fa">
|
||||
<label>Six-digit code<input class="otp-input" type="text" name="code" required inputmode="numeric" pattern="[0-9]{6}" maxlength="6" autocomplete="one-time-code" autofocus></label>
|
||||
<button class="button primary full" type="submit">Verify and continue</button>
|
||||
</form>
|
||||
</div>
|
||||
Reference in New Issue
Block a user