fix(recurring): refuse a confirmation date that stops the series
A confirmation is an ordinary Purchase, and the ordinary purchase dialog can edit its date. Under lastCharge anchoring that date is what the walk measures the next occurrence from, so moving it back more than a cycle made the walk step backwards and throw out of foldBalances -- every month route, the year view and the summary down at once, from an edit that never mentioned recurring costs. updatePurchase is the write that can invalidate an already-valid confirmation, so it now asks wouldAdvancePast -- the same helper ExpectedBand asks before offering a row -- and refuses the edit instead. The walk's throw is unchanged: a step that lands backwards really is bad data, and this stops it being created. A purchase with no source is not checked at all, and a source naming a cost that no longer exists falls through. PurchaseDialog was closing on save regardless of the outcome, with its useMutate error rendered nowhere, so a refusal looked like a save that did nothing. mutate now reports whether the write landed, and the dialog stays open and shows the reason when it did not. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
92e6b763f2
commit
0c9df88a45
4 files changed
+188
-21
No files matched your search
@@ -43,7 +43,7 @@ interface Props {
|
||||
|
||||
export function PurchaseDialog({ monthId, purchase, trigger }: Props) {
|
||||
const dataset = useDataset();
|
||||
const { mutate } = useMutate();
|
||||
const { mutate, error, clearError } = useMutate();
|
||||
const activePosts = dataset.posts
|
||||
.filter((p) => !p.archived)
|
||||
.sort((a, b) => a.order - b.order);
|
||||
@@ -74,23 +74,25 @@ export function PurchaseDialog({ monthId, purchase, trigger }: Props) {
|
||||
setBulk(emptyBulkDraft(monthId, activePosts[0]?.id ?? ""));
|
||||
}
|
||||
|
||||
// A write can be REFUSED, not merely fail on a full disk: editing the date
|
||||
// of a purchase that confirms a recurring occurrence is rejected when the
|
||||
// new date would stop the series advancing. Closing the dialog regardless
|
||||
// would show the user a save that silently did nothing, so the dialog stays
|
||||
// open and shows `error` instead. `mutate` never rejects.
|
||||
function save() {
|
||||
if (errors.length > 0) return;
|
||||
if (bulkMode) {
|
||||
// One mutate for the whole batch: a single write, one queue entry, and no
|
||||
// way to half-commit a shopping trip.
|
||||
const created = toPurchases(bulk);
|
||||
mutate((data) => {
|
||||
for (const p of created) addPurchase(data, p);
|
||||
});
|
||||
} else {
|
||||
mutate((data) => {
|
||||
if (purchase) updatePurchase(data, purchase.id, toPurchase(draft));
|
||||
else addPurchase(data, toPurchase(draft));
|
||||
});
|
||||
}
|
||||
setOpen(false);
|
||||
if (!purchase) reset();
|
||||
// One mutate for the whole batch: a single write, one queue entry, and no
|
||||
// way to half-commit a shopping trip.
|
||||
const created = bulkMode ? toPurchases(bulk) : null;
|
||||
void mutate((data) => {
|
||||
if (created) for (const p of created) addPurchase(data, p);
|
||||
else if (purchase) updatePurchase(data, purchase.id, toPurchase(draft));
|
||||
else addPurchase(data, toPurchase(draft));
|
||||
}).then((saved) => {
|
||||
if (!saved) return;
|
||||
setOpen(false);
|
||||
if (!purchase) reset();
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -98,7 +100,10 @@ export function PurchaseDialog({ monthId, purchase, trigger }: Props) {
|
||||
open={open}
|
||||
onOpenChange={(next) => {
|
||||
setOpen(next);
|
||||
if (next) reset();
|
||||
if (next) {
|
||||
reset();
|
||||
clearError();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DialogTrigger asChild>{trigger}</DialogTrigger>
|
||||
@@ -321,6 +326,12 @@ export function PurchaseDialog({ monthId, purchase, trigger }: Props) {
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<p role="alert" className="text-sm text-destructive">
|
||||
Could not save: {error}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
|
||||
Reference in new issue
Block a user