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/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..afa104d
--- /dev/null
+++ b/apps/web/src/app/customers/lists.tsx
@@ -0,0 +1,310 @@
+'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 {
+ 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
+ legalName: string
+ whatsAppNumber: string
+ email: string
+}
+
+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
+ 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
+ })
+ }
+
+ const onSelectInvoiceToDelete = (customerId: string) => {
+ setCustomerToDelete(customerId)
+ setAlertForDelete(true)
+ }
+
+ const onCloseDeleteAlertDialog = () => {
+ setAlertForDelete(false)
+ }
+
+ return (
+ <>
+
+
+
+
+ # |
+ Legal Name |
+ Phone Number |
+ Email |
+ |
+
+
+
+
+ {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,
+ onSelectCustomerToDelete
+}: {
+ customers: CustomerInfo[]
+ onSelectCustomerToDelete: (customerId: string) => void
+}) => {
+ return (
+
+ {customers?.map((customer, ind) => {
+ return (
+
+ {ind + 1} |
+
+
+
+
+
+
+
+
+
+ {customer?.legalName || '-'}
+
+
+ |
+ {customer.whatsAppNumber} |
+ {customer.email} |
+
+
+
+
+
+
+ Customer Action
+
+ Update
+ {
+ onSelectCustomerToDelete(customer.id)
+ }}
+ >
+ Delete
+
+
+
+ |
+
+ )
+ })}
+
+ )
+}
+
+const PaginationUI = ({
+ pageCount,
+ currentPage,
+ onHandleNextButton,
+ onHandlePreviousButton
+}: {
+ pageCount: number
+ currentPage: number
+ onHandleNextButton: () => void
+ onHandlePreviousButton: () => void
+}) => {
+ return (
+
+
+ Page {currentPage} of {pageCount}
+
+
+
+
+
+
+ )
+}
+
+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/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 ? (
+
+ ) : (
+
+
+
+ )}
+ >
+ )
+}
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 })
+ }
+}
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?