mirror of
https://github.com/spliit-app/spliit.git
synced 2026-03-04 20:06:11 +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",
|
"@prisma/client": "5.6.0",
|
||||||
"@radix-ui/react-checkbox": "^1.0.4",
|
"@radix-ui/react-checkbox": "^1.0.4",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.0.6",
|
"@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-icons": "^1.3.0",
|
||||||
"@radix-ui/react-label": "^2.0.2",
|
"@radix-ui/react-label": "^2.0.2",
|
||||||
"@radix-ui/react-popover": "^1.0.7",
|
"@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": {
|
"node_modules/@radix-ui/react-icons": {
|
||||||
"version": "1.3.0",
|
"version": "1.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-icons/-/react-icons-1.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/@radix-ui/react-icons/-/react-icons-1.3.0.tgz",
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
"@prisma/client": "5.6.0",
|
"@prisma/client": "5.6.0",
|
||||||
"@radix-ui/react-checkbox": "^1.0.4",
|
"@radix-ui/react-checkbox": "^1.0.4",
|
||||||
"@radix-ui/react-dropdown-menu": "^2.0.6",
|
"@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-icons": "^1.3.0",
|
||||||
"@radix-ui/react-label": "^2.0.2",
|
"@radix-ui/react-label": "^2.0.2",
|
||||||
"@radix-ui/react-popover": "^1.0.7",
|
"@radix-ui/react-popover": "^1.0.7",
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { GroupForm } from '@/components/group-form'
|
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 { groupFormSchema } from '@/lib/schemas'
|
||||||
import { Metadata } from 'next'
|
import { Metadata } from 'next'
|
||||||
import { notFound, redirect } from 'next/navigation'
|
import { notFound, redirect } from 'next/navigation'
|
||||||
@@ -23,5 +23,12 @@ export default async function EditGroupPage({
|
|||||||
redirect(`/groups/${group.id}`)
|
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,
|
FormLabel,
|
||||||
FormMessage,
|
FormMessage,
|
||||||
} from '@/components/ui/form'
|
} from '@/components/ui/form'
|
||||||
|
import {
|
||||||
|
HoverCard,
|
||||||
|
HoverCardContent,
|
||||||
|
HoverCardTrigger,
|
||||||
|
} from '@/components/ui/hover-card'
|
||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
import { getGroup } from '@/lib/api'
|
import { getGroup } from '@/lib/api'
|
||||||
import { GroupFormValues, groupFormSchema } from '@/lib/schemas'
|
import { GroupFormValues, groupFormSchema } from '@/lib/schemas'
|
||||||
@@ -28,9 +33,14 @@ import { useFieldArray, useForm } from 'react-hook-form'
|
|||||||
export type Props = {
|
export type Props = {
|
||||||
group?: NonNullable<Awaited<ReturnType<typeof getGroup>>>
|
group?: NonNullable<Awaited<ReturnType<typeof getGroup>>>
|
||||||
onSubmit: (groupFormValues: GroupFormValues) => Promise<void>
|
onSubmit: (groupFormValues: GroupFormValues) => Promise<void>
|
||||||
|
protectedParticipantIds?: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export function GroupForm({ group, onSubmit }: Props) {
|
export function GroupForm({
|
||||||
|
group,
|
||||||
|
onSubmit,
|
||||||
|
protectedParticipantIds = [],
|
||||||
|
}: Props) {
|
||||||
const form = useForm<GroupFormValues>({
|
const form = useForm<GroupFormValues>({
|
||||||
resolver: zodResolver(groupFormSchema),
|
resolver: zodResolver(groupFormSchema),
|
||||||
defaultValues: group
|
defaultValues: group
|
||||||
@@ -48,6 +58,7 @@ export function GroupForm({ group, onSubmit }: Props) {
|
|||||||
const { fields, append, remove } = useFieldArray({
|
const { fields, append, remove } = useFieldArray({
|
||||||
control: form.control,
|
control: form.control,
|
||||||
name: 'participants',
|
name: 'participants',
|
||||||
|
keyName: 'key',
|
||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -115,7 +126,7 @@ export function GroupForm({ group, onSubmit }: Props) {
|
|||||||
</CardDescription>
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<ul className="flex flex-col gap-4">
|
<ul className="flex flex-col gap-2">
|
||||||
{fields.map((item, index) => (
|
{fields.map((item, index) => (
|
||||||
<li key={item.id}>
|
<li key={item.id}>
|
||||||
<FormField
|
<FormField
|
||||||
@@ -129,15 +140,39 @@ export function GroupForm({ group, onSubmit }: Props) {
|
|||||||
<FormControl>
|
<FormControl>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Input className="text-base" {...field} />
|
<Input className="text-base" {...field} />
|
||||||
<Button
|
{item.id &&
|
||||||
variant="ghost"
|
protectedParticipantIds.includes(item.id) ? (
|
||||||
className="text-destructive"
|
<HoverCard>
|
||||||
onClick={() => remove(index)}
|
<HoverCardTrigger>
|
||||||
type="button"
|
<Button
|
||||||
size="icon"
|
variant="ghost"
|
||||||
>
|
className="text-destructive-"
|
||||||
<Trash2 className="w-4 h-4" />
|
type="button"
|
||||||
</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>
|
</div>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<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(
|
export async function updateExpense(
|
||||||
groupId: string,
|
groupId: string,
|
||||||
expenseId: string,
|
expenseId: string,
|
||||||
|
|||||||
Reference in New Issue
Block a user