fix(purchase): disable recurring options a month-only date would reject
Review finding I2: choosing "+ New recurring cost..." with unit "days" (or
"months" anchored to the last charge) while the purchase's date is month-only
threw a store-level error naming a field ("startDate") the purchase dialog
never shows, with no mention of the "set exact date" toggle that fixes it.
Per the controller ruling, this stops OFFERING the action instead of
rewording the throw: newCostOptionIsSelectable (src/ui/recurringForm.ts)
mirrors the store's own needsDay rule and drives `disabled` on each unit and
anchoring <option>, with a hint pointing at "set exact date". Switching to
month-only also resets a stranded selection (e.g. "days") back to the one
pairing that always works, so a previously valid choice never sits selected
in a now-disabled state.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
87f309a32c
commit
04a089eb06
@@ -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) {
|
||||
}}
|
||||
/>
|
||||
<DateModeToggle
|
||||
onClick={() => 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
|
||||
</DateModeToggle>
|
||||
@@ -425,9 +444,26 @@ export function PurchaseDialog({ monthId, purchase, trigger }: Props) {
|
||||
);
|
||||
}}
|
||||
>
|
||||
<option value="everyNDays">days</option>
|
||||
<option value="everyNWeeks">weeks</option>
|
||||
<option value="everyNMonths">months</option>
|
||||
<option
|
||||
value="everyNDays"
|
||||
disabled={!newCostOptionIsSelectable("everyNDays", newAnchoring, draft.date)}
|
||||
>
|
||||
days
|
||||
</option>
|
||||
<option
|
||||
value="everyNWeeks"
|
||||
disabled={!newCostOptionIsSelectable("everyNWeeks", newAnchoring, draft.date)}
|
||||
>
|
||||
weeks
|
||||
</option>
|
||||
<option
|
||||
value="everyNMonths"
|
||||
disabled={
|
||||
!newCostOptionIsSelectable("everyNMonths", newAnchoring, draft.date)
|
||||
}
|
||||
>
|
||||
months
|
||||
</option>
|
||||
</NativeSelect>
|
||||
</div>
|
||||
{newRecurrence.kind === "everyNWeeks" && (
|
||||
@@ -463,11 +499,30 @@ export function PurchaseDialog({ monthId, purchase, trigger }: Props) {
|
||||
setNewAnchoring(anchoring);
|
||||
}}
|
||||
>
|
||||
<option value="calendar">the calendar</option>
|
||||
<option value="lastCharge">the last charge</option>
|
||||
<option
|
||||
value="calendar"
|
||||
disabled={!newCostOptionIsSelectable(newRecurrence.kind, "calendar", draft.date)}
|
||||
>
|
||||
the calendar
|
||||
</option>
|
||||
<option
|
||||
value="lastCharge"
|
||||
disabled={
|
||||
!newCostOptionIsSelectable(newRecurrence.kind, "lastCharge", draft.date)
|
||||
}
|
||||
>
|
||||
the last charge
|
||||
</option>
|
||||
</NativeSelect>
|
||||
</div>
|
||||
</div>
|
||||
{draft.date.length <= 7 && (
|
||||
<p className="text-xs text-budget-ink-muted">
|
||||
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.
|
||||
</p>
|
||||
)}
|
||||
<p className="text-xs text-budget-ink-muted">
|
||||
This purchase becomes its first charge. Anchored to the last
|
||||
charge, its cycle restarts from today rather than from a fixed
|
||||
|
||||
Reference in New Issue
Block a user