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

123 lines
10 KiB
PHP

<?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>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"><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>