2025-09-11 11:39:50 +00:00
|
|
|
"use client"
|
|
|
|
|
|
|
|
|
|
import * as React from "react"
|
|
|
|
|
import { zodResolver } from "@hookform/resolvers/zod"
|
2025-09-25 21:29:58 +00:00
|
|
|
import { useForm, useFieldArray } from "react-hook-form"
|
2025-09-11 11:39:50 +00:00
|
|
|
import * as z from "zod"
|
2025-09-12 12:37:10 +00:00
|
|
|
import { useState, useEffect, useCallback } from "react" // Import useState and useEffect
|
2025-09-11 11:39:50 +00:00
|
|
|
|
|
|
|
|
import { Button } from "@/components/ui/button"
|
|
|
|
|
import {
|
|
|
|
|
Form,
|
|
|
|
|
FormControl,
|
|
|
|
|
FormField,
|
|
|
|
|
FormItem,
|
|
|
|
|
FormLabel,
|
|
|
|
|
FormMessage,
|
|
|
|
|
} from "@/components/ui/form"
|
|
|
|
|
import { Input } from "@/components/ui/input"
|
2025-09-12 12:37:10 +00:00
|
|
|
import {
|
|
|
|
|
Select,
|
|
|
|
|
SelectContent,
|
|
|
|
|
SelectItem,
|
|
|
|
|
SelectTrigger,
|
|
|
|
|
SelectValue,
|
|
|
|
|
} from "@/components/ui/select" // Import Select components
|
2025-09-12 12:33:05 +00:00
|
|
|
import { showSuccess, showError } from "@/utils/toast"
|
|
|
|
|
import pb from "@/lib/pocketbase"
|
2025-09-12 12:37:10 +00:00
|
|
|
|
|
|
|
|
// Define the schema for a single item in the purchase order
|
|
|
|
|
const itemSchema = z.object({
|
2025-09-12 12:42:55 +00:00
|
|
|
stockItemId: z.string().min(1, { message: "Stock item ID is required." }),
|
|
|
|
|
itemName: z.string().min(1, { message: "Item name is required." }),
|
2025-09-12 12:37:10 +00:00
|
|
|
quantity: z.number().int().positive(),
|
|
|
|
|
sku: z.string(),
|
|
|
|
|
costPrice: z.number(),
|
2025-09-11 11:39:50 +00:00
|
|
|
})
|
|
|
|
|
|
2025-09-12 12:37:10 +00:00
|
|
|
// 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." }),
|
|
|
|
|
})
|
2025-09-12 12:33:05 +00:00
|
|
|
|
2025-09-11 11:39:50 +00:00
|
|
|
interface PurchaseOrderFormProps {
|
2025-09-12 12:33:05 +00:00
|
|
|
setOpen: (open: boolean) => void
|
|
|
|
|
refreshData: () => void
|
2025-09-11 11:39:50 +00:00
|
|
|
}
|
|
|
|
|
|
2025-09-12 12:37:10 +00:00
|
|
|
// Define the type for a supplier
|
|
|
|
|
type Supplier = {
|
|
|
|
|
id: string
|
|
|
|
|
name: string
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-12 12:42:55 +00:00
|
|
|
// Define the type for a stock item
|
|
|
|
|
type StockItem = {
|
|
|
|
|
id: string
|
|
|
|
|
name: string
|
|
|
|
|
sku: string
|
|
|
|
|
costPrice: number
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-12 12:33:05 +00:00
|
|
|
export function PurchaseOrderForm({ setOpen, refreshData }: PurchaseOrderFormProps) {
|
2025-09-12 12:37:10 +00:00
|
|
|
const [suppliers, setSuppliers] = useState<Supplier[]>([]) // State to hold suppliers
|
2025-09-12 12:42:55 +00:00
|
|
|
const [stockItems, setStockItems] = useState<StockItem[]>([]) // State to hold stock items
|
2025-09-12 12:37:10 +00:00
|
|
|
|
|
|
|
|
// Fetch suppliers when the component mounts
|
|
|
|
|
const fetchSuppliers = useCallback(async () => {
|
|
|
|
|
try {
|
2025-09-29 07:17:39 +00:00
|
|
|
const records = await pb.collection('suppliers').getFullList({
|
2025-09-12 12:37:10 +00:00
|
|
|
sort: '-created',
|
|
|
|
|
});
|
2025-09-29 07:17:39 +00:00
|
|
|
const formattedSuppliers: Supplier[] = records.map((item: any) => ({
|
2025-09-12 12:37:10 +00:00
|
|
|
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.");
|
2025-09-12 12:33:05 +00:00
|
|
|
}
|
2025-09-12 12:37:10 +00:00
|
|
|
}, []);
|
|
|
|
|
|
2025-09-12 12:42:55 +00:00
|
|
|
// Fetch stock items when the component mounts
|
|
|
|
|
const fetchStockItems = useCallback(async () => {
|
|
|
|
|
try {
|
2025-09-29 07:17:39 +00:00
|
|
|
const records = await pb.collection('stock_items').getFullList({
|
2025-09-12 12:42:55 +00:00
|
|
|
sort: '-created',
|
|
|
|
|
});
|
2025-09-29 07:17:39 +00:00
|
|
|
const formattedStockItems: StockItem[] = records.map((item: any) => ({
|
2025-09-12 12:42:55 +00:00
|
|
|
id: item.id,
|
|
|
|
|
name: item.name,
|
|
|
|
|
sku: item.sku,
|
|
|
|
|
costPrice: item.cost_price, // Assuming cost_price is the field in stock_items
|
|
|
|
|
}));
|
|
|
|
|
setStockItems(formattedStockItems);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error("Error fetching stock items:", error);
|
|
|
|
|
showError("Failed to load stock items.");
|
|
|
|
|
}
|
|
|
|
|
}, []);
|
|
|
|
|
|
2025-09-12 12:37:10 +00:00
|
|
|
useEffect(() => {
|
|
|
|
|
fetchSuppliers();
|
2025-09-12 12:42:55 +00:00
|
|
|
fetchStockItems();
|
|
|
|
|
}, [fetchSuppliers, fetchStockItems]);
|
2025-09-12 12:33:05 +00:00
|
|
|
|
2025-09-12 12:37:10 +00:00
|
|
|
const form = useForm<z.infer<typeof purchaseOrderFormSchema>>({
|
|
|
|
|
resolver: zodResolver(purchaseOrderFormSchema),
|
2025-09-11 11:39:50 +00:00
|
|
|
defaultValues: {
|
2025-09-12 12:33:05 +00:00
|
|
|
supplierName: "",
|
2025-09-12 12:37:10 +00:00
|
|
|
orderDate: new Date(),
|
|
|
|
|
items: [{
|
|
|
|
|
stockItemId: "",
|
|
|
|
|
itemName: "",
|
|
|
|
|
quantity: 1,
|
|
|
|
|
sku: "",
|
|
|
|
|
costPrice: 0,
|
|
|
|
|
}],
|
2025-09-11 11:39:50 +00:00
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
|
2025-09-25 21:29:58 +00:00
|
|
|
const { fields, append, remove } = useFieldArray({
|
|
|
|
|
control: form.control,
|
|
|
|
|
name: "items",
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
2025-09-12 12:37:10 +00:00
|
|
|
// Function to add a new item row
|
|
|
|
|
const addItem = () => {
|
2025-09-25 21:29:58 +00:00
|
|
|
append({
|
2025-09-12 12:37:10 +00:00
|
|
|
stockItemId: "",
|
|
|
|
|
itemName: "",
|
|
|
|
|
quantity: 1,
|
|
|
|
|
sku: "",
|
|
|
|
|
costPrice: 0,
|
2025-09-25 21:29:58 +00:00
|
|
|
});
|
2025-09-12 12:37:10 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Function to remove an item row
|
|
|
|
|
const removeItem = (index: number) => {
|
2025-09-25 21:29:58 +00:00
|
|
|
remove(index);
|
2025-09-12 12:37:10 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Function to handle changes in item fields
|
|
|
|
|
const handleItemChange = (index: number, field: keyof ItemSchemaType, value: any) => {
|
|
|
|
|
const currentItems = form.getValues("items");
|
|
|
|
|
const newItems = [...currentItems];
|
|
|
|
|
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;
|
|
|
|
|
}
|
2025-09-12 12:42:55 +00:00
|
|
|
newItems[index] = currentItem;
|
2025-09-12 12:37:10 +00:00
|
|
|
form.setValue("items", newItems);
|
|
|
|
|
}
|
2025-09-12 12:33:05 +00:00
|
|
|
|
2025-09-12 12:42:55 +00:00
|
|
|
// Function to handle stock item selection from dropdown
|
|
|
|
|
const handleStockItemSelect = (index: number, selectedItemName: string) => {
|
|
|
|
|
const selectedStockItem = stockItems.find(item => item.name === selectedItemName);
|
|
|
|
|
if (selectedStockItem) {
|
|
|
|
|
// Update the form fields with the selected stock item's details
|
|
|
|
|
handleItemChange(index, "stockItemId", selectedStockItem.id);
|
|
|
|
|
handleItemChange(index, "itemName", selectedStockItem.name);
|
|
|
|
|
handleItemChange(index, "sku", selectedStockItem.sku);
|
|
|
|
|
handleItemChange(index, "costPrice", selectedStockItem.costPrice);
|
|
|
|
|
// Quantity remains as is or defaults to 1 if not set
|
|
|
|
|
if (form.getValues("items")[index].quantity === 0) {
|
|
|
|
|
handleItemChange(index, "quantity", 1);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-12 12:37:10 +00:00
|
|
|
async function onSubmit(values: z.infer<typeof purchaseOrderFormSchema>) {
|
2025-09-11 11:39:50 +00:00
|
|
|
try {
|
2025-09-12 12:37:10 +00:00
|
|
|
// Find the selected supplier's ID
|
|
|
|
|
const selectedSupplier = suppliers.find(s => s.name === values.supplierName);
|
|
|
|
|
if (!selectedSupplier) {
|
|
|
|
|
showError("Invalid supplier selected.");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 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",
|
2025-09-25 21:44:06 +00:00
|
|
|
items: values.items.map(item => item.stockItemId),
|
2025-09-12 14:12:47 +00:00
|
|
|
quantities: values.items.map(item => item.quantity), // Keep quantities array as well if needed elsewhere
|
2025-09-12 12:37:10 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
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.");
|
2025-09-11 11:39:50 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<Form {...form}>
|
2025-09-12 12:37:10 +00:00
|
|
|
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
|
2025-09-11 11:39:50 +00:00
|
|
|
<FormField
|
|
|
|
|
control={form.control}
|
2025-09-12 12:33:05 +00:00
|
|
|
name="supplierName"
|
2025-09-11 11:39:50 +00:00
|
|
|
render={({ field }) => (
|
|
|
|
|
<FormItem>
|
2025-09-12 12:37:10 +00:00
|
|
|
<FormLabel>Supplier</FormLabel>
|
|
|
|
|
<Select onValueChange={(value) => {
|
2025-09-12 12:42:55 +00:00
|
|
|
field.onChange(value);
|
2025-09-12 12:37:10 +00:00
|
|
|
}} value={field.value}>
|
|
|
|
|
<FormControl>
|
|
|
|
|
<SelectTrigger>
|
|
|
|
|
<SelectValue placeholder="Select a supplier" />
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
</FormControl>
|
|
|
|
|
<SelectContent>
|
|
|
|
|
{suppliers.map((supplier) => (
|
|
|
|
|
<SelectItem key={supplier.id} value={supplier.name}>
|
|
|
|
|
{supplier.name}
|
|
|
|
|
</SelectItem>
|
|
|
|
|
))}
|
|
|
|
|
</SelectContent>
|
|
|
|
|
</Select>
|
2025-09-11 11:39:50 +00:00
|
|
|
<FormMessage />
|
|
|
|
|
</FormItem>
|
|
|
|
|
)}
|
|
|
|
|
/>
|
2025-09-12 12:37:10 +00:00
|
|
|
|
2025-09-11 11:39:50 +00:00
|
|
|
<FormField
|
|
|
|
|
control={form.control}
|
2025-09-12 12:37:10 +00:00
|
|
|
name="orderDate"
|
2025-09-11 11:39:50 +00:00
|
|
|
render={({ field }) => (
|
2025-09-12 12:37:10 +00:00
|
|
|
<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>
|
2025-09-11 11:39:50 +00:00
|
|
|
<FormMessage />
|
|
|
|
|
</FormItem>
|
|
|
|
|
)}
|
|
|
|
|
/>
|
2025-09-12 12:37:10 +00:00
|
|
|
|
|
|
|
|
{/* Items Section */}
|
|
|
|
|
<div className="border p-4 rounded-md">
|
|
|
|
|
<h3 className="text-lg font-semibold mb-4">Order Items</h3>
|
2025-09-25 21:29:58 +00:00
|
|
|
{fields.map((item, index) => (
|
|
|
|
|
<div key={item.id} className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-[2fr,1fr,1fr,1fr,auto] gap-4 mb-4 items-center">
|
2025-09-12 12:37:10 +00:00
|
|
|
<FormField
|
|
|
|
|
control={form.control}
|
|
|
|
|
name={`items.${index}.itemName`}
|
|
|
|
|
render={({ field }) => (
|
|
|
|
|
<FormItem>
|
|
|
|
|
<FormLabel>Item Name</FormLabel>
|
2025-09-12 14:12:47 +00:00
|
|
|
<Select
|
|
|
|
|
onValueChange={(selectedValue) => handleStockItemSelect(index, selectedValue)}
|
|
|
|
|
value={field.value} // Set the current value of the item name
|
|
|
|
|
>
|
|
|
|
|
<FormControl>
|
|
|
|
|
<SelectTrigger>
|
|
|
|
|
<SelectValue placeholder="Select item" />
|
|
|
|
|
</SelectTrigger>
|
|
|
|
|
</FormControl>
|
|
|
|
|
<SelectContent>
|
|
|
|
|
{stockItems.map((stockItem) => (
|
|
|
|
|
<SelectItem key={stockItem.id} value={stockItem.name}>
|
|
|
|
|
{stockItem.name}
|
|
|
|
|
</SelectItem>
|
|
|
|
|
))}
|
|
|
|
|
</SelectContent>
|
|
|
|
|
</Select>
|
2025-09-12 12:37:10 +00:00
|
|
|
<FormMessage />
|
|
|
|
|
</FormItem>
|
|
|
|
|
)}
|
|
|
|
|
/>
|
2025-09-12 12:33:05 +00:00
|
|
|
<FormField
|
|
|
|
|
control={form.control}
|
2025-09-12 12:37:10 +00:00
|
|
|
name={`items.${index}.sku`}
|
2025-09-12 12:33:05 +00:00
|
|
|
render={({ field }) => (
|
2025-09-12 12:37:10 +00:00
|
|
|
<FormItem>
|
|
|
|
|
<FormLabel>SKU</FormLabel>
|
|
|
|
|
<FormControl>
|
2025-09-12 14:12:47 +00:00
|
|
|
{/* Display SKU, but it's managed by handleStockItemSelect */}
|
2025-09-12 12:42:55 +00:00
|
|
|
<Input {...field} readOnly placeholder="SKU" />
|
2025-09-12 12:37:10 +00:00
|
|
|
</FormControl>
|
2025-09-12 12:33:05 +00:00
|
|
|
<FormMessage />
|
|
|
|
|
</FormItem>
|
|
|
|
|
)}
|
|
|
|
|
/>
|
|
|
|
|
<FormField
|
|
|
|
|
control={form.control}
|
|
|
|
|
name={`items.${index}.quantity`}
|
|
|
|
|
render={({ field }) => (
|
2025-09-12 12:37:10 +00:00
|
|
|
<FormItem>
|
|
|
|
|
<FormLabel>Quantity</FormLabel>
|
|
|
|
|
<FormControl>
|
|
|
|
|
<Input type="number" {...field} onChange={(e) => handleItemChange(index, "quantity", e.target.value)} />
|
|
|
|
|
</FormControl>
|
2025-09-12 12:33:05 +00:00
|
|
|
<FormMessage />
|
|
|
|
|
</FormItem>
|
|
|
|
|
)}
|
|
|
|
|
/>
|
2025-09-12 12:37:10 +00:00
|
|
|
<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>
|
|
|
|
|
)}
|
|
|
|
|
/>
|
2025-09-25 21:18:24 +00:00
|
|
|
<div className="flex justify-center md:justify-start">
|
2025-09-25 21:29:58 +00:00
|
|
|
{fields.length > 1 && (
|
2025-09-25 21:18:24 +00:00
|
|
|
<Button type="button" variant="outline" size="icon" onClick={() => removeItem(index)} className="w-full sm:w-auto">
|
2025-09-12 12:37:10 +00:00
|
|
|
-
|
|
|
|
|
</Button>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
2025-09-12 12:33:05 +00:00
|
|
|
</div>
|
|
|
|
|
))}
|
2025-09-12 12:37:10 +00:00
|
|
|
<Button type="button" variant="outline" onClick={addItem} className="mt-2">
|
2025-09-12 12:33:05 +00:00
|
|
|
Add Item
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
2025-09-12 12:37:10 +00:00
|
|
|
|
|
|
|
|
<Button type="submit" className="w-full">Create Purchase Order</Button>
|
2025-09-11 11:39:50 +00:00
|
|
|
</form>
|
|
|
|
|
</Form>
|
|
|
|
|
)
|
|
|
|
|
}
|