[dyad] Built the customer management page - wrote 11 file(s)

This commit is contained in:
davidels1 2025-09-09 01:05:22 +02:00
parent 248eec947b
commit da1deb8ca0
5 changed files with 519 additions and 6 deletions

View file

@ -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<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
companyName: "",
},
})
function onSubmit(values: z.infer<typeof formSchema>) {
// 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 (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<FormField
control={form.control}
name="companyName"
render={({ field }) => (
<FormItem>
<FormLabel>Company Name</FormLabel>
<FormControl>
<Input placeholder="e.g., Acme Inc." {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="billingCompany"
render={({ field }) => (
<FormItem className="flex flex-col">
<FormLabel>Billing Company</FormLabel>
<BillingCompanyCombobox
companies={billingCompanies}
value={field.value}
onChange={field.onChange}
onCompanyCreate={handleCompanyCreate}
/>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit" className="w-full">Add Customer</Button>
</form>
</Form>
)
}

View file

@ -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 (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={open}
className="w-full justify-between"
>
{value
? companies.find((company) => company.value === value)?.label
: "Select a company..."}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-[--radix-popover-trigger-width] p-0">
<Command>
<CommandInput
placeholder="Search or create..."
value={inputValue}
onValueChange={setInputValue}
/>
<CommandList>
<CommandEmpty>
<Button variant="ghost" className="w-full" onClick={handleCreate}>
Create "{inputValue}"
</Button>
</CommandEmpty>
<CommandGroup>
{companies.map((company) => (
<CommandItem
key={company.value}
value={company.value}
onSelect={handleSelect}
>
<Check
className={cn(
"mr-2 h-4 w-4",
value === company.value ? "opacity-100" : "opacity-0"
)}
/>
{company.label}
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
)
}

View file

@ -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<Customer>[] = [
{
id: "select",
header: ({ table }) => (
<Checkbox
checked={
table.getIsAllPageRowsSelected()
? true
: table.getIsSomePageRowsSelected()
? "indeterminate"
: false
}
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
aria-label="Select all"
/>
),
cell: ({ row }) => (
<Checkbox
checked={row.getIsSelected()}
onCheckedChange={(value) => 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 (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="h-8 w-8 p-0">
<span className="sr-only">Open menu</span>
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuLabel>Actions</DropdownMenuLabel>
<DropdownMenuItem
onClick={() => navigator.clipboard.writeText(customer.id)}
>
Copy customer ID
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem>Edit customer</DropdownMenuItem>
<DropdownMenuItem className="text-red-600">Delete customer</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)
},
},
]

View file

@ -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<TData, TValue> {
columns: ColumnDef<TData, TValue>[]
data: TData[]
}
export function DataTable<TData, TValue>({
columns,
data,
}: DataTableProps<TData, TValue>) {
const [sorting, setSorting] = React.useState<SortingState>([])
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>([])
const [columnVisibility, setColumnVisibility] = React.useState<VisibilityState>({})
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 (
<div>
<div className="flex items-center py-4">
<Input
placeholder="Filter by company name..."
value={(table.getColumn("companyName")?.getFilterValue() as string) ?? ""}
onChange={(event) =>
table.getColumn("companyName")?.setFilterValue(event.target.value)
}
className="max-w-sm"
/>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="icon" className="ml-auto">
<Columns className="h-4 w-4" />
<span className="sr-only">Toggle columns</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
{table
.getAllColumns()
.filter(
(column) => column.getCanHide()
)
.map((column) => {
return (
<DropdownMenuCheckboxItem
key={column.id}
className="capitalize"
checked={column.getIsVisible()}
onCheckedChange={(value) =>
column.toggleVisibility(!!value)
}
>
{column.id}
</DropdownMenuCheckboxItem>
)
})}
</DropdownMenuContent>
</DropdownMenu>
</div>
<div className="rounded-md border bg-white">
<Table>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => {
return (
<TableHead key={header.id}>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext()
)}
</TableHead>
)
})}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow
key={row.id}
data-state={row.getIsSelected() && "selected"}
>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={columns.length} className="h-24 text-center">
No results.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
<div className="flex items-center justify-end space-x-2 py-4">
<div className="flex-1 text-sm text-muted-foreground">
{table.getFilteredSelectedRowModel().rows.length} of{" "}
{table.getFilteredRowModel().rows.length} row(s) selected.
</div>
<Button
variant="outline"
size="sm"
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
>
Previous
</Button>
<Button
variant="outline"
size="sm"
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
>
Next
</Button>
</div>
</div>
)
}

View file

@ -1,7 +1,73 @@
const Customers = () => (
<div>
<h1 className="text-2xl font-bold">Customers</h1>
<p>This is where customer information will be managed.</p>
</div>
);
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 (
<div>
<div className="flex flex-col md:flex-row md:justify-between md:items-center gap-4 mb-6">
<div>
<h1 className="text-3xl font-bold">Customers</h1>
<p className="text-gray-500">Manage your customers here.</p>
</div>
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button>
<PlusCircle className="mr-2 h-4 w-4" />
Add New Customer
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-[425px]">
<DialogHeader>
<DialogTitle>Add New Customer</DialogTitle>
<DialogDescription>
Fill in the details below to add a new customer.
</DialogDescription>
</DialogHeader>
<AddCustomerForm setOpen={setOpen} />
</DialogContent>
</Dialog>
</div>
<DataTable columns={columns} data={data} />
</div>
)
}
export default Customers;