From e76bd68d2b6552c31ca2f4248456389d431e68ec Mon Sep 17 00:00:00 2001 From: b-at-neu Date: Fri, 10 Apr 2026 13:44:43 -0400 Subject: [PATCH 1/5] #654 fix purchase amounts not showing two decimal places Co-Authored-By: Claude Sonnet 4.6 --- lib/utils.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/lib/utils.ts b/lib/utils.ts index 639d59a5..fab47db1 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -7,8 +7,10 @@ export function cn(...inputs: ClassValue[]) { } function formatAbsoluteNumber(value: number) { - const rounded = Math.round(value * 100) / 100; - return rounded.toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ','); + return new Intl.NumberFormat('en-US', { + minimumFractionDigits: 2, + maximumFractionDigits: 2, + }).format(value); } export function formatNumber(value: number) { From ff5339a9692748bdb1a3910552da4814bdced235 Mon Sep 17 00:00:00 2001 From: b-at-neu Date: Fri, 10 Apr 2026 23:28:45 -0400 Subject: [PATCH 2/5] #654 fix amount input not showing two decimal places in edit purchase Co-Authored-By: Claude Sonnet 4.6 --- components/ui/form-input.tsx | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/components/ui/form-input.tsx b/components/ui/form-input.tsx index 29af70b5..68975ce2 100644 --- a/components/ui/form-input.tsx +++ b/components/ui/form-input.tsx @@ -37,6 +37,7 @@ function FormInput({ ...inputProps }: FormInputProps) { const form = useFormContext(); + const [isFocused, setIsFocused] = React.useState(false); return ( ({ setIsFocused(true)} + onBlur={() => { + setIsFocused(false); + field.onBlur(); + }} className={cn('rounded-l-none', inputProps.className)} /> From 6110ab9915579889189290e602fe895de67fc038 Mon Sep 17 00:00:00 2001 From: b-at-neu Date: Fri, 10 Apr 2026 23:39:52 -0400 Subject: [PATCH 3/5] #654 show two decimal places on focus in currency input Co-Authored-By: Claude Sonnet 4.6 --- components/ui/form-input.tsx | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/components/ui/form-input.tsx b/components/ui/form-input.tsx index 68975ce2..0bcffd52 100644 --- a/components/ui/form-input.tsx +++ b/components/ui/form-input.tsx @@ -37,7 +37,7 @@ function FormInput({ ...inputProps }: FormInputProps) { const form = useFormContext(); - const [isFocused, setIsFocused] = React.useState(false); + const [displayValue, setDisplayValue] = React.useState(''); return ( ({ + setDisplayValue(Number(field.value || 0).toFixed(2)) } - onFocus={() => setIsFocused(true)} + onChange={(e) => { + setDisplayValue(e.target.value); + field.onChange(e.target.value); + }} onBlur={() => { - setIsFocused(false); + setDisplayValue(''); field.onBlur(); }} className={cn('rounded-l-none', inputProps.className)} From 97f6e3620193e1e319aa975a6f8ccc02ecf848b4 Mon Sep 17 00:00:00 2001 From: pataniaeli Date: Wed, 7 Oct 2026 12:26:22 -0400 Subject: [PATCH 4/5] #654 fix currency input clearing placeholders and nan display Co-Authored-By: Claude Sonnet 4.6 --- components/ui/form-input.tsx | 65 ++++++++++++++++++++++++++++-------- 1 file changed, 52 insertions(+), 13 deletions(-) diff --git a/components/ui/form-input.tsx b/components/ui/form-input.tsx index 0bcffd52..ff045711 100644 --- a/components/ui/form-input.tsx +++ b/components/ui/form-input.tsx @@ -27,6 +27,21 @@ interface FormInputProps extends Omit< numbersOnly?: boolean; } +/** + * Renders a form value as the text shown in a currency input when it is not + * focused. Anything that is not a usable amount — unset, empty, unparseable + * text such as `abc` or `1,234`, or the zero every amount field defaults to — + * renders as an empty string so the field's placeholder stays visible instead + * of a misleading `0.00` or a literal `NaN`. + */ +function formatCurrencyValue(value: unknown): string { + if (value === null || value === undefined || value === '') return ''; + const parsed = + typeof value === 'number' ? value : Number(String(value).trim()); + if (!Number.isFinite(parsed) || parsed === 0) return ''; + return parsed.toFixed(2); +} + function FormInput({ name, label, @@ -34,10 +49,14 @@ function FormInput({ currency, prefix, numbersOnly, + onFocus, + onChange, + onBlur, ...inputProps }: FormInputProps) { const form = useFormContext(); - const [displayValue, setDisplayValue] = React.useState(''); + // `null` means "not being edited": the formatted form value is shown instead. + const [editingValue, setEditingValue] = React.useState(null); return ( ({ - setDisplayValue(Number(field.value || 0).toFixed(2)) - } + value={editingValue ?? formatCurrencyValue(field.value)} + onFocus={(e) => { + setEditingValue(formatCurrencyValue(field.value)); + onFocus?.(e); + }} onChange={(e) => { - setDisplayValue(e.target.value); + setEditingValue(e.target.value); field.onChange(e.target.value); + onChange?.(e); }} - onBlur={() => { - setDisplayValue(''); + onBlur={(e) => { + setEditingValue(null); field.onBlur(); + onBlur?.(e); }} className={cn('rounded-l-none', inputProps.className)} /> @@ -79,17 +101,34 @@ function FormInput({ {...inputProps} {...field} className={cn('rounded-l-none', inputProps.className)} + onFocus={onFocus} onChange={(e) => { - let value = e.target.value; - if (numbersOnly) { - value = value.replace(/\D/g, ''); - } + const value = numbersOnly + ? e.target.value.replace(/\D/g, '') + : e.target.value; field.onChange(value); + onChange?.(e); + }} + onBlur={(e) => { + field.onBlur(); + onBlur?.(e); }} /> ) : ( - + { + field.onChange(e); + onChange?.(e); + }} + onBlur={(e) => { + field.onBlur(); + onBlur?.(e); + }} + /> )} {description && {description}} From d3a7f3602f98c01bd90efef5d4839f6f5fb5e105 Mon Sep 17 00:00:00 2001 From: pataniaeli Date: Wed, 7 Oct 2026 12:26:47 -0400 Subject: [PATCH 5/5] #654 hoist currency number formatter to module scope Co-Authored-By: Claude Sonnet 4.6 --- lib/utils.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/lib/utils.ts b/lib/utils.ts index fab47db1..3273e8d4 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -6,11 +6,13 @@ export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); } +const numberFormatter = new Intl.NumberFormat('en-US', { + minimumFractionDigits: 2, + maximumFractionDigits: 2, +}); + function formatAbsoluteNumber(value: number) { - return new Intl.NumberFormat('en-US', { - minimumFractionDigits: 2, - maximumFractionDigits: 2, - }).format(value); + return numberFormatter.format(value); } export function formatNumber(value: number) {