#
tokens: 48920/50000 82/85 files (page 1/2)
lines: on (toggle) GitHub
raw markdown copy reset
This is page 1 of 2. Use http://codebase.md/ralfarishi/cheese-stick-koe-dashboard?lines=true&page={x} to view the full context.

# Directory Structure

```
├── .gitignore
├── components.json
├── eslint.config.mjs
├── jsconfig.json
├── middleware.js
├── next.config.mjs
├── package-lock.json
├── package.json
├── postcss.config.mjs
├── public
│   ├── file.svg
│   ├── globe.svg
│   ├── logo.png
│   ├── next.svg
│   ├── qris.png
│   ├── vercel.svg
│   └── window.svg
├── README.md
└── src
    ├── app
    │   ├── _components
    │   │   └── LoginForm.jsx
    │   ├── api
    │   │   └── logout
    │   │       └── route.js
    │   ├── dashboard
    │   │   ├── invoices
    │   │   │   ├── _components
    │   │   │   │   ├── DeleteInvoiceModal.jsx
    │   │   │   │   ├── InvoiceDownloadModal.jsx
    │   │   │   │   ├── InvoicePreview.jsx
    │   │   │   │   └── StatusCombobox.jsx
    │   │   │   ├── [invoiceNumber]
    │   │   │   │   └── page.jsx
    │   │   │   ├── create
    │   │   │   │   ├── _components
    │   │   │   │   │   ├── ProductsCombobox.jsx
    │   │   │   │   │   └── SizeCombobox.jsx
    │   │   │   │   ├── CreateInvoicePage.jsx
    │   │   │   │   └── page.js
    │   │   │   ├── InvoicePage.jsx
    │   │   │   ├── page.js
    │   │   │   ├── Table.jsx
    │   │   │   └── UpdateInvoiceForm.jsx
    │   │   ├── layout.jsx
    │   │   ├── page.jsx
    │   │   ├── products
    │   │   │   ├── _components
    │   │   │   │   ├── ProductDeleteModal.jsx
    │   │   │   │   ├── ProductEditModal.jsx
    │   │   │   │   ├── ProductModal.jsx
    │   │   │   │   ├── ProductModalButton.jsx
    │   │   │   │   └── ProductTable.jsx
    │   │   │   ├── page.js
    │   │   │   └── ProductPage.jsx
    │   │   └── size-pricing
    │   │       ├── _components
    │   │       │   ├── AddSizeButton.jsx
    │   │       │   ├── AddSizeModal.jsx
    │   │       │   ├── DeleteSizeModal.jsx
    │   │       │   ├── EditSizeModal.jsx
    │   │       │   └── Table.jsx
    │   │       ├── page.js
    │   │       └── SizePage.jsx
    │   ├── favicon.ico
    │   ├── globals.css
    │   ├── layout.js
    │   ├── not-found.js
    │   ├── page.js
    │   └── unauthorized.js
    ├── components
    │   ├── dashboard
    │   │   ├── DatePicker.jsx
    │   │   ├── Modal.jsx
    │   │   └── Sidebar.jsx
    │   └── ui
    │       ├── badge.jsx
    │       ├── button.jsx
    │       ├── calendar.jsx
    │       ├── card.jsx
    │       ├── command.jsx
    │       ├── dialog.jsx
    │       ├── drawer.jsx
    │       ├── input.jsx
    │       ├── label.jsx
    │       ├── popover.jsx
    │       ├── radio-group.jsx
    │       ├── select.jsx
    │       └── sonner.jsx
    └── lib
        ├── actions
        │   ├── invoice
        │   │   ├── deleteInvoice.js
        │   │   ├── getAll.js
        │   │   ├── getInvoiceByNumber.js
        │   │   ├── getInvoiceWithItem.js
        │   │   ├── submitInvoice.js
        │   │   └── updateInvoice.js
        │   ├── products
        │   │   ├── addProduct.js
        │   │   ├── deleteProduct.js
        │   │   ├── getAllProducts.js
        │   │   └── updateProduct.js
        │   └── size-price
        │       ├── addSize.js
        │       ├── deleteSize.js
        │       ├── getAll.js
        │       └── updateSize.js
        ├── exportToPng.js
        ├── supabaseBrowser.js
        ├── supabaseServer.js
        ├── utils.js
        └── verifySession.js
```

# Files

--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------

```
 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
 2 | 
 3 | # dependencies
 4 | /node_modules
 5 | /.pnp
 6 | .pnp.*
 7 | .yarn/*
 8 | !.yarn/patches
 9 | !.yarn/plugins
10 | !.yarn/releases
11 | !.yarn/versions
12 | 
13 | # testing
14 | /coverage
15 | 
16 | # next.js
17 | /.next/
18 | /out/
19 | 
20 | # production
21 | /build
22 | 
23 | # misc
24 | .DS_Store
25 | *.pem
26 | 
27 | # debug
28 | npm-debug.log*
29 | yarn-debug.log*
30 | yarn-error.log*
31 | .pnpm-debug.log*
32 | 
33 | # env files (can opt-in for committing if needed)
34 | .env*
35 | 
36 | # vercel
37 | .vercel
38 | 
39 | # typescript
40 | *.tsbuildinfo
41 | next-env.d.ts
42 | 
43 | /src/generated/prisma
44 | 
45 | prisma
46 | 
47 | db_backups
```

--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------

```markdown
1 | Tech stack:
2 | 
3 | - Next.js
4 | - shadcn + tailwind
5 | - supabase + prisma
6 | 
7 | Please visit [Cheese Stick Koe](https://cheesestick-koe.my.id). Thanks ♥️
8 | 
```

--------------------------------------------------------------------------------
/jsconfig.json:
--------------------------------------------------------------------------------

```json
1 | {
2 |   "compilerOptions": {
3 |     "paths": {
4 |       "@/*": ["./src/*"]
5 |     }
6 |   }
7 | }
8 | 
```

--------------------------------------------------------------------------------
/postcss.config.mjs:
--------------------------------------------------------------------------------

```
1 | const config = {
2 |   plugins: ["@tailwindcss/postcss"],
3 | };
4 | 
5 | export default config;
6 | 
```

--------------------------------------------------------------------------------
/public/vercel.svg:
--------------------------------------------------------------------------------

```
1 | <svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>
```

--------------------------------------------------------------------------------
/next.config.mjs:
--------------------------------------------------------------------------------

```
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {
3 | 	experimental: {
4 | 		authInterrupts: true,
5 | 	},
6 | };
7 | 
8 | export default nextConfig;
9 | 
```

--------------------------------------------------------------------------------
/src/lib/supabaseBrowser.js:
--------------------------------------------------------------------------------

```javascript
1 | import { createBrowserClient } from "@supabase/ssr";
2 | 
3 | export function supabaseBrowser() {
4 | 	return createBrowserClient(
5 | 		process.env.NEXT_PUBLIC_SUPABASE_URL,
6 | 		process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY
7 | 	);
8 | }
9 | 
```

--------------------------------------------------------------------------------
/src/lib/verifySession.js:
--------------------------------------------------------------------------------

```javascript
 1 | import { supabaseServer } from "@/lib/supabaseServer";
 2 | 
 3 | export async function verifySession() {
 4 | 	const supabase = await supabaseServer();
 5 | 	const {
 6 | 		data: { session },
 7 | 	} = await supabase.auth.getSession();
 8 | 
 9 | 	return session;
10 | }
11 | 
```

--------------------------------------------------------------------------------
/src/app/dashboard/invoices/page.js:
--------------------------------------------------------------------------------

```javascript
 1 | import { unauthorized } from "next/navigation";
 2 | 
 3 | import { verifySession } from "@/lib/verifySession";
 4 | 
 5 | import InvoicePage from "./InvoicePage";
 6 | 
 7 | export default async function InvoicesPage() {
 8 | 	const session = await verifySession();
 9 | 
10 | 	if (!session) {
11 | 		unauthorized();
12 | 	}
13 | 
14 | 	return <InvoicePage />;
15 | }
16 | 
```

--------------------------------------------------------------------------------
/src/app/dashboard/invoices/create/page.js:
--------------------------------------------------------------------------------

```javascript
 1 | import { unauthorized } from "next/navigation";
 2 | 
 3 | import { verifySession } from "@/lib/verifySession";
 4 | 
 5 | import CreateInvoicePage from "./CreateInvoicePage";
 6 | 
 7 | export default async function Page() {
 8 | 	const session = await verifySession();
 9 | 
10 | 	if (!session) {
11 | 		unauthorized();
12 | 	}
13 | 
14 | 	return <CreateInvoicePage />;
15 | }
16 | 
```

--------------------------------------------------------------------------------
/src/lib/actions/size-price/addSize.js:
--------------------------------------------------------------------------------

```javascript
 1 | "use server";
 2 | 
 3 | import { supabaseServer } from "@/lib/supabaseServer";
 4 | 
 5 | export async function addSize({ size, price }) {
 6 | 	const supabase = await supabaseServer();
 7 | 	const { data, error } = await supabase
 8 | 		.from("ProductSizePrice")
 9 | 		.insert([{ size, price }])
10 | 		.select()
11 | 		.single();
12 | 	return { data, error };
13 | }
14 | 
```

--------------------------------------------------------------------------------
/src/app/api/logout/route.js:
--------------------------------------------------------------------------------

```javascript
 1 | import { cookies } from "next/headers";
 2 | import { NextResponse } from "next/server";
 3 | 
 4 | import { createRouteHandlerClient } from "@supabase/auth-helpers-nextjs";
 5 | 
 6 | export async function POST() {
 7 | 	const supabase = createRouteHandlerClient({ cookies });
 8 | 
 9 | 	await supabase.auth.signOut();
10 | 
11 | 	return NextResponse.json({ message: "Logged out" });
12 | }
13 | 
```

--------------------------------------------------------------------------------
/src/lib/actions/products/addProduct.js:
--------------------------------------------------------------------------------

```javascript
 1 | "use server";
 2 | 
 3 | import { supabaseServer } from "@/lib/supabaseServer";
 4 | 
 5 | export async function addProduct({ name, description }) {
 6 | 	const supabase = await supabaseServer();
 7 | 	const { data, error } = await supabase
 8 | 		.from("Product")
 9 | 		.insert([{ name, description: description || null }])
10 | 		.select()
11 | 		.single();
12 | 	return { data, error };
13 | }
14 | 
```

--------------------------------------------------------------------------------
/src/app/dashboard/layout.jsx:
--------------------------------------------------------------------------------

```javascript
 1 | import { Sidebar } from "@/components/dashboard/Sidebar";
 2 | 
 3 | export default function DashboardLayout({ children }) {
 4 | 	return (
 5 | 		<div className="min-h-screen flex flex-col md:flex-row bg-[#fffaf0]">
 6 | 			<Sidebar />
 7 | 			<main className="flex-1 p-4 md:p-6 bg-white rounded-tl-xl md:rounded-tl-none shadow-inner">
 8 | 				{children}
 9 | 			</main>
10 | 		</div>
11 | 	);
12 | }
13 | 
```

--------------------------------------------------------------------------------
/src/lib/actions/products/getAllProducts.js:
--------------------------------------------------------------------------------

```javascript
 1 | "use server";
 2 | 
 3 | import { supabaseServer } from "@/lib/supabaseServer";
 4 | 
 5 | export async function getAllProducts(sortOrder = "asc") {
 6 | 	const supabase = await supabaseServer();
 7 | 
 8 | 	const { data, error } = await supabase
 9 | 		.from("Product")
10 | 		.select("id, name, description, createdAt")
11 | 		.order("name", { ascending: sortOrder === "asc" });
12 | 
13 | 	return { data, error };
14 | }
15 | 
```

--------------------------------------------------------------------------------
/src/lib/actions/size-price/getAll.js:
--------------------------------------------------------------------------------

```javascript
 1 | "use server";
 2 | 
 3 | import { supabaseServer } from "@/lib/supabaseServer";
 4 | 
 5 | export async function getAllSizePrice(sortOrder = "asc") {
 6 | 	const supabase = await supabaseServer();
 7 | 
 8 | 	const { data, error } = await supabase
 9 | 		.from("ProductSizePrice")
10 | 		.select("id, size, price, createdAt")
11 | 		.order("size", { ascending: sortOrder === "asc" });
12 | 
13 | 	return { data, error };
14 | }
15 | 
```

--------------------------------------------------------------------------------
/eslint.config.mjs:
--------------------------------------------------------------------------------

```
 1 | import { dirname } from "path";
 2 | import { fileURLToPath } from "url";
 3 | import { FlatCompat } from "@eslint/eslintrc";
 4 | 
 5 | const __filename = fileURLToPath(import.meta.url);
 6 | const __dirname = dirname(__filename);
 7 | 
 8 | const compat = new FlatCompat({
 9 |   baseDirectory: __dirname,
10 | });
11 | 
12 | const eslintConfig = [...compat.extends("next/core-web-vitals")];
13 | 
14 | export default eslintConfig;
15 | 
```

--------------------------------------------------------------------------------
/src/app/dashboard/size-pricing/page.js:
--------------------------------------------------------------------------------

```javascript
 1 | import { unauthorized } from "next/navigation";
 2 | 
 3 | import { getPageTitle } from "@/lib/utils";
 4 | import { verifySession } from "@/lib/verifySession";
 5 | 
 6 | import SizePage from "./SizePage";
 7 | 
 8 | export const metadata = {
 9 | 	title: getPageTitle("Size"),
10 | };
11 | 
12 | export default function page() {
13 | 	const session = verifySession();
14 | 
15 | 	if (!session) {
16 | 		unauthorized();
17 | 	}
18 | 
19 | 	return <SizePage />;
20 | }
21 | 
```

--------------------------------------------------------------------------------
/src/app/page.js:
--------------------------------------------------------------------------------

```javascript
 1 | import { redirect } from "next/navigation";
 2 | 
 3 | import { supabaseServer } from "@/lib/supabaseServer";
 4 | 
 5 | import LoginForm from "./_components/LoginForm";
 6 | 
 7 | export default async function LoginPage() {
 8 | 	const supabase = await supabaseServer();
 9 | 
10 | 	const {
11 | 		data: { session },
12 | 	} = await supabase.auth.getSession();
13 | 
14 | 	if (session) {
15 | 		redirect("/dashboard");
16 | 	}
17 | 
18 | 	return <LoginForm />;
19 | }
20 | 
```

--------------------------------------------------------------------------------
/public/window.svg:
--------------------------------------------------------------------------------

```
1 | <svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>
```

--------------------------------------------------------------------------------
/src/app/dashboard/products/page.js:
--------------------------------------------------------------------------------

```javascript
 1 | import { unauthorized } from "next/navigation";
 2 | 
 3 | import { getPageTitle } from "@/lib/utils";
 4 | import { verifySession } from "@/lib/verifySession";
 5 | 
 6 | import ProductPage from "./ProductPage";
 7 | 
 8 | export const metadata = {
 9 | 	title: getPageTitle("Products"),
10 | };
11 | 
12 | export default function Page() {
13 | 	const session = verifySession();
14 | 
15 | 	if (!session) {
16 | 		unauthorized();
17 | 	}
18 | 
19 | 	return <ProductPage />;
20 | }
21 | 
```

--------------------------------------------------------------------------------
/public/file.svg:
--------------------------------------------------------------------------------

```
1 | <svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>
```

--------------------------------------------------------------------------------
/src/lib/actions/invoice/getAll.js:
--------------------------------------------------------------------------------

```javascript
 1 | "use server";
 2 | 
 3 | import { supabaseServer } from "@/lib/supabaseServer";
 4 | 
 5 | export async function getAllInvoice(sortOrder = "asc") {
 6 | 	const supabase = await supabaseServer();
 7 | 
 8 | 	const { data, error } = await supabase
 9 | 		.from("Invoice")
10 | 		.select("id, invoiceNumber, buyerName, totalPrice, invoiceDate, status, createdAt")
11 | 		.order("invoiceNumber", { ascending: sortOrder === "asc" });
12 | 
13 | 	return { data, error };
14 | }
15 | 
```

