6686388834
Account creation and guarded onboarding flow. Current-cash starting balance with automatic rollover enabled. Dynamic bill generation, shared/custom categories, recurring targets, and optional same-day onboarding transactions. Optional dormant reserve and additional categories. Mailgun email or authenticator-app 2FA configuration. Responsive dark/light UI with no overflow at 390px. Fixed administrator session-ID creation bug. Core implementation: [OnboardingService.php (line 59)](/Users/tyemeclifford/Documents/GH/budget/app/OnboardingService.php:59), [onboarding.php (line 19)](/Users/tyemeclifford/Documents/GH/budget/views/onboarding.php:19), and [index.php (line 143)](/Users/tyemeclifford/Documents/GH/budget/public/index.php:143). Verification passed: financial self-test, onboarding integration test, full browser walkthrough, mobile layout, routing guards, and zero browser console errors. Live email delivery requires actual Mailgun credentials and was not sent during testing.
133 lines
11 KiB
PHP
133 lines
11 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>
|
|
<div class="form-grid">
|
|
<label>Two-factor method
|
|
<select name="security_2fa_method">
|
|
<option value="totp" <?= $s['security.2fa_method'] === 'totp' ? 'selected' : '' ?>>Authenticator app</option>
|
|
<option value="email" <?= $s['security.2fa_method'] === 'email' ? 'selected' : '' ?>>Email through Mailgun</option>
|
|
</select>
|
|
</label>
|
|
<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>Email delivery requires complete Mailgun settings above.</small></span></label>
|
|
</div>
|
|
<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">
|
|
<?php if (($s['security.2fa_method'] ?? 'totp') === 'totp'): ?>
|
|
<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 endif; ?>
|
|
<?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>
|