Delete expense

This commit is contained in:
Sebastien Castiel
2023-12-06 15:08:52 -05:00
parent fee1963284
commit 570aa713b1
8 changed files with 104 additions and 16 deletions

View 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;

View File

@@ -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

View File

@@ -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}
/> />
) )
} }

View 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>
)
}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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}

View File

@@ -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,