Add computed shares per expense to fix #127 (#269)

* Added computed expenses per balance to fix #127

* add missing import that got lost during merge

* if we are in percentage mode or amount mode, the shares have to be multiplied by 100
This commit is contained in:
Daniel Thiem
2025-04-19 21:16:37 +02:00
committed by GitHub
parent 9fec8f9eaa
commit 728e072376
2 changed files with 83 additions and 38 deletions

View File

@@ -40,6 +40,7 @@ import {
SplittingOptions,
expenseFormSchema,
} from '@/lib/schemas'
import { calculateShare } from '@/lib/totals'
import { cn } from '@/lib/utils'
import { AppRouterOutput } from '@/trpc/routers/_app'
import { zodResolver } from '@hookform/resolvers/zod'
@@ -591,6 +592,42 @@ export function ExpenseForm({
</FormControl>
<FormLabel className="text-sm font-normal flex-1">
{name}
{field.value?.some(
({ participant }) => participant === id,
) &&
!form.watch('isReimbursement') && (
<span className="text-muted-foreground ml-2">
({group.currency}{' '}
{(
calculateShare(id, {
amount:
Number(form.watch('amount')) * 100, // Convert to cents
paidFor: field.value.map(
({ participant, shares }) => ({
participant: {
id: participant,
name: '',
groupId: '',
},
shares:
form.watch('splitMode') ===
'BY_PERCENTAGE' ||
form.watch('splitMode') ===
'BY_AMOUNT'
? Number(shares) * 100 // Convert percentage to basis points (e.g., 50% -> 5000)
: shares,
expenseId: '',
participantId: '',
}),
),
splitMode: form.watch('splitMode'),
isReimbursement:
form.watch('isReimbursement'),
}) / 100
).toFixed(2)}
)
</span>
)}
</FormLabel>
</FormItem>
{form.getValues().splitMode !== 'EVENLY' && (

View File

@@ -23,48 +23,56 @@ export function getTotalActiveUserPaidFor(
)
}
type Expense = NonNullable<Awaited<ReturnType<typeof getGroupExpenses>>>[number]
export function calculateShare(
participantId: string | null,
expense: Pick<
Expense,
'amount' | 'paidFor' | 'splitMode' | 'isReimbursement'
>,
): number {
if (expense.isReimbursement) return 0
const paidFors = expense.paidFor
const userPaidFor = paidFors.find(
(paidFor) => paidFor.participant.id === participantId,
)
if (!userPaidFor) return 0
const shares = Number(userPaidFor.shares)
switch (expense.splitMode) {
case 'EVENLY':
// Divide the total expense evenly among all participants
return expense.amount / paidFors.length
case 'BY_AMOUNT':
// Directly add the user's share if the split mode is BY_AMOUNT
return shares
case 'BY_PERCENTAGE':
// Calculate the user's share based on their percentage of the total expense
return (expense.amount * shares) / 10000 // Assuming shares are out of 10000 for percentage
case 'BY_SHARES':
// Calculate the user's share based on their shares relative to the total shares
const totalShares = paidFors.reduce(
(sum, paidFor) => sum + Number(paidFor.shares),
0,
)
return (expense.amount * shares) / totalShares
default:
return 0
}
}
export function getTotalActiveUserShare(
activeUserId: string | null,
expenses: NonNullable<Awaited<ReturnType<typeof getGroupExpenses>>>,
): number {
let total = 0
expenses.forEach((expense) => {
if (expense.isReimbursement) return
const paidFors = expense.paidFor
const userPaidFor = paidFors.find(
(paidFor) => paidFor.participant.id === activeUserId,
)
if (!userPaidFor) {
// If the active user is not involved in the expense, skip it
return
}
switch (expense.splitMode) {
case 'EVENLY':
// Divide the total expense evenly among all participants
total += expense.amount / paidFors.length
break
case 'BY_AMOUNT':
// Directly add the user's share if the split mode is BY_AMOUNT
total += userPaidFor.shares
break
case 'BY_PERCENTAGE':
// Calculate the user's share based on their percentage of the total expense
total += (expense.amount * userPaidFor.shares) / 10000 // Assuming shares are out of 10000 for percentage
break
case 'BY_SHARES':
// Calculate the user's share based on their shares relative to the total shares
const totalShares = paidFors.reduce(
(sum, paidFor) => sum + paidFor.shares,
0,
)
total += (expense.amount * userPaidFor.shares) / totalShares
break
}
})
const total = expenses.reduce(
(sum, expense) => sum + calculateShare(activeUserId, expense),
0,
)
return parseFloat(total.toFixed(2))
}