diff options
| author | Sebastien Castiel <sebastien@castiel.me> | 2023-12-06 15:08:52 -0500 |
|---|---|---|
| committer | Sebastien Castiel <sebastien@castiel.me> | 2023-12-06 15:08:52 -0500 |
| commit | 570aa713b1b2becf7c06c89610c75d2cb119b532 (patch) | |
| tree | b6e3ae339bd55a7a00d77ee64532ece9fc43c13f | |
| parent | fee1963284eafa6db7887103a156799b7610e13d (diff) | |
Delete expense
| -rw-r--r-- | prisma/migrations/20231206195936_add_cascades/migration.sql | 11 | ||||
| -rw-r--r-- | prisma/schema.prisma | 4 | ||||
| -rw-r--r-- | src/app/groups/[groupId]/expenses/[expenseId]/edit/page.tsx | 9 | ||||
| -rw-r--r-- | src/components/async-button.tsx | 42 | ||||
| -rw-r--r-- | src/components/expense-form.tsx | 27 | ||||
| -rw-r--r-- | src/components/group-form.tsx | 7 | ||||
| -rw-r--r-- | src/components/submit-button.tsx | 12 | ||||
| -rw-r--r-- | src/lib/api.ts | 8 |
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, |
