diff options
| author | Sebastien Castiel <sebastien@castiel.me> | 2023-12-07 20:12:40 -0500 |
|---|---|---|
| committer | Sebastien Castiel <sebastien@castiel.me> | 2023-12-07 20:12:40 -0500 |
| commit | 6f077f141edcc45f2d7467b7c796e62a8f0316f8 (patch) | |
| tree | 617f8883be6140eac9827bde5c600f85606d85f9 /src | |
| parent | fb6cff2fe3101e80d2932285c0b03e0d5be872f8 (diff) | |
Prevent removing participants with expenses
Diffstat (limited to 'src')
| -rw-r--r-- | src/app/groups/[groupId]/edit/page.tsx | 11 | ||||
| -rw-r--r-- | src/components/group-form.tsx | 57 | ||||
| -rw-r--r-- | src/components/ui/hover-card.tsx | 29 | ||||
| -rw-r--r-- | src/lib/api.ts | 12 |
4 files changed, 96 insertions, 13 deletions
diff --git a/src/app/groups/[groupId]/edit/page.tsx b/src/app/groups/[groupId]/edit/page.tsx index 1fcf225..be1a5a8 100644 --- a/src/app/groups/[groupId]/edit/page.tsx +++ b/src/app/groups/[groupId]/edit/page.tsx @@ -1,5 +1,5 @@ import { GroupForm } from '@/components/group-form' -import { getGroup, updateGroup } from '@/lib/api' +import { getGroup, getGroupExpensesParticipants, updateGroup } from '@/lib/api' import { groupFormSchema } from '@/lib/schemas' import { Metadata } from 'next' import { notFound, redirect } from 'next/navigation' @@ -23,5 +23,12 @@ export default async function EditGroupPage({ redirect(`/groups/${group.id}`) } - return <GroupForm group={group} onSubmit={updateGroupAction} /> + const protectedParticipantIds = await getGroupExpensesParticipants(groupId) + return ( + <GroupForm + group={group} + onSubmit={updateGroupAction} + protectedParticipantIds={protectedParticipantIds} + /> + ) } diff --git a/src/components/group-form.tsx b/src/components/group-form.tsx index d2140f0..7bf0f8e 100644 --- a/src/components/group-form.tsx +++ b/src/components/group-form.tsx @@ -18,6 +18,11 @@ import { FormLabel, FormMessage, } from '@/components/ui/form' +import { + HoverCard, + HoverCardContent, + HoverCardTrigger, +} from '@/components/ui/hover-card' import { Input } from '@/components/ui/input' import { getGroup } from '@/lib/api' import { GroupFormValues, groupFormSchema } from '@/lib/schemas' @@ -28,9 +33,14 @@ import { useFieldArray, useForm } from 'react-hook-form' export type Props = { group?: NonNullable<Awaited<ReturnType<typeof getGroup>>> onSubmit: (groupFormValues: GroupFormValues) => Promise<void> + protectedParticipantIds?: string[] } -export function GroupForm({ group, onSubmit }: Props) { +export function GroupForm({ + group, + onSubmit, + protectedParticipantIds = [], +}: Props) { const form = useForm<GroupFormValues>({ resolver: zodResolver(groupFormSchema), defaultValues: group @@ -48,6 +58,7 @@ export function GroupForm({ group, onSubmit }: Props) { const { fields, append, remove } = useFieldArray({ control: form.control, name: 'participants', + keyName: 'key', }) return ( @@ -115,7 +126,7 @@ export function GroupForm({ group, onSubmit }: Props) { </CardDescription> </CardHeader> <CardContent> - <ul className="flex flex-col gap-4"> + <ul className="flex flex-col gap-2"> {fields.map((item, index) => ( <li key={item.id}> <FormField @@ -129,15 +140,39 @@ export function GroupForm({ group, onSubmit }: Props) { <FormControl> <div className="flex gap-2"> <Input className="text-base" {...field} /> - <Button - variant="ghost" - className="text-destructive" - onClick={() => remove(index)} - type="button" - size="icon" - > - <Trash2 className="w-4 h-4" /> - </Button> + {item.id && + protectedParticipantIds.includes(item.id) ? ( + <HoverCard> + <HoverCardTrigger> + <Button + variant="ghost" + className="text-destructive-" + type="button" + size="icon" + disabled + > + <Trash2 className="w-4 h-4 text-destructive opacity-50" /> + </Button> + </HoverCardTrigger> + <HoverCardContent + align="end" + className="text-sm" + > + This participant is part of expenses, and can + not be removed. + </HoverCardContent> + </HoverCard> + ) : ( + <Button + variant="ghost" + className="text-destructive" + onClick={() => remove(index)} + type="button" + size="icon" + > + <Trash2 className="w-4 h-4" /> + </Button> + )} </div> </FormControl> <FormMessage /> diff --git a/src/components/ui/hover-card.tsx b/src/components/ui/hover-card.tsx new file mode 100644 index 0000000..e54d91c --- /dev/null +++ b/src/components/ui/hover-card.tsx @@ -0,0 +1,29 @@ +"use client" + +import * as React from "react" +import * as HoverCardPrimitive from "@radix-ui/react-hover-card" + +import { cn } from "@/lib/utils" + +const HoverCard = HoverCardPrimitive.Root + +const HoverCardTrigger = HoverCardPrimitive.Trigger + +const HoverCardContent = React.forwardRef< + React.ElementRef<typeof HoverCardPrimitive.Content>, + React.ComponentPropsWithoutRef<typeof HoverCardPrimitive.Content> +>(({ className, align = "center", sideOffset = 4, ...props }, ref) => ( + <HoverCardPrimitive.Content + ref={ref} + align={align} + sideOffset={sideOffset} + className={cn( + "z-50 w-64 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2", + className + )} + {...props} + /> +)) +HoverCardContent.displayName = HoverCardPrimitive.Content.displayName + +export { HoverCard, HoverCardTrigger, HoverCardContent } diff --git a/src/lib/api.ts b/src/lib/api.ts index 4ae88ef..2968cae 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -66,6 +66,18 @@ export async function deleteExpense(expenseId: string) { }) } +export async function getGroupExpensesParticipants(groupId: string) { + const expenses = await getGroupExpenses(groupId) + return Array.from( + new Set( + expenses.flatMap((e) => [ + e.paidById, + ...e.paidFor.map((pf) => pf.participantId), + ]), + ), + ) +} + export async function updateExpense( groupId: string, expenseId: string, |