--------------------------------------------------------------------------------
/components.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 |   "$schema": "https://ui.shadcn.com/schema.json",
 3 |   "style": "new-york",
 4 |   "rsc": true,
 5 |   "tsx": false,
 6 |   "tailwind": {
 7 |     "config": "",
 8 |     "css": "src/app/globals.css",
 9 |     "baseColor": "neutral",
10 |     "cssVariables": true,
11 |     "prefix": ""
12 |   },
13 |   "aliases": {
14 |     "components": "@/components",
15 |     "utils": "@/lib/utils",
16 |     "ui": "@/components/ui",
17 |     "lib": "@/lib",
18 |     "hooks": "@/hooks"
19 |   },
20 |   "iconLibrary": "lucide"
21 | }
```

--------------------------------------------------------------------------------
/src/app/not-found.js:
--------------------------------------------------------------------------------

```javascript
 1 | export default function NotFound() {
 2 | 	return (
 3 | 		<main className="grid min-h-screen place-items-center bg-white px-6 py-24 sm:py-32 lg:px-8">
 4 | 			<div className="text-center">
 5 | 				<p className="text-9xl font-semibold text-[#6d2315]">404</p>
 6 | 				<h1 className="mt-4 text-2xl font-bold tracking-tight text-gray-500 sm:text-5xl">
 7 | 					Not Found
 8 | 				</h1>
 9 | 				<p className="mt-6 text-lg leading-8 text-gray-400">Could not find requested resource</p>
10 | 			</div>
11 | 		</main>
12 | 	);
13 | }
14 | 
```

--------------------------------------------------------------------------------
/src/lib/actions/invoice/getInvoiceWithItem.js:
--------------------------------------------------------------------------------

```javascript
 1 | import { supabaseBrowser } from "@/lib/supabaseBrowser";
 2 | 
 3 | export async function getInvoiceWithItems(invoiceId) {
 4 | 	const supabase = supabaseBrowser();
 5 | 
 6 | 	const { data: invoice, error: err1 } = await supabase
 7 | 		.from("Invoice")
 8 | 		.select("*")
 9 | 		.eq("id", invoiceId)
10 | 		.single();
11 | 
12 | 	const { data: items, error: err2 } = await supabase
13 | 		.from("InvoiceItem")
14 | 		.select("*, product:Product(name), size:ProductSizePrice(size)")
15 | 		.eq("invoiceId", invoiceId);
16 | 
17 | 	return { invoice, items };
18 | }
19 | 
```

--------------------------------------------------------------------------------
/src/components/ui/sonner.jsx:
--------------------------------------------------------------------------------

```javascript
 1 | "use client"
 2 | 
 3 | import { useTheme } from "next-themes"
 4 | import { Toaster as Sonner } from "sonner";
 5 | 
 6 | const Toaster = ({
 7 |   ...props
 8 | }) => {
 9 |   const { theme = "system" } = useTheme()
10 | 
11 |   return (
12 |     <Sonner
13 |       theme={theme}
14 |       className="toaster group"
15 |       style={
16 |         {
17 |           "--normal-bg": "var(--popover)",
18 |           "--normal-text": "var(--popover-foreground)",
19 |           "--normal-border": "var(--border)"
20 |         }
21 |       }
22 |       {...props} />
23 |   );
24 | }
25 | 
26 | export { Toaster }
27 | 
```

--------------------------------------------------------------------------------
/src/app/unauthorized.js:
--------------------------------------------------------------------------------

```javascript
 1 | export default function UnauthorizedPage() {
 2 | 	return (
 3 | 		<main className="grid min-h-screen place-items-center bg-white px-6 py-24 sm:py-32 lg:px-8">
 4 | 			<div className="text-center">
 5 | 				<p className="text-9xl font-semibold text-[#6d2315]">401</p>
 6 | 				<h1 className="mt-4 text-2xl font-bold tracking-tight text-gray-500 sm:text-5xl">
 7 | 					Unauthorized Access
 8 | 				</h1>
 9 | 				<p className="mt-6 text-lg leading-8 text-gray-400">
10 | 					Sorry, you don&apos;t have permission to access this page. account.
11 | 				</p>
12 | 			</div>
13 | 		</main>
14 | 	);
15 | }
16 | 
```

--------------------------------------------------------------------------------
/src/app/dashboard/size-pricing/_components/AddSizeButton.jsx:
--------------------------------------------------------------------------------

```javascript
 1 | "use client";
 2 | 
 3 | import { useState } from "react";
 4 | 
 5 | import { Button } from "@/components/ui/button";
 6 | import AddSizeModal from "./AddSizeModal";
 7 | 
 8 | export default function AddSizeButton({ onSizeAdded }) {
 9 | 	const [open, setOpen] = useState(false);
10 | 
11 | 	return (
12 | 		<>
13 | 			<Button
14 | 				onClick={() => setOpen(true)}
15 | 				className="bg-[#6D2315] hover:bg-[#591c10] text-white px-4 py-2 rounded-md"
16 | 			>
17 | 				Add Size
18 | 			</Button>
19 | 			<AddSizeModal
20 | 				open={open}
21 | 				setOpen={setOpen}
22 | 				onSuccess={() => {
23 | 					onSizeAdded?.();
24 | 				}}
25 | 			/>
26 | 		</>
27 | 	);
28 | }
29 | 
```

--------------------------------------------------------------------------------
/src/lib/actions/invoice/getInvoiceByNumber.js:
--------------------------------------------------------------------------------

```javascript
 1 | import { supabaseServer } from "@/lib/supabaseServer";
 2 | 
 3 | export const getInvoiceByNumber = async (invoiceNumber) => {
 4 | 	const supabase = await supabaseServer();
 5 | 
 6 | 	const { data: invoice, error } = await supabase
 7 | 		.from("Invoice")
 8 | 		.select(
 9 | 			`
10 | 			*,
11 | 			items:InvoiceItem(
12 | 				id,
13 | 				productId,
14 | 				sizePriceId,
15 | 				quantity,
16 | 				discountAmount,
17 | 				product:Product(id, name),
18 | 				sizePrice:ProductSizePrice(id, price, size)
19 | 			)
20 | 		`
21 | 		)
22 | 		.eq("invoiceNumber", invoiceNumber)
23 | 		.single();
24 | 
25 | 	if (error) return { error };
26 | 	return { data: invoice };
27 | };
28 | 
```

--------------------------------------------------------------------------------
/src/app/dashboard/products/_components/ProductModalButton.jsx:
--------------------------------------------------------------------------------

```javascript
 1 | "use client";
 2 | 
 3 | import { useState } from "react";
 4 | 
 5 | import { Button } from "@/components/ui/button";
 6 | 
 7 | import ProductModal from "./ProductModal";
 8 | 
 9 | export default function ProductModalButton({ onProductAdded }) {
10 | 	const [open, setOpen] = useState(false);
11 | 
12 | 	return (
13 | 		<>
14 | 			<Button
15 | 				onClick={() => setOpen(true)}
16 | 				className="bg-[#6D2315] hover:bg-[#591c10] text-white px-4 py-2 rounded-md"
17 | 			>
18 | 				Add Product
19 | 			</Button>
20 | 			<ProductModal
21 | 				open={open}
22 | 				setOpen={setOpen}
23 | 				onSuccess={() => {
24 | 					onProductAdded?.();
25 | 				}}
26 | 			/>
27 | 		</>
28 | 	);
29 | }
30 | 
```

--------------------------------------------------------------------------------
/src/components/ui/label.jsx:
--------------------------------------------------------------------------------

```javascript
 1 | "use client"
 2 | 
 3 | import * as React from "react"
 4 | import * as LabelPrimitive from "@radix-ui/react-label"
 5 | 
 6 | import { cn } from "@/lib/utils"
 7 | 
 8 | function Label({
 9 |   className,
10 |   ...props
11 | }) {
12 |   return (
13 |     <LabelPrimitive.Root
14 |       data-slot="label"
15 |       className={cn(
16 |         "flex items-center gap-2 text-sm leading-none font-medium select-none group-data-[disabled=true]:pointer-events-none group-data-[disabled=true]:opacity-50 peer-disabled:cursor-not-allowed peer-disabled:opacity-50",
17 |         className
18 |       )}
19 |       {...props} />
20 |   );
21 | }
22 | 
23 | export { Label }
24 | 
```

--------------------------------------------------------------------------------
/src/app/dashboard/size-pricing/SizePage.jsx:
--------------------------------------------------------------------------------

```javascript
 1 | "use client";
 2 | 
 3 | import { useRef } from "react";
 4 | 
 5 | import SizePriceTable from "./_components/Table";
 6 | import AddSizeButton from "./_components/AddSizeButton";
 7 | 
 8 | export default function SizePage() {
 9 | 	const tableRef = useRef();
10 | 
11 | 	return (
12 | 		<section className="p-4 space-y-4">
13 | 			<div className="flex justify-between items-center mb-2">
14 | 				<h1 className="text-2xl font-bold text-[#6D2315] tracking-tight">Size & Price List</h1>
15 | 				<AddSizeButton
16 | 					onSizeAdded={() => {
17 | 						tableRef.current.refetch();
18 | 					}}
19 | 				/>
20 | 			</div>
21 | 
22 | 			<SizePriceTable ref={tableRef} />
23 | 		</section>
24 | 	);
25 | }
26 | 
```

--------------------------------------------------------------------------------
/src/lib/supabaseServer.js:
--------------------------------------------------------------------------------

```javascript
 1 | import { createServerClient } from "@supabase/ssr";
 2 | import { cookies } from "next/headers";
 3 | 
 4 | export const supabaseServer = async () => {
 5 | 	const cookieStore = await cookies();
 6 | 
 7 | 	return createServerClient(
 8 | 		process.env.NEXT_PUBLIC_SUPABASE_URL,
 9 | 		process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY,
10 | 		{
11 | 			cookies: {
12 | 				getAll: async () => cookieStore.getAll(),
13 | 				setAll: async (cookiesToSet) => {
14 | 					cookiesToSet.forEach(({ name, value, options }) => cookieStore.set(name, value, options));
15 | 				},
16 | 				remove: async (name, options) => cookieStore.delete(name, options),
17 | 			},
18 | 		}
19 | 	);
20 | };
21 | 
```

--------------------------------------------------------------------------------
/src/lib/exportToPng.js:
--------------------------------------------------------------------------------

```javascript
 1 | import html2canvas from "html2canvas-pro";
 2 | 
 3 | export async function exportInvoiceToPng(element, filename = "Invoice.png") {
 4 | 	if (!element) return;
 5 | 
 6 | 	const width = element.scrollWidth;
 7 | 	const height = element.scrollHeight;
 8 | 
 9 | 	const canvas = await html2canvas(element, {
10 | 		scale: 2,
11 | 		scrollX: 0,
12 | 		scrollY: 0,
13 | 		width,
14 | 		height,
15 | 		windowWidth: width,
16 | 		windowHeight: height,
17 | 		backgroundColor: "#ffffff",
18 | 	});
19 | 
20 | 	const link = document.createElement("a");
21 | 	link.download = filename;
22 | 	link.href = canvas.toDataURL("image/png");
23 | 
24 | 	return new Promise((resolve) => {
25 | 		link.click();
26 | 		resolve();
27 | 	});
28 | }
29 | 
```

--------------------------------------------------------------------------------
/src/app/dashboard/products/ProductPage.jsx:
--------------------------------------------------------------------------------

```javascript
 1 | "use client";
 2 | 
 3 | import { useRef } from "react";
 4 | 
 5 | import ProductModalButton from "./_components/ProductModalButton";
 6 | import ProductTable from "./_components/ProductTable";
 7 | 
 8 | export default function ProductPage() {
 9 | 	const tableRef = useRef();
10 | 
11 | 	return (
12 | 		<section className="p-4 space-y-4">
13 | 			<div className="flex justify-between items-center mb-2">
14 | 				<h1 className="text-2xl font-bold text-[#6D2315] tracking-tight">Product List</h1>
15 | 				<ProductModalButton
16 | 					onProductAdded={() => {
17 | 						tableRef.current?.refetch();
18 | 					}}
19 | 				/>
20 | 			</div>
21 | 
22 | 			<ProductTable ref={tableRef} />
23 | 		</section>
24 | 	);
25 | }
26 | 
```

--------------------------------------------------------------------------------
/src/lib/actions/size-price/deleteSize.js:
--------------------------------------------------------------------------------

```javascript
 1 | "use server";
 2 | 
 3 | import { supabaseServer } from "@/lib/supabaseServer";
 4 | import { revalidatePath } from "next/cache";
 5 | 
 6 | export async function deleteSize(sizeId) {
 7 | 	const supabase = await supabaseServer();
 8 | 
 9 | 	try {
10 | 		const { error } = await supabase.from("ProductSizePrice").delete().match({ id: sizeId });
11 | 
12 | 		if (error) {
13 | 			console.error("❌ Supabase delete error:", error);
14 | 			return { success: false, message: "Failed to delete size" };
15 | 		}
16 | 
17 | 		revalidatePath("/dashboard/size-pricing");
18 | 
19 | 		return { success: true };
20 | 	} catch (err) {
21 | 		console.log(err);
22 | 		return { success: false, message: "Failed to delete size" };
23 | 	}
24 | }
25 | 
```

--------------------------------------------------------------------------------
/src/lib/actions/invoice/deleteInvoice.js:
--------------------------------------------------------------------------------

```javascript
 1 | "use server";
 2 | 
 3 | import { supabaseServer } from "@/lib/supabaseServer";
 4 | import { revalidatePath } from "next/cache";
 5 | 
 6 | export async function deleteInvoice(invoiceId) {
 7 | 	const supabase = await supabaseServer();
 8 | 
 9 | 	try {
10 | 		const { error } = await supabase.from("Invoice").delete().match({ id: invoiceId });
11 | 
12 | 		if (error) {
13 | 			console.error("❌ Supabase delete error:", error);
14 | 			return { success: false, message: "Failed to delete invoice" };
15 | 		}
16 | 
17 | 		revalidatePath("/dashboard/invoices");
18 | 
19 | 		return { success: true };
20 | 	} catch (err) {
21 | 		console.log(err);
22 | 		return { success: false, message: "Failed to delete invoice" };
23 | 	}
24 | }
25 | 
```

--------------------------------------------------------------------------------
/src/lib/actions/products/deleteProduct.js:
--------------------------------------------------------------------------------

```javascript
 1 | "use server";
 2 | 
 3 | import { supabaseServer } from "@/lib/supabaseServer";
 4 | import { revalidatePath } from "next/cache";
 5 | 
 6 | export async function deleteProduct(productId) {
 7 | 	const supabase = await supabaseServer();
 8 | 
 9 | 	try {
10 | 		const { error } = await supabase.from("Product").delete().match({ id: productId });
11 | 
12 | 		if (error) {
13 | 			console.error("❌ Supabase delete error:", error);
14 | 			return { success: false, message: "Failed to delete product" };
15 | 		}
16 | 
17 | 		revalidatePath("/dashboard/products");
18 | 
19 | 		return { success: true };
20 | 	} catch (err) {
21 | 		console.log(err);
22 | 		return { success: false, message: "Failed to delete product" };
23 | 	}
24 | }
25 | 
```

--------------------------------------------------------------------------------
/src/lib/actions/size-price/updateSize.js:
--------------------------------------------------------------------------------

```javascript
 1 | "use server";
 2 | 
 3 | import { supabaseServer } from "@/lib/supabaseServer";
 4 | import { revalidatePath } from "next/cache";
 5 | 
 6 | export async function updateSize(id, { size, price }) {
 7 | 	const supabase = await supabaseServer();
 8 | 
 9 | 	try {
10 | 		const { error } = await supabase.from("ProductSizePrice").update({ size, price }).eq("id", id);
11 | 
12 | 		if (error) {
13 | 			console.error("❌ Supabase delete error:", error);
14 | 			return { success: false, message: "Failed to update size" };
15 | 		}
16 | 
17 | 		revalidatePath("/dashboard/size-pricing");
18 | 
19 | 		return { success: true };
20 | 	} catch (err) {
21 | 		console.log(err);
22 | 		return { success: false, message: "Failed to update size" };
23 | 	}
24 | }
25 | 
```

--------------------------------------------------------------------------------
/src/lib/actions/products/updateProduct.js:
--------------------------------------------------------------------------------

```javascript
 1 | "use server";
 2 | 
 3 | import { supabaseServer } from "@/lib/supabaseServer";
 4 | import { revalidatePath } from "next/cache";
 5 | 
 6 | export async function updateProduct(id, { name, description }) {
 7 | 	const supabase = await supabaseServer();
 8 | 
 9 | 	try {
10 | 		const { error } = await supabase.from("Product").update({ name, description }).eq("id", id);
11 | 
12 | 		if (error) {
13 | 			console.error("❌ Supabase delete error:", error);
14 | 			return { success: false, message: "Failed to update product" };
15 | 		}
16 | 
17 | 		revalidatePath("/dashboard/products");
18 | 
19 | 		return { success: true };
20 | 	} catch (err) {
21 | 		console.log(err);
22 | 		return { success: false, message: "Failed to update product" };
23 | 	}
24 | }
25 | 
```

--------------------------------------------------------------------------------
/src/app/layout.js:
--------------------------------------------------------------------------------

```javascript
 1 | import { Geist, Geist_Mono } from "next/font/google";
 2 | import "./globals.css";
 3 | 
 4 | import { Toaster } from "sonner";
 5 | 
 6 | const geistSans = Geist({
 7 | 	variable: "--font-geist-sans",
 8 | 	subsets: ["latin"],
 9 | });
10 | 
11 | const geistMono = Geist_Mono({
12 | 	variable: "--font-geist-mono",
13 | 	subsets: ["latin"],
14 | });
15 | 
16 | export const metadata = {
17 | 	title: "Cheese Stick Koe",
18 | 	description: "Invoice generator for Cheese Stick Koe company",
19 | };
20 | 
21 | export default function RootLayout({ children }) {
22 | 	return (
23 | 		<html lang="en">
24 | 			<body className={`${geistSans.variable} ${geistMono.variable} antialiased`}>
25 | 				{children}
26 | 				<Toaster position="top-center" richColors />
27 | 			</body>
28 | 		</html>
29 | 	);
30 | }
31 | 
```

--------------------------------------------------------------------------------
/src/app/dashboard/invoices/InvoicePage.jsx:
--------------------------------------------------------------------------------

```javascript
 1 | "use client";
 2 | 
 3 | import { useRef } from "react";
 4 | import Link from "next/link";
 5 | 
 6 | import { Button } from "@/components/ui/button";
 7 | 
 8 | import InvoicesTable from "./Table";
 9 | 
10 | export default function InvoicePage() {
11 | 	const tableRef = useRef();
12 | 
13 | 	return (
14 | 		<section className="p-4 space-y-4">
15 | 			<div className="flex justify-between items-center mb-2">
16 | 				<h1 className="text-2xl font-bold text-[#6D2315] tracking-tight">Invoices List</h1>
17 | 				<Button asChild className="bg-[#6D2315] hover:bg-[#591c10] text-white px-4 py-2 rounded-md">
18 | 					<Link href="/dashboard/invoices/create">Create Invoice</Link>
19 | 				</Button>
20 | 			</div>
21 | 
22 | 			<InvoicesTable ref={tableRef} />
23 | 		</section>
24 | 	);
25 | }
26 | 
```

--------------------------------------------------------------------------------
/middleware.js:
--------------------------------------------------------------------------------

```javascript
 1 | import { createMiddlewareClient } from "@supabase/auth-helpers-nextjs";
 2 | import { NextResponse } from "next/server";
 3 | 
 4 | export async function middleware(req) {
 5 | 	const res = NextResponse.next();
 6 | 	const supabase = createMiddlewareClient({ req, res });
 7 | 
 8 | 	const {
 9 | 		data: { session },
10 | 	} = await supabase.auth.getSession();
11 | 
12 | 	const { pathname } = req.nextUrl;
13 | 
14 | 	if (session && pathname === "/") {
15 | 		return NextResponse.redirect(new URL("/dashboard", req.url));
16 | 	}
17 | 
18 | 	// trying to access protected route
19 | 	if (!session && pathname.startsWith("/dashboard")) {
20 | 		return NextResponse.redirect(new URL("/", req.url));
21 | 	}
22 | 
23 | 	return res;
24 | }
25 | 
26 | export const config = {
27 | 	matcher: ["/", "/dashboard/:path*"],
28 | };
29 | 
```

--------------------------------------------------------------------------------
/src/app/dashboard/invoices/[invoiceNumber]/page.jsx:
--------------------------------------------------------------------------------

```javascript
 1 | import { unauthorized } from "next/navigation";
 2 | import { verifySession } from "@/lib/verifySession";
 3 | 
 4 | import UpdateInvoiceForm from "../UpdateInvoiceForm";
 5 | 
 6 | import { getInvoiceByNumber } from "@/lib/actions/invoice/getInvoiceByNumber";
 7 | 
 8 | export default async function UpdateInvoicePage(props) {
 9 | 	const session = await verifySession();
10 | 
11 | 	if (!session) {
12 | 		unauthorized();
13 | 	}
14 | 
15 | 	const { invoiceNumber } = await props.params;
16 | 
17 | 	if (!invoiceNumber) return <div className="text-red-500">Invoice number not found</div>;
18 | 
19 | 	const { data: invoice, error } = await getInvoiceByNumber(invoiceNumber);
20 | 
21 | 	if (error || !invoice) {
22 | 		return <div className="text-red-500">Invoice not found</div>;
23 | 	}
24 | 
25 | 	return <UpdateInvoiceForm invoice={invoice} />;
26 | }
27 | 
```

--------------------------------------------------------------------------------
/src/components/ui/input.jsx:
--------------------------------------------------------------------------------

```javascript
 1 | import * as React from "react"
 2 | 
 3 | import { cn } from "@/lib/utils"
 4 | 
 5 | function Input({
 6 |   className,
 7 |   type,
 8 |   ...props
 9 | }) {
10 |   return (
11 |     <input
12 |       type={type}
13 |       data-slot="input"
14 |       className={cn(
15 |         "file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
16 |         "focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
17 |         "aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
18 |         className
19 |       )}
20 |       {...props} />
21 |   );
22 | }
23 | 
24 | export { Input }
25 | 
```

--------------------------------------------------------------------------------
/src/app/dashboard/size-pricing/_components/DeleteSizeModal.jsx:
--------------------------------------------------------------------------------

```javascript
 1 | "use client";
 2 | 
 3 | import { toast } from "sonner";
 4 | 
 5 | import { DialogFooter } from "@/components/ui/dialog";
 6 | import { Button } from "@/components/ui/button";
 7 | 
 8 | import Modal from "@/components/dashboard/Modal";
 9 | 
10 | import { deleteSize } from "@/lib/actions/size-price/deleteSize";
11 | 
12 | export default function DeleteSizeModal({ open, onOpenChange, sizeId, onSuccess }) {
13 | 	const handleDelete = async () => {
14 | 		const result = await deleteSize(sizeId);
15 | 
16 | 		if (result?.success) {
17 | 			toast.success("Size has been deleted");
18 | 			onSuccess?.();
19 | 			onOpenChange(false);
20 | 		} else {
21 | 			toast.error(result?.message || "Failed to delete size");
22 | 		}
23 | 	};
24 | 
25 | 	return (
26 | 		<Modal
27 | 			open={open}
28 | 			onOpenChange={onOpenChange}
29 | 			title="Delete Size"
30 | 			color="red"
31 | 			submitLabel="Delete"
32 | 			showCancel={false}
33 | 		>
34 | 			<p>Are you sure want to delete this size?</p>
35 | 			<DialogFooter>
36 | 				<Button
37 | 					variant="destructive"
38 | 					onClick={handleDelete}
39 | 					className={"bg-rose-600 hover:bg-red-600 w-24"}
40 | 				>
41 | 					Delete
42 | 				</Button>
43 | 			</DialogFooter>
44 | 		</Modal>
45 | 	);
46 | }
47 | 
```

--------------------------------------------------------------------------------
/public/globe.svg:
--------------------------------------------------------------------------------

```
1 | <svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>
```

--------------------------------------------------------------------------------
/src/app/dashboard/invoices/_components/DeleteInvoiceModal.jsx:
--------------------------------------------------------------------------------

```javascript
 1 | "use client";
 2 | 
 3 | import { DialogFooter } from "@/components/ui/dialog";
 4 | import { Button } from "@/components/ui/button";
 5 | 
 6 | import Modal from "@/components/dashboard/Modal";
 7 | 
 8 | import { deleteInvoice } from "@/lib/actions/invoice/deleteInvoice";
 9 | 
10 | import { toast } from "sonner";
11 | 
12 | export default function DeleteInvoiceModal({ open, onOpenChange, invoiceId, onSuccess }) {
13 | 	const handleDelete = async () => {
14 | 		if (!invoiceId) {
15 | 			toast.error("Invoice ID not found");
16 | 			return;
17 | 		}
18 | 
19 | 		const result = await deleteInvoice(invoiceId);
20 | 
21 | 		if (result?.success) {
22 | 			toast.success("Invoice has been deleted");
23 | 			onSuccess?.();
24 | 			onOpenChange(false);
25 | 		} else {
26 | 			toast.error(result?.message || "Failed to delete invoice");
27 | 		}
28 | 	};
29 | 
30 | 	return (
31 | 		<Modal
32 | 			open={open}
33 | 			onOpenChange={onOpenChange}
34 | 			title="Delete Invoice"
35 | 			color="red"
36 | 			submitLabel="Delete"
37 | 			showCancel={false}
38 | 		>
39 | 			<p>Are you sure want to delete this invoice?</p>
40 | 			<DialogFooter>
41 | 				<Button
42 | 					variant="destructive"
43 | 					onClick={handleDelete}
44 | 					className={"bg-rose-600 hover:bg-red-600 w-24"}
45 | 				>
46 | 					Delete
47 | 				</Button>
48 | 			</DialogFooter>
49 | 		</Modal>
50 | 	);
51 | }
52 | 
```

--------------------------------------------------------------------------------
/src/app/dashboard/products/_components/ProductModal.jsx:
--------------------------------------------------------------------------------

```javascript
 1 | "use client";
 2 | 
 3 | import { useState } from "react";
 4 | 
 5 | import Modal from "@/components/dashboard/Modal";
 6 | 
 7 | import { addProduct } from "@/lib/actions/products/addProduct";
 8 | 
 9 | import { toast } from "sonner";
