Store amounts as cents

This commit is contained in:
Sebastien Castiel
2023-12-07 11:11:53 -05:00
parent ec981fd237
commit 6611e3a187
7 changed files with 67 additions and 52 deletions

View File

@@ -15,51 +15,38 @@ export function BalancesList({ balances, participants, currency }: Props) {
return ( return (
<div className="text-sm"> <div className="text-sm">
{participants.map((participant) => ( {participants.map((participant) => {
<div const balance = balances[participant.id]?.total ?? 0
key={participant.id} const isLeft = balance >= 0
className={cn( return (
'flex',
balances[participant.id]?.total >= 0 || 'flex-row-reverse',
)}
>
<div <div
className={cn( key={participant.id}
'w-1/2 p-2', className={cn('flex', isLeft || 'flex-row-reverse')}
balances[participant.id]?.total >= 0 && 'text-right',
)}
> >
{participant.name} <div className={cn('w-1/2 p-2', isLeft && 'text-right')}>
</div> {participant.name}
<div </div>
className={cn( <div className={cn('w-1/2 relative', isLeft || 'text-right')}>
'w-1/2 relative', <div className="absolute inset-0 p-2 z-20">
balances[participant.id]?.total >= 0 || 'text-right', {currency} {(balance / 100).toFixed(2)}
)} </div>
> {balance !== 0 && (
<div className="absolute inset-0 p-2 z-20"> <div
{currency} {(balances[participant.id]?.total ?? 0).toFixed(2)} className={cn(
'absolute top-1 h-7 z-10',
isLeft
? 'bg-green-200 left-0 rounded-r-lg border border-green-300'
: 'bg-red-200 right-0 rounded-l-lg border border-red-300',
)}
style={{
width: (Math.abs(balance) / maxBalance) * 100 + '%',
}}
></div>
)}
</div> </div>
{balances[participant.id]?.total !== 0 && (
<div
className={cn(
'absolute top-1 h-7 z-10',
balances[participant.id]?.total > 0
? 'bg-green-200 left-0 rounded-r-lg border border-green-300'
: 'bg-red-200 right-0 rounded-l-lg border border-red-300',
)}
style={{
width:
(Math.abs(balances[participant.id]?.total ?? 0) /
maxBalance) *
100 +
'%',
}}
></div>
)}
</div> </div>
</div> )
))} })}
</div> </div>
) )
} }

View File

@@ -53,7 +53,7 @@ export function ExpenseList({
</div> </div>
<div className="flex items-center"> <div className="flex items-center">
<div className="tabular-nums whitespace-nowrap font-bold"> <div className="tabular-nums whitespace-nowrap font-bold">
{currency} {expense.amount.toFixed(2)} {currency} {(expense.amount / 100).toFixed(2)}
</div> </div>
<Button size="icon" variant="link" className="-my-2" asChild> <Button size="icon" variant="link" className="-my-2" asChild>
<Link href={`/groups/${groupId}/expenses/${expense.id}/edit`}> <Link href={`/groups/${groupId}/expenses/${expense.id}/edit`}>

View File

@@ -16,6 +16,14 @@ export function ReimbursementList({
currency, currency,
groupId, groupId,
}: Props) { }: Props) {
if (reimbursements.length === 0) {
return (
<p className="px-6 text-sm pb-6">
It looks like your group doesnt need any reimbursement 😁
</p>
)
}
const getParticipant = (id: string) => participants.find((p) => p.id === id) const getParticipant = (id: string) => participants.find((p) => p.id === id)
return ( return (
<div className="text-sm"> <div className="text-sm">
@@ -35,7 +43,7 @@ export function ReimbursementList({
</Button> </Button>
</div> </div>
<div> <div>
{currency} {reimbursement.amount.toFixed(2)} {currency} {(reimbursement.amount / 100).toFixed(2)}
</div> </div>
</div> </div>
))} ))}

View File

