diff --git a/client/src/components/Budget/CostsPanel.test.tsx b/client/src/components/Budget/CostsPanel.test.tsx index 1d3fe503..2fddc8d2 100644 --- a/client/src/components/Budget/CostsPanel.test.tsx +++ b/client/src/components/Budget/CostsPanel.test.tsx @@ -107,7 +107,7 @@ describe('CostsPanel — settlements in the ledger', () => { await user.click(await screen.findByRole('button', { name: 'Add expense' })) await user.type(await screen.findByPlaceholderText('e.g. Dinner, souvenirs, gas…'), 'Dinner') - const nums = () => screen.getAllByRole('spinbutton') as HTMLInputElement[] + const nums = () => screen.getAllByPlaceholderText('0.00') as HTMLInputElement[] await user.type(nums()[0], '100') // total → auto equal-split across the 2 participants await waitFor(() => expect(nums()[1].value).toBe('50')) expect(nums()[2].value).toBe('50') @@ -125,6 +125,30 @@ describe('CostsPanel — settlements in the ledger', () => { ])) }) + it('accepts a comma as the decimal separator in the total amount (#1256)', async () => { + let posted: Record | null = null + server.use( + http.get('/api/trips/1/budget', () => HttpResponse.json({ items: [] })), + http.get('/api/trips/1/budget/settlement', () => HttpResponse.json({ balances: [], flows: [], settlements: [] })), + http.post('/api/trips/1/budget', async ({ request }) => { + posted = await request.json() as Record + return HttpResponse.json({ item: { ...buildBudgetItem({ trip_id: 1, name: 'AirTags' }), id: 6 } }) + }), + ) + const { default: userEvent } = await import('@testing-library/user-event') + const user = userEvent.setup() + render() + + await user.click(await screen.findByRole('button', { name: 'Add expense' })) + await user.type(await screen.findByPlaceholderText('e.g. Dinner, souvenirs, gas…'), 'AirTags') + await user.type(screen.getAllByPlaceholderText('0.00')[0], '39,99') // comma → normalized to 39.99 + + const addBtns = screen.getAllByRole('button', { name: 'Add expense' }) + await user.click(addBtns[addBtns.length - 1]) // footer submit + await waitFor(() => expect(posted).toBeTruthy()) + expect(posted!.total_price).toBe(39.99) + }) + it('marks an expense with no payer as Unfinished', async () => { const item = { ...buildBudgetItem({ trip_id: 1, category: 'food', name: 'Hotel' }), total_price: 90, payers: [], members: [{ user_id: 1, username: 'alice', paid: 0 }] } server.use( diff --git a/client/src/components/Budget/CostsPanel.tsx b/client/src/components/Budget/CostsPanel.tsx index 9541275a..843034d9 100644 --- a/client/src/components/Budget/CostsPanel.tsx +++ b/client/src/components/Budget/CostsPanel.tsx @@ -754,8 +754,8 @@ function SettlementModal({ tripId, people, me, editing, onClose, onSaved }: {
- setAmount(e.target.value)} className={inputCls} style={{ borderRadius: 10, padding: '11px 13px', fontSize: 14, outline: 'none', fontWeight: 600 }} /> + setAmount(e.target.value.replace(',', '.'))} className={inputCls} style={{ borderRadius: 10, padding: '11px 13px', fontSize: 14, outline: 'none', fontWeight: 600 }} />
@@ -833,10 +833,12 @@ export function ExpenseModal({ tripId, base, people, me, editing, prefill, onClo } const onTotalChange = (v: string) => { + v = v.replace(',', '.') setTotal(v) setPaid(prev => rebalance(prev, dirty, participants, parseFloat(v) || 0)) } const onPaidChange = (id: number, v: string) => { + v = v.replace(',', '.') const nextDirty = new Set(dirty); nextDirty.add(id) setDirty(nextDirty) setPaid(prev => rebalance({ ...prev, [id]: v }, nextDirty, participants, totalNum)) @@ -896,7 +898,7 @@ export function ExpenseModal({ tripId, base, people, me, editing, prefill, onClo
{sym(currency)} - onTotalChange(e.target.value)} className="text-content" style={{ flex: 1, border: 0, background: 'none', outline: 'none', fontSize: 15, fontWeight: 600, paddingLeft: 6, width: '100%' }} />
@@ -956,7 +958,7 @@ export function ExpenseModal({ tripId, base, people, me, editing, prefill, onClo {on ? (
{sym(currency)} - onPaidChange(p.id, e.target.value)} className="text-content" style={{ width: '100%', border: 0, background: 'none', outline: 'none', fontSize: 14, fontWeight: 600, padding: '8px 0', textAlign: 'right' }} />