diff --git a/src/components/book-out/BookOutForm.tsx b/src/components/book-out/BookOutForm.tsx index 7b4fce3..e414b5d 100644 --- a/src/components/book-out/BookOutForm.tsx +++ b/src/components/book-out/BookOutForm.tsx @@ -4,6 +4,7 @@ import * as React from "react" import { zodResolver } from "@hookform/resolvers/zod" import { useForm } from "react-hook-form" import * as z from "zod" +import { useState, useEffect, useCallback } from "react" import { Button } from "@/components/ui/button" import { @@ -15,160 +16,247 @@ import { FormMessage, } from "@/components/ui/form" import { Input } from "@/components/ui/input" +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select" import { showSuccess, showError } from "@/utils/toast" -import { CustomerCombobox } from "./CustomerCombobox" -import { StockItemCombobox } from "./StockItemCombobox" // Import the new combobox import pb from "@/lib/pocketbase" +import { StockItemCombobox } from "@/components/book-out/StockItemCombobox" // Assuming this path is correct -const formSchema = z.object({ - customer: z.string({ required_error: "Please select a customer." }), - stockItemId: z.string({ required_error: "Please select an item." }), // Changed from 'item' to 'stockItemId' - quantity: z.coerce.number().int().positive({ message: "Quantity must be a positive number." }), +// Define the schema for a single item in the book out form +const bookOutItemSchema = z.object({ + stockItemId: z.string().min(1, { message: "Stock item ID is required." }), + itemName: z.string().min(1, { message: "Item name is required." }), + quantity: z.number().int().positive(), + sku: z.string(), +}) + +// Define the type for the items array +type BookOutItemSchemaType = z.infer; + +// Define the schema for the book out form +const bookOutFormSchema = z.object({ + bookOutDate: z.date({ required_error: "Book out date is required." }), + items: z.array(bookOutItemSchema).min(1, { message: "At least one item is required." }), }) interface BookOutFormProps { - setOpen: (open: boolean) => void; - refreshData: () => void; // Added refreshData prop + setOpen: (open: boolean) => void + refreshData: () => void +} + +// Define the type for a stock item +type StockItem = { + id: string + name: string + sku: string + costPrice: number // Although not used in BookOut, keeping for consistency if needed later } export function BookOutForm({ setOpen, refreshData }: BookOutFormProps) { - const [customerOptions, setCustomerOptions] = React.useState<{ value: string; label: string }[]>([]); - const [stockItemOptions, setStockItemOptions] = React.useState<{ value: string; label: string; quantity: number }[]>([]); + const [stockItems, setStockItems] = useState([]) // State to hold stock items - const fetchCustomers = React.useCallback(async () => { - try { - const records = await pb.collection('customers').getList(1, 50); - const options = records.items.map((customer: any) => ({ - value: customer.id, - label: customer.comapny_name, - })); - setCustomerOptions(options); - } catch (error) { - console.error("Error fetching customers for combobox:", error); - showError("Failed to load customers."); - } - }, []); - - const fetchStockItems = React.useCallback(async () => { + // Fetch stock items when the component mounts + const fetchStockItems = useCallback(async () => { try { const records = await pb.collection('stock_items').getList(1, 50, { - filter: 'quantity > 0', // Only show items with quantity > 0 + sort: '-created', }); - const options = records.items.map((item: any) => ({ - value: item.id, - label: item.name, - quantity: item.quantity, + // Ensure the fetched data conforms to the StockItem type + const formattedStockItems: StockItem[] = records.items.map((item: any) => ({ + id: item.id, + name: item.name, + sku: item.sku, + costPrice: item.cost_price, // Assuming cost_price is the field in stock_items })); - setStockItemOptions(options); + setStockItems(formattedStockItems); } catch (error) { - console.error("Error fetching stock items for combobox:", error); + console.error("Error fetching stock items:", error); showError("Failed to load stock items."); } }, []); - React.useEffect(() => { - fetchCustomers(); + useEffect(() => { fetchStockItems(); - }, [fetchCustomers, fetchStockItems]); + }, [fetchStockItems]); - const form = useForm>({ - resolver: zodResolver(formSchema), + const form = useForm>({ + resolver: zodResolver(bookOutFormSchema), defaultValues: { - quantity: 1, + bookOutDate: new Date(), + items: [{ + stockItemId: "", + itemName: "", + quantity: 1, + sku: "", + }], }, }) - async function onSubmit(values: z.infer) { - try { - // Fetch the latest stock item data to prevent race conditions - const selectedStockItem = await pb.collection('stock_items').getOne(values.stockItemId); + // Function to add a new item row + const addItem = () => { + const currentItems = form.getValues("items"); + form.setValue("items", [...currentItems, { + stockItemId: "", + itemName: "", + quantity: 1, + sku: "", + }]); + } - if (!selectedStockItem) { - showError("Selected item not found."); - return; + // 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 + const handleItemChange = (index: number, field: keyof BookOutItemSchemaType, value: any) => { + const currentItems = form.getValues("items"); + const newItems = [...currentItems]; + const currentItem = newItems[index] as BookOutItemSchemaType; + + if (field === "quantity") { + currentItem[field] = parseInt(value, 10) || 0; + } else { + currentItem[field] = value; + } + newItems[index] = currentItem; + form.setValue("items", newItems); + } + + // 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); + // Quantity remains as is or defaults to 1 if not set + if (form.getValues("items")[index].quantity === 0) { + handleItemChange(index, "quantity", 1); } - - const currentQuantity = selectedStockItem.quantity; - const bookedOutQuantity = values.quantity; - - if (bookedOutQuantity > currentQuantity) { - showError(`Not enough stock. Only ${currentQuantity} available.`); - return; - } - - const newQuantity = currentQuantity - bookedOutQuantity; - - // Update the stock item quantity in PocketBase - await pb.collection('stock_items').update(values.stockItemId, { - quantity: newQuantity, - }); - - // Create a new record in the 'book_out_records' collection - await pb.collection('book_out_records').create({ - customer: values.customer, - items: values.stockItemId, // 'items' is a relation field to 'stock_items' - quantity: bookedOutQuantity, - date_booked_out: new Date().toISOString(), // Record the current date/time - }); - - showSuccess(`Successfully booked out ${bookedOutQuantity} of ${selectedStockItem.name}!`); - form.reset(); - setOpen(false); - fetchStockItems(); // Re-fetch stock items to update quantities in the combobox - refreshData(); // Trigger refresh in parent component if needed - } catch (error) { - console.error("Error booking out item:", error); - showError("Failed to book out item. Please try again."); } } + async function onSubmit(values: z.infer) { + try { + // Prepare data for PocketBase + const bookOutData = { + bookOutDate: values.bookOutDate.toISOString(), + // PocketBase expects an array of item objects for the 'items' field + items: values.items.map(item => ({ + stockItemId: item.stockItemId, + itemName: item.itemName, + quantity: item.quantity, + sku: item.sku, + })), + }; + + await pb.collection('book_out_records').create(bookOutData); + showSuccess("Stock booked out successfully!"); + form.reset(); + setOpen(false); + refreshData(); + } catch (error) { + console.error("Error booking out stock:", error); + showError("Failed to book out stock. Please check the form and try again."); + } + } + + // Filter stock items based on search term (if search functionality is added to Combobox) + // For now, we pass all stockItems and let the Combobox handle filtering internally. + return (
( - Customer - - - - )} - /> - ( - - Item - - - - )} - /> - ( - - Quantity + Book Out Date - + field.onChange(new Date(e.target.value))} + value={field.value instanceof Date ? field.value.toISOString().split('T')[0] : ''} + /> )} /> - + + {/* Items Section */} +
+

