aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorSebastien Castiel <sebastien@castiel.me>2023-12-06 15:08:52 -0500
committerSebastien Castiel <sebastien@castiel.me>2023-12-06 15:08:52 -0500
commit570aa713b1b2becf7c06c89610c75d2cb119b532 (patch)
treeb6e3ae339bd55a7a00d77ee64532ece9fc43c13f
parentfee1963284eafa6db7887103a156799b7610e13d (diff)
Delete expense
-rw-r--r--prisma/migrations/20231206195936_add_cascades/migration.sql11
-rw-r--r--prisma/schema.prisma4
-rw-r--r--src/app/groups/[groupId]/expenses/[expenseId]/edit/page.tsx9
-rw-r--r--src/components/async-button.tsx42
-rw-r--r--src/components/expense-form.tsx27
-rw-r--r--src/components/group-form.tsx7
-rw-r--r--src/components/submit-button.tsx12
-rw-r--r--src/lib/api.ts8
8 files changed, 104 insertions, 16 deletions
diff --git a/prisma/migrations/20231206195936_add_cascades/migration.sql b/prisma/migrations/20231206195936_add_cascades/migration.sql
new file mode 100644
index 0000000..4075d7a
--- /dev/null
+++ b/prisma/migrations/20231206195936_add_cascades/migration.sql
@@ -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;
diff --git a/prisma/schema.prisma b/prisma/schema.prisma
index 73c3ad1..d073378 100644
--- a/prisma/schema.prisma
+++ b/prisma/schema.prisma
@@ -21,7 +21,7 @@ model Group {
model Participant {
id String @id
name String
- group Group @relation(fields: [groupId], references: [id])
+ group Group @relation(fields: [groupId], references: [id], onDelete: Cascade)
groupId String
expensesPaidBy Expense[]
expensesPaidFor ExpensePaidFor[]
@@ -39,7 +39,7 @@ model Expense {
}
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])
expenseId String
participantId String
diff --git a/src/app/groups/[groupId]/expenses/[expenseId]/edit/page.tsx b/src/app/groups/[groupId]/expenses/[expenseId]/edit/page.tsx
index 934d346..0623f50 100644
--- a/src/app/groups/[groupId]/expenses/[expenseId]/edit/page.tsx
+++ b/src/app/groups/[groupId]/expenses/[expenseId]/edit/page.tsx
@@ -1,5 +1,5 @@
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 { notFound, redirect } from 'next/navigation'
@@ -20,11 +20,18 @@ export default async function EditExpensePage({
redirect(`/groups/${groupId}`)
}
+ async function deleteExpenseAction() {
+ 'use server'
+ await deleteExpense(expenseId)
+ redirect(`/groups/${groupId}`)
+ }
+
return (
<ExpenseForm
group={group}
expense={expense}
onSubmit={updateExpenseAction}
+ onDelete={deleteExpenseAction}
/>
)
}
diff --git a/src/components/async-button.tsx b/src/components/async-button.tsx
new file mode 100644
index 0000000..7d2bf93
--- /dev/null
+++ b/src/components/async-button.tsx
@@ -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>
+ )
+}
diff --git a/src/components/expense-form.tsx b/src/components/expense-form.tsx
index 35c6aaf..3202788 100644
--- a/src/components/expense-form.tsx
+++ b/src/components/expense-form.tsx
@@ -1,4 +1,5 @@
'use client'
+import { AsyncButton } from '@/components/async-button'
import { SubmitButton } from '@/components/submit-button'
import {
Card,
@@ -33,10 +34,12 @@ import { useForm } from 'react-hook-form'
export type Props = {
group: NonNullable<Awaited<ReturnType<typeof getGroup>>>
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>({
resolver: zodResolver(expenseFormSchema),
defaultValues: expense
@@ -54,7 +57,9 @@ export function ExpenseForm({ group, expense, onSubmit }: Props) {
<form onSubmit={form.handleSubmit((values) => onSubmit(values))}>
<Card>
<CardHeader>
- <CardTitle>Expense information</CardTitle>
+ <CardTitle>
+ {isCreate ? <>Create expense</> : <>Edit expense</>}
+ </CardTitle>
</CardHeader>
<CardContent className="grid grid-cols-1 sm:grid-cols-2 gap-6">
<FormField
@@ -179,8 +184,20 @@ export function ExpenseForm({ group, expense, onSubmit }: Props) {
/>
</CardContent>
- <CardFooter>
- <SubmitButton loadingContent="Submitting…">Submit</SubmitButton>
+ <CardFooter className="gap-2">
+ <SubmitButton loadingContent="Submitting…">
+ {isCreate ? <>Create</> : <>Save</>}
+ </SubmitButton>
+ {!isCreate && onDelete && (
+ <AsyncButton
+ type="button"
+ variant="destructive"
+ loadingContent="Deleting…"
+ action={onDelete}
+ >
+ Delete
+ </AsyncButton>
+ )}
</CardFooter>
</Card>
</form>
diff --git a/src/components/group-form.tsx b/src/components/group-form.tsx
index 5ddc865..2e21aea 100644
--- a/src/components/group-form.tsx
+++ b/src/components/group-form.tsx
@@ -22,6 +22,7 @@ import { Input } from '@/components/ui/input'
import { getGroup } from '@/lib/api'
import { GroupFormValues, groupFormSchema } from '@/lib/schemas'
import { zodResolver } from '@hookform/resolvers/zod'
+import { Trash2 } from 'lucide-react'
import { useFieldArray, useForm } from 'react-hook-form'
export type Props = {
@@ -105,11 +106,13 @@ export function GroupForm({ group, onSubmit }: Props) {
<div className="flex gap-2">
<Input className="text-base" {...field} />
<Button
- variant="destructive"
+ variant="ghost"
+ className="text-destructive"
onClick={() => remove(index)}
type="button"
+ size="icon"
>
- Remove
+ <Trash2 className="w-4 h-4" />
</Button>
</div>
</FormControl>
diff --git a/src/components/submit-button.tsx b/src/components/submit-button.tsx
index 7489c6c..c5215c5 100644
--- a/src/components/submit-button.tsx
+++ b/src/components/submit-button.tsx
@@ -1,16 +1,16 @@
-import { Button } from '@/components/ui/button'
+import { Button, ButtonProps } from '@/components/ui/button'
import { Loader2 } from 'lucide-react'
-import { PropsWithChildren, ReactNode } from 'react'
+import { ReactNode } from 'react'
import { useFormState } from 'react-hook-form'
-type Props = PropsWithChildren<{
+type Props = {
loadingContent: ReactNode
-}>
+} & ButtonProps
-export function SubmitButton({ children, loadingContent }: Props) {
+export function SubmitButton({ children, loadingContent, ...props }: Props) {
const { isSubmitting } = useFormState()
return (
- <Button type="submit" disabled={isSubmitting}>
+ <Button type="submit" disabled={isSubmitting} {...props}>
{isSubmitting ? (
<>
<Loader2 className="w-4 h-4 mr-2 animate-spin" /> {loadingContent}
diff --git a/src/lib/api.ts b/src/lib/api.ts
index 10f0b3d..ed3acb1 100644
--- a/src/lib/api.ts
+++ b/src/lib/api.ts
@@ -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(
groupId: string,
expenseId: string,