Skip to content

Commit a25f964

Browse files
authored
Currency handling improvements (#437)
* Hide confusing share info when split is incorrect * Minor UI improvements * Use localized currency helpers and new currency input type * Use currency input in split section * Fix conversion errors by eliminating the number middle step * Do not shuffle pennies on broken splits * Fix runtime bugs * Rewrite the test suite and fix some edge cases * Fix missing balance metedata on simplify and make recalculate more robust * Improve responsiveness of split text * Negative number handling fix * Responsive invite buttons * Truncate large sums in split modal * More fixes for numeric inputs
1 parent 8c55e1b commit a25f964

40 files changed

+2625
-651
lines changed

public/locales/en/common.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@
8989
"name_required": "Name is required",
9090
"saving_expense": "Error while saving expense",
9191
"something_went_wrong": "Something went wrong",
92+
"group_balances_malformed": "Group balances data is malformed, please report an issue or try recalculating.",
9293
"valid_email": "Enter valid email"
9394
}
9495
}

public/locales/en/expense_details.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
},
3131
"adjustment": {
3232
"title": "Adjustment",
33-
"remaining_to_split_equally": "Remaining to split eauqlly"
33+
"remaining_to_split_equally": "Remaining to split equally"
3434
}
3535
}
3636
},

src/components/AddExpense/AddExpensePage.tsx

Lines changed: 45 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import { type CurrencyCode } from '~/lib/currency';
88
import { cn } from '~/lib/utils';
99
import { useAddExpenseStore } from '~/store/addStore';
1010
import { api } from '~/utils/api';
11-
import { toSafeBigInt } from '~/utils/numbers';
1211

1312
import { Button } from '../ui/button';
1413
import { Calendar } from '../ui/calendar';
@@ -17,18 +16,18 @@ import { Popover, PopoverContent, PopoverTrigger } from '../ui/popover';
1716
import { CategoryPicker } from './CategoryPicker';
1817
import { CurrencyPicker } from './CurrencyPicker';
1918
import { SelectUserOrGroup } from './SelectUserOrGroup';
20-
import { SplitTypeSection } from './SplitTypeSection';
19+
import { PayerSelectionForm, SplitExpenseForm } from './SplitTypeSection';
2120
import { UploadFile } from './UploadFile';
2221
import { UserInput } from './UserInput';
2322
import { toast } from 'sonner';
2423
import { useTranslationWithUtils } from '~/hooks/useTranslationWithUtils';
24+
import { CurrencyInput } from '../ui/currency-input';
2525

