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