@@ -12,7 +12,7 @@ type Props = {
export function SaveGroupLocally({ group }: Props) { export function SaveGroupLocally({ group }: Props) {
useEffect(() => { useEffect(() => {
saveRecentGroup(group) saveRecentGroup(group)
}, []) }, [group])
return null return null
} }

View File

@@ -47,7 +47,7 @@ export function ExpenseForm({ group, expense, onSubmit, onDelete }: Props) {
defaultValues: expense defaultValues: expense
? { ? {
title: expense.title, title: expense.title,
amount: expense.amount, amount: String(expense.amount / 100) as unknown as number, // hack
paidBy: expense.paidById, paidBy: expense.paidById,
paidFor: expense.paidFor.map(({ participantId }) => participantId), paidFor: expense.paidFor.map(({ participantId }) => participantId),
isReimbursement: expense.isReimbursement, isReimbursement: expense.isReimbursement,
@@ -55,7 +55,9 @@ export function ExpenseForm({ group, expense, onSubmit, onDelete }: Props) {
: searchParams.get('reimbursement') : searchParams.get('reimbursement')
? { ? {
title: 'Reimbursement', title: 'Reimbursement',
amount: Number(searchParams.get('amount')) || 0, amount: String(
(Number(searchParams.get('amount')) || 0) / 100,
) as unknown as number, // hack
paidBy: searchParams.get('from') ?? undefined, paidBy: searchParams.get('from') ?? undefined,
paidFor: [searchParams.get('to') ?? undefined], paidFor: [searchParams.get('to') ?? undefined],
isReimbursement: true, isReimbursement: true,

View File

@@ -42,7 +42,7 @@ export function getBalances(
} }
function divide(total: number, count: number, isLast: boolean): number { function divide(total: number, count: number, isLast: boolean): number {
if (!isLast) return Math.floor((total * 100) / count) / 100 if (!isLast) return Math.floor(total / count)
return total - divide(total, count, false) * (count - 1) return total - divide(total, count, false) * (count - 1)
} }
@@ -58,7 +58,7 @@ export function getSuggestedReimbursements(
while (balancesArray.length > 1) { while (balancesArray.length > 1) {
const first = balancesArray[0] const first = balancesArray[0]
const last = balancesArray[balancesArray.length - 1] const last = balancesArray[balancesArray.length - 1]
const amount = Math.round(first.total * 100 + last.total * 100) / 100 const amount = first.total + last.total
if (first.total > -last.total) { if (first.total > -last.total) {
reimbursements.push({ reimbursements.push({
from: last.participantId, from: last.participantId,
@@ -77,5 +77,5 @@ export function getSuggestedReimbursements(
balancesArray.shift() balancesArray.shift()
} }
} }
return reimbursements return reimbursements.filter(({ amount }) => amount !== 0)
} }

View File

@@ -42,9 +42,27 @@ export const expenseFormSchema = z.object({
title: z title: z
.string({ required_error: 'Please enter a title.' }) .string({ required_error: 'Please enter a title.' })
.min(2, 'Enter at least two characters.'), .min(2, 'Enter at least two characters.'),
amount: z.coerce amount: z
.number({ required_error: 'You must enter an amount.' }) .union(
.min(0.01, 'The amount must be higher than 0.01.'), [
z.number(),
z.string().transform((value, ctx) => {
const valueAsNumber = Number(value)
if (Number.isNaN(valueAsNumber))
ctx.addIssue({
code: z.ZodIssueCode.custom,
message: 'Invalid number.',
})
return Math.floor(valueAsNumber * 100)
}),
],
{ required_error: 'You must enter an amount.' },
)
.refine((amount) => amount >= 1, 'The amount must be higher than 0.01.')
.refine(
(amount) => amount <= 10_000_000_00,
'The amount must be lower than 10,000,000.',
),
paidBy: z.string({ required_error: 'You must select a participant.' }), paidBy: z.string({ required_error: 'You must select a participant.' }),
paidFor: z paidFor: z
.array(z.string()) .array(z.string())