2626
export const AddOrEditExpensePage: React.FC<{
2727
isStorageConfigured: boolean;
2828
enableSendingInvites: boolean;
2929
expenseId?: string;
3030
}> = ({ isStorageConfigured, enableSendingInvites, expenseId }) => {
31-
const { t, toUIDate } = useTranslationWithUtils(['expense_details']);
3231
const showFriends = useAddExpenseStore((s) => s.showFriends);
3332
const amount = useAddExpenseStore((s) => s.amount);
3433
const isNegative = useAddExpenseStore((s) => s.isNegative);
@@ -44,6 +43,11 @@ export const AddOrEditExpensePage: React.FC<{
4443
const paidBy = useAddExpenseStore((s) => s.paidBy);
4544
const splitType = useAddExpenseStore((s) => s.splitType);
4645
const fileKey = useAddExpenseStore((s) => s.fileKey);
46+
const currentUser = useAddExpenseStore((s) => s.currentUser);
47+
const splitShares = useAddExpenseStore((s) => s.splitShares);
48+
49+
const { t, toUIDate, displayName, generateSplitDescription } =
50+
useTranslationWithUtils('expense_details');
4751

4852
const {
4953
setCurrency,
@@ -71,10 +75,13 @@ export const AddOrEditExpensePage: React.FC<{
7175
const router = useRouter();
7276

7377
const onUpdateAmount = useCallback(
74-
(amt: string) => {
75-
const _amt = amt.replace(',', '.');
76-
setAmountStr(_amt);
77-
setAmount(toSafeBigInt(_amt));
78+
({ strValue, bigIntValue }: { strValue?: string; bigIntValue?: bigint }) => {
79+
if (strValue !== undefined) {
80+
setAmountStr(strValue);
81+
}
82+
if (bigIntValue !== undefined) {
83+
setAmount(bigIntValue);
84+
}
7885
},
7986
[setAmount, setAmountStr],
8087
);
@@ -156,14 +163,6 @@ export const AddOrEditExpensePage: React.FC<{
156163
[setDescription],
157164
);
158165

159-
const onAmountChange = useCallback(
160-
(e: React.ChangeEvent<HTMLInputElement>) => {
161-
const { value } = e.target;
162-
onUpdateAmount(value);
163-
},
164-
[onUpdateAmount],
165-
);
166-
167166
return (
168167
<div className="flex flex-col gap-4">
169168
<div className="flex items-center justify-between">
@@ -203,19 +202,43 @@ export const AddOrEditExpensePage: React.FC<{
203202
</div>
204203
<div className="flex gap-2">
205204
<CurrencyPicker currentCurrency={currency} onCurrencyPick={onCurrencyPick} />
206-
<Input
205+
<CurrencyInput
207206
placeholder={t('ui.add_expense_details.amount_placeholder')}
208-
className="text-lg placeholder:text-sm"
209-
type="number"
210-
inputMode="decimal"
211-
value={amtStr}
212-
onChange={onAmountChange}
207+
currency={currency}
208+
strValue={amtStr}
209+
bigIntValue={amount}
210+
allowNegative
211+
hideSymbol
212+
onValueChange={onUpdateAmount}
213213
/>
214214
</div>
215215
<div className="h-[180px]">
216216
{amount && '' !== description ? (
217217
<>
218-
<SplitTypeSection />
218+
<div className="flex flex-col items-center justify-center text-sm text-gray-400 sm:mt-4 sm:flex-row">
219+
<p>
220+
{t(`ui.expense.${isNegative ? 'received_by' : 'paid_by'}`, {
221+
ns: 'common',
222+
})}
223+
</p>
224+
<PayerSelectionForm>
225+
<Button variant="ghost" className="text-primary h-8 px-1.5 py-0 text-base">
226+
{displayName(paidBy, currentUser?.id, 'dativus')}
227+
</Button>
228+
</PayerSelectionForm>
229+
<p>{t('ui.and', { ns: 'common' })} </p>
230+
<SplitExpenseForm>
231+
<Button variant="ghost" className="text-primary h-8 px-1.5 py-0 text-base">
232+
{generateSplitDescription(
233+
splitType,
234+
participants,
235+
splitShares,
236+
paidBy,
237+
currentUser,
238+
)}
239+
</Button>
240+
</SplitExpenseForm>
241+
</div>
219242

220243
<div className="mt-4 flex items-center justify-between sm:mt-10">
221244
<div className="flex flex-wrap items-center gap-4">

src/components/AddExpense/CategoryPicker.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ export const CategoryPicker: React.FC<{
1414

1515
const trigger = useMemo(
1616
() => (
17-
<div className="flex w-[70px] justify-center rounded-lg border py-2">
17+
<div className="flex w-[70px] cursor-pointer justify-center rounded-lg border py-2">
1818
<CategoryIcon category={category} size={20} />
1919
</div>
2020
),

src/components/AddExpense/CurrencyPicker.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ function CurrencyPickerInner({
2323

2424
const trigger = useMemo(
2525
() => (
26-
<div className="flex w-[70px] justify-center rounded-lg border py-2 text-center text-base">
26+
<div className="flex w-[70px] cursor-pointer justify-center rounded-lg border py-2 text-center text-base">
2727
{currentCurrency}
2828
</div>
2929
),

src/components/AddExpense/SelectUserOrGroup.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -93,10 +93,10 @@ export const SelectUserOrGroup: React.FC<{
9393
<div>{t('ui.add_expense_details.select_user_or_group.note')}</div>
9494
)}
9595
</div>
96-
<div className="flex justify-center gap-4">
96+
<div className="flex flex-wrap justify-center gap-x-4">
9797
{enableSendingInvites && (
9898
<Button
99-
className="mt-4 w-full text-cyan-500 hover:text-cyan-500"
99+
className="mt-4 text-cyan-500 hover:text-cyan-500"
100100
variant="outline"
101101
disabled={!isEmail.success}
102102
onClick={() => onAddEmailClick(false)}
@@ -106,7 +106,7 @@ export const SelectUserOrGroup: React.FC<{
106106
</Button>
107107
)}
108108
<Button
109-
className="mt-4 w-full text-cyan-500 hover:text-cyan-500"
109+
className="mt-4 text-cyan-500 hover:text-cyan-500"
110110
variant="outline"
111111
disabled={!isEmail.success}
112112
onClick={() => onAddEmailClick(false)}

0 commit comments

Comments
 (0)