Commit Graph
9 Commits
Author SHA1 Message Date
Lucas Winther 725c836788 feat: organize posts and bills in settings 2026-09-30 00:35:45 +02:00
Lucas Winther 6de115843b feat: choose recurring bill destinations 2026-09-29 20:49:18 +02:00
Lucas Winther c34aa846b9 feat: add budget folder schema and migration 2026-09-29 20:25:47 +02:00
Lucas Winther aed2313167 fix(ui): harden summary and settings workflows 2026-09-29 19:41:58 +02:00
Lucas Winther b276573ffe feat(recurring): datable cancellation via an "Ends" column
Gap 2 (spec §10): endedFrom is typed as any IsoDate and the spec calls it
"cancellation", but Settings only ever wrote currentMonth — backdating or
forward-dating a cancellation was import-only.

Add an "Ends" column beside "Starts" in the Recurring costs table, edited and
validated the same way (isValidEndedFrom — no granularity rule, per §10, but
still refuses a calendar-impossible date like "2026-02-30"). Empty means "not
ended" and clears the cancellation.

Coherence rule: `archived` mirrors "endedFrom is set". setRecurringCostEndedFrom
is the one place that now enforces it — endRecurringCost and
restoreRecurringCost (the one-click row buttons) delegate to it, so the column
and the buttons can never leave a bill dimmed-but-live or ended-but-undimmed.

Also closes the same calendar-impossible gap in the JSON importer, which
checked endedFrom's shape but not its calendar validity, unlike startDate.
2026-09-04 08:46:57 +02:00
Lucas WintherandClaude Opus 5 042bc8597d fix(recurring): reject calendar-impossible startDate, not just wrongly-shaped (C1 residual)
requireStartDateGranularity (store/actions.ts) and its import-path twin
(export/json.ts) checked date SHAPE with a regex, not date VALIDITY, so
"2026-09-31" (September has 30 days) passed both and threw later out of
foldBalances instead of at the write boundary. isValidStartDate
(RecurringSection.tsx) had the same hole for a day-shaped value under
everyNMonths. Separately, monthOf's regex (domain/months.ts) was
unanchored at the end, so "2026-091" was silently read as September 2026.

Route day-shaped values through toDayOrdinal and month-shaped values
through monthOf (now anchored to accept only "YYYY-MM" or "YYYY-MM-DD" in
full, never a prefix) at all three write boundaries, converting whatever
they throw into each file's own error style.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-04 08:16:03 +02:00
Lucas Winther eb13f98610 fix(ui): normalise startDate on a kind switch, validate the Starts field (C1 UI, C2)
The unit dropdown could rewrite a cost's recurrence kind to a day-granular
one without touching its month-only startDate, committing exactly the
pairing the store now refuses. It now expands startDate to the first of
the month in the SAME mutate call that changes the kind.

The Starts field itself was free text committed on every keystroke,
including an emptied field ("" reaches monthOf() and throws with the same
blast radius). It is now an uncontrolled input that only commits on blur,
and only when the text parses as an IsoDate of the granularity the cost's
recurrence kind requires; an invalid edit is discarded rather than stored.

isValidStartDate and normalizedStartDateFor are exported and covered
directly in RecurringSection.test.ts, since this is what stands between
this field and a repeat of C1/C2.
2026-09-04 07:57:37 +02:00
Lucas WintherandClaude Opus 5 824e94a9bd feat(ui): surface the projected balance and fix interval clipping
PostTable gained a Projected column (both the sm: table and the
mobile block markup) so the fold's second balance — what is left
after unconfirmed recurring bills — is finally visible, muted so
Remaining stays the primary figure. The legend now explains it.

RecurringSection's interval input widened from w-14 to w-20 so
two-digit values like 28 and 30 no longer collide with the native
number spinner.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-03 18:52:25 +02:00
Lucas Winther 72471914ba feat(settings): define recurring costs
Follows PostsSection: same table, same xs ghost row actions, same add row.
Definition is the rare act, so it lives here rather than in the month view.

Placed after Posts because a cost's splits reference posts, and before the
data section so the destructive import controls stay last.

'end' sets endedFrom to the current month and archives in one action, which is
what the owner means by cancelling a subscription. Past occurrences are
untouched, so no historical figure moves.
2026-09-03 18:30:22 +02:00