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
+578 -339
View File
File diff suppressed because it is too large Load Diff
+28 -4
View File
@@ -13,9 +13,11 @@
document.querySelectorAll('[data-trend-chart]').forEach(drawTrendChart);
});
document.querySelector('[data-menu-toggle]')?.addEventListener('click', () => {
document.querySelector('#sidebar')?.classList.toggle('open');
});
const sidebar = document.querySelector('#sidebar');
const closeMenu = () => sidebar?.classList.remove('open');
document.querySelector('[data-menu-toggle]')?.addEventListener('click', () => sidebar?.classList.toggle('open'));
document.querySelector('[data-menu-close]')?.addEventListener('click', closeMenu);
sidebar?.querySelectorAll('a').forEach((link) => link.addEventListener('click', closeMenu));
document.querySelector('[data-month-picker]')?.addEventListener('change', (event) => {
const picker = event.currentTarget;
@@ -34,12 +36,19 @@
if (event.target === dialog) dialog.close();
});
});
const requestedDialog = new URLSearchParams(window.location.search).get('open');
if (requestedDialog) document.getElementById(requestedDialog)?.showModal();
document.querySelectorAll('[data-confirm]').forEach((form) => {
form.addEventListener('submit', (event) => {
if (!window.confirm(form.dataset.confirm)) event.preventDefault();
});
});
document.querySelectorAll('[data-confirm-button]').forEach((button) => {
button.addEventListener('click', (event) => {
if (!window.confirm(button.dataset.confirmButton)) event.preventDefault();
});
});
document.querySelectorAll('.flash button').forEach((button) => {
button.addEventListener('click', () => button.closest('.flash')?.remove());
@@ -71,6 +80,21 @@
});
}
document.querySelectorAll('[data-dead-transfer-form]').forEach((form) => {
const direction = form.querySelector('[name="direction"]');
const restoreFields = form.querySelector('[data-restore-fields]');
const restoreType = form.querySelector('[data-restore-type]');
const restoreItem = form.querySelector('[data-restore-item]');
const syncRestoreFields = () => {
const restoring = direction?.value === 'restore';
if (restoreFields) restoreFields.hidden = !restoring;
if (restoreItem) restoreItem.hidden = !restoring || restoreType?.value !== 'item';
};
direction?.addEventListener('change', syncRestoreFields);
restoreType?.addEventListener('change', syncRestoreFields);
syncRestoreFields();
});
function drawTrendChart(canvas) {
let data;
try {
@@ -91,7 +115,7 @@
const styles = getComputedStyle(document.documentElement);
const text = styles.getPropertyValue('--muted').trim();
const border = styles.getPropertyValue('--border-strong').trim();
const purple = styles.getPropertyValue('--purple-bright').trim();
const purple = styles.getPropertyValue('--purple-light').trim();
const pink = styles.getPropertyValue('--pink').trim();
const padding = { top: 20, right: 12, bottom: 32, left: 48 };
const plotWidth = width - padding.left - padding.right;
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB