From 89417608277fec86a9aee0a5db9cd732fd9617ab Mon Sep 17 00:00:00 2001 From: Markus Thielker Date: Mon, 11 Mar 2024 00:36:36 +0100 Subject: [PATCH] N-FIN-7: add payment form --- src/app/payments/columns.tsx | 65 +++++ src/components/form/paymentForm.tsx | 361 ++++++++++++++++++++++++++++ 2 files changed, 426 insertions(+) create mode 100644 src/app/payments/columns.tsx create mode 100644 src/components/form/paymentForm.tsx diff --git a/src/app/payments/columns.tsx b/src/app/payments/columns.tsx new file mode 100644 index 0000000..b092e50 --- /dev/null +++ b/src/app/payments/columns.tsx @@ -0,0 +1,65 @@ +'use client'; + +import { ColumnDef } from '@tanstack/react-table'; +import { Category, Entity, Payment } from '@prisma/client'; +import { CellContext, ColumnDefTemplate } from '@tanstack/table-core'; +import { format } from 'date-fns'; + +export const columns = ( + actionCell: ColumnDefTemplate>, + entities: Entity[], + categories: Category[], +) => { + + return [ + { + accessorKey: 'date', + header: 'Date', + cell: ({row}) => { + return format(row.original.date, 'PPP'); + }, + }, + { + accessorKey: 'amount', + header: 'Amount', + cell: ({row}) => { + return new Intl.NumberFormat('en-US', { + style: 'currency', + currency: 'EUR', + }).format(row.getValue('amount') as number / 100); + }, + }, + { + accessorKey: 'payorId', + header: 'Payor', + cell: ({row}) => { + const entity = entities.find((entity) => entity.id === row.original.payorId); + return entity?.name ?? '-'; + }, + }, + { + accessorKey: 'payeeId', + header: 'Payee', + cell: ({row}) => { + const entity = entities.find((entity) => entity.id === row.original.payeeId); + return entity?.name ?? '-'; + }, + }, + { + accessorKey: 'categoryId', + header: 'Category', + cell: ({row}) => { + const category = categories.find((category) => category.id === row.original.categoryId); + return category?.name ?? '-'; + }, + }, + { + accessorKey: 'note', + header: 'Note', + }, + { + id: 'actions', + cell: actionCell, + }, + ] as ColumnDef[]; +}; diff --git a/src/components/form/paymentForm.tsx b/src/components/form/paymentForm.tsx new file mode 100644 index 0000000..ddf66dc --- /dev/null +++ b/src/components/form/paymentForm.tsx @@ -0,0 +1,361 @@ +'use client'; + +import { zodResolver } from '@hookform/resolvers/zod'; +import { useForm } from 'react-hook-form'; +import { z } from 'zod'; +import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form'; +import { Input } from '@/components/ui/input'; +import React, { useState } from 'react'; +import { Button } from '@/components/ui/button'; +import { ActionResponse } from '@/lib/types/ActionResponse'; +import { useRouter } from 'next/navigation'; +import { toast } from 'sonner'; +import { sonnerContent } from '@/components/ui/sonner'; +import { Category, Entity, Payment } from '@prisma/client'; +import { paymentFormSchema } from '@/lib/form-schemas/paymentFormSchema'; +import CurrencyInput from '@/components/ui/currency-input'; +import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'; +import { cn } from '@/lib/utils'; +import { CalendarIcon, Check, ChevronsUpDown } from 'lucide-react'; +import { format } from 'date-fns'; +import { Calendar } from '@/components/ui/calendar'; +import { ScrollArea, ScrollBar } from '@/components/ui/scroll-area'; +import { Textarea } from '@/components/ui/textarea'; + +export default function PaymentForm({value, entities, categories, onSubmit, className}: { + value: Payment | undefined, + entities: Entity[], + categories: Category[], + onSubmit: (data: z.infer) => Promise + className?: string +}) { + + const router = useRouter(); + + const [filter, setFilter] = useState(''); + + const [payorOpen, setPayorOpen] = useState(false); + const [payeeOpen, setPayeeOpen] = useState(false); + const [categoryOpen, setCategoryOpen] = useState(false); + + const form = useForm>({ + resolver: zodResolver(paymentFormSchema), + defaultValues: { + id: value?.id ?? undefined, + amount: value?.amount ?? 0, + date: value?.date ?? new Date(), + payorId: value?.payorId ?? undefined, + payeeId: value?.payeeId ?? undefined, + categoryId: value?.categoryId ?? undefined, + note: value?.note ?? '', + }, + }); + + const handleSubmit = async (data: z.infer) => { + const response = await onSubmit(data); + toast(sonnerContent(response)); + if (response.redirect) { + router.push(response.redirect); + } + }; + + const entitiesMapped = entities?.map((entity) => { + return { + label: entity.name, + value: entity.id, + }; + }) ?? []; + + const categoriesMapped = categories?.map((category) => { + return { + label: category.name, + value: category.id, + }; + }) ?? []; + + return ( +
+ + + ( + + + + + + + )} + /> + +
+ + + + ( + + Date + + + + + + + + { + field.onChange(e); + }} + initialFocus + /> + + + + + )} + /> + + ( + + Payor + { + setPayorOpen(open); + setFilter(''); + }}> + + + + + + + setFilter(e.target.value)} + className="flex h-10 w-full rounded-md border-b border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50" + placeholder="Search..."/> + + {entitiesMapped + .filter((entity) => entity.label.toLowerCase().includes(filter.toLowerCase())) + .map((item) => ( +
{ + field.onChange(item.value); + setPayorOpen(false); + }}> + + {item.label} +
+ ))} + +
+
+
+ +
+ )} + /> + + ( + + Payee + { + setPayeeOpen(open); + setFilter(''); + }}> + + + + + + + setFilter(e.target.value)} + className="flex h-10 w-full rounded-md border-b border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50" + placeholder="Search..."/> + + {entitiesMapped + .filter((entity) => entity.label.toLowerCase().includes(filter.toLowerCase())) + .map((item) => ( +
{ + field.onChange(item.value); + setPayeeOpen(false); + }} + > + + {item.label} +
+ ))} + +
+
+
+ +
+ )} + /> + + ( + + Category + { + setCategoryOpen(open); + setFilter(''); + }}> + + + + + + + setFilter(e.target.value)} + className="flex h-10 w-full rounded-md border-b border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50" + placeholder="Search..."/> + + {categoriesMapped + .filter((entity) => entity.label.toLowerCase().includes(filter.toLowerCase())) + .map((item) => ( +
{ + field.onChange(item.value); + setCategoryOpen(false); + }} + > + + {item.label} +
+ ))} + +
+
+
+ +
+ )} + /> +
+ + ( + + Note + +