fix(costs): amounts follow their currency's decimals and format, and payments count on the local day

Several small things in the costs views, on desktop and phone alike:

- The outstanding card no longer says "You're all settled up" while a
  payment is still open in Settle up.
- The custom split shows its suggested shares as a faint placeholder in
  the currency's own form (30000 for won, not 30000.00), so it no longer
  reads like an amount already typed.
- Amount fields take only the decimals the currency has: won and yen take
  none, KWD and the other three-decimal currencies three. A switch of
  currency cuts what was already typed. 1000.555 won was saved as typed,
  listed as 1,001 and reopened as 1001.
- The expense sheet on the phone prints shares and sums with the same
  formatter as the list, with grouping and the currency's decimals.
- A payment recorded with Settle or Settle all is dated on the viewer's
  own day, and one without a date of its own is grouped by its UTC record
  time read on the local calendar. In Seoul before 09:00 it landed on the
  day before.
- "1 expenses need a payer" now uses the plural forms of each language.

Refs #2604
This commit is contained in:
Maurice
2026-10-12 15:04:32 +02:00
parent 9064dbb2f0
commit dbbbd07b1c
37 changed files with 424 additions and 74 deletions
+10 -1
View File
@@ -1,5 +1,5 @@
import { describe, it, expect } from 'vitest'
import { splitReservationDateTime, resolveDayId, formatMoney, formatMoneySum, formatPriceText, currencyDecimals, localizeAmountInput, amountToInputString } from './formatters'
import { splitReservationDateTime, resolveDayId, formatMoney, formatMoneySum, formatPriceText, currencyDecimals, localizeAmountInput, amountToInputString, zeroAmountInput } from './formatters'
import { CURRENCIES, SYMBOLS, currenciesWith } from '../components/Budget/BudgetPanel.constants'
import type { Day } from '../types'
@@ -40,6 +40,15 @@ describe('localizeAmountInput (#1624)', () => {
})
})
describe('zeroAmountInput (#2604)', () => {
it('shows an empty amount the way the currency writes it', () => {
expect(zeroAmountInput('EUR')).toBe('0,00')
expect(zeroAmountInput('USD')).toBe('0.00')
expect(zeroAmountInput('KRW')).toBe('0')
expect(zeroAmountInput('KWD')).toBe('0.000')
})
})
describe('amountToInputString (#2175)', () => {
it('pads a stored number to the currency decimals so the edit form matches the list', () => {
expect(amountToInputString(4.9, 'EUR')).toBe('4.90')
+5
View File
@@ -115,6 +115,11 @@ export function amountToInputString(value: number | null | undefined, currency:
return cleanAmount(value).toFixed(currencyDecimals(currency))
}
/** An empty amount field's placeholder in the currency's own form: "0,00" in euros, "0" in won (#2604). */
export function zeroAmountInput(currency: string): string {
return localizeAmountInput(amountToInputString(0, currency), currency)
}
/**
* A price held as free text, tidied only when it really is a number. Import
* writes whatever the confirmation said into this field, so anything that does