From 0a8e56f80033ce889b38b01f2287cd6fbbe36dd6 Mon Sep 17 00:00:00 2001 From: Sebastien Castiel Date: Mon, 8 Jan 2024 18:11:11 +0100 Subject: Add splitmode and shares to expenses (#11) * Add splitmode and shares to expenses * Update balances based on shares * Change field size * Form validation * Redesign expense form * Split unevenly by amount --- src/components/expense-form.tsx | 390 +++++++++++++++++++++++++++++----------- 1 file changed, 285 insertions(+), 105 deletions(-) (limited to 'src/components/expense-form.tsx') diff --git a/src/components/expense-form.tsx b/src/components/expense-form.tsx index bdee9d0..af3a923 100644 --- a/src/components/expense-form.tsx +++ b/src/components/expense-form.tsx @@ -5,11 +5,16 @@ import { Button } from '@/components/ui/button' import { Card, CardContent, - CardFooter, + CardDescription, CardHeader, CardTitle, } from '@/components/ui/card' import { Checkbox } from '@/components/ui/checkbox' +import { + Collapsible, + CollapsibleContent, + CollapsibleTrigger, +} from '@/components/ui/collapsible' import { Form, FormControl, @@ -29,9 +34,12 @@ import { } from '@/components/ui/select' import { getExpense, getGroup } from '@/lib/api' import { ExpenseFormValues, expenseFormSchema } from '@/lib/schemas' +import { cn } from '@/lib/utils' import { zodResolver } from '@hookform/resolvers/zod' +import { Save, Trash2 } from 'lucide-react' import { useSearchParams } from 'next/navigation' import { useForm } from 'react-hook-form' +import { match } from 'ts-pattern' export type Props = { group: NonNullable>> @@ -50,7 +58,11 @@ export function ExpenseForm({ group, expense, onSubmit, onDelete }: Props) { title: expense.title, amount: String(expense.amount / 100) as unknown as number, // hack paidBy: expense.paidById, - paidFor: expense.paidFor.map(({ participantId }) => participantId), + paidFor: expense.paidFor.map(({ participantId, shares }) => ({ + participant: participantId, + shares: String(shares / 100) as unknown as number, + })), + splitMode: expense.splitMode, isReimbursement: expense.isReimbursement, } : searchParams.get('reimbursement') @@ -60,10 +72,20 @@ export function ExpenseForm({ group, expense, onSubmit, onDelete }: Props) { (Number(searchParams.get('amount')) || 0) / 100, ) as unknown as number, // hack paidBy: searchParams.get('from') ?? undefined, - paidFor: [searchParams.get('to') ?? undefined], + paidFor: [ + searchParams.get('to') + ? { participant: searchParams.get('to')! } + : undefined, + ], isReimbursement: true, } - : { title: '', amount: 0, paidFor: [], isReimbursement: false }, + : { + title: '', + amount: 0, + paidFor: [], + isReimbursement: false, + splitMode: 'EVENLY', + }, }) return ( @@ -75,12 +97,12 @@ export function ExpenseForm({ group, expense, onSubmit, onDelete }: Props) { {isCreate ? <>Create expense : <>Edit expense} - + ( - + Expense title - ( - - Paid by - - - Select the participant who paid the expense. - - - - )} - /> - ( - + Amount
{group.currency} @@ -168,44 +161,82 @@ export function ExpenseForm({ group, expense, onSubmit, onDelete }: Props) { )} /> + ( + + Paid by + + + Select the participant who paid the expense. + + + + )} + /> + + + + + + + Paid for + + + + Select who the expense was paid for. + + + ( - -
- - Paid for - - - - Select who the expense was paid for. - -
+ {group.participants.map(({ id, name }) => ( { return ( - - - { - return checked - ? field.onChange([...field.value, id]) - : field.onChange( - field.value?.filter( - (value) => value !== id, + + + participant === id, + )} + onCheckedChange={(checked) => { + return checked + ? field.onChange([ + ...field.value, + { + participant: id, + shares: '1', + }, + ]) + : field.onChange( + field.value?.filter( + (value) => value.participant !== id, + ), + ) + }} + /> + + + {name} + + + {form.getValues().splitMode !== 'EVENLY' && ( + participant === id, + )}].shares`} + render={() => { + const sharesLabel = ( + + participant === id, ), - ) + })} + > + {match(form.getValues().splitMode) + .with('BY_SHARES', () => <>share(s)) + .with('BY_PERCENTAGE', () => <>%) + .with('BY_AMOUNT', () => ( + <>{group.currency} + )) + .otherwise(() => ( + <> + ))} + + ) + return ( +
+
+ {form.getValues().splitMode === + 'BY_AMOUNT' && sharesLabel} + + + participant === id, + ), + )} + className="text-base w-[80px] -my-2" + type="number" + disabled={ + !field.value?.some( + ({ participant }) => + participant === id, + ) + } + value={ + field.value?.find( + ({ participant }) => + participant === id, + )?.shares + } + onChange={(event) => + field.onChange( + field.value.map((p) => + p.participant === id + ? { + participant: id, + shares: + event.target.value, + } + : p, + ), + ) + } + inputMode="numeric" + step={1} + /> + + {[ + 'BY_SHARES', + 'BY_PERCENTAGE', + ].includes( + form.getValues().splitMode, + ) && sharesLabel} +
+ +
+ ) }} /> -
- - {name} - -
+ )} +
) }} /> @@ -243,26 +369,80 @@ export function ExpenseForm({ group, expense, onSubmit, onDelete }: Props) {
)} /> + + + + + + +
+ ( + + Split mode + + + + + Select how to split the expense. + + + )} + /> +
+
+
+ - - Creating… : <>Saving…} +
+ Creating… : <>Saving…} + > + + {isCreate ? <>Create : <>Save} + + {!isCreate && onDelete && ( + - {isCreate ? <>Create : <>Save} - - {!isCreate && onDelete && ( - - Delete - - )} - - + + Delete + + )} +
) -- cgit v1.3