From ba29ad4d9aedcae9907e71f9a8f39e508df30852 Mon Sep 17 00:00:00 2001 From: Manav Gupta <2006468@kiit.ac.in> Date: Sun, 28 Apr 2024 16:24:04 +0530 Subject: [PATCH 1/2] added : customer lists page --- apps/web/src/app/customers/filter.tsx | 24 +++ apps/web/src/app/customers/layout.tsx | 7 + apps/web/src/app/customers/lists.tsx | 192 ++++++++++++++++++++++ apps/web/src/app/customers/page.tsx | 89 ++++++++++ apps/web/src/components/customer-form.tsx | 6 +- apps/web/src/components/sidebar.tsx | 15 +- apps/web/src/crud/customer.ts | 37 +++++ 7 files changed, 360 insertions(+), 10 deletions(-) create mode 100644 apps/web/src/app/customers/filter.tsx create mode 100644 apps/web/src/app/customers/layout.tsx create mode 100644 apps/web/src/app/customers/lists.tsx create mode 100644 apps/web/src/app/customers/page.tsx create mode 100644 apps/web/src/crud/customer.ts diff --git a/apps/web/src/app/customers/filter.tsx b/apps/web/src/app/customers/filter.tsx new file mode 100644 index 0000000..9ae58fb --- /dev/null +++ b/apps/web/src/app/customers/filter.tsx @@ -0,0 +1,24 @@ +'use client' +import { Input } from '@/components/ui/input' +import { useState } from 'react' + +const CustomerFilter = () => { + const [search, setSearch] = useState('') + return ( +
+
+ { + setSearch(e.target.value) + }} + placeholder='Search Customer...' + type='text' + className='h-9 max-w-xs bg-transparent dark:border-zinc-700 border-zinc-200 hover:bg-zinc-100/80 hover:dark:bg-zinc-800/50 min-w-[300px]' + /> +
+
+ ) +} + +export default CustomerFilter diff --git a/apps/web/src/app/customers/layout.tsx b/apps/web/src/app/customers/layout.tsx new file mode 100644 index 0000000..bb55093 --- /dev/null +++ b/apps/web/src/app/customers/layout.tsx @@ -0,0 +1,7 @@ +export default function CutomersLayout({ + children +}: { + children: React.ReactNode +}) { + return <>{children} +} diff --git a/apps/web/src/app/customers/lists.tsx b/apps/web/src/app/customers/lists.tsx new file mode 100644 index 0000000..8f3280c --- /dev/null +++ b/apps/web/src/app/customers/lists.tsx @@ -0,0 +1,192 @@ +'use client' + +import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar' +import { Button } from '@/components/ui/button' +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger +} from '@/components/ui/dropdown-menu' +import { DotsHorizontalIcon } from '@radix-ui/react-icons' +import { + BookUser, + ChevronLeftIcon, + ChevronRightIcon, + User2Icon +} from 'lucide-react' +import { useState } from 'react' + +type CustomerInfo = { + id: string + legalName: string + whatsAppNumber: string + email: string +} + +const CustomerTable = ({ lists: customers }: { lists: CustomerInfo[] }) => { + const [currentPage, setPageNumber] = useState(1) + const customersPerPage = 10 + const itemOffset = (currentPage - 1) * customersPerPage + const endOffset = itemOffset + customersPerPage + const currentCustomers = customers?.slice(itemOffset, endOffset) + const pageCount = customers + ? Math.ceil(customers.length / customersPerPage) + : 0 + + const onHandleNextButton = () => { + if (currentPage === pageCount) { + return + } + setPageNumber((state) => { + return state + 1 + }) + } + + const onHandlePreviousButton = () => { + if (currentPage === 1) { + return + } + setPageNumber((state) => { + return state - 1 + }) + } + + return ( + <> +
+ + + + + + + + + + + +
#Legal NamePhone NumberEmail
+ {customers?.length === 0 && ( +
+ +

+ There are no customers to display at the moment. You're all caught + up! +

+
+ )} +
{' '} + {customers?.length > 0 && ( + + )} + + ) +} + +export default CustomerTable + +const CustomerBody = ({ customers }: { customers: CustomerInfo[] }) => { + return ( + + {customers?.map((customer, ind) => { + return ( + + {ind + 1} + +
+ + + + + + + + {customer?.legalName || '-'} + +
+ + {customer.whatsAppNumber} + {customer.email} + + + + + + + Customer Action + + Update + Delete + + + + + ) + })} + + ) +} + +const PaginationUI = ({ + pageCount, + currentPage, + onHandleNextButton, + onHandlePreviousButton +}: { + pageCount: number + currentPage: number + onHandleNextButton: () => void + onHandlePreviousButton: () => void +}) => { + return ( +
+
+ Page {currentPage} of {pageCount} +
+
+ + +
+
+ ) +} diff --git a/apps/web/src/app/customers/page.tsx b/apps/web/src/app/customers/page.tsx new file mode 100644 index 0000000..2889c2c --- /dev/null +++ b/apps/web/src/app/customers/page.tsx @@ -0,0 +1,89 @@ +import { Button } from '@/components/ui/button' +import CustomerFilter from './filter' +import { Sheet, SheetTrigger } from '@/components/ui/sheet' +import { CustomerForm } from '@/components/customer-form' +import { Suspense } from 'react' +import { Loader2Icon } from 'lucide-react' +import { getServerSession } from 'next-auth' +import { authOptions } from '../api/auth/[...nextauth]/route' +import Image from 'next/image' +import err from '@/svgs/err.svg' +import { getCustomers } from '@/crud/customer' +import CustomerTable from './lists' + +const CustomerLists = ({ + searchParams +}: { + searchParams: { [key: string]: string | string[] | undefined } +}) => { + const search = + typeof searchParams.search === 'string' ? searchParams.search : null + return ( + +
+
+
+

+ Overview +

+

Customer Lists

+
+ + + +
+ +
+ + +
+ +

Getting customers...

+
+
+ } + > + + +
+ + +
+ ) +} + +export default CustomerLists + +const Lists = async ({ query }: { query: string | null }) => { + const getCustomerLists = async () => { + const session = await getServerSession(authOptions) + const email = session?.user?.email + const response = await getCustomers(email, query) + return JSON.parse(response) + } + const customerLists = await getCustomerLists() + return ( + <> + {customerLists.ok ? ( + + ) : ( +
+ error +
+ )} + + ) +} diff --git a/apps/web/src/components/customer-form.tsx b/apps/web/src/components/customer-form.tsx index 406ea7c..2418960 100644 --- a/apps/web/src/components/customer-form.tsx +++ b/apps/web/src/components/customer-form.tsx @@ -1,3 +1,4 @@ +'use client' import { z } from 'zod' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' @@ -12,13 +13,15 @@ import { import { Customer } from '@/types/invoice' import { Loader } from 'lucide-react' import { useEffect, useState } from 'react' +import { useRouter } from 'next/navigation' import { toast } from 'sonner' -export const CustomerForm = ({ query }: { query: string }) => { +export const CustomerForm = ({ query }: { query?: string }) => { const [legalName, setLegalName] = useState(query) const [whatsAppNumber, setWhatsApp] = useState() const [email, setEmail] = useState() const [loading, setLoading] = useState(false) + const router = useRouter() useEffect(() => { setLegalName(query) }, [query]) @@ -58,6 +61,7 @@ export const CustomerForm = ({ query }: { query: string }) => { setEmail(undefined) setWhatsApp(undefined) setLegalName(undefined) + router.refresh() } else { toast.error('Something went wrong while creating customer') } diff --git a/apps/web/src/components/sidebar.tsx b/apps/web/src/components/sidebar.tsx index ddf1904..ddc379d 100644 --- a/apps/web/src/components/sidebar.tsx +++ b/apps/web/src/components/sidebar.tsx @@ -33,6 +33,7 @@ import { LogOut, Rocket, Settings, + UsersRound, Zap } from 'lucide-react' import { Beta } from './beta-badge' @@ -250,16 +251,12 @@ const SideItems = () => { // { label: 'Create', to: '/invoice/create' } // ] // } + }, + { + label: 'Customer', + to: '/customers', + icon: } - // { - // label: 'Customer', - // to: '/customer', - // icon: , - // subType: { - // status: true, - // values: [{ label: 'Crete', to: '/create' }] - // } - // } // { label: 'Customers', to: '/customers' }, // { label: 'Products', to: '/products' }, // { label: 'Setting', to: '/setting' } diff --git a/apps/web/src/crud/customer.ts b/apps/web/src/crud/customer.ts new file mode 100644 index 0000000..fc203ac --- /dev/null +++ b/apps/web/src/crud/customer.ts @@ -0,0 +1,37 @@ +import { db } from '@/lib/db' +import { getOrgId } from './organization' + +export const getCustomers = async ( + email: string | null | undefined, + query: string | null +) => { + try { + if (!email) { + console.error('Error:', 'Not Authorized') + return JSON.stringify({ ok: false, data: null, status: 401 }) + } + const orgId = await getOrgId(email) + + if (!orgId) { + console.error('Error:', 'Organization Not Found') + return JSON.stringify({ ok: false, data: null, status: 404 }) + } + + const response = await db.customerInfo.findMany({ + where: { + organisationId: orgId + }, + select: { + id: true, + email: true, + whatsAppNumber: true, + legalName: true + } + }) + + return JSON.stringify({ ok: true, data: response, status: 200 }) + } catch (error) { + console.error('Error:', error) + return JSON.stringify({ ok: false, data: null, status: 500 }) + } +} From de55ae3099f0ec8d24c76520cbe24ebaa571226c Mon Sep 17 00:00:00 2001 From: Manav Gupta <2006468@kiit.ac.in> Date: Mon, 29 Apr 2024 23:33:25 +0530 Subject: [PATCH 2/2] fix : foreign key constraint with ON DELETE CASCADE to Invoice table --- apps/web/src/app/api/customer/[id]/route.ts | 42 ++++++ apps/web/src/app/customers/lists.tsx | 124 +++++++++++++++++- .../migration.sql | 5 + packages/db/prisma/schema.prisma | 2 +- 4 files changed, 169 insertions(+), 4 deletions(-) create mode 100644 apps/web/src/app/api/customer/[id]/route.ts create mode 100644 packages/db/prisma/migrations/20240429175343_added_delete_cascade/migration.sql diff --git a/apps/web/src/app/api/customer/[id]/route.ts b/apps/web/src/app/api/customer/[id]/route.ts new file mode 100644 index 0000000..c817019 --- /dev/null +++ b/apps/web/src/app/api/customer/[id]/route.ts @@ -0,0 +1,42 @@ +import { getOrgId } from '@/crud/organization' +import { db } from '@/lib/db' +import { getServerSession } from 'next-auth' +import { authOptions } from '../../auth/[...nextauth]/route' + +export async function DELETE( + request: Request, + { params }: { params: { id: string } } +) { + try { + const session = await getServerSession(authOptions) + const email = session?.user?.email + if (!email) { + console.error('Error:', 'Not Authorized') + return Response.json({ ok: false, data: null, status: 401 }) + } + const orgId = await getOrgId(email) + + if (!orgId) { + console.error('Error:', 'Organization Not Found') + return Response.json({ ok: false, data: null, status: 404 }) + } + + const customerId = params.id + + const response = await db.customerInfo.delete({ + where: { + id: customerId, + organisationId: orgId + } + }) + + return Response.json({ + ok: true, + data: response.legalName, + status: 200 + }) + } catch (error) { + console.error('Error:', error) + return Response.json({ ok: false, data: null, status: 500 }) + } +} diff --git a/apps/web/src/app/customers/lists.tsx b/apps/web/src/app/customers/lists.tsx index 8f3280c..afa104d 100644 --- a/apps/web/src/app/customers/lists.tsx +++ b/apps/web/src/app/customers/lists.tsx @@ -10,14 +10,25 @@ import { DropdownMenuSeparator, DropdownMenuTrigger } from '@/components/ui/dropdown-menu' +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle +} from '@/components/ui/dialog' import { DotsHorizontalIcon } from '@radix-ui/react-icons' import { BookUser, ChevronLeftIcon, ChevronRightIcon, + Loader, User2Icon } from 'lucide-react' +import { useRouter } from 'next/navigation' import { useState } from 'react' +import { toast } from 'sonner' type CustomerInfo = { id: string @@ -27,6 +38,10 @@ type CustomerInfo = { } const CustomerTable = ({ lists: customers }: { lists: CustomerInfo[] }) => { + const [deleteAlert, setAlertForDelete] = useState(false) + const [selectedCustomerToDelete, setCustomerToDelete] = useState< + string | null + >(null) const [currentPage, setPageNumber] = useState(1) const customersPerPage = 10 const itemOffset = (currentPage - 1) * customersPerPage @@ -54,6 +69,15 @@ const CustomerTable = ({ lists: customers }: { lists: CustomerInfo[] }) => { }) } + const onSelectInvoiceToDelete = (customerId: string) => { + setCustomerToDelete(customerId) + setAlertForDelete(true) + } + + const onCloseDeleteAlertDialog = () => { + setAlertForDelete(false) + } + return ( <>
@@ -67,7 +91,10 @@ const CustomerTable = ({ lists: customers }: { lists: CustomerInfo[] }) => { - + {customers?.length === 0 && (
@@ -87,13 +114,27 @@ const CustomerTable = ({ lists: customers }: { lists: CustomerInfo[] }) => { onHandlePreviousButton={onHandlePreviousButton} /> )} + + { + return value.id === selectedCustomerToDelete + })} + onCloseAlertDialog={onCloseDeleteAlertDialog} + /> + ) } export default CustomerTable -const CustomerBody = ({ customers }: { customers: CustomerInfo[] }) => { +const CustomerBody = ({ + customers, + onSelectCustomerToDelete +}: { + customers: CustomerInfo[] + onSelectCustomerToDelete: (customerId: string) => void +}) => { return ( {customers?.map((customer, ind) => { @@ -140,7 +181,14 @@ const CustomerBody = ({ customers }: { customers: CustomerInfo[] }) => { Customer Action Update - Delete + { + onSelectCustomerToDelete(customer.id) + }} + > + Delete + @@ -190,3 +238,73 @@ const PaginationUI = ({
) } + +const DeleteAlert = ({ + customerInfo, + onCloseAlertDialog +}: { + customerInfo: CustomerInfo | undefined + onCloseAlertDialog: () => void +}) => { + const [isDeleting, setDeleting] = useState(false) + const router = useRouter() + + console.log(customerInfo) + async function onDeleteInvoice() { + setDeleting(true) + try { + const response = await fetch(`/api/customer/${customerInfo?.id}`, { + method: 'DELETE' + }) + const delRes = await response.json() + if (delRes.ok) { + toast.success( + 'You successfully deleted customer ' + customerInfo?.legalName, + { + position: 'top-right' + } + ) + setDeleting(false) + router.refresh() + onCloseAlertDialog() + return + } else { + throw new Error(`Error Deleting: ${delRes.data}`) + } + } catch (error) { + console.error('Error deleting customer:', error) + toast.error(`Error deleting customer: ${error}`, { + position: 'top-right' + }) + setDeleting(false) + } + } + + return ( + + + Delete customer {customerInfo?.legalName} + + Are you sure you want to delete? This action cannot be undone. + + + + + + + + ) +} diff --git a/packages/db/prisma/migrations/20240429175343_added_delete_cascade/migration.sql b/packages/db/prisma/migrations/20240429175343_added_delete_cascade/migration.sql new file mode 100644 index 0000000..71220de --- /dev/null +++ b/packages/db/prisma/migrations/20240429175343_added_delete_cascade/migration.sql @@ -0,0 +1,5 @@ +-- DropForeignKey +ALTER TABLE "Invoice" DROP CONSTRAINT "Invoice_customerId_fkey"; + +-- AddForeignKey +ALTER TABLE "Invoice" ADD CONSTRAINT "Invoice_customerId_fkey" FOREIGN KEY ("customerId") REFERENCES "CustomerInfo"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/packages/db/prisma/schema.prisma b/packages/db/prisma/schema.prisma index bb11fd1..a84bca2 100644 --- a/packages/db/prisma/schema.prisma +++ b/packages/db/prisma/schema.prisma @@ -151,7 +151,7 @@ model Invoice { instantInvoiceLink String? sendingMethod SendMethods @default(MAIL) customerId String - customerInfo CustomerInfo @relation(fields: [customerId], references: [id]) + customerInfo CustomerInfo @relation(fields: [customerId], references: [id], onDelete: Cascade) dateIssue DateTime dueDate DateTime shippingCharge Float?