diff --git a/src/components/book-out/BookOutForm.tsx b/src/components/book-out/BookOutForm.tsx index e414b5d..7b4fce3 100644 --- a/src/components/book-out/BookOutForm.tsx +++ b/src/components/book-out/BookOutForm.tsx @@ -4,7 +4,6 @@ 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 { @@ -16,247 +15,160 @@ 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 -// 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." }), +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." }), }) interface BookOutFormProps { - 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 + setOpen: (open: boolean) => void; + refreshData: () => void; // Added refreshData prop } export function BookOutForm({ setOpen, refreshData }: BookOutFormProps) { - const [stockItems, setStockItems] = useState([]) // State to hold stock items + const [customerOptions, setCustomerOptions] = React.useState<{ value: string; label: string }[]>([]); + const [stockItemOptions, setStockItemOptions] = React.useState<{ value: string; label: string; quantity: number }[]>([]); - // Fetch stock items when the component mounts - const fetchStockItems = useCallback(async () => { + 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 () => { try { const records = await pb.collection('stock_items').getList(1, 50, { - sort: '-created', + filter: 'quantity > 0', // Only show items with quantity > 0 }); - // 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 + const options = records.items.map((item: any) => ({ + value: item.id, + label: item.name, + quantity: item.quantity, })); - setStockItems(formattedStockItems); + setStockItemOptions(options); } catch (error) { - console.error("Error fetching stock items:", error); + console.error("Error fetching stock items for combobox:", error); showError("Failed to load stock items."); } }, []); - useEffect(() => { + React.useEffect(() => { + fetchCustomers(); fetchStockItems(); - }, [fetchStockItems]); + }, [fetchCustomers, fetchStockItems]); - const form = useForm>({ - resolver: zodResolver(bookOutFormSchema), + const form = useForm>({ + resolver: zodResolver(formSchema), defaultValues: { - bookOutDate: new Date(), - items: [{ - stockItemId: "", - itemName: "", - quantity: 1, - sku: "", - }], + quantity: 1, }, }) - // Function to add a new item row - const addItem = () => { - const currentItems = form.getValues("items"); - form.setValue("items", [...currentItems, { - stockItemId: "", - itemName: "", - quantity: 1, - sku: "", - }]); - } - - // 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); - } - } - } - - async function onSubmit(values: z.infer) { + 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, - })), - }; + // Fetch the latest stock item data to prevent race conditions + const selectedStockItem = await pb.collection('stock_items').getOne(values.stockItemId); - await pb.collection('book_out_records').create(bookOutData); - showSuccess("Stock booked out successfully!"); + if (!selectedStockItem) { + showError("Selected item not found."); + return; + } + + 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); - refreshData(); + 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 stock:", error); - showError("Failed to book out stock. Please check the form and try again."); + console.error("Error booking out item:", error); + showError("Failed to book out item. Please 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 (
( - Book Out Date + Customer + + + + )} + /> + ( + + Item + + + + )} + /> + ( + + Quantity - 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 46715f2..597329c 100644 --- a/src/components/book-out/StockItemCombobox.tsx +++ b/src/components/book-out/StockItemCombobox.tsx @@ -11,7 +11,6 @@ import { CommandGroup, CommandInput, CommandItem, - CommandList, // Import CommandList for scrolling } from "@/components/ui/command" import { Popover, @@ -19,24 +18,20 @@ import { PopoverTrigger, } from "@/components/ui/popover" -// Define the type for a stock item -type StockItem = { - id: string - name: string - sku: string - costPrice: number +interface StockItemOption { + value: string; // PocketBase ID of the stock item + label: string; // Item name + quantity: number; // Current quantity } interface StockItemComboboxProps { - stockItems: StockItem[]; - selectedItemName: string; - onSelectItem: (itemName: string) => void; - placeholder: string; + stockItems: StockItemOption[]; + value: string; + onChange: (value: string) => void; } -export function StockItemCombobox({ stockItems, selectedItemName, onSelectItem, placeholder }: StockItemComboboxProps) { +export function StockItemCombobox({ stockItems, value, onChange }: StockItemComboboxProps) { const [open, setOpen] = React.useState(false) - const [search, setSearch] = React.useState("") return ( @@ -47,48 +42,36 @@ export function StockItemCombobox({ stockItems, selectedItemName, onSelectItem, aria-expanded={open} className="w-full justify-between" > - {selectedItemName ? stockItems.find(item => item.name === selectedItemName)?.name : placeholder} + {value + ? stockItems.find((item) => item.value === value)?.label + : "Select an item..."} - {/* Adjust width as needed */} + - - {/* 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}
-
-
- ))} -
-
+ + No item found. + + {stockItems.map((item) => ( + { + onChange(item.value === value ? "" : item.value); + setOpen(false); + }} + > + + {item.label} (Qty: {item.quantity}) + + ))} +
diff --git a/src/components/purchase-orders/PurchaseOrderForm.tsx b/src/components/purchase-orders/PurchaseOrderForm.tsx index de20a67..b42a57a 100644 --- a/src/components/purchase-orders/PurchaseOrderForm.tsx +++ b/src/components/purchase-orders/PurchaseOrderForm.tsx @@ -25,7 +25,6 @@ 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({ @@ -192,6 +191,8 @@ export function PurchaseOrderForm({ setOpen, refreshData }: PurchaseOrderFormPro supplier: selectedSupplier.id, // This will be the relation to the suppliers collection orderDate: values.orderDate.toISOString(), status: "Pending Approval", + // Ensure the structure matches what your PocketBase 'purchase_orders' collection expects for 'items' + // Based on your schema, it seems 'embedded_items' is used for storing item details directly. embedded_items: values.items.map(item => ({ stockItemId: item.stockItemId, itemName: item.itemName, @@ -199,7 +200,7 @@ export function PurchaseOrderForm({ setOpen, refreshData }: PurchaseOrderFormPro sku: item.sku, costPrice: item.costPrice, })), - quantities: values.items.map(item => item.quantity), + quantities: values.items.map(item => item.quantity), // Keep quantities array as well if needed elsewhere }; await pb.collection('purchase_orders').create(purchaseOrderData); @@ -273,12 +274,23 @@ export function PurchaseOrderForm({ setOpen, refreshData }: PurchaseOrderFormPro render={({ field }) => ( Item Name - handleStockItemSelect(index, selectedItemName)} - placeholder="Search items..." - /> + )} @@ -290,6 +302,7 @@ export function PurchaseOrderForm({ setOpen, refreshData }: PurchaseOrderFormPro SKU + {/* Display SKU, but it's managed by handleStockItemSelect */}