10 | 
11 | export default function ProductModal({ open, setOpen, onSuccess }) {
12 | 	const [name, setName] = useState("");
13 | 	const [description, setDescription] = useState("");
14 | 
15 | 	const handleSubmit = async (e) => {
16 | 		e.preventDefault();
17 | 
18 | 		const { data, error } = await addProduct({ name, description });
19 | 
20 | 		if (error && error.message) {
21 | 			toast.error("Failed to add product: " + error.message);
22 | 		} else {
23 | 			toast.success("Product has been added");
24 | 			onSuccess?.(data);
25 | 			setOpen(false);
26 | 			setName("");
27 | 			setDescription("");
28 | 		}
29 | 	};
30 | 
31 | 	return (
32 | 		<Modal
33 | 			open={open}
34 | 			onOpenChange={setOpen}
35 | 			title="Add Product"
36 | 			color="default"
37 | 			fields={[
38 | 				{
39 | 					label: "Product Name",
40 | 					value: name,
41 | 					onChange: (e) => setName(e.target.value),
42 | 					required: true,
43 | 				},
44 | 				{
45 | 					label: "Description",
46 | 					value: description,
47 | 					onChange: (e) => setDescription(e.target.value),
48 | 				},
49 | 			]}
50 | 			onSubmit={handleSubmit}
51 | 			submitLabel="Add"
52 | 			showCancel={false}
53 | 			buttonStyling="bg-primary"
54 | 		/>
55 | 	);
56 | }
57 | 
```

--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------

```json
 1 | {
 2 | 	"name": "invoice-generator",
 3 | 	"version": "0.1.0",
 4 | 	"private": true,
 5 | 	"scripts": {
 6 | 		"dev": "next dev --turbopack",
 7 | 		"build": "next build",
 8 | 		"start": "next start",
 9 | 		"lint": "next lint",
10 | 		"seed": "node prisma/seed.js"
11 | 	},
12 | 	"dependencies": {
13 | 		"@prisma/client": "^6.11.1",
14 | 		"@radix-ui/react-dialog": "^1.1.14",
15 | 		"@radix-ui/react-label": "^2.1.7",
16 | 		"@radix-ui/react-popover": "^1.1.14",
17 | 		"@radix-ui/react-radio-group": "^1.3.8",
18 | 		"@radix-ui/react-select": "^2.2.5",
19 | 		"@radix-ui/react-slot": "^1.2.3",
20 | 		"@supabase/auth-helpers-nextjs": "^0.10.0",
21 | 		"@supabase/ssr": "^0.6.1",
22 | 		"class-variance-authority": "^0.7.1",
23 | 		"clsx": "^2.1.1",
24 | 		"cmdk": "^1.1.1",
25 | 		"date-fns": "^4.1.0",
26 | 		"dotenv": "^17.2.0",
27 | 		"html2canvas-pro": "^1.5.11",
28 | 		"lucide-react": "^0.525.0",
29 | 		"next": "15.4.1",
30 | 		"next-themes": "^0.4.6",
31 | 		"prisma": "^6.11.1",
32 | 		"react": "19.1.0",
33 | 		"react-day-picker": "^9.8.0",
34 | 		"react-dom": "19.1.0",
35 | 		"react-hook-form": "^7.62.0",
36 | 		"sonner": "^2.0.6",
37 | 		"tailwind-merge": "^3.3.1",
38 | 		"vaul": "^1.1.2"
39 | 	},
40 | 	"devDependencies": {
41 | 		"@eslint/eslintrc": "^3",
42 | 		"@tailwindcss/postcss": "^4",
43 | 		"autoprefixer": "^10.4.21",
44 | 		"eslint": "^9",
45 | 		"eslint-config-next": "15.4.1",
46 | 		"postcss": "^8.5.6",
47 | 		"tailwindcss": "^4.1.11",
48 | 		"tw-animate-css": "^1.3.5"
49 | 	}
50 | }
51 | 
```

--------------------------------------------------------------------------------
/src/app/dashboard/size-pricing/_components/AddSizeModal.jsx:
--------------------------------------------------------------------------------

```javascript
 1 | "use client";
 2 | 
 3 | import { useState } from "react";
 4 | 
 5 | import Modal from "@/components/dashboard/Modal";
 6 | 
 7 | import { addSize } from "@/lib/actions/size-price/addSize";
 8 | 
 9 | import { toast } from "sonner";
10 | 
11 | export default function AddSizeModal({ open, setOpen, onSuccess }) {
12 | 	const [size, setSize] = useState("");
13 | 	const [price, setPrice] = useState("");
14 | 
15 | 	const handleSubmit = async (e) => {
16 | 		e.preventDefault();
17 | 
18 | 		const { data, error } = await addSize({ size, price });
19 | 
20 | 		if (error && error.message) {
21 | 			toast.error("Failed to add size: " + error.message);
22 | 		} else {
23 | 			toast.success("Size has been added");
24 | 			onSuccess?.(data);
25 | 			setOpen(false);
26 | 			setSize("");
27 | 			setPrice("");
28 | 		}
29 | 	};
30 | 
31 | 	return (
32 | 		<Modal
33 | 			open={open}
34 | 			onOpenChange={setOpen}
35 | 			title="Add Size"
36 | 			color="default"
37 | 			fields={[
38 | 				{
39 | 					label: "Size name",
40 | 					value: size,
41 | 					onChange: (e) => setSize(e.target.value),
42 | 					required: true,
43 | 				},
44 | 				{
45 | 					type: "number",
46 | 					label: "Price",
47 | 					value: price === "" ? "" : Number(price),
48 | 					onChange: (e) => {
49 | 						const val = e.target.value;
50 | 						setPrice(val === "" ? "" : parseInt(val));
51 | 					},
52 | 					required: true,
53 | 				},
54 | 			]}
55 | 			onSubmit={handleSubmit}
56 | 			submitLabel="Add"
57 | 			showCancel={false}
58 | 			buttonStyling="bg-primary"
59 | 		/>
60 | 	);
61 | }
62 | 
```

--------------------------------------------------------------------------------
/src/components/ui/radio-group.jsx:
--------------------------------------------------------------------------------

```javascript
 1 | "use client"
 2 | 
 3 | import * as React from "react"
 4 | import * as RadioGroupPrimitive from "@radix-ui/react-radio-group"
 5 | import { CircleIcon } from "lucide-react"
 6 | 
 7 | import { cn } from "@/lib/utils"
 8 | 
 9 | function RadioGroup({
10 |   className,
11 |   ...props
12 | }) {
13 |   return (
14 |     <RadioGroupPrimitive.Root
15 |       data-slot="radio-group"
16 |       className={cn("grid gap-3", className)}
17 |       {...props} />
18 |   );
19 | }
20 | 
21 | function RadioGroupItem({
22 |   className,
23 |   ...props
24 | }) {
25 |   return (
26 |     <RadioGroupPrimitive.Item
27 |       data-slot="radio-group-item"
28 |       className={cn(
29 |         "border-input text-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 aspect-square size-4 shrink-0 rounded-full border shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
30 |         className
31 |       )}
32 |       {...props}>
33 |       <RadioGroupPrimitive.Indicator
34 |         data-slot="radio-group-indicator"
35 |         className="relative flex items-center justify-center">
36 |         <CircleIcon
37 |           className="fill-primary absolute top-1/2 left-1/2 size-2 -translate-x-1/2 -translate-y-1/2" />
38 |       </RadioGroupPrimitive.Indicator>
39 |     </RadioGroupPrimitive.Item>
40 |   );
41 | }
42 | 
43 | export { RadioGroup, RadioGroupItem }
44 | 
```

--------------------------------------------------------------------------------
/public/next.svg:
--------------------------------------------------------------------------------

```
1 | <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>
```

--------------------------------------------------------------------------------
/src/components/ui/popover.jsx:
--------------------------------------------------------------------------------

```javascript
 1 | "use client"
 2 | 
 3 | import * as React from "react"
 4 | import * as PopoverPrimitive from "@radix-ui/react-popover"
 5 | 
 6 | import { cn } from "@/lib/utils"
 7 | 
 8 | function Popover({
 9 |   ...props
10 | }) {
11 |   return <PopoverPrimitive.Root data-slot="popover" {...props} />;
12 | }
13 | 
14 | function PopoverTrigger({
15 |   ...props
16 | }) {
17 |   return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />;
18 | }
19 | 
20 | function PopoverContent({
21 |   className,
22 |   align = "center",
23 |   sideOffset = 4,
24 |   ...props
25 | }) {
26 |   return (
27 |     <PopoverPrimitive.Portal>
28 |       <PopoverPrimitive.Content
29 |         data-slot="popover-content"
30 |         align={align}
31 |         sideOffset={sideOffset}
32 |         className={cn(
33 |           "bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-md border p-4 shadow-md outline-hidden",
34 |           className
35 |         )}
36 |         {...props} />
37 |     </PopoverPrimitive.Portal>
38 |   );
39 | }
40 | 
41 | function PopoverAnchor({
42 |   ...props
43 | }) {
44 |   return <PopoverPrimitive.Anchor data-slot="popover-anchor" {...props} />;
45 | }
46 | 
47 | export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor }
48 | 
```

--------------------------------------------------------------------------------
/src/components/ui/badge.jsx:
--------------------------------------------------------------------------------

```javascript
 1 | import * as React from "react"
 2 | import { Slot } from "@radix-ui/react-slot"
 3 | import { cva } from "class-variance-authority";
 4 | 
 5 | import { cn } from "@/lib/utils"
 6 | 
 7 | const badgeVariants = cva(
 8 |   "inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
 9 |   {
10 |     variants: {
11 |       variant: {
12 |         default:
13 |           "border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
14 |         secondary:
15 |           "border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
16 |         destructive:
17 |           "border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
18 |         outline:
19 |           "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
20 |       },
21 |     },
22 |     defaultVariants: {
23 |       variant: "default",
24 |     },
25 |   }
26 | )
27 | 
28 | function Badge({
29 |   className,
30 |   variant,
31 |   asChild = false,
32 |   ...props
33 | }) {
34 |   const Comp = asChild ? Slot : "span"
35 | 
36 |   return (
37 |     <Comp
38 |       data-slot="badge"
39 |       className={cn(badgeVariants({ variant }), className)}
40 |       {...props} />
41 |   );
42 | }
43 | 
44 | export { Badge, badgeVariants }
45 | 
```

--------------------------------------------------------------------------------
/src/app/dashboard/invoices/create/_components/SizeCombobox.jsx:
--------------------------------------------------------------------------------

```javascript
 1 | import { useState } from "react";
 2 | 
 3 | import { Button } from "@/components/ui/button";
 4 | import {
 5 | 	Command,
 6 | 	CommandEmpty,
 7 | 	CommandGroup,
 8 | 	CommandInput,
 9 | 	CommandItem,
10 | 	CommandList,
11 | } from "@/components/ui/command";
12 | import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
13 | 
14 | import { Check, ChevronsUpDown } from "lucide-react";
15 | 
16 | import { cn } from "@/lib/utils";
17 | 
18 | export default function SizeCombobox({ sizes, value, onChange }) {
19 | 	const [open, setOpen] = useState(false);
20 | 
21 | 	return (
22 | 		<Popover open={open} onOpenChange={setOpen}>
23 | 			<PopoverTrigger asChild>
24 | 				<Button
25 | 					variant="outline"
26 | 					role="combobox"
27 | 					aria-expanded={open}
28 | 					className="w-full justify-between"
29 | 				>
30 | 					{value ? sizes.find((s) => s.id === value)?.size : "Choose size..."}
31 | 					<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
32 | 				</Button>
33 | 			</PopoverTrigger>
34 | 			<PopoverContent className="w-full p-0">
35 | 				<Command>
36 | 					<CommandInput placeholder="Search size..." />
37 | 					<CommandList>
38 | 						<CommandEmpty>Size not found</CommandEmpty>
39 | 						<CommandGroup>
40 | 							{sizes.map((s) => (
41 | 								<CommandItem
42 | 									key={s.id}
43 | 									value={s.size}
44 | 									onSelect={() => {
45 | 										onChange(s.id, s.price);
46 | 										setOpen(false);
47 | 									}}
48 | 								>
49 | 									{s.size}
50 | 									<Check
51 | 										className={cn("ml-auto h-4 w-4", value === s.id ? "opacity-100" : "opacity-0")}
52 | 									/>
53 | 								</CommandItem>
54 | 							))}
55 | 						</CommandGroup>
56 | 					</CommandList>
57 | 				</Command>
58 | 			</PopoverContent>
59 | 		</Popover>
60 | 	);
61 | }
62 | 
```

--------------------------------------------------------------------------------
/src/app/dashboard/size-pricing/_components/EditSizeModal.jsx:
--------------------------------------------------------------------------------

```javascript
 1 | "use client";
 2 | 
 3 | import { useState, useEffect } from "react";
 4 | 
 5 | import { DialogFooter } from "@/components/ui/dialog";
 6 | import { Button } from "@/components/ui/button";
 7 | import { Input } from "@/components/ui/input";
 8 | 
 9 | import Modal from "@/components/dashboard/Modal";
10 | 
11 | import { updateSize } from "@/lib/actions/size-price/updateSize";
12 | 
13 | import { toast } from "sonner";
14 | 
15 | export default function EditSizeModal({ open, onOpenChange, data, onSuccess }) {
16 | 	const [size, setSize] = useState("");
17 | 	const [price, setPrice] = useState("");
18 | 
19 | 	useEffect(() => {
20 | 		if (data) {
21 | 			setSize(data.size || "");
22 | 			setPrice(data.price || "");
23 | 		}
24 | 	}, [data]);
25 | 
26 | 	const handleUpdate = async () => {
27 | 		const result = await updateSize(data.id, { size, price });
28 | 
29 | 		if (result?.success) {
30 | 			toast.success("Product has been updated");
31 | 			onSuccess?.({ ...data, size, price });
32 | 			onOpenChange(false);
33 | 		} else {
34 | 			toast.error(result?.message || "Failed to update size");
35 | 		}
36 | 	};
37 | 
38 | 	return (
39 | 		<Modal
40 | 			open={open}
41 | 			onOpenChange={onOpenChange}
42 | 			title="Edit Size"
43 | 			color="blue"
44 | 			submitLabel="Update"
45 | 			showCancel={false}
46 | 		>
47 | 			<div className="space-y-2">
48 | 				<Input placeholder="Size name" value={size} onChange={(e) => setSize(e.target.value)} />
49 | 				<Input
50 | 					placeholder="Price"
51 | 					type="number"
52 | 					value={price}
53 | 					onChange={(e) => {
54 | 						const val = e.target.value;
55 | 						setPrice(val === "" ? "" : parseInt(val));
56 | 					}}
57 | 				/>
58 | 			</div>
59 | 
60 | 			<DialogFooter className="pt-4">
61 | 				<Button onClick={handleUpdate} className="bg-sky-500 hover:bg-blue-500 w-full">
62 | 					Update
63 | 				</Button>
64 | 			</DialogFooter>
65 | 		</Modal>
66 | 	);
67 | }
68 | 
```

--------------------------------------------------------------------------------
/src/app/dashboard/products/_components/ProductEditModal.jsx:
--------------------------------------------------------------------------------

```javascript
 1 | "use client";
 2 | 
 3 | import { useState, useEffect } from "react";
 4 | 
 5 | import { DialogFooter } from "@/components/ui/dialog";
 6 | import { Button } from "@/components/ui/button";
 7 | import { Input } from "@/components/ui/input";
 8 | 
 9 | import Modal from "@/components/dashboard/Modal";
10 | 
11 | import { updateProduct } from "@/lib/actions/products/updateProduct";
12 | 
13 | import { toast } from "sonner";
14 | 
15 | export default function ProductEditModal({ open, onOpenChange, product, onSuccess }) {
16 | 	const [name, setName] = useState("");
17 | 	const [description, setDescription] = useState("");
18 | 
19 | 	useEffect(() => {
20 | 		if (product) {
21 | 			setName(product.name || "");
22 | 			setDescription(product.description || "");
23 | 		}
24 | 	}, [product]);
25 | 
26 | 	const handleUpdate = async () => {
27 | 		const result = await updateProduct(product.id, { name, description });
28 | 
29 | 		if (result?.success) {
30 | 			toast.success("Product has been updated");
31 | 			onSuccess?.({ ...product, name, description });
32 | 			onOpenChange(false);
33 | 		} else {
34 | 			toast.error(result?.message || "Failed to update product");
35 | 		}
36 | 	};
37 | 
38 | 	return (
39 | 		<Modal
40 | 			open={open}
41 | 			onOpenChange={onOpenChange}
42 | 			title="Edit Product"
43 | 			color="blue"
44 | 			submitLabel="Update"
45 | 			showCancel={false}
46 | 		>
47 | 			<div className="space-y-2">
48 | 				<Input placeholder="Product name" value={name} onChange={(e) => setName(e.target.value)} />
49 | 				<Input
50 | 					placeholder="Description"
51 | 					value={description}
52 | 					onChange={(e) => setDescription(e.target.value)}
53 | 				/>
54 | 			</div>
55 | 
56 | 			<DialogFooter className="pt-4">
57 | 				<Button onClick={handleUpdate} className="bg-sky-500 hover:bg-blue-500 w-full">
58 | 					Update
59 | 				</Button>
60 | 			</DialogFooter>
61 | 		</Modal>
62 | 	);
63 | }
64 | 
```

--------------------------------------------------------------------------------
/src/app/dashboard/products/_components/ProductDeleteModal.jsx:
--------------------------------------------------------------------------------

```javascript
 1 | "use client";
 2 | 
 3 | import { DialogFooter } from "@/components/ui/dialog";
 4 | import { Button } from "@/components/ui/button";
 5 | 
 6 | import Modal from "@/components/dashboard/Modal";
 7 | 
 8 | import { deleteProduct } from "@/lib/actions/products/deleteProduct";
 9 | 
