mirror of
https://github.com/spliit-app/spliit.git
synced 2026-03-09 21:49:06 +01:00
Delete expense
This commit is contained in:
11
prisma/migrations/20231206195936_add_cascades/migration.sql
Normal file
11
prisma/migrations/20231206195936_add_cascades/migration.sql
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
-- DropForeignKey
|
||||||
|
ALTER TABLE "ExpensePaidFor" DROP CONSTRAINT "ExpensePaidFor_expenseId_fkey";
|
||||||
|
|
||||||
|
-- DropForeignKey
|
||||||
|
ALTER TABLE "Participant" DROP CONSTRAINT "Participant_groupId_fkey";
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "Participant" ADD CONSTRAINT "Participant_groupId_fkey" FOREIGN KEY ("groupId") REFERENCES "Group"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "ExpensePaidFor" ADD CONSTRAINT "ExpensePaidFor_expenseId_fkey" FOREIGN KEY ("expenseId") REFERENCES "Expense"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
@@ -21,7 +21,7 @@ model Group {
|
|||||||
model Participant {
|
model Participant {
|
||||||
id String @id
|
id String @id
|
||||||
name String
|
name String
|
||||||
group Group @relation(fields: [groupId], references: [id])
|
group Group @relation(fields: [groupId], references: [id], onDelete: Cascade)
|
||||||
groupId String
|
groupId String
|
||||||
expensesPaidBy Expense[]
|
expensesPaidBy Expense[]
|
||||||
expensesPaidFor ExpensePaidFor[]
|
expensesPaidFor ExpensePaidFor[]
|
||||||
@@ -39,7 +39,7 @@ model Expense {
|
|||||||
}
|
}
|
||||||
|
|
||||||
model ExpensePaidFor {
|
model ExpensePaidFor {
|
||||||
expense Expense @relation(fields: [expenseId], references: [id])
|
expense Expense @relation(fields: [expenseId], references: [id], onDelete: Cascade)
|
||||||
participant Participant @relation(fields: [participantId], references: [id])
|
participant Participant @relation(fields: [participantId], references: [id])
|
||||||
expenseId String
|
expenseId String
|
||||||
participantId String
|
participantId String
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { ExpenseForm } from '@/components/expense-form'
|
import { ExpenseForm } from '@/components/expense-form'
|
||||||
import { getExpense, getGroup, updateExpense } from '@/lib/api'
|
import { deleteExpense, getExpense, getGroup, updateExpense } from '@/lib/api'
|
||||||
import { expenseFormSchema } from '@/lib/schemas'
|
import { expenseFormSchema } from '@/lib/schemas'
|
||||||
import { notFound, redirect } from 'next/navigation'
|
import { notFound, redirect } from 'next/navigation'
|
||||||
|
|
||||||
@@ -20,11 +20,18 @@ export default async function EditExpensePage({
|
|||||||
redirect(`/groups/${groupId}`)
|
redirect(`/groups/${groupId}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function deleteExpenseAction() {
|
||||||
|
'use server'
|
||||||
|
await deleteExpense(expenseId)
|
||||||
|
redirect(`/groups/${groupId}`)
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ExpenseForm
|
<ExpenseForm
|
||||||
group={group}
|
group={group}
|
||||||
expense={expense}
|
expense={expense}
|
||||||
onSubmit={updateExpenseAction}
|
onSubmit={updateExpenseAction}
|
||||||
|
onDelete={deleteExpenseAction}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
42
src/components/async-button.tsx
Normal file
42
src/components/async-button.tsx
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
'use client'
|
||||||
|
import { Button, ButtonProps } from '@/components/ui/button'
|
||||||
|
import { Loader2 } from 'lucide-react'
|
||||||
|
import { ReactNode, useState } from 'react'
|
||||||
|
|
||||||
|
type Props = ButtonProps & {
|
||||||
|
action?: () => Promise<void>
|
||||||
|
loadingContent?: ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AsyncButton({
|
||||||
|
action,
|
||||||
|
children,
|
||||||
|
loadingContent,
|
||||||
|
...props
|
||||||
|
}: Props) {
|
||||||
|
const [loading, setLoading] = useState(false)
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
onClick={async () => {
|
||||||
|
try {
|
||||||
|
setLoading(true)
|
||||||
|
await action?.()
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err)
|
||||||
|
} finally {
|
||||||
|
setLoading(false)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{loading ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="w-4 h-4 mr-2 animate-spin" />{' '}
|
||||||
|
{loadingContent ?? children}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
children
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
import { AsyncButton } from '@/components/async-button'
|
||||||
import { SubmitButton } from '@/components/submit-button'
|
import { SubmitButton } from '@/components/submit-button'
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
@@ -33,10 +34,12 @@ import { useForm } from 'react-hook-form'
|
|||||||
export type Props = {
|
export type Props = {
|
||||||
group: NonNullable<Awaited<ReturnType<typeof getGroup>>>
|
group: NonNullable<Awaited<ReturnType<typeof getGroup>>>
|
||||||
expense?: NonNullable<Awaited<ReturnType<typeof getExpense>>>
|
expense?: NonNullable<Awaited<ReturnType<typeof getExpense>>>
|
||||||
onSubmit: (values: ExpenseFormValues) => void
|
onSubmit: (values: ExpenseFormValues) => Promise<void>
|
||||||
|
onDelete?: () => Promise<void>
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ExpenseForm({ group, expense, onSubmit }: Props) {
|
export function ExpenseForm({ group, expense, onSubmit, onDelete }: Props) {
|
||||||
|
const isCreate = expense === undefined
|
||||||
const form = useForm<ExpenseFormValues>({
|
const form = useForm<ExpenseFormValues>({
|
||||||
resolver: zodResolver(expenseFormSchema),
|
resolver: zodResolver(expenseFormSchema),
|
||||||
defaultValues: expense
|
defaultValues: expense
|
||||||
@@ -54,7 +57,9 @@ export function ExpenseForm({ group, expense, onSubmit }: Props) {
|
|||||||
<form onSubmit={form.handleSubmit((values) => onSubmit(values))}>
|
<form onSubmit={form.handleSubmit((values) => onSubmit(values))}>
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Expense information</CardTitle>
|
<CardTitle>
|
||||||
|
{isCreate ? <>Create expense</> : <>Edit expense</>}
|
||||||
|
</CardTitle>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="grid grid-cols-1 sm:grid-cols-2 gap-6">
|
<CardContent className="grid grid-cols-1 sm:grid-cols-2 gap-6">
|
||||||
<FormField
|
<FormField
|
||||||
@@ -179,8 +184,20 @@ export function ExpenseForm({ group, expense, onSubmit }: Props) {
|
|||||||
/>
|
/>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|
||||||
<CardFooter>
|
<CardFooter className="gap-2">
|
||||||
<SubmitButton loadingContent="Submitting…">Submit</SubmitButton>
|
<SubmitButton loadingContent="Submitting…">
|
||||||
|
{isCreate ? <>Create</> : <>Save</>}
|
||||||
|
</SubmitButton>
|
||||||
|
{!isCreate && onDelete && (
|
||||||
|
<AsyncButton
|
||||||
|
type="button"
|
||||||
|
variant="destructive"
|
||||||
|
loadingContent="Deleting…"
|
||||||
|
action={onDelete}
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</AsyncButton>
|
||||||
|
)}
|
||||||
</CardFooter>
|
</CardFooter>
|
||||||
</Card>
|
</Card>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ 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'
|
||||||
import { zodResolver } from '@hookform/resolvers/zod'
|
import { zodResolver } from '@hookform/resolvers/zod'
|
||||||
|
import { Trash2 } from 'lucide-react'
|
||||||
import { useFieldArray, useForm } from 'react-hook-form'
|
import { useFieldArray, useForm } from 'react-hook-form'
|
||||||
|
|
||||||
export type Props = {
|
export type Props = {
|
||||||
@@ -105,11 +106,13 @@ export function GroupForm({ group, onSubmit }: Props) {
|
|||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
<Input className="text-base" {...field} />
|
<Input className="text-base" {...field} />
|
||||||
<Button
|
<Button
|
||||||
variant="destructive"
|
variant="ghost"
|
||||||
|
className="text-destructive"
|
||||||
onClick={() => remove(index)}
|
onClick={() => remove(index)}
|
||||||
type="button"
|
type="button"
|
||||||
|
size="icon"
|
||||||
>
|
>
|
||||||
Remove
|
<Trash2 className="w-4 h-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
import { Button } from '@/components/ui/button'
|
import { Button, ButtonProps } from '@/components/ui/button'
|
||||||
import { Loader2 } from 'lucide-react'
|
import { Loader2 } from 'lucide-react'
|
||||||
import { PropsWithChildren, ReactNode } from 'react'
|
import { ReactNode } from 'react'
|
||||||
import { useFormState } from 'react-hook-form'
|
import { useFormState } from 'react-hook-form'
|
||||||
|
|
||||||
type Props = PropsWithChildren<{
|
type Props = {
|
||||||
loadingContent: ReactNode
|
loadingContent: ReactNode
|
||||||
}>
|
} & ButtonProps
|
||||||
|
|
||||||
export function SubmitButton({ children, loadingContent }: Props) {
|
export function SubmitButton({ children, loadingContent, ...props }: Props) {
|
||||||
const { isSubmitting } = useFormState()
|
const { isSubmitting } = useFormState()
|
||||||
return (
|
return (
|
||||||
<Button type="submit" disabled={isSubmitting}>
|
<Button type="submit" disabled={isSubmitting} {...props}>
|
||||||
{isSubmitting ? (
|
{isSubmitting ? (
|
||||||
<>
|
<>
|
||||||
<Loader2 className="w-4 h-4 mr-2 animate-spin" /> {loadingContent}
|
<Loader2 className="w-4 h-4 mr-2 animate-spin" /> {loadingContent}
|
||||||
|
|||||||
@@ -56,6 +56,14 @@ export async function createExpense(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function deleteExpense(expenseId: string) {
|
||||||
|
const prisma = await getPrisma()
|
||||||
|
await prisma.expense.delete({
|
||||||
|
where: { id: expenseId },
|
||||||
|
include: { paidFor: true, paidBy: true },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
export async function updateExpense(
|
export async function updateExpense(
|
||||||
groupId: string,
|
groupId: string,
|
||||||
expenseId: string,
|
expenseId: string,
|
||||||
|
|||||||
Reference in New Issue
Block a user