diff options
| author | Sebastien Castiel <sebastien@castiel.me> | 2023-12-06 17:02:37 -0500 |
|---|---|---|
| committer | Sebastien Castiel <sebastien@castiel.me> | 2023-12-06 17:02:59 -0500 |
| commit | 12b1cdb52a9d2723c319083b45a4e9a21896b3b4 (patch) | |
| tree | 0521ab02b3a3fbab7c8955acf20da20f919b48e6 | |
| parent | 72fe19987957e78cbd1d97ccfd8b87d85fea5c88 (diff) | |
Balances
| -rw-r--r-- | src/app/groups/[groupId]/balances-list.tsx | 63 | ||||
| -rw-r--r-- | src/app/groups/[groupId]/page.tsx | 20 | ||||
| -rw-r--r-- | src/lib/balances.ts | 42 |
3 files changed, 118 insertions, 7 deletions
diff --git a/src/app/groups/[groupId]/balances-list.tsx b/src/app/groups/[groupId]/balances-list.tsx new file mode 100644 index 0000000..c0cffa9 --- /dev/null +++ b/src/app/groups/[groupId]/balances-list.tsx @@ -0,0 +1,63 @@ +import { Balances } from '@/lib/balances' +import { cn } from '@/lib/utils' +import { Participant } from '@prisma/client' + +type Props = { + balances: Balances + participants: Participant[] + currency: string +} + +export function BalancesList({ balances, participants, currency }: Props) { + const maxBalance = Math.max( + ...Object.values(balances).map((b) => Math.abs(b.total)), + ) + + return ( + <div className="text-sm"> + {participants.map((participant) => ( + <div + key={participant.id} + className={cn( + 'flex', + balances[participant.id]?.total > 0 || 'flex-row-reverse', + )} + > + <div + className={cn( + 'w-1/2 p-2', + balances[participant.id]?.total > 0 && 'text-right', + )} + > + {participant.name} + </div> + <div + className={cn( + 'w-1/2 relative', + balances[participant.id]?.total > 0 || 'text-right', + )} + > + <div className="absolute inset-0 p-2 z-20"> + {currency} {(balances[participant.id]?.total ?? 0).toFixed(2)} + </div> + <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> + ) +} diff --git a/src/app/groups/[groupId]/page.tsx b/src/app/groups/[groupId]/page.tsx index 83fc3ef..7fffcb3 100644 --- a/src/app/groups/[groupId]/page.tsx +++ b/src/app/groups/[groupId]/page.tsx @@ -1,3 +1,4 @@ +import { BalancesList } from '@/app/groups/[groupId]/balances-list' import { SaveGroupLocally } from '@/app/groups/[groupId]/save-recent-group' import { Badge } from '@/components/ui/badge' import { Button } from '@/components/ui/button' @@ -17,6 +18,7 @@ import { TableRow, } from '@/components/ui/table' import { getGroup, getGroupExpenses } from '@/lib/api' +import { getBalances } from '@/lib/balances' import { ChevronRight, Plus } from 'lucide-react' import Link from 'next/link' import { notFound } from 'next/navigation' @@ -30,6 +32,7 @@ export default async function GroupPage({ if (!group) notFound() const expenses = await getGroupExpenses(groupId) + const balances = getBalances(expenses) return ( <> @@ -52,7 +55,7 @@ export default async function GroupPage({ <CardContent className="p-0"> {expenses.length > 0 ? ( - <Table className=""> + <Table> <TableHeader> <TableRow> <TableHead>Title</TableHead> @@ -115,14 +118,17 @@ export default async function GroupPage({ <Card className="mb-4"> <CardHeader> - <CardTitle>Participants</CardTitle> + <CardTitle>Balances</CardTitle> + <CardDescription> + This is the amount that each participant paid or was paid for. + </CardDescription> </CardHeader> <CardContent> - <ul> - {group.participants.map((participant) => ( - <li key={participant.id}>{participant.name}</li> - ))} - </ul> + <BalancesList + balances={balances} + participants={group.participants} + currency={group.currency} + /> </CardContent> </Card> <SaveGroupLocally group={{ id: group.id, name: group.name }} /> diff --git a/src/lib/balances.ts b/src/lib/balances.ts new file mode 100644 index 0000000..fa00640 --- /dev/null +++ b/src/lib/balances.ts @@ -0,0 +1,42 @@ +import { getGroupExpenses } from '@/lib/api' +import { Participant } from '@prisma/client' + +export type Balances = Record< + Participant['id'], + { paid: number; paidFor: number; total: number } +> + +export function getBalances( + expenses: NonNullable<Awaited<ReturnType<typeof getGroupExpenses>>>, +): Balances { + const balances: Balances = {} + + for (const expense of expenses) { + const paidBy = expense.paidById + const paidFors = expense.paidFor.map((p) => p.participantId) + + if (!balances[paidBy]) balances[paidBy] = { paid: 0, paidFor: 0, total: 0 } + balances[paidBy].paid += expense.amount + balances[paidBy].total += expense.amount + paidFors.forEach((paidFor, index) => { + if (!balances[paidFor]) + balances[paidFor] = { paid: 0, paidFor: 0, total: 0 } + + const dividedAmount = divide( + expense.amount, + paidFors.length, + index === paidFors.length - 1, + ) + balances[paidFor].paidFor += dividedAmount + balances[paidFor].total -= dividedAmount + }) + } + + return balances +} + +function divide(total: number, count: number, isLast: boolean): number { + if (!isLast) return Math.floor((total * 100) / count) / 100 + + return total - divide(total, count, false) * (count - 1) +} |
