- 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>
239 lines
9.1 KiB
TypeScript
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>
|
|
);
|
|
}
|