WFDASH-WEBAPP/src/components/order-form.tsx
SJCoetz 4444842ef5 feat: collection-map updates, todays-stops, quote/approval rework, buyer-tasks, e2e
- Add todays-stops test and component to collection-map
- Expand collection-map sidebar and pin-model
- Rework orders/[id]/process page
- Add quotes/[id]/page and process pages
- Add quotes/approvals/[id] page
- Add buyer-tasks generate-po route
- Add quotes partial-approve and pdf routes
- Add purchase-orders [id]/pdf route
- Add e2e create-flows spec
- Add superpowers plan doc
- Update pb_schema-2.json
- Update form components (company, customer, order, quote, supplier)
- Update UI components (button, command, dialog)
- Add collection-pool test components

Co-authored-by: Orca <help@stably.ai>
2026-08-03 09:33:45 +02:00

239 lines
9.1 KiB
TypeScript

"use client";
import { useState, useEffect, useCallback } 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 { 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 } 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: "",
notes: "",
});
const loadDropdowns = useCallback(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 = useCallback(async () => {
try {
const orderNum = await generateOrderNumber();
setForm({
order_number: orderNum,
customer: "",
company: "",
quote: "",
notes: "",
});
} catch {
toast.error("Failed to generate order number");
}
}, []);
useEffect(() => {
if (!open) return;
const editing = order;
queueMicrotask(() => {
if (editing) {
setForm({
order_number: editing.order_number,
customer: editing.customer || "",
company: editing.company,
quote: editing.quote || "",
notes: editing.notes || "",
});
} else {
void initNewOrder();
}
});
// Defer loadDropdowns to a microtask too so the effect body itself
// never calls setState synchronously (loadDropdowns writes
// customers/companies/suppliers on resolution).
queueMicrotask(() => {
void loadDropdowns();
});
}, [open, order, loadDropdowns, initNewOrder]);
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>
<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>
);
}