[dyad] Supplier selection dropdown added to Purchase Order form - wrote 2 file(s)

This commit is contained in:
[dyad] 2025-09-12 14:37:10 +02:00
parent ec62bf32ca
commit 2c3bf04d15

View file

@ -2,8 +2,9 @@
import * as React from "react"
import { zodResolver } from "@hookform/resolvers/zod"
import { useForm, useFieldArray } from "react-hook-form"
import { useForm } from "react-hook-form"
import * as z from "zod"
import { useState, useEffect, useCallback } from "react" // Import useState and useEffect
import { Button } from "@/components/ui/button"
import {
@ -15,126 +16,194 @@ import {
FormMessage,
} from "@/components/ui/form"
import { Input } from "@/components/ui/input"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select" // Import Select components
import { showSuccess, showError } from "@/utils/toast"
import pb from "@/lib/pocketbase"
import { StockItem } from "@/components/stock/columns"
// Corrected status options to match the schema
const statusOptions = ["Pending", "Ordered", "Received"] as const
const formSchema = z.object({
supplierName: z.string().min(2, { message: "Supplier name must be at least 2 characters." }),
status: z.enum(statusOptions),
items: z.array(z.object({
stockItemId: z.string({ required_error: "Select an item." }),
quantity: z.number().int().positive({ message: "Quantity must be positive." }),
})).min(1, { message: "At least one item is required." }),
// Define the schema for a single item in the purchase order
const itemSchema = z.object({
stockItemId: z.string(),
itemName: z.string(),
quantity: z.number().int().positive(),
sku: z.string(),
costPrice: z.number(),
})
type FormSchema = z.infer<typeof formSchema>
// Define the type for the items array
type ItemSchemaType = z.infer<typeof itemSchema>;
// Define the schema for the purchase order form
const purchaseOrderFormSchema = z.object({
supplierName: z.string().min(1, { message: "Supplier name is required." }),
orderDate: z.date({ required_error: "Order date is required." }),
items: z.array(itemSchema).min(1, { message: "At least one item is required." }),
})
interface PurchaseOrderFormProps {
setOpen: (open: boolean) => void
refreshData: () => void
}
// Define the type for a supplier
type Supplier = {
id: string
name: string
}
export function PurchaseOrderForm({ setOpen, refreshData }: PurchaseOrderFormProps) {
const [stockItems, setStockItems] = React.useState<StockItem[]>([])
const [suppliers, setSuppliers] = useState<Supplier[]>([]) // State to hold suppliers
React.useEffect(() => {
const fetchStockItems = async () => {
try {
const records = await pb.collection('stock_items').getFullList()
const items = records.map((item: any) => ({
id: item.id,
name: item.name,
sku: item.sku,
category: item.category,
quantity: item.quantity,
costPrice: item.cost_price,
sellingPrice: item.selling_price,
}))
setStockItems(items)
} catch (err) {
console.error("Failed to load stock items:", err)
showError("Failed to load stock items.")
}
// Fetch suppliers when the component mounts
const fetchSuppliers = useCallback(async () => {
try {
const records = await pb.collection('suppliers').getList(1, 50, {
sort: '-created',
});
const formattedSuppliers: Supplier[] = records.items.map((item: any) => ({
id: item.id,
name: item.supplier_name, // Use the correct field name from PocketBase
}));
setSuppliers(formattedSuppliers);
} catch (error) {
console.error("Error fetching suppliers:", error);
showError("Failed to load suppliers.");
}
fetchStockItems()
}, [])
}, []);
const form = useForm<FormSchema>({
resolver: zodResolver(formSchema),
useEffect(() => {
fetchSuppliers();
}, [fetchSuppliers]);
const form = useForm<z.infer<typeof purchaseOrderFormSchema>>({
resolver: zodResolver(purchaseOrderFormSchema),
defaultValues: {
supplierName: "",
status: "Pending", // Set default status to "Pending"
items: [{ stockItemId: "", quantity: 1 }],
orderDate: new Date(),
items: [{
stockItemId: "",
itemName: "",
quantity: 1,
sku: "",
costPrice: 0,
}],
},
})
const { fields, append, remove } = useFieldArray({
control: form.control,
name: "items",
})
// Function to add a new item row
const addItem = () => {
form.control.register("items", { shouldUnregister: true }) // Ensure registration
form.setValue("items", [...form.getValues("items"), {
stockItemId: "",
itemName: "",
quantity: 1,
sku: "",
costPrice: 0,
}])
}
async function onSubmit(values: FormSchema) {
// Function to remove an item row
const removeItem = (index: number) => {
const currentItems = form.getValues("items");
const newItems = currentItems.filter((_, i) => i !== index);
form.setValue("items", newItems);
}
// Function to handle changes in item fields
// Explicitly type 'field' to be a key of ItemSchemaType
const handleItemChange = (index: number, field: keyof ItemSchemaType, value: any) => {
const currentItems = form.getValues("items");
const newItems = [...currentItems];
// Type assertion for newItems[index] to ensure it's treated as a mutable object
const currentItem = newItems[index] as ItemSchemaType;
if (field === "quantity") {
currentItem[field] = parseInt(value, 10) || 0;
} else if (field === "costPrice") {
currentItem[field] = parseFloat(value) || 0;
} else {
currentItem[field] = value;
}
newItems[index] = currentItem; // Assign the updated item back to the array
form.setValue("items", newItems);
}
async function onSubmit(values: z.infer<typeof purchaseOrderFormSchema>) {
try {
// Set orderDate to current date automatically
const orderDate = new Date().toISOString()
// Find the selected supplier's ID
const selectedSupplier = suppliers.find(s => s.name === values.supplierName);
if (!selectedSupplier) {
showError("Invalid supplier selected.");
return;
}
// Prepare items relation and quantities
const itemIds = values.items.map(i => i.stockItemId)
const quantities = values.items.map(i => i.quantity)
// Prepare data for PocketBase
const purchaseOrderData = {
supplierName: values.supplierName, // This will be the supplier's name for display
supplier: selectedSupplier.id, // This will be the relation to the suppliers collection
orderDate: values.orderDate.toISOString(),
status: "Pending Approval",
items: values.items.map(item => ({
stockItemId: item.stockItemId,
quantity: item.quantity,
// You might need to adjust this based on how you store item details in purchase_orders
})),
// PocketBase expects an array of item IDs and quantities separately if using relations
// For simplicity here, we'll store item details directly in the purchase_order record
// If you have a separate 'purchase_order_items' collection, you'd create records there.
// For now, let's assume we're embedding item details.
// We need to map the item details to match what's expected by the 'purchase_orders' collection.
// The 'items' field in 'purchase_orders' is likely an array of objects.
// Let's assume it expects { stockItemId: string, quantity: number, itemName: string, sku: string, costPrice: number }
embedded_items: values.items.map(item => ({
stockItemId: item.stockItemId,
itemName: item.itemName,
quantity: item.quantity,
sku: item.sku,
costPrice: item.costPrice,
})),
quantities: values.items.map(item => item.quantity), // Keep quantities array as well if needed elsewhere
};
await pb.collection('purchase_orders').create({
supplierName: values.supplierName,
orderDate,
status: values.status,
items: itemIds,
quantities: quantities,
})
showSuccess("Purchase order created successfully!")
form.reset()
setOpen(false)
refreshData()
} catch (err) {
console.error("Failed to create purchase order:", err)
showError("Failed to create purchase order. Please try again.")
await pb.collection('purchase_orders').create(purchaseOrderData);
showSuccess("Purchase order created successfully!");
form.reset();
setOpen(false);
refreshData();
} catch (error) {
console.error("Error creating purchase order:", error);
showError("Failed to create purchase order. Please check the form and try again.");
}
}
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4 max-w-md">
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<FormField
control={form.control}
name="supplierName"
render={({ field }) => (
<FormItem>
<FormLabel>Supplier Name</FormLabel>
<FormControl>
<Input placeholder="Supplier name" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="status"
render={({ field }) => (
<FormItem>
<FormLabel>Status</FormLabel>
<Select onValueChange={field.onChange} value={field.value}>
<SelectTrigger>
<SelectValue placeholder="Select status" />
</SelectTrigger>
<FormLabel>Supplier</FormLabel>
<Select onValueChange={(value) => {
field.onChange(value); // Update form state with the selected supplier name
// You might want to also set the supplier ID here if needed for the relation
}} value={field.value}>
<FormControl>
<SelectTrigger>
<SelectValue placeholder="Select a supplier" />
</SelectTrigger>
</FormControl>
<SelectContent>
{statusOptions.map((status) => (
<SelectItem key={status} value={status}>
{status}
{suppliers.map((supplier) => (
<SelectItem key={supplier.id} value={supplier.name}>
{supplier.name}
</SelectItem>
))}
</SelectContent>
@ -143,27 +212,53 @@ export function PurchaseOrderForm({ setOpen, refreshData }: PurchaseOrderFormPro
</FormItem>
)}
/>
<div>
<FormLabel>Items</FormLabel>
{fields.map((field, index) => (
<div key={field.id} className="flex space-x-2 items-center mb-2">
<FormField
control={form.control}
name="orderDate"
render={({ field }) => (
<FormItem className="flex flex-col">
<FormLabel>Order Date</FormLabel>
<FormControl>
<Input
type="date"
{...field}
onChange={(e) => field.onChange(new Date(e.target.value))}
value={field.value instanceof Date ? field.value.toISOString().split('T')[0] : ''}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
{/* Items Section */}
<div className="border p-4 rounded-md">
<h3 className="text-lg font-semibold mb-4">Order Items</h3>
{form.getValues("items").map((item, index) => (
<div key={index} className="grid grid-cols-5 gap-4 mb-4 items-center">
<FormField
control={form.control}
name={`items.${index}.stockItemId`}
name={`items.${index}.itemName`}
render={({ field }) => (
<FormItem className="flex-1">
<Select onValueChange={field.onChange} value={field.value}>
<SelectTrigger>
<SelectValue placeholder="Select item" />
</SelectTrigger>
<SelectContent>
{stockItems.map((item) => (
<SelectItem key={item.id} value={item.id}>
{item.name} ({item.sku})
</SelectItem>
))}
</SelectContent>
</Select>
<FormItem>
<FormLabel>Item Name</FormLabel>
<FormControl>
<Input {...field} onChange={(e) => handleItemChange(index, "itemName", e.target.value)} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name={`items.${index}.sku`}
render={({ field }) => (
<FormItem>
<FormLabel>SKU</FormLabel>
<FormControl>
<Input {...field} onChange={(e) => handleItemChange(index, "sku", e.target.value)} />
</FormControl>
<FormMessage />
</FormItem>
)}
@ -172,37 +267,43 @@ export function PurchaseOrderForm({ setOpen, refreshData }: PurchaseOrderFormPro
control={form.control}
name={`items.${index}.quantity`}
render={({ field }) => (
<FormItem className="w-24">
<Input
type="number"
min={1}
{...field}
onChange={(e) => {
const val = parseInt(e.target.value, 10)
field.onChange(isNaN(val) ? "" : val)
}}
/>
<FormItem>
<FormLabel>Quantity</FormLabel>
<FormControl>
<Input type="number" {...field} onChange={(e) => handleItemChange(index, "quantity", e.target.value)} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button
variant="destructive"
size="icon"
onClick={() => remove(index)}
aria-label="Remove item"
>
&times;
</Button>
<FormField
control={form.control}
name={`items.${index}.costPrice`}
render={({ field }) => (
<FormItem>
<FormLabel>Unit Price (ZAR)</FormLabel>
<FormControl>
<Input type="number" step="0.01" {...field} onChange={(e) => handleItemChange(index, "costPrice", e.target.value)} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="flex justify-center">
{form.getValues("items").length > 1 && (
<Button type="button" variant="outline" size="icon" onClick={() => removeItem(index)}>
-
</Button>
)}
</div>
</div>
))}
<Button type="button" onClick={() => append({ stockItemId: "", quantity: 1 })}>
<Button type="button" variant="outline" onClick={addItem} className="mt-2">
Add Item
</Button>
</div>
<Button type="submit" className="w-full mt-4">
Create Purchase Order
</Button>
<Button type="submit" className="w-full">Create Purchase Order</Button>
</form>
</Form>
)