mirror of
https://github.com/spliit-app/spliit.git
synced 2026-03-08 05:09:05 +01:00
Store amounts as cents
This commit is contained in:
@@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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`}>
|
||||||
|
|||||||
@@ -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 doesn’t 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>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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())
|
||||||
|
|||||||
Reference in New Issue
Block a user