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.
This commit is contained in:
Maurice
2026-09-12 13:25:14 +02:00
parent 9307025ed0
commit a066c94ec5
2 changed files with 20 additions and 3 deletions
@@ -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<CostsSettlementResponse | null>(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 */}
<div className="mt-2 rounded-2xl border border-[color:var(--m-rowbr)] bg-m-card p-[13px]">
<div className="font-geist text-[0.625rem] font-bold uppercase tracking-[.09em] text-m-faint">{t('costs.finalBudget')}</div>
{tripMembers.map(p => {
{settlementError ? (
<p className="py-[14px] text-center font-geist text-[0.71875rem] text-m-muted">{t('common.unknownError')}</p>
) : tripMembers.map(p => {
const row = finalBudgetFor(settlement?.finalBudgets || [], p)
const open = expandedFinalId === p.id
return (
@@ -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(<MCostsTab planner={planner()} shell={buildShell()} />)
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' }