Items to Book Out

+ {form.getValues("items").map((item, index) => ( +
+ ( + {/* Make item name take more space */} + Item Name + handleStockItemSelect(index, selectedItemName)} + placeholder="Search items..." + /> + + + )} + /> + ( + + SKU + + + + + + )} + /> + ( + + Quantity + + handleItemChange(index, "quantity", e.target.value)} /> + + + + )} + /> +
{/* Adjust margin-top for alignment */} + {form.getValues("items").length > 1 && ( + + )} +
+
+ ))} + +
+ + ) diff --git a/src/components/book-out/StockItemCombobox.tsx b/src/components/book-out/StockItemCombobox.tsx index 597329c..46715f2 100644 --- a/src/components/book-out/StockItemCombobox.tsx +++ b/src/components/book-out/StockItemCombobox.tsx @@ -11,6 +11,7 @@ import { CommandGroup, CommandInput, CommandItem, + CommandList, // Import CommandList for scrolling } from "@/components/ui/command" import { Popover, @@ -18,20 +19,24 @@ import { PopoverTrigger, } from "@/components/ui/popover" -interface StockItemOption { - value: string; // PocketBase ID of the stock item - label: string; // Item name - quantity: number; // Current quantity +// Define the type for a stock item +type StockItem = { + id: string + name: string + sku: string + costPrice: number } interface StockItemComboboxProps { - stockItems: StockItemOption[]; - value: string; - onChange: (value: string) => void; + stockItems: StockItem[]; + selectedItemName: string; + onSelectItem: (itemName: string) => void; + placeholder: string; } -export function StockItemCombobox({ stockItems, value, onChange }: StockItemComboboxProps) { +export function StockItemCombobox({ stockItems, selectedItemName, onSelectItem, placeholder }: StockItemComboboxProps) { const [open, setOpen] = React.useState(false) + const [search, setSearch] = React.useState("") return ( @@ -42,36 +47,48 @@ export function StockItemCombobox({ stockItems, value, onChange }: StockItemComb aria-expanded={open} className="w-full justify-between" > - {value - ? stockItems.find((item) => item.value === value)?.label - : "Select an item..."} + {selectedItemName ? stockItems.find(item => item.name === selectedItemName)?.name : placeholder} - + {/* Adjust width as needed */} - - No item found. - - {stockItems.map((item) => ( - { - onChange(item.value === value ? "" : item.value); - setOpen(false); - }} - > - - {item.label} (Qty: {item.quantity}) - - ))} - + + {/* Use CommandList for scrolling */} + No items found. + + {stockItems + .filter(item => + item.name.toLowerCase().includes(search.toLowerCase()) || + item.sku.toLowerCase().includes(search.toLowerCase()) + ) + .map((item) => ( + { + onSelectItem(value); + setOpen(false); + }} + > + +
+
{item.name}
+
{item.sku}
+
+
+ ))} +
+
diff --git a/src/components/purchase-orders/PurchaseOrderForm.tsx b/src/components/purchase-orders/PurchaseOrderForm.tsx index 7588ad0..de20a67 100644 --- a/src/components/purchase-orders/PurchaseOrderForm.tsx +++ b/src/components/purchase-orders/PurchaseOrderForm.tsx @@ -25,6 +25,7 @@ import { } from "@/components/ui/select" // Import Select components import { showSuccess, showError } from "@/utils/toast" import pb from "@/lib/pocketbase" +import { StockItemCombobox } from "@/components/book-out/StockItemCombobox" // Import the new Combobox component // Define the schema for a single item in the purchase order const itemSchema = z.object({ @@ -67,7 +68,6 @@ type StockItem = { export function PurchaseOrderForm({ setOpen, refreshData }: PurchaseOrderFormProps) { const [suppliers, setSuppliers] = useState([]) // State to hold suppliers const [stockItems, setStockItems] = useState([]) // State to hold stock items - const [searchTerm, setSearchTerm] = useState(""); // State for search term // Fetch suppliers when the component mounts const fetchSuppliers = useCallback(async () => { @@ -213,12 +213,6 @@ export function PurchaseOrderForm({ setOpen, refreshData }: PurchaseOrderFormPro } } - // Filter stock items based on search term - const filteredStockItems = stockItems.filter(item => - item.name.toLowerCase().includes(searchTerm.toLowerCase()) || - item.sku.toLowerCase().includes(searchTerm.toLowerCase()) - ); - return (
@@ -279,36 +273,12 @@ export function PurchaseOrderForm({ setOpen, refreshData }: PurchaseOrderFormPro render={({ field }) => ( Item Name - setSearchTerm(e.target.value)} - className="h-8" - /> - - {filteredStockItems.length > 0 ? ( - filteredStockItems.map((stockItem) => ( - - {stockItem.name} - - )) - ) : ( - No items found - )} - - + handleStockItemSelect(index, selectedItemName)} + placeholder="Search items..." + /> )}