diff --git a/lang/es.json b/lang/es.json index 9845cae4..66138f8e 100644 --- a/lang/es.json +++ b/lang/es.json @@ -1038,6 +1038,8 @@ "Main Checking": "Cuenta Corriente Principal", "Maintain and promptly update your account information": "Mantén y actualiza prontamente tu información de cuenta", "Maintain and promptly update your account\\n information": "Mantén y actualiza puntualmente la información de tu cuenta", + "Make amount negative": "Hacer el importe negativo", + "Make amount positive": "Hacer el importe positivo", "Make child categories top level": "Convertir las categorías hijas en categorías principales", "mail support": "escribir a soporte", "Making your money talk...": "Haciendo que tu dinero hable...", diff --git a/resources/js/components/transactions/edit-transaction-dialog.tsx b/resources/js/components/transactions/edit-transaction-dialog.tsx index 159934a2..20194352 100644 --- a/resources/js/components/transactions/edit-transaction-dialog.tsx +++ b/resources/js/components/transactions/edit-transaction-dialog.tsx @@ -753,9 +753,10 @@ export function EditTransactionDialog({ selectedAccount?.currency_code || 'USD' } - placeholder="-25.00" + placeholder="25.00" disabled={isSubmitting} required + allowNegative /> {!selectedAccount?.banking_connection_id && ( diff --git a/resources/js/components/ui/amount-input.test.tsx b/resources/js/components/ui/amount-input.test.tsx new file mode 100644 index 00000000..9cf8e528 --- /dev/null +++ b/resources/js/components/ui/amount-input.test.tsx @@ -0,0 +1,63 @@ +import { fireEvent, render, screen } from '@testing-library/react'; +import { describe, expect, it, vi } from 'vitest'; +import { AmountInput } from './amount-input'; + +vi.mock('@inertiajs/react', () => ({ + usePage: () => ({ props: { locale: 'en-US' } }), +})); + +describe('AmountInput sign toggle', () => { + it('is hidden unless allowNegative is set', () => { + const { rerender } = render( + , + ); + expect(screen.queryByRole('button')).toBeNull(); + + rerender( + , + ); + expect(screen.getByRole('button')).toBeInTheDocument(); + }); + + it('flips a typed positive amount to negative', () => { + const onChange = vi.fn(); + render( + , + ); + + const input = screen.getByRole('textbox'); + fireEvent.focus(input); + fireEvent.change(input, { target: { value: '25' } }); + + fireEvent.click(screen.getByRole('button')); + + expect(onChange).toHaveBeenCalledWith(-2500); + }); + + it('flips a negative amount back to positive', () => { + const onChange = vi.fn(); + render( + , + ); + + const input = screen.getByRole('textbox'); + fireEvent.focus(input); + fireEvent.change(input, { target: { value: '-25' } }); + + fireEvent.click(screen.getByRole('button')); + + expect(onChange).toHaveBeenLastCalledWith(2500); + }); + + it('keeps the negative sign when focusing after toggling an empty field', () => { + render( + , + ); + + const input = screen.getByRole('textbox'); + fireEvent.click(screen.getByRole('button')); + fireEvent.focus(input); + + expect(input).toHaveValue('-'); + }); +}); diff --git a/resources/js/components/ui/amount-input.tsx b/resources/js/components/ui/amount-input.tsx index bde95c63..44572481 100644 --- a/resources/js/components/ui/amount-input.tsx +++ b/resources/js/components/ui/amount-input.tsx @@ -3,6 +3,7 @@ import * as React from 'react'; import { Input } from '@/components/ui/input'; import { useLocale } from '@/hooks/use-locale'; import { cn } from '@/lib/utils'; +import { __ } from '@/utils/i18n'; interface AmountInputProps { value: number; @@ -13,6 +14,7 @@ interface AmountInputProps { placeholder?: string; id?: string; className?: string; + allowNegative?: boolean; } const getCurrencyInfo = ( @@ -153,6 +155,9 @@ const evaluateMathExpression = (input: string): number | null => { } }; +const resolveCents = (input: string): number => + evaluateMathExpression(input) ?? parseInputValue(input); + export const AmountInput = React.forwardRef( ( { @@ -164,6 +169,7 @@ export const AmountInput = React.forwardRef( placeholder = '0.00', id, className = '', + allowNegative = false, }, ref, ) => { @@ -186,19 +192,15 @@ export const AmountInput = React.forwardRef( if (value !== 0) { const amount = (value / 100).toFixed(2); setDisplayValue(amount); - } else { + } else if (!displayValue.startsWith('-')) { + // Keep a lone '-' the user set via the toggle before typing. setDisplayValue(''); } }; const handleBlur = () => { setIsFocused(false); - - // Try to evaluate as math expression first - const mathResult = evaluateMathExpression(displayValue); - const valueInCents = mathResult !== null ? mathResult : parseInputValue(displayValue); - - onChange(valueInCents); + onChange(resolveCents(displayValue)); }; const handleChange = (e: React.ChangeEvent) => { @@ -207,20 +209,35 @@ export const AmountInput = React.forwardRef( const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === 'Enter') { - // Try to evaluate as math expression first - const mathResult = evaluateMathExpression(displayValue); - const valueInCents = mathResult !== null ? mathResult : parseInputValue(displayValue); - - onChange(valueInCents); + onChange(resolveCents(displayValue)); } }; + // iOS numeric keypads (inputMode="decimal") have no minus key, so + // negative amounts need an explicit toggle. onClick handles both pointer + // and keyboard; onPointerDown preventDefault keeps the input focused, + // avoiding the blur/reformat race with the effect above. + const toggleSign = () => { + const next = displayValue.trim().startsWith('-') + ? displayValue.replace('-', '') + : `-${displayValue}`; + setDisplayValue(next); + onChange(resolveCents(next)); + }; + + const isNegative = displayValue.trim().startsWith('-'); + const { symbol: currencySymbol, position: symbolPosition } = getCurrencyInfo(currencyCode, locale); return (
{symbolPosition === 'prefix' && ( - + {currencySymbol} )} @@ -239,7 +256,12 @@ export const AmountInput = React.forwardRef( required={required} className={cn([ 'bg-background', - symbolPosition === 'prefix' ? 'pl-9' : 'pr-9', + symbolPosition === 'suffix' && 'pr-9', + allowNegative + ? symbolPosition === 'prefix' + ? 'pl-14' + : 'pl-11' + : symbolPosition === 'prefix' && 'pl-9', className, ])} /> @@ -248,6 +270,24 @@ export const AmountInput = React.forwardRef( {currencySymbol} )} + {allowNegative && ( + + )}
); },