diff --git a/src/ui/components/PurchaseDialog.tsx b/src/ui/components/PurchaseDialog.tsx index 4e3d8e2..ff5b76c 100644 --- a/src/ui/components/PurchaseDialog.tsx +++ b/src/ui/components/PurchaseDialog.tsx @@ -37,7 +37,12 @@ import { cancelScheduleFrom, updatePurchase, } from "../../store/actions.ts"; -import { offerableCosts, seedCostFromDraft, sourceFor } from "../recurringForm.ts"; +import { + newCostOptionIsSelectable, + offerableCosts, + seedCostFromDraft, + sourceFor, +} from "../recurringForm.ts"; import { WEEKDAYS } from "../routes/settings/RecurringSection.tsx"; import type { Anchoring, MonthId, Purchase, Recurrence } from "../../domain/types.ts"; @@ -280,7 +285,21 @@ export function PurchaseDialog({ monthId, purchase, trigger }: Props) { }} /> setDraft((d) => ({ ...d, date: d.date.slice(0, 7) }))} + onClick={() => { + const monthOnly = draft.date.slice(0, 7); + setDraft((d) => ({ ...d, date: monthOnly })); + // Dropping to month-only can strand a "+ New recurring + // cost…" selection that needed the day just removed + // (e.g. "days", or "months" anchored to the last + // charge) — its option would show `disabled` below but + // stay selected, so reset to the one pairing that is + // always valid rather than leave state pointing at a + // choice the owner can no longer make. + if (!newCostOptionIsSelectable(newRecurrence.kind, newAnchoring, monthOnly)) { + setNewRecurrence((r) => ({ kind: "everyNMonths", n: r.n })); + setNewAnchoring("calendar"); + } + }} > use the month only @@ -425,9 +444,26 @@ export function PurchaseDialog({ monthId, purchase, trigger }: Props) { ); }} > - - - + + + {newRecurrence.kind === "everyNWeeks" && ( @@ -463,11 +499,30 @@ export function PurchaseDialog({ monthId, purchase, trigger }: Props) { setNewAnchoring(anchoring); }} > - - + + + {draft.date.length <= 7 && ( +

+ Days, weeks, and "the last charge" need an exact date — use + "set exact date" next to the date above, then pick the unit + or anchoring you want. +

+ )}

This purchase becomes its first charge. Anchored to the last charge, its cycle restarts from today rather than from a fixed diff --git a/src/ui/recurringForm.test.ts b/src/ui/recurringForm.test.ts index be4a05e..c6042c5 100644 --- a/src/ui/recurringForm.test.ts +++ b/src/ui/recurringForm.test.ts @@ -1,5 +1,10 @@ import { describe, expect, test } from "bun:test"; -import { offerableCosts, seedCostFromDraft, sourceFor } from "./recurringForm.ts"; +import { + newCostOptionIsSelectable, + offerableCosts, + seedCostFromDraft, + sourceFor, +} from "./recurringForm.ts"; import { createSeedDataset } from "../domain/seed.ts"; import * as actions from "../store/actions.ts"; import type { Dataset } from "../domain/types.ts"; @@ -100,6 +105,40 @@ describe("sourceFor", () => { }); }); +// I2 fix: a month-only purchase date can only ever seed "everyNMonths" + +// "calendar" (rent occurs IN a month). Every other pairing hands the date +// straight to day-precise arithmetic and would throw a store-level error +// naming a field ("startDate") the purchase dialog never shows — so the +// dialog must not OFFER those options while the date is month-only, rather +// than reword the throw. This predicate is what decides which options are +// offered; it is tested directly, since a DOM test cannot exercise a +// predicate the way it can a rendered