From a066c94ec5333807c5cc13aae603a47f5a47e7fb Mon Sep 17 00:00:00 2001 From: Maurice Date: Sat, 12 Sep 2026 13:25:14 +0200 Subject: [PATCH] fix(costs): tell the phone when the settlement did not load The mobile Costs tab swallowed a failed settlement request, and the final budget card then filled every traveler's row from the empty fallback: "0.00" for everyone, which reads as "the trip cost nobody anything". The desktop panel already keeps a settlementError flag and prints its notice instead of a number; the mobile tab now does the same in the final budget card. --- client/src/mobile/screens/trip/tabs/MCostsTab.tsx | 11 +++++++++-- client/tests/unit/mobile/trip/MCostsTab.test.tsx | 12 +++++++++++- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/client/src/mobile/screens/trip/tabs/MCostsTab.tsx b/client/src/mobile/screens/trip/tabs/MCostsTab.tsx index c11fbcd85..dd28c4d11 100644 --- a/client/src/mobile/screens/trip/tabs/MCostsTab.tsx +++ b/client/src/mobile/screens/trip/tabs/MCostsTab.tsx @@ -58,8 +58,13 @@ export default function MCostsTab({ planner, shell }: MTabScreenProps) { const ctx: CostsCtx = useMemo(() => ({ me, tripCurrency, convert }), [me, tripCurrency, convert]) const [settlement, setSettlement] = useState(null) + // A failed settlement read leaves `settlement` null, and the final budget would + // read that as "the trip cost nobody anything", a claim we cannot make. + const [settlementError, setSettlementError] = useState(false) const loadSettlement = useCallback(() => { - budgetApi.settlement(tripId, base).then(setSettlement).catch(() => {}) + budgetApi.settlement(tripId, base) + .then(s => { setSettlement(s); setSettlementError(false) }) + .catch(() => setSettlementError(true)) }, [tripId, base]) // Mirrors CostsPanel.tsx: items reload on trip change, settlement reloads on @@ -312,7 +317,9 @@ export default function MCostsTab({ planner, shell }: MTabScreenProps) { {/* Final budget — what the trip costs each traveler; the arithmetic opens on tap */}
{t('costs.finalBudget')}
- {tripMembers.map(p => { + {settlementError ? ( +

{t('common.unknownError')}

+ ) : tripMembers.map(p => { const row = finalBudgetFor(settlement?.finalBudgets || [], p) const open = expandedFinalId === p.id return ( diff --git a/client/tests/unit/mobile/trip/MCostsTab.test.tsx b/client/tests/unit/mobile/trip/MCostsTab.test.tsx index 7e6b65c90..6c6d11d3d 100644 --- a/client/tests/unit/mobile/trip/MCostsTab.test.tsx +++ b/client/tests/unit/mobile/trip/MCostsTab.test.tsx @@ -14,7 +14,7 @@ import { resetAllStores, seedStore } from '../../../helpers/store' import { server } from '../../../helpers/msw/server' import { fireEvent, render, screen, waitFor, within } from '../../../helpers/render' -// FE-MOB-COSTT-001 to FE-MOB-COSTT-043 +// FE-MOB-COSTT-001 to FE-MOB-COSTT-044 // The add/edit expense sheet is the shared desktop-sized form; the panel only // owns when it opens and what happens on save, so it is stubbed here. @@ -646,6 +646,16 @@ describe('MCostsTab', () => { expect(within(card).queryByText('+$58.00')).not.toBeInTheDocument() }) + it('FE-MOB-COSTT-044: a settlement read that fails says so instead of costing everyone nothing', async () => { + serveSettlement(SETTLEMENT, true) + render() + + const card = up(await screen.findByText('costs.finalBudget'), 1) + await waitFor(() => expect(within(card).getByText('common.unknownError')).toBeInTheDocument()) + expect(within(card).queryByText('$0.00')).not.toBeInTheDocument() + expect(within(card).queryByRole('button')).not.toBeInTheDocument() + }) + it('FE-MOB-COSTT-040: the expense filters apply to payments the same way they do on desktop', async () => { const mine = { id: 503, from_user_id: 1, to_user_id: 2, amount: 8, currency: 'USD', created_at: '2026-05-02T09:00:00Z' } const others = { id: 504, from_user_id: 2, to_user_id: 3, amount: 9, currency: 'USD', created_at: '2026-05-02T09:00:00Z' }