[dyad] Supplier selection dropdown added to Purchase Order form - wrote 2 file(s)
This commit is contained in:
parent
ec62bf32ca
commit
2c3bf04d15
1 changed files with 227 additions and 126 deletions
|
|
@ -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"
|
||||
>
|
||||
×
|
||||
</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>
|
||||
)
|
||||
|
|
|
|||
Loading…
Reference in a new issue