10 | import { toast } from "sonner";
11 | 
12 | export default function ProductDeleteModal({ open, onOpenChange, productId, onSuccess }) {
13 | 	const handleDelete = async () => {
14 | 		const result = await deleteProduct(productId);
15 | 
16 | 		if (result?.success) {
17 | 			toast.success("Product has been deleted");
18 | 			onSuccess?.();
19 | 			onOpenChange(false);
20 | 		} else {
21 | 			toast.error(result?.message || "Failed to delete product");
22 | 		}
23 | 	};
24 | 
25 | 	return (
26 | 		<Modal
27 | 			open={open}
28 | 			onOpenChange={onOpenChange}
29 | 			title="Delete Product"
30 | 			color="red"
31 | 			submitLabel="Delete"
32 | 			showCancel={false}
33 | 		>
34 | 			<p>Are you sure want to delete this product?</p>
35 | 			<DialogFooter>
36 | 				<Button
37 | 					variant="destructive"
38 | 					onClick={handleDelete}
39 | 					className={"bg-rose-600 hover:bg-red-600 w-24"}
40 | 				>
41 | 					Delete
42 | 				</Button>
43 | 			</DialogFooter>
44 | 		</Modal>
45 | 
46 | 		// <Dialog open={open} onOpenChange={onOpenChange}>
47 | 		// 	<DialogContent>
48 | 		// 		<DialogHeader>
49 | 		// 			<DialogTitle className={"text-red-500"}>Delete Product</DialogTitle>
50 | 		// 		</DialogHeader>
51 | 		// 		<p>Are you sure want to delete this product?</p>
52 | 		// 		<DialogFooter>
53 | 		// 			<Button variant="outline" onClick={() => onOpenChange(false)}>
54 | 		// 				Cancel
55 | 		// 			</Button>
56 | 		// 			<Button
57 | 		// 				variant="destructive"
58 | 		// 				onClick={handleDelete}
59 | 		// 				className={"bg-rose-600 hover:bg-red-600"}
60 | 		// 			>
61 | 		// 				Delete
62 | 		// 			</Button>
63 | 		// 		</DialogFooter>
64 | 		// 	</DialogContent>
65 | 		// </Dialog>
66 | 	);
67 | }
68 | 
```

--------------------------------------------------------------------------------
/src/app/dashboard/invoices/create/_components/ProductsCombobox.jsx:
--------------------------------------------------------------------------------

```javascript
 1 | import { useState } from "react";
 2 | 
 3 | import { Button } from "@/components/ui/button";
 4 | import {
 5 | 	Command,
 6 | 	CommandEmpty,
 7 | 	CommandGroup,
 8 | 	CommandInput,
 9 | 	CommandItem,
10 | 	CommandList,
11 | } from "@/components/ui/command";
12 | import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
13 | 
14 | import { Check, ChevronsUpDown } from "lucide-react";
15 | 
16 | import { cn } from "@/lib/utils";
17 | 
18 | export default function ProductCombobox({ products, value, onChange }) {
19 | 	const [open, setOpen] = useState(false);
20 | 
21 | 	return (
22 | 		<Popover open={open} onOpenChange={setOpen}>
23 | 			<PopoverTrigger asChild>
24 | 				<Button
25 | 					variant="outline"
26 | 					role="combobox"
27 | 					aria-expanded={open}
28 | 					className="w-full justify-between"
29 | 				>
30 | 					{value ? products.find((p) => p.id === value)?.name : "Choose item..."}
31 | 					<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
32 | 				</Button>
33 | 			</PopoverTrigger>
34 | 			<PopoverContent className="w-full p-0">
35 | 				<Command>
36 | 					<CommandInput placeholder="Search item..." />
37 | 					<CommandList>
38 | 						<CommandEmpty>Item not found</CommandEmpty>
39 | 						<CommandGroup>
40 | 							{products.map((product) => (
41 | 								<CommandItem
42 | 									key={product.id}
43 | 									value={product.name}
44 | 									onSelect={(currentValue) => {
45 | 										const selected = products.find((p) => p.name === currentValue);
46 | 										if (selected) {
47 | 											onChange(selected.id);
48 | 											setOpen(false);
49 | 										}
50 | 									}}
51 | 								>
52 | 									{product.name}
53 | 									<Check
54 | 										className={cn(
55 | 											"ml-auto h-4 w-4",
56 | 											value === product.id ? "opacity-100" : "opacity-0"
57 | 										)}
58 | 									/>
59 | 								</CommandItem>
60 | 							))}
61 | 						</CommandGroup>
62 | 					</CommandList>
63 | 				</Command>
64 | 			</PopoverContent>
65 | 		</Popover>
66 | 	);
67 | }
68 | 
```

--------------------------------------------------------------------------------
/src/lib/actions/invoice/updateInvoice.js:
--------------------------------------------------------------------------------

```javascript
 1 | "use server";
 2 | 
 3 | import { supabaseServer } from "@/lib/supabaseServer";
 4 | 
 5 | export async function updateInvoice({ invoiceId, invoiceData, items }) {
 6 | 	const supabase = await supabaseServer();
 7 | 
 8 | 	try {
 9 | 		// is invoice number already exist?
10 | 		const { data: existing, error: checkError } = await supabase
11 | 			.from("Invoice")
12 | 			.select("id")
13 | 			.eq("invoiceNumber", invoiceData.invoiceNumber)
14 | 			.neq("id", invoiceId) // exclude current invoice number
15 | 			.maybeSingle();
16 | 
17 | 		if (checkError) throw checkError;
18 | 		if (existing) return { error: "Invoice number already existed!" };
19 | 
20 | 		const { error: invoiceError } = await supabase
21 | 			.from("Invoice")
22 | 			.update({
23 | 				invoiceNumber: invoiceData.invoiceNumber,
24 | 				buyerName: invoiceData.buyerName,
25 | 				invoiceDate: invoiceData.invoiceDate,
26 | 				totalPrice: invoiceData.totalPrice,
27 | 				discount: invoiceData.discount,
28 | 				shipping: parseInt(invoiceData.shipping) || 0,
29 | 				status: invoiceData.status,
30 | 			})
31 | 			.eq("id", invoiceId);
32 | 
33 | 		if (invoiceError) {
34 | 			throw invoiceError;
35 | 		}
36 | 
37 | 		const { error: deleteError } = await supabase
38 | 			.from("InvoiceItem")
39 | 			.delete()
40 | 			.eq("invoiceId", invoiceId);
41 | 
42 | 		if (deleteError) {
43 | 			throw deleteError;
44 | 		}
45 | 
46 | 		// insert new items
47 | 		const itemsToInsert = items.map((item) => ({
48 | 			invoiceId,
49 | 			productId: item.productId,
50 | 			sizePriceId: item.sizePriceId,
51 | 			quantity: item.quantity,
52 | 			subtotal: item.quantity * (item.price || 0) - (item.discountAmount || 0),
53 | 			discountAmount: item.discountAmount || 0,
54 | 		}));
55 | 
56 | 		const { error: insertError } = await supabase.from("InvoiceItem").insert(itemsToInsert);
57 | 
58 | 		if (insertError) {
59 | 			throw insertError;
60 | 		}
61 | 
62 | 		return { success: true };
63 | 	} catch (err) {
64 | 		console.error("Error:", err);
65 | 		return { success: false, error: err.message };
66 | 	}
67 | }
68 | 
```

--------------------------------------------------------------------------------
/src/components/ui/card.jsx:
--------------------------------------------------------------------------------

```javascript
  1 | import * as React from "react"
  2 | 
  3 | import { cn } from "@/lib/utils"
  4 | 
  5 | function Card({
  6 |   className,
  7 |   ...props
  8 | }) {
  9 |   return (
 10 |     <div
 11 |       data-slot="card"
 12 |       className={cn(
 13 |         "bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm",
 14 |         className
 15 |       )}
 16 |       {...props} />
 17 |   );
 18 | }
 19 | 
 20 | function CardHeader({
 21 |   className,
 22 |   ...props
 23 | }) {
 24 |   return (
 25 |     <div
 26 |       data-slot="card-header"
 27 |       className={cn(
 28 |         "@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-1.5 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6",
 29 |         className
 30 |       )}
 31 |       {...props} />
 32 |   );
 33 | }
 34 | 
 35 | function CardTitle({
 36 |   className,
 37 |   ...props
 38 | }) {
 39 |   return (
 40 |     <div
 41 |       data-slot="card-title"
 42 |       className={cn("leading-none font-semibold", className)}
 43 |       {...props} />
 44 |   );
 45 | }
 46 | 
 47 | function CardDescription({
 48 |   className,
 49 |   ...props
 50 | }) {
 51 |   return (
 52 |     <div
 53 |       data-slot="card-description"
 54 |       className={cn("text-muted-foreground text-sm", className)}
 55 |       {...props} />
 56 |   );
 57 | }
 58 | 
 59 | function CardAction({
 60 |   className,
 61 |   ...props
 62 | }) {
 63 |   return (
 64 |     <div
 65 |       data-slot="card-action"
 66 |       className={cn(
 67 |         "col-start-2 row-span-2 row-start-1 self-start justify-self-end",
 68 |         className
 69 |       )}
 70 |       {...props} />
 71 |   );
 72 | }
 73 | 
 74 | function CardContent({
 75 |   className,
 76 |   ...props
 77 | }) {
 78 |   return (<div data-slot="card-content" className={cn("px-6", className)} {...props} />);
 79 | }
 80 | 
 81 | function CardFooter({
 82 |   className,
 83 |   ...props
 84 | }) {
 85 |   return (
 86 |     <div
 87 |       data-slot="card-footer"
 88 |       className={cn("flex items-center px-6 [.border-t]:pt-6", className)}
 89 |       {...props} />
 90 |   );
 91 | }
 92 | 
 93 | export {
 94 |   Card,
 95 |   CardHeader,
 96 |   CardFooter,
 97 |   CardTitle,
 98 |   CardAction,
 99 |   CardDescription,
100 |   CardContent,
101 | }
102 | 
```

--------------------------------------------------------------------------------
/src/app/dashboard/invoices/_components/StatusCombobox.jsx:
--------------------------------------------------------------------------------

```javascript
 1 | "use client";
 2 | 
 3 | import { useState } from "react";
 4 | 
 5 | import { Button } from "@/components/ui/button";
 6 | import {
 7 | 	Command,
 8 | 	CommandEmpty,
 9 | 	CommandGroup,
10 | 	CommandInput,
11 | 	CommandItem,
12 | 	CommandList,
13 | } from "@/components/ui/command";
14 | import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
15 | 
16 | import { Check, ChevronsUpDown } from "lucide-react";
17 | 
18 | import { cn } from "@/lib/utils";
19 | 
20 | const statuses = [
21 | 	{ label: "Pending", value: "pending" },
22 | 	{ label: "Success", value: "success" },
23 | 	{ label: "Canceled", value: "canceled" },
24 | ];
25 | 
26 | export default function StatusCombobox({ value, onChange }) {
27 | 	const [open, setOpen] = useState(false);
28 | 
29 | 	const selected = statuses.find((s) => s.value === value);
30 | 
31 | 	return (
32 | 		<Popover open={open} onOpenChange={setOpen}>
33 | 			<PopoverTrigger asChild>
34 | 				<Button
35 | 					variant="outline"
36 | 					role="combobox"
37 | 					aria-expanded={open}
38 | 					className="w-full justify-between"
39 | 				>
40 | 					{selected ? selected.label : "Choose status..."}
41 | 					<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
42 | 				</Button>
43 | 			</PopoverTrigger>
44 | 			<PopoverContent className="w-full p-0">
45 | 				<Command>
46 | 					<CommandInput placeholder="Search status..." />
47 | 					<CommandList>
48 | 						<CommandEmpty>Status not found</CommandEmpty>
49 | 						<CommandGroup>
50 | 							{statuses.map((s) => (
51 | 								<CommandItem
52 | 									key={s.value}
53 | 									value={s.value}
54 | 									onSelect={(currentVal) => {
55 | 										onChange(currentVal);
56 | 										setOpen(false);
57 | 									}}
58 | 								>
59 | 									{s.label}
60 | 									<Check
61 | 										className={cn(
62 | 											"ml-auto h-4 w-4",
63 | 											value === s.value ? "opacity-100" : "opacity-0"
64 | 										)}
65 | 									/>
66 | 								</CommandItem>
67 | 							))}
68 | 						</CommandGroup>
69 | 					</CommandList>
70 | 				</Command>
71 | 			</PopoverContent>
72 | 		</Popover>
73 | 	);
74 | }
75 | 
```

--------------------------------------------------------------------------------
/src/components/dashboard/Modal.jsx:
--------------------------------------------------------------------------------

```javascript
 1 | "use client";
 2 | 
 3 | import {
 4 | 	Dialog,
 5 | 	DialogContent,
 6 | 	DialogHeader,
 7 | 	DialogTitle,
 8 | 	DialogFooter,
 9 | } from "@/components/ui/dialog";
