WFDASH-WEBAPP/src/components/order-form.tsx
SJCoetz 363786ea36 feat: add UI components for separator, sheet, sonner, switch, table, tabs, textarea, tooltip
- Introduced a new Separator component for visual separation in UI.
- Added Sheet component for modal-like functionality with triggers and overlays.
- Implemented Toaster component for notifications using Sonner library.
- Created Switch component for toggle functionality.
- Developed Table component with headers, body, footer, and rows for structured data display.
- Added Tabs component for organizing content into tabbed interfaces.
- Implemented Textarea component for multi-line text input.
- Created Tooltip component for displaying additional information on hover.

feat: add PocketBase helpers and types

- Implemented various helper functions for interacting with PocketBase collections (customers, companies, suppliers, quotes, orders).
- Added type definitions for entities such as Customer, Company, Supplier, Quote, Order, and their respective items.
- Included utility functions for generating unique identifiers for quotes, orders, and purchase orders.
- Added functions for calculating totals and formatting currency and dates.

chore: add TypeScript configuration

- Added tsconfig.json for TypeScript support with strict type checking and module resolution.
2026-05-24 16:31:01 +02:00

255 lines
9.5 KiB
TypeScript

"use client";
import { useState, useEffect } from "react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetDescription } from "@/components/ui/sheet";
import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from "@/components/ui/command";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import { toast } from "sonner";
import { Loader2, ChevronsUpDown, Check } from "lucide-react";
import getPocketBase from "@/lib/pocketbase";
import {
getAllCustomers,
getAllCompanies,
getAllSuppliers,
generateOrderNumber,
} from "@/lib/pb-helpers";
import { cn } from "@/lib/utils";
import type { Customer, Company, Supplier, Order, OrderStatus } from "@/lib/types";
interface OrderFormProps {
open: boolean;
onOpenChange: (open: boolean) => void;
order?: Order | null;
onSaved: () => void;
}
export function OrderForm({ open, onOpenChange, order, onSaved }: OrderFormProps) {
const [loading, setLoading] = useState(false);
const [customers, setCustomers] = useState<Customer[]>([]);
const [companies, setCompanies] = useState<Company[]>([]);
const [suppliers, setSuppliers] = useState<Supplier[]>([]);
const [customerOpen, setCustomerOpen] = useState(false);
const [companyOpen, setCompanyOpen] = useState(false);
const [form, setForm] = useState({
order_number: "",
customer: "",
company: "",
quote: "",
status: "new" as OrderStatus,
notes: "",
});
useEffect(() => {
if (open) {
loadDropdowns();
if (order) {
setForm({
order_number: order.order_number,
customer: order.customer || "",
company: order.company,
quote: order.quote || "",
status: order.status,
notes: order.notes || "",
});
} else {
initNewOrder();
}
}
}, [open, order]);
const loadDropdowns = async () => {
try {
const [custs, comps, supps] = await Promise.all([
getAllCustomers(),
getAllCompanies(),
getAllSuppliers(),
]);
setCustomers(custs);
setCompanies(comps);
setSuppliers(supps);
} catch {
toast.error("Failed to load dropdowns");
}
};
const initNewOrder = async () => {
try {
const orderNum = await generateOrderNumber();
setForm({
order_number: orderNum,
customer: "",
company: "",
quote: "",
status: "new",
notes: "",
});
} catch {
toast.error("Failed to generate order number");
}
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!form.company) {
toast.error("Please select a company");
return;
}
setLoading(true);
try {
const pb = getPocketBase();
if (order) {
await pb.collection("orders").update(order.id, form);
toast.success("Order updated");
} else {
await pb.collection("orders").create(form);
toast.success("Order created");
}
onSaved();
onOpenChange(false);
} catch (err: unknown) {
const message = err instanceof Error ? err.message : "Failed to save order";
toast.error(message);
} finally {
setLoading(false);
}
};
const getCustomerName = (id: string) => customers.find((c) => c.id === id)?.customer || "";
const getCompanyName = (id: string) => companies.find((c) => c.id === id)?.company || "";
return (
<Sheet open={open} onOpenChange={onOpenChange}>
<SheetContent className="w-full sm:max-w-lg overflow-y-auto">
<SheetHeader>
<SheetTitle>{order ? "Edit Order" : "New Order"}</SheetTitle>
<SheetDescription>
{order ? `Editing ${order.order_number}` : "Create a new order"}
</SheetDescription>
</SheetHeader>
<form onSubmit={handleSubmit} className="space-y-6 mt-6">
<div className="space-y-2">
<Label>Order Number</Label>
<Input value={form.order_number} readOnly className="bg-muted" />
</div>
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label>Customer</Label>
<Popover open={customerOpen} onOpenChange={setCustomerOpen}>
<PopoverTrigger
className="w-full justify-between inline-flex items-center rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1"
>
{form.customer ? getCustomerName(form.customer) : "Select customer..."}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</PopoverTrigger>
<PopoverContent className="w-full p-0">
<Command>
<CommandInput placeholder="Search customers..." />
<CommandList>
<CommandEmpty>No customer found.</CommandEmpty>
<CommandGroup>
{customers.map((c) => (
<CommandItem
key={c.id}
value={c.customer}
onSelect={() => {
setForm({ ...form, customer: c.id });
setCustomerOpen(false);
}}
>
<Check className={cn("mr-2 h-4 w-4", form.customer === c.id ? "opacity-100" : "opacity-0")} />
{c.customer}
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
</div>
<div className="space-y-2">
<Label>Company *</Label>
<Popover open={companyOpen} onOpenChange={setCompanyOpen}>
<PopoverTrigger
className="w-full justify-between inline-flex items-center rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1"
>
{form.company ? getCompanyName(form.company) : "Select company..."}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</PopoverTrigger>
<PopoverContent className="w-full p-0">
<Command>
<CommandInput placeholder="Search companies..." />
<CommandList>
<CommandEmpty>No company found.</CommandEmpty>
<CommandGroup>
{companies.map((c) => (
<CommandItem
key={c.id}
value={c.company}
onSelect={() => {
setForm({ ...form, company: c.id });
setCompanyOpen(false);
}}
>
<Check className={cn("mr-2 h-4 w-4", form.company === c.id ? "opacity-100" : "opacity-0")} />
{c.company}
</CommandItem>
))}
</CommandGroup>
</CommandList>
</Command>
</PopoverContent>
</Popover>
</div>
</div>
{order && (
<div className="space-y-2">
<Label>Status</Label>
<Select value={form.status} onValueChange={(v) => setForm({ ...form, status: (v || "new") as OrderStatus })}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="new">New</SelectItem>
<SelectItem value="processing">Processing</SelectItem>
<SelectItem value="order_ready">Order Ready</SelectItem>
<SelectItem value="po_generated">PO Generated</SelectItem>
<SelectItem value="completed">Completed</SelectItem>
<SelectItem value="cancelled">Cancelled</SelectItem>
</SelectContent>
</Select>
</div>
)}
<div className="space-y-2">
<Label>Notes</Label>
<Textarea
value={form.notes}
onChange={(e) => setForm({ ...form, notes: e.target.value })}
rows={3}
/>
</div>
<div className="flex gap-3 pt-4">
<Button type="submit" disabled={loading} className="flex-1">
{loading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
{order ? "Update" : "Create"} Order
</Button>
<Button type="button" variant="outline" onClick={() => onOpenChange(false)}>
Cancel
</Button>
</div>
</form>
</SheetContent>
</Sheet>
);
}