mirror of
https://github.com/spliit-app/spliit.git
synced 2026-02-12 10:36:12 +01:00
Prevent removing participants with expenses
This commit is contained in:
32
package-lock.json
generated
32
package-lock.json
generated
@@ -13,6 +13,7 @@
|
||||
"@prisma/client": "5.6.0",
|
||||
"@radix-ui/react-checkbox": "^1.0.4",
|
||||
"@radix-ui/react-dropdown-menu": "^2.0.6",
|
||||
"@radix-ui/react-hover-card": "^1.0.7",
|
||||
"@radix-ui/react-icons": "^1.3.0",
|
||||
"@radix-ui/react-label": "^2.0.2",
|
||||
"@radix-ui/react-popover": "^1.0.7",
|
||||
@@ -755,6 +756,37 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-hover-card": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-hover-card/-/react-hover-card-1.0.7.tgz",
|
||||
"integrity": "sha512-OcUN2FU0YpmajD/qkph3XzMcK/NmSk9hGWnjV68p6QiZMgILugusgQwnLSDs3oFSJYGKf3Y49zgFedhGh04k9A==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.13.10",
|
||||
"@radix-ui/primitive": "1.0.1",
|
||||
"@radix-ui/react-compose-refs": "1.0.1",
|
||||
"@radix-ui/react-context": "1.0.1",
|
||||
"@radix-ui/react-dismissable-layer": "1.0.5",
|
||||
"@radix-ui/react-popper": "1.1.3",
|
||||
"@radix-ui/react-portal": "1.0.4",
|
||||
"@radix-ui/react-presence": "1.0.1",
|
||||
"@radix-ui/react-primitive": "1.0.3",
|
||||
"@radix-ui/react-use-controllable-state": "1.0.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"react": "^16.8 || ^17.0 || ^18.0",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/react": {
|
||||
"optional": true
|
||||
},
|
||||
"@types/react-dom": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-icons": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-icons/-/react-icons-1.3.0.tgz",
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
"@prisma/client": "5.6.0",
|
||||
"@radix-ui/react-checkbox": "^1.0.4",
|
||||
"@radix-ui/react-dropdown-menu": "^2.0.6",
|
||||
"@radix-ui/react-hover-card": "^1.0.7",
|
||||
"@radix-ui/react-icons": "^1.3.0",
|
||||
"@radix-ui/react-label": "^2.0.2",
|
||||
"@radix-ui/react-popover": "^1.0.7",
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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 />
|
||||
|
||||
29
src/components/ui/hover-card.tsx
Normal file
29
src/components/ui/hover-card.tsx
Normal file
@@ -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 }
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user