10 | import { Button } from "@/components/ui/button";
11 | import { Input } from "@/components/ui/input";
12 | import { Label } from "@/components/ui/label";
13 | 
14 | export default function Modal({
15 | 	open,
16 | 	onOpenChange,
17 | 	title,
18 | 	color = "default", // 'default' | 'red' | 'blue' | 'green' (styling title/button)
19 | 	fields = [], // [{ label, placeholder, value, onChange, required }]
20 | 	onSubmit,
21 | 	submitLabel = "Submit",
22 | 	footerButtons, // override footer
23 | 	children, // optional custom content
24 | 	buttonStyling,
25 | }) {
26 | 	const getColorClass = () => {
27 | 		switch (color) {
28 | 			case "red":
29 | 				return "text-red-500";
30 | 			case "blue":
31 | 				return "text-blue-500";
32 | 			case "green":
33 | 				return "text-green-500";
34 | 			default:
35 | 				return "";
36 | 		}
37 | 	};
38 | 
39 | 	return (
40 | 		<Dialog open={open} onOpenChange={onOpenChange}>
41 | 			<DialogContent>
42 | 				<DialogHeader>
43 | 					<DialogTitle className={getColorClass()}>{title}</DialogTitle>
44 | 				</DialogHeader>
45 | 
46 | 				{/* CASE 1:  children custom (non-form) */}
47 | 				{children && children}
48 | 
49 | 				{!children && fields.length > 0 && (
50 | 					<form onSubmit={onSubmit} className="space-y-4 mt-4">
51 | 						{fields.map((field) => (
52 | 							<div className="space-y-1" key={field.label || field.placeholder || field.value}>
53 | 								{field.label && <Label>{field.label}</Label>}
54 | 								<Input
55 | 									type={field.type || "text"}
56 | 									placeholder={field.placeholder}
57 | 									value={field.value}
58 | 									onChange={field.onChange}
59 | 									required={field.required}
60 | 								/>
61 | 							</div>
62 | 						))}
63 | 						<DialogFooter>
64 | 							<Button type="submit" className={`w-full ${buttonStyling}`}>
65 | 								{submitLabel}
66 | 							</Button>
67 | 						</DialogFooter>
68 | 					</form>
69 | 				)}
70 | 
71 | 				{footerButtons && <DialogFooter>{footerButtons}</DialogFooter>}
72 | 			</DialogContent>
73 | 		</Dialog>
74 | 	);
75 | }
76 | 
```

--------------------------------------------------------------------------------
/src/components/ui/button.jsx:
--------------------------------------------------------------------------------

```javascript
 1 | import * as React from "react"
 2 | import { Slot } from "@radix-ui/react-slot"
 3 | import { cva } from "class-variance-authority";
 4 | 
 5 | import { cn } from "@/lib/utils"
 6 | 
 7 | const buttonVariants = cva(
 8 |   "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
 9 |   {
10 |     variants: {
11 |       variant: {
12 |         default:
13 |           "bg-primary text-primary-foreground shadow-xs hover:bg-primary/90",
14 |         destructive:
15 |           "bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
16 |         outline:
17 |           "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50",
18 |         secondary:
19 |           "bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80",
20 |         ghost:
21 |           "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
22 |         link: "text-primary underline-offset-4 hover:underline",
23 |       },
24 |       size: {
25 |         default: "h-9 px-4 py-2 has-[>svg]:px-3",
26 |         sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
27 |         lg: "h-10 rounded-md px-6 has-[>svg]:px-4",
28 |         icon: "size-9",
29 |       },
30 |     },
31 |     defaultVariants: {
32 |       variant: "default",
33 |       size: "default",
34 |     },
35 |   }
36 | )
37 | 
38 | function Button({
39 |   className,
40 |   variant,
41 |   size,
42 |   asChild = false,
43 |   ...props
44 | }) {
45 |   const Comp = asChild ? Slot : "button"
46 | 
47 |   return (
48 |     <Comp
49 |       data-slot="button"
50 |       className={cn(buttonVariants({ variant, size, className }))}
51 |       {...props} />
52 |   );
53 | }
54 | 
55 | export { Button, buttonVariants }
56 | 
```

--------------------------------------------------------------------------------
/src/lib/actions/invoice/submitInvoice.js:
--------------------------------------------------------------------------------

```javascript
 1 | "use server";
 2 | 
 3 | import { supabaseServer } from "@/lib/supabaseServer";
 4 | 
 5 | export const submitInvoice = async ({
 6 | 	invoiceNumber,
 7 | 	buyerName,
 8 | 	invoiceDate,
 9 | 	shippingPrice,
10 | 	discountAmount = 0,
11 | 	totalPrice,
12 | 	items,
13 | 	user,
14 | }) => {
15 | 	if (!user) {
16 | 		return { error: "User is not login!" };
17 | 	}
18 | 
19 | 	if (!invoiceNumber.trim() || !buyerName.trim() || items.length === 0) {
20 | 		return { error: "Invoice number, buyer name, and at least one item are required!" };
21 | 	}
22 | 
23 | 	const supabase = await supabaseServer();
24 | 
25 | 	// validate number inputs
26 | 	const shipping = Number.isNaN(parseInt(shippingPrice)) ? 0 : parseInt(shippingPrice);
27 | 	const discount = Number.isNaN(parseInt(discountAmount)) ? 0 : parseInt(discountAmount);
28 | 	const total = Number.isNaN(parseInt(totalPrice)) ? 0 : parseInt(totalPrice);
29 | 
30 | 	try {
31 | 		// is invoice number already exist?
32 | 		const { data: existing, error: checkError } = await supabase
33 | 			.from("Invoice")
34 | 			.select("id")
35 | 			.eq("invoiceNumber", invoiceNumber)
36 | 			.maybeSingle();
37 | 
38 | 		if (checkError) throw checkError;
39 | 		if (existing) return { error: "Invoice number already existed!" };
40 | 
41 | 		// insert invoice
42 | 		const { data: invoice, error: invoiceError } = await supabase
43 | 			.from("Invoice")
44 | 			.insert([
45 | 				{
46 | 					invoiceNumber,
47 | 					buyerName,
48 | 					invoiceDate: new Date(invoiceDate),
49 | 					shipping,
50 | 					discount,
51 | 					totalPrice: total,
52 | 					status: "pending",
53 | 					userId: user.id,
54 | 				},
55 | 			])
56 | 			.select()
57 | 			.single();
58 | 
59 | 		if (invoiceError || !invoice) throw invoiceError || new Error("Failed to insert invoice!");
60 | 
61 | 		// insert invoice items
62 | 		const invoiceItems = items.map((item) => ({
63 | 			invoiceId: invoice.id,
64 | 			productId: item.productId,
65 | 			sizePriceId: item.sizePriceId,
66 | 			quantity: item.quantity,
67 | 			subtotal: item.total,
68 | 			discountAmount: item.discountAmount || 0,
69 | 		}));
70 | 
71 | 		const { error: itemError } = await supabase.from("InvoiceItem").insert(invoiceItems);
72 | 
73 | 		if (itemError) throw itemError;
74 | 
75 | 		return { success: true, message: "Invoice has been created", invoice };
76 | 	} catch (err) {
77 | 		return { error: "Something went wrong while saving invoice!" };
78 | 	}
79 | };
80 | 
```

--------------------------------------------------------------------------------
/src/components/dashboard/DatePicker.jsx:
--------------------------------------------------------------------------------

```javascript
  1 | "use client";
  2 | 
  3 | import { useState } from "react";
  4 | import { CalendarIcon } from "lucide-react";
  5 | 
  6 | import { Button } from "@/components/ui/button";
  7 | import { Calendar } from "@/components/ui/calendar";
  8 | import { Input } from "@/components/ui/input";
  9 | import { Label } from "@/components/ui/label";
 10 | import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
 11 | 
 12 | function formatDate(date) {
 13 | 	if (!date) {
 14 | 		return "";
 15 | 	}
 16 | 
 17 | 	return date.toLocaleDateString("id-ID", {
 18 | 		day: "2-digit",
 19 | 		month: "long",
 20 | 		year: "numeric",
 21 | 	});
 22 | }
 23 | 
 24 | function isValidDate(date) {
 25 | 	if (!date) {
 26 | 		return false;
 27 | 	}
 28 | 	return !isNaN(date.getTime());
 29 | }
 30 | 
 31 | export default function DatePicker({ invoiceDate, setInvoiceDate }) {
 32 | 	const [open, setOpen] = useState(false);
 33 | 	const [selectedDate, setSelectedDate] = useState(
 34 | 		invoiceDate ? new Date(invoiceDate) : new Date()
 35 | 	);
 36 | 
 37 | 	return (
 38 | 		<div className="flex flex-col gap-3">
 39 | 			<div className="relative flex gap-2">
 40 | 				<Input
 41 | 					id="date"
 42 | 					value={formatDate(new Date(invoiceDate))}
 43 | 					placeholder="June 01, 2025"
 44 | 					className="bg-background pr-10"
 45 | 					onChange={(e) => {
 46 | 						const date = new Date(e.target.value);
 47 | 						if (!isNaN(date)) {
 48 | 							date.setHours(12, 0, 0, 0);
 49 | 							setSelectedDate(date);
 50 | 							setInvoiceDate(date.toISOString());
 51 | 						}
 52 | 					}}
 53 | 					onKeyDown={(e) => {
 54 | 						if (e.key === "ArrowDown") {
 55 | 							e.preventDefault();
 56 | 							setOpen(true);
 57 | 						}
 58 | 					}}
 59 | 				/>
 60 | 				<Popover open={open} onOpenChange={setOpen}>
 61 | 					<PopoverTrigger asChild>
 62 | 						<Button
 63 | 							id="date-picker"
 64 | 							variant="ghost"
 65 | 							className="absolute top-1/2 right-2 size-6 -translate-y-1/2"
 66 | 						>
 67 | 							<CalendarIcon className="size-3.5" />
 68 | 							<span className="sr-only">Select date</span>
 69 | 						</Button>
 70 | 					</PopoverTrigger>
 71 | 					<PopoverContent
 72 | 						className="w-auto overflow-hidden p-0"
 73 | 						align="end"
 74 | 						alignOffset={-8}
 75 | 						sideOffset={10}
 76 | 					>
 77 | 						<Calendar
 78 | 							mode="single"
 79 | 							selected={selectedDate}
 80 | 							captionLayout="dropdown"
 81 | 							month={selectedDate}
 82 | 							onMonthChange={(month) => setSelectedDate(month)}
 83 | 							onSelect={(date) => {
 84 | 								if (date) {
 85 | 									setSelectedDate(date);
 86 | 
 87 | 									const withNoon = new Date(date);
 88 | 									withNoon.setHours(12, 0, 0, 0);
 89 | 									setInvoiceDate(withNoon.toISOString());
 90 | 
 91 | 									setOpen(false);
 92 | 								}
 93 | 							}}
 94 | 						/>
 95 | 					</PopoverContent>
 96 | 				</Popover>
 97 | 			</div>
 98 | 		</div>
 99 | 	);
100 | }
101 | 
```

--------------------------------------------------------------------------------
/src/lib/utils.js:
--------------------------------------------------------------------------------

```javascript
 1 | import { clsx } from "clsx";
 2 | import { twMerge } from "tailwind-merge";
 3 | 
 4 | export function cn(...inputs) {
 5 | 	return twMerge(clsx(inputs));
 6 | }
 7 | 
 8 | export function getStatusVariant(status) {
 9 | 	switch (status) {
10 | 		case "success":
11 | 			return "bg-green-100 text-green-800 text-xs font-medium me-2 px-2.5 py-0.5 rounded-full dark:bg-green-900 dark:text-green-300";
12 | 		case "pending":
13 | 			return "bg-gray-100 text-gray-800 text-xs font-medium me-2 px-2.5 py-0.5 rounded-full dark:bg-gray-700 dark:text-gray-300";
14 | 		case "canceled":
15 | 			return "bg-red-100 text-red-800 text-xs font-medium me-2 px-2.5 py-0.5 rounded-full dark:bg-red-900 dark:text-red-300";
16 | 		default:
17 | 			return "bg-purple-100 text-purple-800 text-xs font-medium me-2 px-2.5 py-0.5 rounded-full dark:bg-purple-900 dark:text-purple-300";
18 | 	}
19 | }
20 | 
21 | export function toTitleCase(str) {
22 | 	return str.replace(
23 | 		/\w\S*/g,
24 | 		(text) => text.charAt(0).toUpperCase() + text.substring(1).toLowerCase()
25 | 	);
26 | }
27 | 
28 | export function formatInvoiceDateTime(dateStr, timeStr) {
29 | 	const date = new Date(dateStr);
30 | 	const time = new Date(timeStr);
31 | 
32 | 	const day = date.getDate().toString().padStart(2, "0");
33 | 	const month = (date.getMonth() + 1).toString().padStart(2, "0");
34 | 	const year = date.getFullYear();
35 | 
36 | 	const hours = time.getHours().toString().padStart(2, "0");
37 | 	const minutes = time.getMinutes().toString().padStart(2, "0");
38 | 
39 | 	return `${day}/${month}/${year} ${hours}:${minutes}`;
40 | }
41 | 
42 | export function formatDateFilename(date) {
43 | 	if (!date) return "";
44 | 
45 | 	const dates = new Date(date);
46 | 	const day = String(dates.getDate()).padStart(2, "0");
47 | 	const month = String(dates.getMonth() + 1).padStart(2, "0");
48 | 	const year = dates.getFullYear();
49 | 
50 | 	return `${day}/${month}/${year}`;
51 | }
52 | 
53 | export function getPageTitle(subTitle) {
54 | 	return `Cheese Stick Koe - ${subTitle}`;
55 | }
56 | 
57 | export function calculateDiscountPercent({
58 | 	quantity,
59 | 	price,
60 | 	discountMode,
61 | 	discountInput,
62 | 	discountAmount,
63 | }) {
64 | 	const rawTotal = (quantity || 0) * (price || 0);
65 | 
66 | 	if (!rawTotal) return "0";
67 | 
68 | 	const amount =
69 | 		discountMode === "percent"
70 | 			? (parseFloat(discountInput) / 100) * rawTotal
71 | 			: parseInt(discountInput) || discountAmount || 0;
72 | 
73 | 	const percent = (amount / rawTotal) * 100;
74 | 
75 | 	return isNaN(percent) ? "0" : percent.toFixed(2);
76 | }
77 | 
78 | export function calculateDiscountAmount({ quantity, price, discountInput, discountMode }) {
79 | 	const rawTotal = (quantity || 0) * (price || 0);
80 | 	if (discountMode === "percent") {
81 | 		return Math.round(((parseFloat(discountInput) || 0) / 100) * rawTotal);
82 | 	}
83 | 	return parseInt(discountInput) || 0;
84 | }
85 | 
```

--------------------------------------------------------------------------------
/src/components/dashboard/Sidebar.jsx:
--------------------------------------------------------------------------------

```javascript
  1 | "use client";
  2 | 
  3 | import Link from "next/link";
  4 | import { usePathname, useRouter } from "next/navigation";
  5 | import { useState } from "react";
  6 | import { Menu, X } from "lucide-react";
  7 | 
  8 | const navItems = [
  9 | 	{ name: "Dashboard", href: "/dashboard" },
 10 | 	{ name: "Products", href: "/dashboard/products" },
 11 | 	{ name: "Size & Price", href: "/dashboard/size-pricing" },
 12 | 	{
 13 | 		name: "Invoices",
 14 | 		href: "/dashboard/invoices",
 15 | 		children: [
 16 | 			{ name: "List Invoices", href: "/dashboard/invoices" },
 17 | 			{ name: "Create Invoice", href: "/dashboard/invoices/create" },
 18 | 		],
 19 | 	},
 20 | ];
 21 | 
 22 | export function Sidebar() {
 23 | 	const pathname = usePathname();
 24 | 	const router = useRouter();
 25 | 	const [open, setOpen] = useState(false);
 26 | 
 27 | 	const handleLogout = async () => {
 28 | 		try {
 29 | 			const res = await fetch("/api/logout", {
 30 | 				method: "POST",
 31 | 			});
 32 | 			if (res.ok) {
 33 | 				router.push("/");
 34 | 			} else {
 35 | 				console.error("Logout failed");
 36 | 			}
 37 | 		} catch (err) {
 38 | 			console.error("Logout error:", err);
 39 | 		}
 40 | 	};
 41 | 
 42 | 	return (
 43 | 		<>
 44 | 			{/* Burger Icon */}
 45 | 			<button onClick={() => setOpen(!open)} className="md:hidden p-4 focus:outline-none">
 46 | 				{open ? <X className="w-6 h-6" /> : <Menu className="w-6 h-6" />}
 47 | 			</button>
 48 | 
 49 | 			{/* Sidebar */}
 50 | 			<aside
 51 | 				className={`${
 52 | 					open ? "block" : "hidden"
 53 | 				} md:block w-full md:w-64 bg-[#fff7f3] border-r border-[#f1e3db] shadow-sm fixed md:static z-50 top-0 left-0 h-full md:h-auto`}
 54 | 			>
 55 | 				{/* Header */}
 56 | 				<div className="p-4 border-b border-[#f1e3db] flex justify-between items-center bg-[#fff2ea]">
 57 | 					<h1 className="text-lg font-bold text-[#6D2315] tracking-wide">Invoice App</h1>
 58 | 					<button onClick={() => setOpen(false)} className="md:hidden">
 59 | 						<X className="w-5 h-5 text-[#6D2315]" />
 60 | 					</button>
 61 | 				</div>
 62 | 
 63 | 				{/* Nav Items */}
 64 | 				<nav className="flex flex-col p-4 space-y-2 text-sm text-gray-700">
 65 | 					{navItems.map((item) => {
 66 | 						const isActive = pathname === item.href || pathname.startsWith(item.href + "/");
 67 | 
 68 | 						if (item.children) {
 69 | 							return (
 70 | 								<div key={item.href} className="space-y-1">
 71 | 									<div
 72 | 										className={`px-3 py-2 rounded-md font-semibold ${
 73 | 											isActive ? "bg-[#f9e0cd] text-[#6D2315]" : "text-gray-700 hover:bg-[#fceee4]"
 74 | 										}`}
 75 | 									>
 76 | 										{item.name}
 77 | 									</div>
 78 | 									<div className="ml-4 space-y-1">
 79 | 										{item.children.map((child) => (
 80 | 											<Link
 81 | 												key={child.href}
 82 | 												href={child.href}
 83 | 												className={`block px-3 py-2 rounded-md text-sm ${
 84 | 													pathname === child.href
 85 | 														? "bg-[#f9e0cd] text-[#6D2315] font-semibold"
 86 | 														: "hover:bg-[#fceee4] text-gray-700"
 87 | 												}`}
 88 | 												onClick={() => setOpen(false)}
 89 | 											>
 90 | 												{child.name}
 91 | 											</Link>
 92 | 										))}
 93 | 									</div>
 94 | 								</div>
 95 | 							);
 96 | 						}
 97 | 
 98 | 						return (
 99 | 							<Link
100 | 								key={item.href}
101 | 								href={item.href}
102 | 								className={`px-3 py-2 rounded-md ${
103 | 									pathname === item.href
104 | 										? "bg-[#f9e0cd] text-[#6D2315] font-semibold"
105 | 										: "hover:bg-[#fceee4] text-gray-700"
106 | 								}`}
107 | 								onClick={() => setOpen(false)}
108 | 							>
109 | 								{item.name}
110 | 							</Link>
111 | 						);
112 | 					})}
113 | 
114 | 					<button
115 | 						onClick={handleLogout}
116 | 						className="mt-4 text-sm text-left text-red-500 hover:bg-red-100 px-3 py-2 rounded-md"
117 | 					>
118 | 						Logout
119 | 					</button>
120 | 				</nav>
121 | 			</aside>
122 | 		</>
123 | 	);
124 | }
125 | 
```

--------------------------------------------------------------------------------
/src/components/ui/dialog.jsx:
--------------------------------------------------------------------------------

```javascript
  1 | "use client"
  2 | 
  3 | import * as React from "react"
  4 | import * as DialogPrimitive from "@radix-ui/react-dialog"
  5 | import { XIcon } from "lucide-react"
  6 | 
  7 | import { cn } from "@/lib/utils"
  8 | 
  9 | function Dialog({
 10 |   ...props
 11 | }) {
 12 |   return <DialogPrimitive.Root data-slot="dialog" {...props} />;
 13 | }
 14 | 
 15 | function DialogTrigger({
 16 |   ...props
 17 | }) {
 18 |   return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />;
 19 | }
 20 | 
 21 | function DialogPortal({
 22 |   ...props
 23 | }) {
 24 |   return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />;
 25 | }
 26 | 
 27 | function DialogClose({
 28 |   ...props
 29 | }) {
 30 |   return <DialogPrimitive.Close data-slot="dialog-close" {...props} />;
 31 | }
 32 | 
 33 | function DialogOverlay({
 34 |   className,
 35 |   ...props
 36 | }) {
 37 |   return (
 38 |     <DialogPrimitive.Overlay
 39 |       data-slot="dialog-overlay"
 40 |       className={cn(
 41 |         "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
 42 |         className
 43 |       )}
 44 |       {...props} />
 45 |   );
 46 | }
 47 | 
 48 | function DialogContent({
 49 |   className,
 50 |   children,
 51 |   showCloseButton = true,
 52 |   ...props
 53 | }) {
 54 |   return (
 55 |     <DialogPortal data-slot="dialog-portal">
 56 |       <DialogOverlay />
 57 |       <DialogPrimitive.Content
 58 |         data-slot="dialog-content"
 59 |         className={cn(
 60 |           "bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 sm:max-w-lg",
 61 |           className
 62 |         )}
 63 |         {...props}>
 64 |         {children}
 65 |         {showCloseButton && (
 66 |           <DialogPrimitive.Close
 67 |             data-slot="dialog-close"
 68 |             className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4">
 69 |             <XIcon />
 70 |             <span className="sr-only">Close</span>
 71 |           </DialogPrimitive.Close>
 72 |         )}
 73 |       </DialogPrimitive.Content>
 74 |     </DialogPortal>
 75 |   );
 76 | }
 77 | 
 78 | function DialogHeader({
 79 |   className,
 80 |   ...props
 81 | }) {
 82 |   return (
 83 |     <div
 84 |       data-slot="dialog-header"
 85 |       className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
 86 |       {...props} />
 87 |   );
 88 | }
 89 | 
 90 | function DialogFooter({
 91 |   className,
 92 |   ...props
 93 | }) {
 94 |   return (
 95 |     <div
 96 |       data-slot="dialog-footer"
 97 |       className={cn("flex flex-col-reverse gap-2 sm:flex-row sm:justify-end", className)}
 98 |       {...props} />
 99 |   );
100 | }
101 | 
102 | function DialogTitle({
103 |   className,
104 |   ...props
105 | }) {
106 |   return (
107 |     <DialogPrimitive.Title
108 |       data-slot="dialog-title"
109 |       className={cn("text-lg leading-none font-semibold", className)}
110 |       {...props} />
111 |   );
112 | }
113 | 
114 | function DialogDescription({
115 |   className,
116 |   ...props
117 | }) {
118 |   return (
119 |     <DialogPrimitive.Description
120 |       data-slot="dialog-description"
121 |       className={cn("text-muted-foreground text-sm", className)}
122 |       {...props} />
123 |   );
124 | }
125 | 
126 | export {
127 |   Dialog,
128 |   DialogClose,
129 |   DialogContent,
130 |   DialogDescription,
131 |   DialogFooter,
132 |   DialogHeader,
133 |   DialogOverlay,
134 |   DialogPortal,
135 |   DialogTitle,
136 |   DialogTrigger,
137 | }
138 | 
```

--------------------------------------------------------------------------------
/src/components/ui/drawer.jsx:
--------------------------------------------------------------------------------

```javascript
  1 | "use client"
  2 | 
  3 | import * as React from "react"
  4 | import { Drawer as DrawerPrimitive } from "vaul"
  5 | 
  6 | import { cn } from "@/lib/utils"
  7 | 
  8 | function Drawer({
  9 |   ...props
 10 | }) {
 11 |   return <DrawerPrimitive.Root data-slot="drawer" {...props} />;
 12 | }
 13 | 
 14 | function DrawerTrigger({
 15 |   ...props
 16 | }) {
 17 |   return <DrawerPrimitive.Trigger data-slot="drawer-trigger" {...props} />;
 18 | }
 19 | 
 20 | function DrawerPortal({
 21 |   ...props
 22 | }) {
 23 |   return <DrawerPrimitive.Portal data-slot="drawer-portal" {...props} />;
 24 | }
 25 | 
 26 | function DrawerClose({
 27 |   ...props
 28 | }) {
 29 |   return <DrawerPrimitive.Close data-slot="drawer-close" {...props} />;
 30 | }
 31 | 
 32 | function DrawerOverlay({
 33 |   className,
 34 |   ...props
 35 | }) {
 36 |   return (
 37 |     <DrawerPrimitive.Overlay
 38 |       data-slot="drawer-overlay"
 39 |       className={cn(
 40 |         "data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
 41 |         className
 42 |       )}
 43 |       {...props} />
 44 |   );
 45 | }
 46 | 
 47 | function DrawerContent({
 48 |   className,
 49 |   children,
 50 |   ...props
 51 | }) {
 52 |   return (
 53 |     <DrawerPortal data-slot="drawer-portal">
 54 |       <DrawerOverlay />
 55 |       <DrawerPrimitive.Content
 56 |         data-slot="drawer-content"
 57 |         className={cn(
 58 |           "group/drawer-content bg-background fixed z-50 flex h-auto flex-col",
 59 |           "data-[vaul-drawer-direction=top]:inset-x-0 data-[vaul-drawer-direction=top]:top-0 data-[vaul-drawer-direction=top]:mb-24 data-[vaul-drawer-direction=top]:max-h-[80vh] data-[vaul-drawer-direction=top]:rounded-b-lg data-[vaul-drawer-direction=top]:border-b",
 60 |           "data-[vaul-drawer-direction=bottom]:inset-x-0 data-[vaul-drawer-direction=bottom]:bottom-0 data-[vaul-drawer-direction=bottom]:mt-24 data-[vaul-drawer-direction=bottom]:max-h-[80vh] data-[vaul-drawer-direction=bottom]:rounded-t-lg data-[vaul-drawer-direction=bottom]:border-t",
 61 |           "data-[vaul-drawer-direction=right]:inset-y-0 data-[vaul-drawer-direction=right]:right-0 data-[vaul-drawer-direction=right]:w-3/4 data-[vaul-drawer-direction=right]:border-l data-[vaul-drawer-direction=right]:sm:max-w-sm",
 62 |           "data-[vaul-drawer-direction=left]:inset-y-0 data-[vaul-drawer-direction=left]:left-0 data-[vaul-drawer-direction=left]:w-3/4 data-[vaul-drawer-direction=left]:border-r data-[vaul-drawer-direction=left]:sm:max-w-sm",
 63 |           className
 64 |         )}
 65 |         {...props}>
 66 |         <div
 67 |           className="bg-muted mx-auto mt-4 hidden h-2 w-[100px] shrink-0 rounded-full group-data-[vaul-drawer-direction=bottom]/drawer-content:block" />
 68 |         {children}
 69 |       </DrawerPrimitive.Content>
 70 |     </DrawerPortal>
 71 |   );
 72 | }
 73 | 
 74 | function DrawerHeader({
 75 |   className,
 76 |   ...props
 77 | }) {
 78 |   return (
 79 |     <div
 80 |       data-slot="drawer-header"
 81 |       className={cn(
 82 |         "flex flex-col gap-0.5 p-4 group-data-[vaul-drawer-direction=bottom]/drawer-content:text-center group-data-[vaul-drawer-direction=top]/drawer-content:text-center md:gap-1.5 md:text-left",
 83 |         className
 84 |       )}
 85 |       {...props} />
 86 |   );
 87 | }
 88 | 
 89 | function DrawerFooter({
 90 |   className,
 91 |   ...props
 92 | }) {
 93 |   return (
 94 |     <div
 95 |       data-slot="drawer-footer"
 96 |       className={cn("mt-auto flex flex-col gap-2 p-4", className)}
 97 |       {...props} />
 98 |   );
 99 | }
