From da1deb8ca030503d39a89188ff2404062240e889 Mon Sep 17 00:00:00 2001 From: davidels1 <87242587+davidels1@users.noreply.github.com> Date: Tue, 9 Sep 2025 01:05:22 +0200 Subject: [PATCH] [dyad] Built the customer management page - wrote 11 file(s) --- src/components/customers/AddCustomerForm.tsx | 89 +++++++++ .../customers/BillingCompanyCombobox.tsx | 96 ++++++++++ src/components/customers/columns.tsx | 85 +++++++++ src/components/customers/data-table.tsx | 177 ++++++++++++++++++ src/pages/Customers.tsx | 78 +++++++- 5 files changed, 519 insertions(+), 6 deletions(-) create mode 100644 src/components/customers/AddCustomerForm.tsx create mode 100644 src/components/customers/BillingCompanyCombobox.tsx create mode 100644 src/components/customers/columns.tsx create mode 100644 src/components/customers/data-table.tsx diff --git a/src/components/customers/AddCustomerForm.tsx b/src/components/customers/AddCustomerForm.tsx new file mode 100644 index 0000000..8ee82bd --- /dev/null +++ b/src/components/customers/AddCustomerForm.tsx @@ -0,0 +1,89 @@ +"use client" + +import * as React from "react" +import { zodResolver } from "@hookform/resolvers/zod" +import { useForm } from "react-hook-form" +import * as z from "zod" + +import { Button } from "../ui/button" +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "../ui/form" +import { Input } from "../ui/input" +import { showSuccess } from "../../utils/toast" +import { BillingCompanyCombobox } from "./BillingCompanyCombobox" + +const formSchema = z.object({ + companyName: z.string().min(2, { message: "Company name must be at least 2 characters." }), + billingCompany: z.string({ required_error: "Please select or create a billing company." }), +}) + +const initialBillingCompanies = ["Isherwood", "CNL"]; + +export function AddCustomerForm({ setOpen }: { setOpen: (open: boolean) => void }) { + const [billingCompanies, setBillingCompanies] = React.useState(initialBillingCompanies.map(c => ({ value: c.toLowerCase(), label: c }))); + + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: { + companyName: "", + }, + }) + + function onSubmit(values: z.infer) { + // In a real app, you'd send this to your API + console.log(values) + showSuccess("New customer added successfully!") + setOpen(false) + } + + const handleCompanyCreate = (newCompany: string) => { + const formattedCompany = { + value: newCompany.toLowerCase(), + label: newCompany, + }; + setBillingCompanies(prev => [...prev, formattedCompany]); + }; + + return ( +
+ + ( + + Company Name + + + + + + )} + /> + ( + + Billing Company + + + + )} + /> + + + + ) +} \ No newline at end of file diff --git a/src/components/customers/BillingCompanyCombobox.tsx b/src/components/customers/BillingCompanyCombobox.tsx new file mode 100644 index 0000000..3c35df4 --- /dev/null +++ b/src/components/customers/BillingCompanyCombobox.tsx @@ -0,0 +1,96 @@ +"use client" + +import * as React from "react" +import { Check, ChevronsUpDown } from "lucide-react" + +import { cn } from "../../lib/utils" +import { Button } from "../ui/button" +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, +} from "../ui/command" +import { + Popover, + PopoverContent, + PopoverTrigger, +} from "../ui/popover" + +interface BillingCompanyComboboxProps { + companies: { value: string; label: string }[] + value: string + onChange: (value: string) => void + onCompanyCreate: (value: string) => void +} + +export function BillingCompanyCombobox({ companies, value, onChange, onCompanyCreate }: BillingCompanyComboboxProps) { + const [open, setOpen] = React.useState(false) + const [inputValue, setInputValue] = React.useState("") + + const handleSelect = (currentValue: string) => { + onChange(currentValue === value ? "" : currentValue) + setOpen(false) + } + + const handleCreate = () => { + if (inputValue && !companies.some(c => c.value.toLowerCase() === inputValue.toLowerCase())) { + onCompanyCreate(inputValue) + onChange(inputValue.toLowerCase()) + setOpen(false) + } + } + + return ( + + + + + + + + + + + + + {companies.map((company) => ( + + + {company.label} + + ))} + + + + + + ) +} \ No newline at end of file diff --git a/src/components/customers/columns.tsx b/src/components/customers/columns.tsx new file mode 100644 index 0000000..48d1afe --- /dev/null +++ b/src/components/customers/columns.tsx @@ -0,0 +1,85 @@ +"use client" + +import { ColumnDef } from "@tanstack/react-table" +import { MoreHorizontal } from "lucide-react" + +import { Button } from "@/components/ui/button" +import { Checkbox } from "@/components/ui/checkbox" +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu" + +export type Customer = { + id: string + companyName: string + billingCompany: string +} + +export const columns: ColumnDef[] = [ + { + id: "select", + header: ({ table }) => ( + table.toggleAllPageRowsSelected(!!value)} + aria-label="Select all" + /> + ), + cell: ({ row }) => ( + row.toggleSelected(!!value)} + aria-label="Select row" + /> + ), + enableSorting: false, + enableHiding: false, + }, + { + accessorKey: "companyName", + header: "Company Name", + }, + { + accessorKey: "billingCompany", + header: "Billing Company", + }, + { + id: "actions", + cell: ({ row }) => { + const customer = row.original + + return ( + + + + + + Actions + navigator.clipboard.writeText(customer.id)} + > + Copy customer ID + + + Edit customer + Delete customer + + + ) + }, + }, +] \ No newline at end of file diff --git a/src/components/customers/data-table.tsx b/src/components/customers/data-table.tsx new file mode 100644 index 0000000..d447981 --- /dev/null +++ b/src/components/customers/data-table.tsx @@ -0,0 +1,177 @@ +"use client" + +import * as React from "react" +import { + ColumnDef, + ColumnFiltersState, + SortingState, + VisibilityState, + flexRender, + getCoreRowModel, + getFilteredRowModel, + getPaginationRowModel, + getSortedRowModel, + useReactTable, +} from "@tanstack/react-table" +import { Columns } from "lucide-react" + +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table" +import { Button } from "@/components/ui/button" +import { Input } from "@/components/ui/input" +import { + DropdownMenu, + DropdownMenuCheckboxItem, + DropdownMenuContent, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu" + +interface DataTableProps { + columns: ColumnDef[] + data: TData[] +} + +export function DataTable({ + columns, + data, +}: DataTableProps) { + const [sorting, setSorting] = React.useState([]) + const [columnFilters, setColumnFilters] = React.useState([]) + const [columnVisibility, setColumnVisibility] = React.useState({}) + const [rowSelection, setRowSelection] = React.useState({}) + + const table = useReactTable({ + data, + columns, + getCoreRowModel: getCoreRowModel(), + getPaginationRowModel: getPaginationRowModel(), + onSortingChange: setSorting, + getSortedRowModel: getSortedRowModel(), + onColumnFiltersChange: setColumnFilters, + getFilteredRowModel: getFilteredRowModel(), + onColumnVisibilityChange: setColumnVisibility, + onRowSelectionChange: setRowSelection, + state: { + sorting, + columnFilters, + columnVisibility, + rowSelection, + }, + }) + + return ( +
+
+ + table.getColumn("companyName")?.setFilterValue(event.target.value) + } + className="max-w-sm" + /> + + + + + + {table + .getAllColumns() + .filter( + (column) => column.getCanHide() + ) + .map((column) => { + return ( + + column.toggleVisibility(!!value) + } + > + {column.id} + + ) + })} + + +
+
+ + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => { + return ( + + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext() + )} + + ) + })} + + ))} + + + {table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map((row) => ( + + {row.getVisibleCells().map((cell) => ( + + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ))} + + )) + ) : ( + + + No results. + + + )} + +
+
+
+
+ {table.getFilteredSelectedRowModel().rows.length} of{" "} + {table.getFilteredRowModel().rows.length} row(s) selected. +
+ + +
+
+ ) +} \ No newline at end of file diff --git a/src/pages/Customers.tsx b/src/pages/Customers.tsx index 2714433..7e11c90 100644 --- a/src/pages/Customers.tsx +++ b/src/pages/Customers.tsx @@ -1,7 +1,73 @@ -const Customers = () => ( -
-

Customers

-

This is where customer information will be managed.

-
-); +import { useState } from "react" +import { columns, Customer } from "../components/customers/columns" +import { DataTable } from "../components/customers/data-table" +import { Button } from "@/components/ui/button" +import { PlusCircle } from "lucide-react" +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog" +import { AddCustomerForm } from "../components/customers/AddCustomerForm" + +// Sample data +const data: Customer[] = [ + { + id: "cust-1", + companyName: "Tech Solutions Ltd.", + billingCompany: "Isherwood", + }, + { + id: "cust-2", + companyName: "Innovate Inc.", + billingCompany: "CNL", + }, + { + id: "cust-3", + companyName: "Global Logistics", + billingCompany: "Isherwood", + }, + { + id: "cust-4", + companyName: "Quantum Creations", + billingCompany: "Quantum Creations", + }, +]; + +const Customers = () => { + const [open, setOpen] = useState(false); + + return ( +
+
+
+

Customers

+

Manage your customers here.

+
+ + + + + + + Add New Customer + + Fill in the details below to add a new customer. + + + + + +
+ +
+ ) +} + export default Customers; \ No newline at end of file