diff --git a/src/domain/views.test.ts b/src/domain/views.test.ts index b6ac8da..f5e855e 100644 --- a/src/domain/views.test.ts +++ b/src/domain/views.test.ts @@ -181,6 +181,15 @@ describe("yearView", () => { }); describe("summaryView", () => { + test("difference follows the dataset's decimal setting", () => { + const precise: Dataset = { + ...data, + settings: { ...data.settings, digits: 3 }, + months: [{ id: "2026-01", income: { amount: 1.234, currency: "DKK" }, ruleOverrides: {} }], + purchases: [spend("small", "food", 0.111, "2026-01-10")], + }; + expect(summaryView(precise, "2026-01", "2026-01").difference).toBe(1.123); + }); test("totals charges by post and by month across a range", () => { const view = summaryView(data, "2026-01", "2026-02"); expect(view.totalCharges).toBe(800); diff --git a/src/domain/views.ts b/src/domain/views.ts index d5f3c21..01145d8 100644 --- a/src/domain/views.ts +++ b/src/domain/views.ts @@ -149,6 +149,7 @@ export interface SummaryViewModel { byMonth: { monthId: MonthId; charges: number }[]; totalCharges: number; totalIncome: number; + difference: number; } export function summaryView( @@ -181,19 +182,23 @@ export function summaryView( ), })); + const totalCharges = roundMoney( + byMonth.reduce((sum, m) => sum + m.charges, 0), + baseDigits, + ); + const totalIncome = roundMoney( + months.reduce((sum, m) => sum + incomeFor(dataset, m), 0), + baseDigits, + ); + return { from, to, byPost, byMonth, - totalCharges: roundMoney( - byMonth.reduce((sum, m) => sum + m.charges, 0), - baseDigits, - ), - totalIncome: roundMoney( - months.reduce((sum, m) => sum + incomeFor(dataset, m), 0), - baseDigits, - ), + totalCharges, + totalIncome, + difference: roundMoney(totalIncome - totalCharges, baseDigits), }; } diff --git a/src/ui/components/ExpectedBand.tsx b/src/ui/components/ExpectedBand.tsx index ee7ba8f..47824cb 100644 --- a/src/ui/components/ExpectedBand.tsx +++ b/src/ui/components/ExpectedBand.tsx @@ -174,7 +174,7 @@ export function expectedGroups( */ export function ExpectedBand({ monthId }: { monthId: MonthId }) { const dataset = useDataset(); - const { mutate } = useMutate(); + const { mutate, error } = useMutate(); const [amounts, setAmounts] = useState>({}); const { pending, overdue, comingUp } = useMemo( @@ -239,7 +239,7 @@ export function ExpectedBand({ monthId }: { monthId: MonthId }) { size="sm" variant="outline" disabled={invalid} - onClick={() => { + onClick={async () => { // Captured before mutate, per AGENTS.md: mutate defers behind // the write queue and React resets the input first, so reading // in the callback commits stale text. `invalid` already guards @@ -247,12 +247,13 @@ export function ExpectedBand({ monthId }: { monthId: MonthId }) { // `parsed` is either a real amount or the field was never // touched. if (invalid) return; - mutate((draft) => + const saved = await mutate((draft) => confirmOccurrence(draft, occurrence.recurringId, occurrence.date, { date: recordedDate ?? undefined, amount: parsed ?? undefined, }), ); + if (!saved) return; setAmounts((current) => { const next = { ...current }; delete next[key]; @@ -272,6 +273,7 @@ export function ExpectedBand({ monthId }: { monthId: MonthId }) { title="Expected" hint="Committed but not yet spent. These do not touch a post's balance until you confirm one, which records it as an ordinary purchase you can edit or delete afterwards." > + {error &&

Could not confirm: {error}

} {pending.length > 0 && (