100 | 
101 | function DrawerTitle({
102 |   className,
103 |   ...props
104 | }) {
105 |   return (
106 |     <DrawerPrimitive.Title
107 |       data-slot="drawer-title"
108 |       className={cn("text-foreground font-semibold", className)}
109 |       {...props} />
110 |   );
111 | }
112 | 
113 | function DrawerDescription({
114 |   className,
115 |   ...props
116 | }) {
117 |   return (
118 |     <DrawerPrimitive.Description
119 |       data-slot="drawer-description"
120 |       className={cn("text-muted-foreground text-sm", className)}
121 |       {...props} />
122 |   );
123 | }
124 | 
125 | export {
126 |   Drawer,
127 |   DrawerPortal,
128 |   DrawerOverlay,
129 |   DrawerTrigger,
130 |   DrawerClose,
131 |   DrawerContent,
132 |   DrawerHeader,
133 |   DrawerFooter,
134 |   DrawerTitle,
135 |   DrawerDescription,
136 | }
137 | 
```

--------------------------------------------------------------------------------
/src/app/_components/LoginForm.jsx:
--------------------------------------------------------------------------------

```javascript
  1 | "use client";
  2 | 
  3 | import { useEffect, useState } from "react";
  4 | import { useRouter } from "next/navigation";
  5 | 
  6 | import { supabaseBrowser } from "@/lib/supabaseBrowser";
  7 | 
  8 | import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
  9 | import { Input } from "@/components/ui/input";
 10 | import { Button } from "@/components/ui/button";
 11 | 
 12 | import { toast } from "sonner";
 13 | import { Loader2Icon, LockIcon } from "lucide-react";
 14 | 
 15 | import { Controller, useForm } from "react-hook-form";
 16 | 
 17 | export default function LoginForm() {
 18 | 	const router = useRouter();
 19 | 	const [loading, setLoading] = useState(false);
 20 | 
 21 | 	useEffect(() => {
 22 | 		// set loading state false when route change complete
 23 | 		const handleComplete = () => setLoading(false);
 24 | 
 25 | 		// Listen to route change events
 26 | 		router.events?.on("routeChangeComplete", handleComplete);
 27 | 		router.events?.on("routeChangeError", handleComplete);
 28 | 
 29 | 		return () => {
 30 | 			router.events?.off("routeChangeComplete", handleComplete);
 31 | 			router.events?.off("routeChangeError", handleComplete);
 32 | 		};
 33 | 	}, [router]);
 34 | 
 35 | 	const {
 36 | 		control,
 37 | 		handleSubmit,
 38 | 		formState: { errors },
 39 | 	} = useForm({
 40 | 		defaultValues: {
 41 | 			email: "",
 42 | 			password: "",
 43 | 		},
 44 | 	});
 45 | 
 46 | 	const onSubmit = async (data) => {
 47 | 		try {
 48 | 			setLoading(true);
 49 | 			const supabase = supabaseBrowser();
 50 | 			const { error } = await supabase.auth.signInWithPassword({
 51 | 				email: data.email,
 52 | 				password: data.password,
 53 | 			});
 54 | 
 55 | 			if (error) throw error;
 56 | 
 57 | 			await router.push("/dashboard");
 58 | 		} catch (err) {
 59 | 			toast.error(err.message);
 60 | 			setLoading(false);
 61 | 		}
 62 | 	};
 63 | 
 64 | 	return (
 65 | 		<div className="min-h-screen flex items-center justify-center px-4 bg-gradient-to-br from-[#fef6f3] to-[#f1f5f9]">
 66 | 			<Card className="w-full max-w-md shadow-xl border border-gray-200">
 67 | 				<CardHeader className="text-center space-y-2">
 68 | 					<div className="flex justify-center">
 69 | 						<div className="w-14 h-14 rounded-full bg-[#6d2315] text-white flex items-center justify-center text-2xl font-bold">
 70 | 							<LockIcon />
 71 | 						</div>
 72 | 					</div>
 73 | 					<CardTitle className="text-2xl font-bold text-[#6d2315]">Welcome Back</CardTitle>
 74 | 					<p className="text-sm text-gray-500">Please login to your account</p>
 75 | 				</CardHeader>
 76 | 
 77 | 				<CardContent>
 78 | 					<form onSubmit={handleSubmit(onSubmit)} className="space-y-5">
 79 | 						<div className="space-y-1">
 80 | 							<label htmlFor="email" className="text-sm font-medium text-gray-700">
 81 | 								Email
 82 | 							</label>
 83 | 							<Controller
 84 | 								name="email"
 85 | 								control={control}
 86 | 								rules={{ required: "Email is required!" }}
 87 | 								render={({ field }) => (
 88 | 									<Input
 89 | 										{...field}
 90 | 										id="email"
 91 | 										type="email"
 92 | 										placeholder="[email protected]"
 93 | 										required
 94 | 									/>
 95 | 								)}
 96 | 							/>
 97 | 							{errors.email && (
 98 | 								<p role="alert" className="text-sm text-red-500">
 99 | 									{errors.email.message}
100 | 								</p>
101 | 							)}
102 | 						</div>
103 | 
104 | 						<div className="space-y-1">
105 | 							<label htmlFor="password" className="text-sm font-medium text-gray-700">
106 | 								Password
107 | 							</label>
108 | 							<Controller
109 | 								name="password"
110 | 								control={control}
111 | 								rules={{ required: "Password is required!" }}
112 | 								render={({ field }) => (
113 | 									<Input {...field} id="password" type="password" placeholder="••••••••" required />
114 | 								)}
115 | 							/>
116 | 							{errors.password && (
117 | 								<p role="alert" className="text-sm text-red-500">
118 | 									{errors.password.message}
119 | 								</p>
120 | 							)}
121 | 						</div>
122 | 
123 | 						{/* {error && <p className="text-sm text-red-500 text-center">{error}</p>} */}
124 | 
125 | 						<Button
126 | 							type="submit"
127 | 							className="w-full bg-[#6d2315] hover:bg-[#591c10]"
128 | 							disabled={loading}
129 | 						>
130 | 							{loading ? (
131 | 								<>
132 | 									<Loader2Icon className="mr-2 h-4 w-4 animate-spin" />
133 | 									Logging in...
134 | 								</>
135 | 							) : (
136 | 								"Login"
137 | 							)}
138 | 						</Button>
139 | 					</form>
140 | 				</CardContent>
141 | 			</Card>
142 | 		</div>
143 | 	);
144 | }
145 | 
```

--------------------------------------------------------------------------------
/src/app/globals.css:
--------------------------------------------------------------------------------

```css
  1 | @import "tailwindcss";
  2 | @import "tw-animate-css";
  3 | 
  4 | @custom-variant dark (&:is(.dark *));
  5 | 
  6 | @theme inline {
  7 | 	--color-background: var(--background);
  8 | 	--color-foreground: var(--foreground);
  9 | 	--font-sans: var(--font-geist-sans);
 10 | 	--font-mono: var(--font-geist-mono);
 11 | 	--color-sidebar-ring: var(--sidebar-ring);
 12 | 	--color-sidebar-border: var(--sidebar-border);
 13 | 	--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
 14 | 	--color-sidebar-accent: var(--sidebar-accent);
 15 | 	--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
 16 | 	--color-sidebar-primary: var(--sidebar-primary);
 17 | 	--color-sidebar-foreground: var(--sidebar-foreground);
 18 | 	--color-sidebar: var(--sidebar);
 19 | 	--color-chart-5: var(--chart-5);
 20 | 	--color-chart-4: var(--chart-4);
 21 | 	--color-chart-3: var(--chart-3);
 22 | 	--color-chart-2: var(--chart-2);
 23 | 	--color-chart-1: var(--chart-1);
 24 | 	--color-ring: var(--ring);
 25 | 	--color-input: var(--input);
 26 | 	--color-border: var(--border);
 27 | 	--color-destructive: var(--destructive);
 28 | 	--color-accent-foreground: var(--accent-foreground);
 29 | 	--color-accent: var(--accent);
 30 | 	--color-muted-foreground: var(--muted-foreground);
 31 | 	--color-muted: var(--muted);
 32 | 	--color-secondary-foreground: var(--secondary-foreground);
 33 | 	--color-secondary: var(--secondary);
 34 | 	--color-primary-foreground: var(--primary-foreground);
 35 | 	--color-primary: var(--primary);
 36 | 	--color-popover-foreground: var(--popover-foreground);
 37 | 	--color-popover: var(--popover);
 38 | 	--color-card-foreground: var(--card-foreground);
 39 | 	--color-card: var(--card);
 40 | 	--radius-sm: calc(var(--radius) - 4px);
 41 | 	--radius-md: calc(var(--radius) - 2px);
 42 | 	--radius-lg: var(--radius);
 43 | 	--radius-xl: calc(var(--radius) + 4px);
 44 | }
 45 | 
 46 | :root {
 47 | 	--radius: 0.625rem;
 48 | 	--background: oklch(1 0 0);
 49 | 	--foreground: oklch(0.145 0 0);
 50 | 	--card: oklch(1 0 0);
 51 | 	--card-foreground: oklch(0.145 0 0);
 52 | 	--popover: oklch(1 0 0);
 53 | 	--popover-foreground: oklch(0.145 0 0);
 54 | 	--primary: oklch(0.205 0 0);
 55 | 	--primary-foreground: oklch(0.985 0 0);
 56 | 	--secondary: oklch(0.97 0 0);
 57 | 	--secondary-foreground: oklch(0.205 0 0);
 58 | 	--muted: oklch(0.97 0 0);
 59 | 	--muted-foreground: oklch(0.556 0 0);
 60 | 	--accent: oklch(0.97 0 0);
 61 | 	--accent-foreground: oklch(0.205 0 0);
 62 | 	--destructive: oklch(0.577 0.245 27.325);
 63 | 	--border: oklch(0.922 0 0);
 64 | 	--input: oklch(0.922 0 0);
 65 | 	--ring: oklch(0.708 0 0);
 66 | 	--chart-1: oklch(0.646 0.222 41.116);
 67 | 	--chart-2: oklch(0.6 0.118 184.704);
 68 | 	--chart-3: oklch(0.398 0.07 227.392);
 69 | 	--chart-4: oklch(0.828 0.189 84.429);
 70 | 	--chart-5: oklch(0.769 0.188 70.08);
 71 | 	--sidebar: oklch(0.985 0 0);
 72 | 	--sidebar-foreground: oklch(0.145 0 0);
 73 | 	--sidebar-primary: oklch(0.205 0 0);
 74 | 	--sidebar-primary-foreground: oklch(0.985 0 0);
 75 | 	--sidebar-accent: oklch(0.97 0 0);
 76 | 	--sidebar-accent-foreground: oklch(0.205 0 0);
 77 | 	--sidebar-border: oklch(0.922 0 0);
 78 | 	--sidebar-ring: oklch(0.708 0 0);
 79 | }
 80 | 
 81 | .dark {
 82 | 	--background: oklch(0.145 0 0);
 83 | 	--foreground: oklch(0.985 0 0);
 84 | 	--card: oklch(0.205 0 0);
 85 | 	--card-foreground: oklch(0.985 0 0);
 86 | 	--popover: oklch(0.205 0 0);
 87 | 	--popover-foreground: oklch(0.985 0 0);
 88 | 	--primary: oklch(0.922 0 0);
 89 | 	--primary-foreground: oklch(0.205 0 0);
 90 | 	--secondary: oklch(0.269 0 0);
 91 | 	--secondary-foreground: oklch(0.985 0 0);
 92 | 	--muted: oklch(0.269 0 0);
 93 | 	--muted-foreground: oklch(0.708 0 0);
 94 | 	--accent: oklch(0.269 0 0);
 95 | 	--accent-foreground: oklch(0.985 0 0);
 96 | 	--destructive: oklch(0.704 0.191 22.216);
 97 | 	--border: oklch(1 0 0 / 10%);
 98 | 	--input: oklch(1 0 0 / 15%);
 99 | 	--ring: oklch(0.556 0 0);
100 | 	--chart-1: oklch(0.488 0.243 264.376);
101 | 	--chart-2: oklch(0.696 0.17 162.48);
102 | 	--chart-3: oklch(0.769 0.188 70.08);
103 | 	--chart-4: oklch(0.627 0.265 303.9);
104 | 	--chart-5: oklch(0.645 0.246 16.439);
105 | 	--sidebar: oklch(0.205 0 0);
106 | 	--sidebar-foreground: oklch(0.985 0 0);
107 | 	--sidebar-primary: oklch(0.488 0.243 264.376);
108 | 	--sidebar-primary-foreground: oklch(0.985 0 0);
109 | 	--sidebar-accent: oklch(0.269 0 0);
110 | 	--sidebar-accent-foreground: oklch(0.985 0 0);
111 | 	--sidebar-border: oklch(1 0 0 / 10%);
112 | 	--sidebar-ring: oklch(0.556 0 0);
113 | }
114 | 
115 | @layer base {
116 | 	* {
117 | 		@apply border-border outline-ring/50;
118 | 	}
119 | 	body {
120 | 		@apply bg-background text-foreground;
121 | 	}
122 | }
123 | 
124 | button {
125 | 	cursor: pointer;
126 | }
127 | 
```

--------------------------------------------------------------------------------
/src/app/dashboard/invoices/_components/InvoiceDownloadModal.jsx:
--------------------------------------------------------------------------------

```javascript
  1 | "use client";
  2 | 
  3 | import { useRef, useState } from "react";
  4 | 
  5 | import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog";
  6 | import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
  7 | import { Button } from "@/components/ui/button";
  8 | import { Label } from "@/components/ui/label";
  9 | 
 10 | import InvoicePreview from "./InvoicePreview";
 11 | 
 12 | import { exportInvoiceToPng } from "@/lib/exportToPng";
 13 | import { formatDateFilename } from "@/lib/utils";
 14 | 
 15 | import { toast } from "sonner";
 16 | 
 17 | export default function InvoiceDownloadModal({ open, onOpenChange, invoice, invoiceItems }) {
 18 | 	const invoiceRef = useRef(null);
 19 | 	const hiddenRef = useRef(null);
 20 | 
 21 | 	const [dataReady, setDataReady] = useState(false);
 22 | 	const [isInvoiceReady, setIsInvoiceReady] = useState(false);
 23 | 	const [isDownloading, setIsDownloading] = useState(false);
 24 | 
 25 | 	const [shippingType, setShippingType] = useState("");
 26 | 
 27 | 	const handleDownload = async () => {
 28 | 		if (isDownloading) {
 29 | 			toast.error("Wait until download complete");
 30 | 			return;
 31 | 		}
 32 | 
 33 | 		if (!isInvoiceReady || !dataReady) {
 34 | 			toast.error("Wait a second. Invoice is loading ..");
 35 | 			return;
 36 | 		}
 37 | 
 38 | 		setIsDownloading(true);
 39 | 
 40 | 		try {
 41 | 			await document.fonts.ready;
 42 | 			await new Promise((r) => setTimeout(r, 200));
 43 | 
 44 | 			const formattedName = `Invoice-${invoice.invoiceNumber}_${
 45 | 				invoice.buyerName
 46 | 			}_${formatDateFilename(invoice?.invoiceDate).replaceAll("/", "")}.png`
 47 | 				.replace(/\s+/g, "-")
 48 | 				.toLowerCase();
 49 | 
 50 | 			await exportInvoiceToPng(hiddenRef.current, formattedName);
 51 | 		} catch (error) {
 52 | 			console.error("Download failed:", error);
 53 | 			toast.error("Failed to export invoice");
 54 | 		} finally {
 55 | 			setTimeout(() => setIsDownloading(false), 3000);
 56 | 		}
 57 | 	};
 58 | 
 59 | 	return (
 60 | 		<Dialog open={open} onOpenChange={onOpenChange}>
 61 | 			<DialogHeader>
 62 | 				<DialogTitle className="sr-only">Preview Invoice</DialogTitle>
 63 | 			</DialogHeader>
 64 | 			<DialogContent className="w-full md:max-w-7xl max-h-[90vh] overflow-y-auto p-0 mx-auto">
 65 | 				<div
 66 | 					ref={hiddenRef}
 67 | 					className="absolute -left-[9999px] top-0 bg-white p-2"
 68 | 					style={{
 69 | 						width: "max-content",
 70 | 						display: "inline-block",
 71 | 						overflow: "visible",
 72 | 					}}
 73 | 				>
 74 | 					<InvoicePreview
 75 | 						invoice={invoice}
 76 | 						invoiceItems={invoiceItems}
 77 | 						onDataReady={setDataReady}
 78 | 						oonReady={() => setIsInvoiceReady(true)}
 79 | 						shippingType={shippingType}
 80 | 						isDownloadVersion
 81 | 					/>
 82 | 				</div>
 83 | 
 84 | 				<div className="flex flex-col md:flex-row gap-0">
 85 | 					{/* main content */}
 86 | 					<div ref={invoiceRef} className="bg-white p-6 flex-1">
 87 | 						<InvoicePreview
 88 | 							invoice={invoice}
 89 | 							invoiceItems={invoiceItems}
 90 | 							onDataReady={setDataReady}
 91 | 							shippingType={shippingType}
 92 | 							onReady={() => setIsInvoiceReady(true)}
 93 | 						/>
 94 | 					</div>
 95 | 
 96 | 					{/* sidebar */}
 97 | 					<div className="md:w-64 bg-white pt-5 flex flex-col items-center md:items-start text-center md:text-left">
 98 | 						<p className="mb-2 font-medium">Pilih Ongkir</p>
 99 | 						<RadioGroup
100 | 							value={shippingType}
101 | 							onValueChange={setShippingType}
102 | 							className="flex flex-col gap-3"
103 | 						>
104 | 							{["", "sameday", "instan", "jne", "j&t"].map((value) => (
105 | 								<div key={value} className="flex items-center space-x-2">
106 | 									<RadioGroupItem value={value} id={value || "default"} />
107 | 									<Label htmlFor={value || "default"} className="text-xs">
108 | 										{value === "" ? "Default (tanpa opsi)" : value}
109 | 									</Label>
110 | 								</div>
111 | 							))}
112 | 						</RadioGroup>
113 | 					</div>
114 | 				</div>
115 | 
116 | 				{/* desktop view */}
117 | 				<div className="hidden md:block sticky bottom-0 z-10 bg-white p-4 border-t border-gray-200 shadow-sm text-center">
118 | 					<Button onClick={handleDownload} disabled={isDownloading}>
119 | 						{isDownloading ? "Please wait ..." : "Download PNG"}
120 | 					</Button>
121 | 				</div>
122 | 
123 | 				{/* mobile view */}
124 | 				<div className="md:hidden sticky bottom-0 z-10 bg-white p-4 border-t border-gray-200">
125 | 					<Button className="w-full" onClick={handleDownload} disabled={isDownloading}>
126 | 						{isDownloading ? "Please wait ..." : "Download PNG"}
127 | 					</Button>
128 | 				</div>
129 | 			</DialogContent>
130 | 		</Dialog>
131 | 	);
132 | }
133 | 
```

--------------------------------------------------------------------------------
/src/components/ui/command.jsx:
--------------------------------------------------------------------------------

```javascript
  1 | "use client"
  2 | 
  3 | import * as React from "react"
  4 | import { Command as CommandPrimitive } from "cmdk"
  5 | import { SearchIcon } from "lucide-react"
  6 | 
  7 | import { cn } from "@/lib/utils"
  8 | import {
  9 |   Dialog,
 10 |   DialogContent,
 11 |   DialogDescription,
 12 |   DialogHeader,
 13 |   DialogTitle,
 14 | } from "@/components/ui/dialog"
 15 | 
 16 | function Command({
 17 |   className,
 18 |   ...props
 19 | }) {
 20 |   return (
 21 |     <CommandPrimitive
 22 |       data-slot="command"
 23 |       className={cn(
 24 |         "bg-popover text-popover-foreground flex h-full w-full flex-col overflow-hidden rounded-md",
 25 |         className
 26 |       )}
 27 |       {...props} />
 28 |   );
 29 | }
 30 | 
 31 | function CommandDialog({
 32 |   title = "Command Palette",
 33 |   description = "Search for a command to run...",
 34 |   children,
 35 |   className,
 36 |   showCloseButton = true,
 37 |   ...props
 38 | }) {
 39 |   return (
 40 |     <Dialog {...props}>
 41 |       <DialogHeader className="sr-only">
 42 |         <DialogTitle>{title}</DialogTitle>
 43 |         <DialogDescription>{description}</DialogDescription>
 44 |       </DialogHeader>
 45 |       <DialogContent
 46 |         className={cn("overflow-hidden p-0", className)}
 47 |         showCloseButton={showCloseButton}>
 48 |         <Command
 49 |           className="[&_[cmdk-group-heading]]:text-muted-foreground **:data-[slot=command-input-wrapper]:h-12 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group]]:px-2 [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
 50 |           {children}
 51 |         </Command>
 52 |       </DialogContent>
 53 |     </Dialog>
 54 |   );
 55 | }
 56 | 
 57 | function CommandInput({
 58 |   className,
 59 |   ...props
 60 | }) {
 61 |   return (
 62 |     <div
 63 |       data-slot="command-input-wrapper"
 64 |       className="flex h-9 items-center gap-2 border-b px-3">
 65 |       <SearchIcon className="size-4 shrink-0 opacity-50" />
 66 |       <CommandPrimitive.Input
 67 |         data-slot="command-input"
 68 |         className={cn(
 69 |           "placeholder:text-muted-foreground flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50",
 70 |           className
 71 |         )}
 72 |         {...props} />
 73 |     </div>
 74 |   );
 75 | }
 76 | 
 77 | function CommandList({
 78 |   className,
 79 |   ...props
 80 | }) {
 81 |   return (
 82 |     <CommandPrimitive.List
 83 |       data-slot="command-list"
 84 |       className={cn("max-h-[300px] scroll-py-1 overflow-x-hidden overflow-y-auto", className)}
 85 |       {...props} />
 86 |   );
 87 | }
 88 | 
 89 | function CommandEmpty({
 90 |   ...props
 91 | }) {
 92 |   return (<CommandPrimitive.Empty data-slot="command-empty" className="py-6 text-center text-sm" {...props} />);
 93 | }
 94 | 
 95 | function CommandGroup({
 96 |   className,
 97 |   ...props
 98 | }) {
 99 |   return (
100 |     <CommandPrimitive.Group
101 |       data-slot="command-group"
102 |       className={cn(
103 |         "text-foreground [&_[cmdk-group-heading]]:text-muted-foreground overflow-hidden p-1 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium",
104 |         className
105 |       )}
106 |       {...props} />
107 |   );
108 | }
109 | 
110 | function CommandSeparator({
111 |   className,
112 |   ...props
113 | }) {
114 |   return (
115 |     <CommandPrimitive.Separator
116 |       data-slot="command-separator"
117 |       className={cn("bg-border -mx-1 h-px", className)}
118 |       {...props} />
119 |   );
120 | }
121 | 
122 | function CommandItem({
123 |   className,
124 |   ...props
125 | }) {
126 |   return (
127 |     <CommandPrimitive.Item
128 |       data-slot="command-item"
129 |       className={cn(
130 |         "data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
131 |         className
132 |       )}
133 |       {...props} />
134 |   );
135 | }
136 | 
137 | function CommandShortcut({
138 |   className,
139 |   ...props
140 | }) {
141 |   return (
142 |     <span
143 |       data-slot="command-shortcut"
144 |       className={cn("text-muted-foreground ml-auto text-xs tracking-widest", className)}
145 |       {...props} />
146 |   );
147 | }
148 | 
149 | export {
150 |   Command,
151 |   CommandDialog,
152 |   CommandInput,
153 |   CommandList,
154 |   CommandEmpty,
155 |   CommandGroup,
156 |   CommandItem,
157 |   CommandShortcut,
158 |   CommandSeparator,
159 | }
160 | 
```

--------------------------------------------------------------------------------
/src/app/dashboard/size-pricing/_components/Table.jsx:
--------------------------------------------------------------------------------

```javascript
  1 | "use client";
  2 | 
  3 | import { useState, useEffect, forwardRef, useImperativeHandle } from "react";
  4 | 
  5 | import { Card } from "@/components/ui/card";
  6 | import { Button } from "@/components/ui/button";
  7 | 
  8 | import EditSizeModal from "./EditSizeModal";
  9 | import DeleteSizeModal from "./DeleteSizeModal";
 10 | 
 11 | import { ArrowUp, ArrowDown, Pencil, Trash2 } from "lucide-react";
 12 | 
 13 | import { getAllSizePrice } from "@/lib/actions/size-price/getAll";
 14 | 
 15 | const ITEMS_PER_PAGE = 10;
 16 | 
 17 | const SizePriceTable = forwardRef(function SizePriceTable(props, ref) {
 18 | 	const [size, setSize] = useState([]);
 19 | 	const [sortOrder, setSortOrder] = useState("asc");
 20 | 
 21 | 	const [error, setError] = useState(null);
 22 | 	const [currentPage, setCurrentPage] = useState(1);
 23 | 
 24 | 	const [selectedSize, setSelectedSize] = useState(null);
 25 | 	const [deleteModalOpen, setDeleteModalOpen] = useState(false);
 26 | 	const [editModalOpen, setEditModalOpen] = useState(false);
 27 | 
 28 | 	if (error) return <p className="text-red-500">Failed to fetch data: {error}</p>;
 29 | 
 30 | 	const fetchData = async () => {
 31 | 		const { data, error } = await getAllSizePrice(sortOrder);
 32 | 		if (error) setError(error.message);
 33 | 		else setSize(data);
 34 | 	};
 35 | 
 36 | 	useEffect(() => {
 37 | 		fetchData();
 38 | 	}, [sortOrder]);
 39 | 
 40 | 	useImperativeHandle(ref, () => ({
 41 | 		refetch: fetchData,
 42 | 	}));
 43 | 
 44 | 	// pagination
 45 | 	const totalPages = Math.ceil(size.length / ITEMS_PER_PAGE);
 46 | 	const paginatedData = size.slice(
 47 | 		(currentPage - 1) * ITEMS_PER_PAGE,
 48 | 		currentPage * ITEMS_PER_PAGE
 49 | 	);
 50 | 
 51 | 	return (
 52 | 		<Card className="p-4 bg-[#fffaf0] border border-[#f4e3d3] shadow-sm">
 53 | 			<div className="overflow-x-auto rounded-lg border border-[#fceee4]">
 54 | 				<table className="w-full text-sm">
 55 | 					<thead className="bg-[#fdf2e9] text-[#6D2315]">
 56 | 						<tr>
 57 | 							<th
 58 | 								className="px-4 py-2 text-left font-semibold cursor-pointer"
 59 | 								onClick={() => {
 60 | 									setSortOrder((prev) => (prev === "asc" ? "desc" : "asc"));
 61 | 									setCurrentPage(1); // reset ke page 1 tiap kali sorting
 62 | 								}}
 63 | 							>
 64 | 								<div className="flex items-center gap-1">
 65 | 									Size
 66 | 									{sortOrder === "asc" ? (
 67 | 										<ArrowUp className="w-4 h-4" />
 68 | 									) : (
 69 | 										<ArrowDown className="w-4 h-4" />
 70 | 									)}
 71 | 								</div>
 72 | 							</th>
 73 | 
 74 | 							<th className="px-4 py-2 text-left font-semibold">Price</th>
 75 | 							<th className="px-4 py-2 text-left font-semibold">Created At</th>
 76 | 							<th className="px-4 py-2 text-left font-semibold">Action</th>
 77 | 						</tr>
 78 | 					</thead>
 79 | 					<tbody>
 80 | 						{paginatedData.map((data) => (
 81 | 							<tr
 82 | 								key={data.id}
 83 | 								className="border-t border-[#fceee4] hover:bg-[#fff3ec] transition-colors"
 84 | 							>
 85 | 								<td className="px-4 py-2 text-gray-800">{data.size}</td>
 86 | 								<td className="px-4 py-2 text-gray-800">
 87 | 									Rp. {(data.price || 0).toLocaleString("id-ID")}
 88 | 								</td>
 89 | 								<td className="px-4 py-2 text-gray-800">
 90 | 									{new Date(data.createdAt).toLocaleString()}
 91 | 								</td>
 92 | 								<td className="px-4 py-2">
 93 | 									<div className="flex gap-2">
 94 | 										<Button
 95 | 											onClick={() => {
 96 | 												setSelectedSize(data);
 97 | 												setEditModalOpen(true);
 98 | 											}}
 99 | 											variant="ghost"
100 | 											size="icon"
101 | 											className="text-blue-500 hover:text-blue-600"
102 | 										>
103 | 											<Pencil className="h-4 w-4" />
104 | 										</Button>
105 | 										<Button
106 | 											onClick={() => {
107 | 												setSelectedSize(data);
108 | 												setDeleteModalOpen(true);
109 | 											}}
110 | 											variant="ghost"
111 | 											size="icon"
112 | 											className="text-red-500 hover:text-red-600"
113 | 										>
114 | 											<Trash2 className="h-4 w-4" />
115 | 										</Button>
116 | 									</div>
117 | 								</td>
118 | 							</tr>
119 | 						))}
120 | 					</tbody>
121 | 				</table>
122 | 			</div>
123 | 
124 | 			{/* Pagination */}
125 | 			<div className="mt-4 flex justify-end flex-wrap gap-2">
126 | 				{Array.from({ length: totalPages }).map((_, idx) => {
127 | 					const page = idx + 1;
128 | 					return (
129 | 						<Button
130 | 							key={page}
131 | 							onClick={() => setCurrentPage(page)}
132 | 							variant={page === currentPage ? "default" : "outline"}
133 | 							size="sm"
134 | 							className={page === currentPage ? "bg-[#6D2315] text-white" : ""}
135 | 						>
136 | 							{page}
137 | 						</Button>
138 | 					);
139 | 				})}
140 | 			</div>
141 | 
142 | 			<EditSizeModal
143 | 				open={editModalOpen}
144 | 				onOpenChange={setEditModalOpen}
145 | 				data={selectedSize}
146 | 				onSuccess={(updatedSize) => {
147 | 					setSize((prev) => prev.map((p) => (p.id === updatedSize.id ? updatedSize : p)));
148 | 					setSelectedSize(null);
149 | 				}}
150 | 			/>
151 | 
152 | 			<DeleteSizeModal
153 | 				open={deleteModalOpen}
154 | 				onOpenChange={setDeleteModalOpen}
155 | 				sizeId={selectedSize?.id}
156 | 				onSuccess={() => {
157 | 					setSize((prev) => prev.filter((p) => p.id !== selectedSize?.id));
158 | 					setSelectedSize(null);
159 | 				}}
160 | 			/>
161 | 		</Card>
162 | 	);
163 | });
164 | 
165 | export default SizePriceTable;
166 | 
```

--------------------------------------------------------------------------------
/src/app/dashboard/products/_components/ProductTable.jsx:
--------------------------------------------------------------------------------

```javascript
  1 | "use client";
  2 | 
  3 | import { useState, useEffect, forwardRef, useImperativeHandle } from "react";
  4 | 
  5 | import { Card } from "@/components/ui/card";
  6 | import { Button } from "@/components/ui/button";
  7 | import { Input } from "@/components/ui/input";
  8 | 
  9 | import ProductDeleteModal from "./ProductDeleteModal";
 10 | import ProductEditModal from "./ProductEditModal";
 11 | 
 12 | import { ArrowUp, ArrowDown, Pencil, Trash2 } from "lucide-react";
 13 | 
 14 | import { getAllProducts } from "@/lib/actions/products/getAllProducts";
 15 | 
 16 | const ITEMS_PER_PAGE = 10;
 17 | 
 18 | const ProductTable = forwardRef(function ProductTable(props, ref) {
 19 | 	const [products, setProducts] = useState([]);
 20 | 	const [sortOrder, setSortOrder] = useState("asc");
 21 | 
 22 | 	const [error, setError] = useState(null);
 23 | 	const [currentPage, setCurrentPage] = useState(1);
 24 | 
 25 | 	const [selectedProduct, setSelectedProduct] = useState(null);
 26 | 	const [deleteModalOpen, setDeleteModalOpen] = useState(false);
 27 | 	const [editModalOpen, setEditModalOpen] = useState(false);
 28 | 
 29 | 	const [searchQuery, setSearchQuery] = useState("");
 30 | 
 31 | 	if (error) return <p className="text-red-500">Failed to fetch data: {error}</p>;
 32 | 
 33 | 	const fetchData = async () => {
 34 | 		const { data, error } = await getAllProducts(sortOrder);
 35 | 		if (error) setError(error.message);
 36 | 		else setProducts(data);
 37 | 	};
 38 | 
 39 | 	useEffect(() => {
 40 | 		fetchData();
 41 | 	}, [sortOrder]);
 42 | 
 43 | 	useImperativeHandle(ref, () => ({
 44 | 		refetch: fetchData,
 45 | 	}));
 46 | 
 47 | 	const filteredData = products.filter((product) => {
 48 | 		const query = searchQuery.toLowerCase();
 49 | 		return product.name.toLowerCase().includes(query);
 50 | 	});
 51 | 
 52 | 	// pagination
 53 | 	const totalPages = Math.ceil(filteredData.length / ITEMS_PER_PAGE);
 54 | 	const paginatedData = filteredData.slice(
 55 | 		(currentPage - 1) * ITEMS_PER_PAGE,
 56 | 		currentPage * ITEMS_PER_PAGE
 57 | 	);
 58 | 
 59 | 	return (
 60 | 		<Card className="p-4 bg-[#fffaf0] border border-[#f4e3d3] shadow-sm">
 61 | 			{/* Search Field */}
 62 | 			<Input
 63 | 				type="text"
 64 | 				placeholder="Search product..."
 65 | 				value={searchQuery}
 66 | 				onChange={(e) => {
 67 | 					setSearchQuery(e.target.value);
 68 | 					setCurrentPage(1);
 69 | 				}}
 70 | 				className="mb-4 w-full sm:w-64 px-3 py-2 text-sm border border-[#6D2315] rounded-md focus:outline-none focus:ring-2 focus:ring-[#6D2315]"
 71 | 			/>
 72 | 
 73 | 			{/* Table */}
 74 | 			<div className="overflow-x-auto rounded-lg border border-[#fceee4]">
 75 | 				<table className="w-full text-sm">
 76 | 					<thead className="bg-[#fdf2e9] text-[#6D2315]">
 77 | 						<tr>
 78 | 							<th
 79 | 								className="px-4 py-2 text-left font-semibold cursor-pointer"
 80 | 								onClick={() => {
 81 | 									setSortOrder((prev) => (prev === "asc" ? "desc" : "asc"));
 82 | 									setCurrentPage(1);
 83 | 								}}
 84 | 							>
 85 | 								<div className="flex items-center gap-1">
 86 | 									Product Name
 87 | 									{sortOrder === "asc" ? (
 88 | 										<ArrowUp className="w-4 h-4" />
 89 | 									) : (
 90 | 										<ArrowDown className="w-4 h-4" />
 91 | 									)}
 92 | 								</div>
 93 | 							</th>
 94 | 							<th className="px-4 py-2 text-left font-semibold">Description</th>
 95 | 							<th className="px-4 py-2 text-left font-semibold">Created At</th>
 96 | 							<th className="px-4 py-2 text-left font-semibold">Action</th>
 97 | 						</tr>
 98 | 					</thead>
 99 | 
100 | 					<tbody>
101 | 						{paginatedData.map((product) => (
102 | 							<tr
103 | 								key={product.id}
104 | 								className="border-t border-[#fceee4] hover:bg-[#fff3ec] transition-colors"
105 | 							>
106 | 								<td className="px-4 py-2 text-gray-800">{product.name}</td>
107 | 								<td className="px-4 py-2 text-gray-600">{product.description || "-"}</td>
108 | 								<td className="px-4 py-2 text-gray-500">
109 | 									{new Date(product.createdAt).toLocaleString()}
110 | 								</td>
111 | 								<td className="px-4 py-2">
112 | 									<div className="flex gap-2">
113 | 										<Button
114 | 											onClick={() => {
115 | 												setSelectedProduct(product);
116 | 												setEditModalOpen(true);
117 | 											}}
118 | 											variant="ghost"
119 | 											size="icon"
120 | 											className="text-blue-500 hover:text-blue-600"
121 | 										>
122 | 											<Pencil className="h-4 w-4" />
123 | 										</Button>
124 | 										<Button
125 | 											onClick={() => {
126 | 												setSelectedProduct(product);
127 | 												setDeleteModalOpen(true);
128 | 											}}
129 | 											variant="ghost"
130 | 											size="icon"
131 | 											className="text-red-500 hover:text-red-600"
132 | 										>
133 | 											<Trash2 className="h-4 w-4" />
134 | 										</Button>
135 | 									</div>
136 | 								</td>
137 | 							</tr>
138 | 						))}
139 | 					</tbody>
140 | 				</table>
141 | 			</div>
142 | 
143 | 			{/* Pagination */}
144 | 			<div className="mt-4 flex justify-end flex-wrap gap-2">
145 | 				{Array.from({ length: totalPages }).map((_, idx) => {
146 | 					const page = idx + 1;
147 | 					return (
148 | 						<Button
149 | 							key={page}
150 | 							onClick={() => setCurrentPage(page)}
151 | 							variant={page === currentPage ? "default" : "outline"}
152 | 							size="sm"
153 | 							className={page === currentPage ? "bg-[#6D2315] text-white" : ""}
154 | 						>
155 | 							{page}
156 | 						</Button>
157 | 					);
158 | 				})}
159 | 			</div>
160 | 
161 | 			{/* Modals */}
162 | 			<ProductDeleteModal
163 | 				open={deleteModalOpen}
164 | 				onOpenChange={setDeleteModalOpen}
165 | 				productId={selectedProduct?.id}
166 | 				onSuccess={() => {
167 | 					setProducts((prev) => prev.filter((p) => p.id !== selectedProduct?.id));
168 | 					setSelectedProduct(null);
169 | 				}}
170 | 			/>
171 | 
172 | 			<ProductEditModal
173 | 				open={editModalOpen}
174 | 				onOpenChange={setEditModalOpen}
175 | 				product={selectedProduct}
176 | 				onSuccess={(updatedProduct) => {
177 | 					setProducts((prev) => prev.map((p) => (p.id === updatedProduct.id ? updatedProduct : p)));
178 | 					setSelectedProduct(null);
179 | 				}}
180 | 			/>
181 | 		</Card>
182 | 	);
183 | });
184 | 
185 | export default ProductTable;
186 | 
```

--------------------------------------------------------------------------------
/src/components/ui/select.jsx:
--------------------------------------------------------------------------------

```javascript
  1 | "use client"
  2 | 
  3 | import * as React from "react"
  4 | import * as SelectPrimitive from "@radix-ui/react-select"
  5 | import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react"
  6 | 
  7 | import { cn } from "@/lib/utils"
  8 | 
  9 | function Select({
 10 |   ...props
 11 | }) {
 12 |   return <SelectPrimitive.Root data-slot="select" {...props} />;
 13 | }
 14 | 
 15 | function SelectGroup({
 16 |   ...props
 17 | }) {
 18 |   return <SelectPrimitive.Group data-slot="select-group" {...props} />;
 19 | }
 20 | 
 21 | function SelectValue({
 22 |   ...props
 23 | }) {
 24 |   return <SelectPrimitive.Value data-slot="select-value" {...props} />;
 25 | }
 26 | 
 27 | function SelectTrigger({
 28 |   className,
 29 |   size = "default",
 30 |   children,
 31 |   ...props
 32 | }) {
 33 |   return (
 34 |     <SelectPrimitive.Trigger
 35 |       data-slot="select-trigger"
 36 |       data-size={size}
 37 |       className={cn(
 38 |         "border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
 39 |         className
 40 |       )}
 41 |       {...props}>
 42 |       {children}
 43 |       <SelectPrimitive.Icon asChild>
 44 |         <ChevronDownIcon className="size-4 opacity-50" />
 45 |       </SelectPrimitive.Icon>
 46 |     </SelectPrimitive.Trigger>
 47 |   );
 48 | }
 49 | 
 50 | function SelectContent({
 51 |   className,
 52 |   children,
 53 |   position = "popper",
 54 |   ...props
 55 | }) {
 56 |   return (
 57 |     <SelectPrimitive.Portal>
 58 |       <SelectPrimitive.Content
 59 |         data-slot="select-content"
 60 |         className={cn(
 61 |           "bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md",
 62 |           position === "popper" &&
 63 |             "data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
 64 |           className
 65 |         )}
 66 |         position={position}
 67 |         {...props}>
 68 |         <SelectScrollUpButton />
 69 |         <SelectPrimitive.Viewport
 70 |           className={cn("p-1", position === "popper" &&
 71 |             "h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1")}>
 72 |           {children}
 73 |         </SelectPrimitive.Viewport>
 74 |         <SelectScrollDownButton />
 75 |       </SelectPrimitive.Content>
 76 |     </SelectPrimitive.Portal>
 77 |   );
 78 | }
 79 | 
 80 | function SelectLabel({
 81 |   className,
 82 |   ...props
 83 | }) {
 84 |   return (
 85 |     <SelectPrimitive.Label
 86 |       data-slot="select-label"
 87 |       className={cn("text-muted-foreground px-2 py-1.5 text-xs", className)}
 88 |       {...props} />
 89 |   );
 90 | }
 91 | 
 92 | function SelectItem({
 93 |   className,
 94 |   children,
 95 |   ...props
 96 | }) {
 97 |   return (
 98 |     <SelectPrimitive.Item
 99 |       data-slot="select-item"
100 |       className={cn(
101 |         "focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
102 |         className
103 |       )}
104 |       {...props}>
105 |       <span className="absolute right-2 flex size-3.5 items-center justify-center">
106 |         <SelectPrimitive.ItemIndicator>
107 |           <CheckIcon className="size-4" />
108 |         </SelectPrimitive.ItemIndicator>
109 |       </span>
110 |       <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
111 |     </SelectPrimitive.Item>
112 |   );
113 | }
114 | 
115 | function SelectSeparator({
116 |   className,
117 |   ...props
118 | }) {
119 |   return (
120 |     <SelectPrimitive.Separator
121 |       data-slot="select-separator"
122 |       className={cn("bg-border pointer-events-none -mx-1 my-1 h-px", className)}
123 |       {...props} />
124 |   );
125 | }
126 | 
127 | function SelectScrollUpButton({
128 |   className,
129 |   ...props
130 | }) {
131 |   return (
132 |     <SelectPrimitive.ScrollUpButton
133 |       data-slot="select-scroll-up-button"
134 |       className={cn("flex cursor-default items-center justify-center py-1", className)}
135 |       {...props}>
136 |       <ChevronUpIcon className="size-4" />
137 |     </SelectPrimitive.ScrollUpButton>
138 |   );
139 | }
140 | 
141 | function SelectScrollDownButton({
142 |   className,
143 |   ...props
144 | }) {
145 |   return (
146 |     <SelectPrimitive.ScrollDownButton
147 |       data-slot="select-scroll-down-button"
148 |       className={cn("flex cursor-default items-center justify-center py-1", className)}
149 |       {...props}>
150 |       <ChevronDownIcon className="size-4" />
151 |     </SelectPrimitive.ScrollDownButton>
152 |   );
153 | }
154 | 
155 | export {
156 |   Select,
157 |   SelectContent,
158 |   SelectGroup,
159 |   SelectItem,
160 |   SelectLabel,
161 |   SelectScrollDownButton,
162 |   SelectScrollUpButton,
163 |   SelectSeparator,
164 |   SelectTrigger,
165 |   SelectValue,
166 | }
167 | 
```

--------------------------------------------------------------------------------
/src/app/dashboard/page.jsx:
--------------------------------------------------------------------------------

```javascript
  1 | import Link from "next/link";
  2 | import { redirect, unauthorized } from "next/navigation";
  3 | 
  4 | import { supabaseServer } from "@/lib/supabaseServer";
  5 | 
  6 | import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
  7 | import { Button } from "@/components/ui/button";
  8 | 
  9 | import { getPageTitle, getStatusVariant, toTitleCase } from "@/lib/utils";
 10 | import { AlertTriangle, CheckCircle, FileText, Package, Users, Wallet } from "lucide-react";
 11 | 
 12 | export const metadata = {
 13 | 	title: getPageTitle("Dashboard"),
 14 | };
 15 | 
 16 | export default async function Dashboard() {
 17 | 	const supabase = await supabaseServer();
 18 | 
 19 | 	const {
 20 | 		data: { session },
 21 | 	} = await supabase.auth.getSession();
 22 | 
 23 | 	if (!session) {
 24 | 		unauthorized();
 25 | 	}
 26 | 
 27 | 	// const user = session.user;
 28 | 
 29 | 	// get all invoices
 30 | 	const { data: invoices } = await supabase
 31 | 		.from("Invoice")
 32 | 		.select("*")
 33 | 		.order("invoiceDate", { ascending: false });
 34 | 
 35 | 	// get total invoice
 36 | 	const totalInvoices = invoices.length;
 37 | 
 38 | 	// get latest invoice data
 39 | 	const latestInvoices = invoices.slice(0, 5);
 40 | 
 41 | 	// count paid invoices
 42 | 	const invoicesSuccess = invoices.filter((inv) => inv.status === "success").length;
 43 | 
 44 | 	// count unpaid invoices
 45 | 	const invoicesUnpaid = invoices.filter((inv) => inv.status === "pending").length;
 46 | 
 47 | 	// count total customers (unique)
 48 | 	const { data } = await supabase.from("Invoice").select("buyerName");
 49 | 	const uniqueCustomers = new Set(data.map((d) => d.buyerName.trim().toLowerCase()));
 50 | 	const totalCustomers = uniqueCustomers.size;
 51 | 
 52 | 	const totalAmount =
 53 | 		invoices
 54 | 			?.filter((inv) => inv.status === "success")
 55 | 			.reduce((acc, curr) => acc + curr.totalPrice, 0) || 0;
 56 | 
 57 | 	// get total products
 58 | 	const { count: totalProducts } = await supabase
 59 | 		.from("Product")
 60 | 		.select("*", { count: "exact", head: true });
 61 | 
 62 | 	return (
 63 | 		<div className="grid gap-6">
 64 | 			{/* Welcome Card */}
 65 | 			<Card className="bg-[#fffaf0] border border-[#f4e3d3] shadow-sm">
 66 | 				<CardHeader>
 67 | 					<CardTitle className="text-xl text-[#6D2315] font-bold">Welcome back, Admin 👋</CardTitle>
 68 | 				</CardHeader>
 69 | 				<CardContent>
 70 | 					<p className="text-sm text-gray-600">Manage your dashboard here.</p>
 71 | 					<div className="mt-4">
 72 | 						<Link href="/dashboard/invoices/create">
 73 | 							<Button className="bg-[#6D2315] hover:bg-[#591c10] text-white">
 74 | 								+ Create Invoice
 75 | 							</Button>
 76 | 						</Link>
 77 | 					</div>
 78 | 				</CardContent>
 79 | 			</Card>
 80 | 
 81 | 			{/* Statistik Ringkas */}
 82 | 			<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-4">
 83 | 				{/* Total Invoices */}
 84 | 				<Card className="bg-[#fef6f3] border-0 shadow-sm text-[#6D2315]">
 85 | 					<CardHeader>
 86 | 						<CardTitle className="text-sm font-medium flex items-center gap-2">
 87 | 							<FileText className="w-4 h-4 text-[#6D2315]" />
 88 | 							Total Invoices
 89 | 						</CardTitle>
 90 | 					</CardHeader>
 91 | 					<CardContent>
 92 | 						<p className="text-3xl font-bold">{totalInvoices || 0}</p>
 93 | 					</CardContent>
 94 | 				</Card>
 95 | 
 96 | 				{/* Total Income */}
 97 | 				<Card className="bg-[#fdf2e9] border-0 shadow-sm text-[#92400e]">
 98 | 					<CardHeader>
 99 | 						<CardTitle className="text-sm font-medium flex items-center gap-2">
100 | 							<Wallet className="w-4 h-4 text-[#92400e]" />
101 | 							Total Income
102 | 						</CardTitle>
103 | 					</CardHeader>
104 | 					<CardContent>
105 | 						<p className="text-3xl font-bold">Rp {totalAmount.toLocaleString("id-ID")}</p>
106 | 					</CardContent>
107 | 				</Card>
108 | 
109 | 				{/* Total Products */}
110 | 				<Card className="bg-[#fef9e7] border-0 shadow-sm text-[#92400e]">
111 | 					<CardHeader>
112 | 						<CardTitle className="text-sm font-medium flex items-center gap-2">
113 | 							<Package className="w-4 h-4 text-[#92400e]" />
114 | 							Total Products
115 | 						</CardTitle>
116 | 					</CardHeader>
117 | 					<CardContent>
118 | 						<p className="text-3xl font-bold">{totalProducts}</p>
119 | 					</CardContent>
120 | 				</Card>
121 | 
122 | 				{/* Paid Invoices */}
123 | 				<Card className="bg-[#f0f9f5] border-0 shadow-sm text-[#065f46]">
124 | 					<CardHeader>
125 | 						<CardTitle className="text-sm font-medium flex items-center gap-2">
126 | 							<CheckCircle className="w-4 h-4 text-[#065f46]" />
127 | 							Paid Invoices
128 | 						</CardTitle>
129 | 					</CardHeader>
130 | 					<CardContent>
131 | 						<p className="text-3xl font-bold">{invoicesSuccess}</p>
132 | 					</CardContent>
133 | 				</Card>
134 | 
135 | 				{/* Unpaid Invoices */}
136 | 				<Card className="bg-[#fef2f2] border-0 shadow-sm text-[#991b1b]">
137 | 					<CardHeader>
138 | 						<CardTitle className="text-sm font-medium flex items-center gap-2">
139 | 							<AlertTriangle className="w-4 h-4 text-[#991b1b]" />
140 | 							Unpaid Invoices
141 | 						</CardTitle>
142 | 					</CardHeader>
143 | 					<CardContent>
144 | 						<p className="text-3xl font-bold">{invoicesUnpaid}</p>
145 | 					</CardContent>
146 | 				</Card>
147 | 
148 | 				{/* Total Customers */}
149 | 				<Card className="bg-[#e8f0fe] border-0 shadow-sm text-[#1e3a8a]">
150 | 					<CardHeader>
151 | 						<CardTitle className="text-sm font-medium flex items-center gap-2">
152 | 							<Users className="w-4 h-4 text-[#1e3a8a]" />
153 | 							Total Customers
154 | 						</CardTitle>
155 | 					</CardHeader>
156 | 					<CardContent>
157 | 						<p className="text-3xl font-bold">{totalCustomers}</p>
158 | 					</CardContent>
159 | 				</Card>
160 | 			</div>
161 | 
162 | 			{/* Latest Invoices */}
163 | 			<Card className="bg-white border border-[#f4e3d3] shadow-sm">
164 | 				<CardHeader>
165 | 					<CardTitle className="text-[#6D2315] font-medium">Latest Invoices</CardTitle>
166 | 				</CardHeader>
167 | 
168 | 				<CardContent className="space-y-3">
169 | 					{latestInvoices && latestInvoices.length > 0 ? (
170 | 						latestInvoices.map((inv) => (
171 | 							<div
172 | 								key={inv.id}
173 | 								className="flex items-center justify-between bg-[#fefaf7] hover:bg-[#fff3ec] transition-colors duration-150 border border-[#fceee4] rounded-md p-3"
174 | 							>
175 | 								{/* Left Section: Icon + Info */}
176 | 								<div className="flex items-center gap-3">
177 | 									<div className="bg-[#fdf0e6] text-[#6D2315] p-2 rounded-md">
178 | 										<FileText className="w-5 h-5" />
179 | 									</div>
180 | 									<div>
181 | 										<p className="font-medium text-gray-800">Invoice #{inv.invoiceNumber}</p>
182 | 										<p className="text-sm text-gray-500">{toTitleCase(inv.buyerName)}</p>
183 | 									</div>
184 | 								</div>
185 | 
186 | 								{/* Right Section: Amount + Status */}
187 | 								<div className="text-right space-y-1">
188 | 									<p className="font-semibold text-gray-800">
189 | 										Rp {inv.totalPrice.toLocaleString("id-ID")}
190 | 									</p>
191 | 									<span className={getStatusVariant(inv.status)}>{toTitleCase(inv.status)}</span>
192 | 								</div>
193 | 							</div>
194 | 						))
195 | 					) : (
196 | 						<p className="text-sm text-gray-500">No invoice data.</p>
197 | 					)}
198 | 				</CardContent>
199 | 			</Card>
200 | 		</div>
201 | 	);
202 | }
203 | 
```

--------------------------------------------------------------------------------
/src/app/dashboard/invoices/Table.jsx:
--------------------------------------------------------------------------------

```javascript
  1 | "use client";
  2 | 
  3 | import { useState, useEffect, forwardRef, useImperativeHandle } from "react";
  4 | import { useRouter } from "next/navigation";
  5 | 
  6 | import { Card } from "@/components/ui/card";
  7 | import { Button } from "@/components/ui/button";
  8 | import { Input } from "@/components/ui/input";
  9 | 
 10 | import DeleteInvoiceModal from "./_components/DeleteInvoiceModal";
 11 | import InvoiceDownloadModal from "./_components/InvoiceDownloadModal";
 12 | 
 13 | import { getAllInvoice } from "@/lib/actions/invoice/getAll";
 14 | import { getInvoiceWithItems } from "@/lib/actions/invoice/getInvoiceWithItem";
 15 | 
 16 | import { formatInvoiceDateTime, getStatusVariant, toTitleCase } from "@/lib/utils";
 17 | 
 18 | import { ArrowUp, ArrowDown, Pencil, Trash2, Download } from "lucide-react";
 19 | 
 20 | const ITEMS_PER_PAGE = 10;
 21 | 
 22 | const InvoicesTable = forwardRef(function InvoicesTable(props, ref) {
 23 | 	const router = useRouter();
 24 | 
 25 | 	const [invoice, setInvoice] = useState([]);
 26 | 	const [sortOrder, setSortOrder] = useState("desc");
 27 | 
 28 | 	const [error, setError] = useState(null);
 29 | 	const [currentPage, setCurrentPage] = useState(1);
 30 | 
 31 | 	const [selectedInvoice, setSelectedInvoice] = useState(null);
 32 | 	const [deleteModalOpen, setDeleteModalOpen] = useState(false);
 33 | 
 34 | 	const [searchQuery, setSearchQuery] = useState("");
 35 | 
 36 | 	const [downloadModalOpen, setDownloadModalOpen] = useState(false);
 37 | 	const [invoiceItems, setInvoiceItems] = useState([]);
 38 | 
 39 | 	if (error) return <p className="text-red-500">Failed to fetch data: {error}</p>;
 40 | 
 41 | 	const fetchData = async () => {
 42 | 		const { data, error } = await getAllInvoice(sortOrder);
 43 | 		if (error) setError(error.message);
 44 | 		else setInvoice(data);
 45 | 	};
 46 | 
 47 | 	useEffect(() => {
 48 | 		fetchData();
 49 | 	}, [sortOrder]);
 50 | 
 51 | 	useImperativeHandle(ref, () => ({
 52 | 		refetch: fetchData,
 53 | 	}));
 54 | 
 55 | 	const filteredData = invoice.filter((inv) => {
 56 | 		const query = searchQuery.toLowerCase();
 57 | 		return (
 58 | 			inv.invoiceNumber.toLowerCase().includes(query) || inv.buyerName.toLowerCase().includes(query)
 59 | 		);
 60 | 	});
 61 | 
 62 | 	// pagination
 63 | 	const totalPages = Math.ceil(filteredData.length / ITEMS_PER_PAGE);
 64 | 	const paginatedData = filteredData.slice(
 65 | 		(currentPage - 1) * ITEMS_PER_PAGE,
 66 | 		currentPage * ITEMS_PER_PAGE
 67 | 	);
 68 | 
 69 | 	const handleDownload = async (invoiceId) => {
 70 | 		const { invoice, items } = await getInvoiceWithItems(invoiceId);
 71 | 		setSelectedInvoice(invoice);
 72 | 		setInvoiceItems(items);
 73 | 		setDownloadModalOpen(true);
 74 | 	};
 75 | 
 76 | 	return (
 77 | 		<Card className="p-4 bg-[#fffaf0] border border-[#f4e3d3] shadow-sm">
 78 | 			<Input
 79 | 				type="text"
 80 | 				placeholder="Search by name/inv.number"
 81 | 				value={searchQuery}
 82 | 				onChange={(e) => {
 83 | 					setSearchQuery(e.target.value);
 84 | 					setCurrentPage(1);
 85 | 				}}
 86 | 				className="mb-4 w-full sm:w-64 px-3 py-2 text-sm border border-[#6D2315] rounded-md focus:outline-none focus:ring-2 focus:ring-[#6D2315]"
 87 | 			/>
 88 | 			<div className="overflow-x-auto rounded-lg border border-[#fceee4]">
 89 | 				<table className="w-full text-sm">
 90 | 					<thead className="bg-[#fdf2e9] text-[#6D2315]">
 91 | 						<tr>
 92 | 							<th
 93 | 								className="px-4 py-2 text-left font-semibold cursor-pointer"
 94 | 								onClick={() => {
 95 | 									setSortOrder((prev) => (prev === "asc" ? "desc" : "asc"));
 96 | 									setCurrentPage(1); // reset ke page 1 tiap kali sorting
 97 | 								}}
 98 | 							>
 99 | 								<div className="flex items-center gap-1">
100 | 									Invoice Number
101 | 									{sortOrder === "desc" ? (
102 | 										<ArrowDown className="w-4 h-4" />
103 | 									) : (
104 | 										<ArrowUp className="w-4 h-4" />
105 | 									)}
106 | 								</div>
107 | 							</th>
108 | 
109 | 							<th className="px-4 py-2 text-left font-semibold">Customer</th>
110 | 							<th className="px-4 py-2 text-left font-semibold">Total Price</th>
111 | 							<th className="px-4 py-2 text-left font-semibold">Date</th>
112 | 							<th className="px-4 py-2 text-left font-semibold">Status</th>
113 | 							<th className="px-4 py-2 text-left font-semibold">Action</th>
114 | 						</tr>
115 | 					</thead>
116 | 					<tbody>
117 | 						{paginatedData && paginatedData.length > 0 ? (
118 | 							paginatedData.map((data) => (
119 | 								<tr
120 | 									key={data.id}
121 | 									className="border-t border-[#fceee4] hover:bg-[#fff3ec] transition-colors"
122 | 								>
123 | 									<td className="px-4 py-2 text-gray-800">{data.invoiceNumber}</td>
124 | 									<td className="px-4 py-2 text-gray-800">{toTitleCase(data.buyerName)}</td>
125 | 									<td className="px-4 py-2 text-gray-800">
126 | 										Rp. {(data.totalPrice || 0).toLocaleString("id-ID")}
127 | 									</td>
128 | 									<td className="px-4 py-2 text-gray-800">
129 | 										{formatInvoiceDateTime(data.invoiceDate, data.createdAt)}
130 | 									</td>
131 | 									<td className="px-4 py-2 text-gray-800">
132 | 										<span className={getStatusVariant(data.status)}>
133 | 											{toTitleCase(data.status)}
134 | 										</span>
135 | 									</td>
136 | 									<td className="px-4 py-2">
137 | 										<div className="flex gap-2">
138 | 											<Button
139 | 												onClick={() => {
140 | 													router.push(`/dashboard/invoices/${data.invoiceNumber}`);
141 | 												}}
142 | 												variant="ghost"
143 | 												size="icon"
144 | 												className="text-blue-500 hover:text-blue-600"
145 | 											>
146 | 												<Pencil className="h-4 w-4" />
147 | 											</Button>
148 | 											<Button
149 | 												onClick={() => {
150 | 													setSelectedInvoice(data);
151 | 													setDeleteModalOpen(true);
152 | 												}}
153 | 												variant="ghost"
154 | 												size="icon"
155 | 												className="text-red-500 hover:text-red-600"
156 | 											>
157 | 												<Trash2 className="h-4 w-4" />
158 | 											</Button>
159 | 											<Button
160 | 												onClick={() => handleDownload(data.id)}
161 | 												variant="ghost"
162 | 												size="icon"
163 | 												className="text-green-500 hover:text-green-600"
164 | 											>
165 | 												<Download className="h-4 w-4" />
166 | 											</Button>
167 | 										</div>
168 | 									</td>
169 | 								</tr>
170 | 							))
171 | 						) : (
172 | 							<tr>
173 | 								<td colSpan="6" className="px-4 py-6 text-center text-gray-500">
174 | 									No invoice data
175 | 								</td>
176 | 							</tr>
177 | 						)}
178 | 					</tbody>
179 | 				</table>
180 | 			</div>
181 | 
182 | 			{/* Pagination */}
183 | 			<div className="mt-4 flex justify-end flex-wrap gap-2">
184 | 				{Array.from({ length: totalPages }).map((_, idx) => {
185 | 					const page = idx + 1;
186 | 					return (
187 | 						<Button
188 | 							key={page}
189 | 							onClick={() => setCurrentPage(page)}
190 | 							variant={page === currentPage ? "default" : "outline"}
191 | 							size="sm"
192 | 							className={page === currentPage ? "bg-[#6D2315] hover:bg-[#8d2e1c] text-white" : ""}
193 | 						>
194 | 							{page}
195 | 						</Button>
196 | 					);
197 | 				})}
198 | 			</div>
199 | 
200 | 			<DeleteInvoiceModal
201 | 				open={deleteModalOpen}
202 | 				onOpenChange={setDeleteModalOpen}
203 | 				invoiceId={selectedInvoice?.id}
204 | 				onSuccess={() => {
205 | 					setInvoice((prev) => prev.filter((p) => p.id !== selectedInvoice?.id));
206 | 					setSelectedInvoice(null);
207 | 				}}
208 | 			/>
209 | 
210 | 			<InvoiceDownloadModal
211 | 				open={downloadModalOpen}
212 | 				onOpenChange={setDownloadModalOpen}
213 | 				invoice={selectedInvoice}
214 | 				invoiceItems={invoiceItems}
215 | 			/>
216 | 		</Card>
217 | 	);
218 | });
219 | 
220 | export default InvoicesTable;
221 | 
```

--------------------------------------------------------------------------------
/src/components/ui/calendar.jsx:
--------------------------------------------------------------------------------

```javascript
  1 | "use client"
  2 | 
  3 | import * as React from "react"
  4 | import {
  5 |   ChevronDownIcon,
  6 |   ChevronLeftIcon,
  7 |   ChevronRightIcon,
  8 | } from "lucide-react"
  9 | import { DayPicker, getDefaultClassNames } from "react-day-picker";
 10 | 
 11 | import { cn } from "@/lib/utils"
 12 | import { Button, buttonVariants } from "@/components/ui/button"
 13 | 
 14 | function Calendar({
 15 |   className,
 16 |   classNames,
 17 |   showOutsideDays = true,
 18 |   captionLayout = "label",
 19 |   buttonVariant = "ghost",
 20 |   formatters,
 21 |   components,
 22 |   ...props
 23 | }) {
 24 |   const defaultClassNames = getDefaultClassNames()
 25 | 
 26 |   return (
 27 |     <DayPicker
 28 |       showOutsideDays={showOutsideDays}
 29 |       className={cn(
 30 |         "bg-background group/calendar p-3 [--cell-size:--spacing(8)] [[data-slot=card-content]_&]:bg-transparent [[data-slot=popover-content]_&]:bg-transparent",
 31 |         String.raw`rtl:**:[.rdp-button\_next>svg]:rotate-180`,
 32 |         String.raw`rtl:**:[.rdp-button\_previous>svg]:rotate-180`,
 33 |         className
 34 |       )}
 35 |       captionLayout={captionLayout}
 36 |       formatters={{
 37 |         formatMonthDropdown: (date) =>
 38 |           date.toLocaleString("default", { month: "short" }),
 39 |         ...formatters,
 40 |       }}
 41 |       classNames={{
 42 |         root: cn("w-fit", defaultClassNames.root),
 43 |         months: cn("flex gap-4 flex-col md:flex-row relative", defaultClassNames.months),
 44 |         month: cn("flex flex-col w-full gap-4", defaultClassNames.month),
 45 |         nav: cn(
 46 |           "flex items-center gap-1 w-full absolute top-0 inset-x-0 justify-between",
 47 |           defaultClassNames.nav
 48 |         ),
 49 |         button_previous: cn(
 50 |           buttonVariants({ variant: buttonVariant }),
 51 |           "size-(--cell-size) aria-disabled:opacity-50 p-0 select-none",
 52 |           defaultClassNames.button_previous
 53 |         ),
 54 |         button_next: cn(
 55 |           buttonVariants({ variant: buttonVariant }),
 56 |           "size-(--cell-size) aria-disabled:opacity-50 p-0 select-none",
 57 |           defaultClassNames.button_next
 58 |         ),
 59 |         month_caption: cn(
 60 |           "flex items-center justify-center h-(--cell-size) w-full px-(--cell-size)",
 61 |           defaultClassNames.month_caption
 62 |         ),
 63 |         dropdowns: cn(
 64 |           "w-full flex items-center text-sm font-medium justify-center h-(--cell-size) gap-1.5",
 65 |           defaultClassNames.dropdowns
 66 |         ),
 67 |         dropdown_root: cn(
 68 |           "relative has-focus:border-ring border border-input shadow-xs has-focus:ring-ring/50 has-focus:ring-[3px] rounded-md",
 69 |           defaultClassNames.dropdown_root
 70 |         ),
 71 |         dropdown: cn("absolute bg-popover inset-0 opacity-0", defaultClassNames.dropdown),
 72 |         caption_label: cn("select-none font-medium", captionLayout === "label"
 73 |           ? "text-sm"
 74 |           : "rounded-md pl-2 pr-1 flex items-center gap-1 text-sm h-8 [&>svg]:text-muted-foreground [&>svg]:size-3.5", defaultClassNames.caption_label),
 75 |         table: "w-full border-collapse",
 76 |         weekdays: cn("flex", defaultClassNames.weekdays),
 77 |         weekday: cn(
 78 |           "text-muted-foreground rounded-md flex-1 font-normal text-[0.8rem] select-none",
 79 |           defaultClassNames.weekday
 80 |         ),
 81 |         week: cn("flex w-full mt-2", defaultClassNames.week),
 82 |         week_number_header: cn("select-none w-(--cell-size)", defaultClassNames.week_number_header),
 83 |         week_number: cn(
 84 |           "text-[0.8rem] select-none text-muted-foreground",
 85 |           defaultClassNames.week_number
 86 |         ),
 87 |         day: cn(
 88 |           "relative w-full h-full p-0 text-center [&:first-child[data-selected=true]_button]:rounded-l-md [&:last-child[data-selected=true]_button]:rounded-r-md group/day aspect-square select-none",
 89 |           defaultClassNames.day
 90 |         ),
 91 |         range_start: cn("rounded-l-md bg-accent", defaultClassNames.range_start),
 92 |         range_middle: cn("rounded-none", defaultClassNames.range_middle),
 93 |         range_end: cn("rounded-r-md bg-accent", defaultClassNames.range_end),
 94 |         today: cn(
 95 |           "bg-accent text-accent-foreground rounded-md data-[selected=true]:rounded-none",
 96 |           defaultClassNames.today
 97 |         ),
 98 |         outside: cn(
 99 |           "text-muted-foreground aria-selected:text-muted-foreground",
100 |           defaultClassNames.outside
101 |         ),
102 |         disabled: cn("text-muted-foreground opacity-50", defaultClassNames.disabled),
103 |         hidden: cn("invisible", defaultClassNames.hidden),
104 |         ...classNames,
105 |       }}
106 |       components={{
107 |         Root: ({ className, rootRef, ...props }) => {
108 |           return (<div data-slot="calendar" ref={rootRef} className={cn(className)} {...props} />);
109 |         },
110 |         Chevron: ({ className, orientation, ...props }) => {
111 |           if (orientation === "left") {
112 |             return (<ChevronLeftIcon className={cn("size-4", className)} {...props} />);
113 |           }
114 | 
115 |           if (orientation === "right") {
116 |             return (<ChevronRightIcon className={cn("size-4", className)} {...props} />);
117 |           }
118 | 
119 |           return (<ChevronDownIcon className={cn("size-4", className)} {...props} />);
120 |         },
121 |         DayButton: CalendarDayButton,
122 |         WeekNumber: ({ children, ...props }) => {
123 |           return (
124 |             <td {...props}>
125 |               <div
126 |                 className="flex size-(--cell-size) items-center justify-center text-center">
127 |                 {children}
128 |               </div>
129 |             </td>
130 |           );
131 |         },
132 |         ...components,
133 |       }}
134 |       {...props} />
135 |   );
136 | }
137 | 
138 | function CalendarDayButton({
139 |   className,
140 |   day,
141 |   modifiers,
142 |   ...props
143 | }) {
144 |   const defaultClassNames = getDefaultClassNames()
145 | 
146 |   const ref = React.useRef(null)
147 |   React.useEffect(() => {
148 |     if (modifiers.focused) ref.current?.focus()
149 |   }, [modifiers.focused])
150 | 
151 |   return (
152 |     <Button
153 |       ref={ref}
154 |       variant="ghost"
155 |       size="icon"
156 |       data-day={day.date.toLocaleDateString()}
157 |       data-selected-single={
158 |         modifiers.selected &&
159 |         !modifiers.range_start &&
160 |         !modifiers.range_end &&
161 |         !modifiers.range_middle
162 |       }
163 |       data-range-start={modifiers.range_start}
164 |       data-range-end={modifiers.range_end}
165 |       data-range-middle={modifiers.range_middle}
166 |       className={cn(
167 |         "data-[selected-single=true]:bg-primary data-[selected-single=true]:text-primary-foreground data-[range-middle=true]:bg-accent data-[range-middle=true]:text-accent-foreground data-[range-start=true]:bg-primary data-[range-start=true]:text-primary-foreground data-[range-end=true]:bg-primary data-[range-end=true]:text-primary-foreground group-data-[focused=true]/day:border-ring group-data-[focused=true]/day:ring-ring/50 dark:hover:text-accent-foreground flex aspect-square size-auto w-full min-w-(--cell-size) flex-col gap-1 leading-none font-normal group-data-[focused=true]/day:relative group-data-[focused=true]/day:z-10 group-data-[focused=true]/day:ring-[3px] data-[range-end=true]:rounded-md data-[range-end=true]:rounded-r-md data-[range-middle=true]:rounded-none data-[range-start=true]:rounded-md data-[range-start=true]:rounded-l-md [&>span]:text-xs [&>span]:opacity-70",
168 |         defaultClassNames.day,
169 |         className
170 |       )}
171 |       {...props} />
172 |   );
173 | }
174 | 
175 | export { Calendar, CalendarDayButton }
176 | 
```
Page 1/2FirstPrevNextLast