diff --git a/src/components/book-out/BookOutForm.tsx b/src/components/book-out/BookOutForm.tsx index 5479a7f..7b4fce3 100644 --- a/src/components/book-out/BookOutForm.tsx +++ b/src/components/book-out/BookOutForm.tsx @@ -4,8 +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" -// Removed unused import: import { useRouter } from "next/navigation" import { Button } from "@/components/ui/button" import { @@ -17,190 +15,110 @@ import { FormMessage, } from "@/components/ui/form" import { Input } from "@/components/ui/input" -import { - Command, - CommandEmpty, - CommandGroup, - CommandInput, - CommandItem, - CommandList, -} from "@/components/ui/command" -import { - Popover, - PopoverContent, - PopoverTrigger, -} from "@/components/ui/popover" -import { - Dialog, - DialogContent, - DialogDescription, - DialogHeader, - DialogTitle, - DialogTrigger, -} from "@/components/ui/dialog" -import { toast } from "@/components/ui/use-toast" // Assuming you use shadcn/ui toast +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/shared/StockItemCombobox" // Corrected import path -// 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(), - costPrice: z.number(), // Added costPrice to the schema -}) - -// 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 + 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('stock_items').getList(1, 50, { - sort: '-created', - }); - 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 records = await pb.collection('customers').getList(1, 50); + const options = records.items.map((customer: any) => ({ + value: customer.id, + label: customer.comapny_name, })); - setStockItems(formattedStockItems); + setCustomerOptions(options); } catch (error) { - console.error("Error fetching stock items:", error); - toast({ - title: "Error", - description: "Failed to load stock items.", - variant: "destructive", - }); + console.error("Error fetching customers for combobox:", error); + showError("Failed to load customers."); } }, []); - useEffect(() => { - fetchStockItems(); - }, [fetchStockItems]); + const fetchStockItems = React.useCallback(async () => { + try { + const records = await pb.collection('stock_items').getList(1, 50, { + filter: 'quantity > 0', // Only show items with quantity > 0 + }); + const options = records.items.map((item: any) => ({ + value: item.id, + label: item.name, + quantity: item.quantity, + })); + setStockItemOptions(options); + } catch (error) { + console.error("Error fetching stock items for combobox:", error); + showError("Failed to load stock items."); + } + }, []); - const form = useForm>({ - resolver: zodResolver(bookOutFormSchema), + React.useEffect(() => { + fetchCustomers(); + fetchStockItems(); + }, [fetchCustomers, fetchStockItems]); + + const form = useForm>({ + resolver: zodResolver(formSchema), defaultValues: { - bookOutDate: new Date(), - items: [{ - stockItemId: "", - itemName: "", - quantity: 1, - sku: "", - costPrice: 0, // Included costPrice in default values - }], + 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: "", - costPrice: 0, // Included costPrice in new item - }]); - } - - // 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 if (field === "costPrice") { // Handle costPrice change - currentItem[field] = parseFloat(value) || 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); - handleItemChange(index, "costPrice", selectedStockItem.costPrice); // Set costPrice - // 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 details for the 'items' field - items: values.items.map(item => ({ - stockItemId: item.stockItemId, - itemName: item.itemName, - quantity: item.quantity, - sku: item.sku, - // costPrice is not explicitly stored in the 'book_outs' items array in PocketBase schema based on previous context, - // but if it were, it would be included here. - })), - }; + // Fetch the latest stock item data to prevent race conditions + const selectedStockItem = await pb.collection('stock_items').getOne(values.stockItemId); - await pb.collection('book_outs').create(bookOutData); - toast({ - title: "Success", - description: "Stock item 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 item:", error); - toast({ - title: "Error", - description: "Failed to book out stock item. Please check the form and try again.", - variant: "destructive", - }); + console.error("Error booking out item:", error); + showError("Failed to book out item. Please try again."); } } @@ -209,86 +127,48 @@ export function BookOutForm({ setOpen, refreshData }: BookOutFormProps) {
( - 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 */} -
-

Book-Out Items

- {form.getValues("items").map((item, index) => ( -
- ( - - Item Name - handleStockItemSelect(index, selectedName)} - placeholder="Search item..." - className="w-full" - /> - - - )} - /> - ( - - SKU - - - - - - )} - /> - ( - - Quantity - - handleItemChange(index, "quantity", e.target.value)} /> - - - - )} - /> -
{/* Adjusted for alignment */} - {form.getValues("items").length > 1 && ( - - )} -
-
- ))} - -
- - + ) diff --git a/src/components/book-out/StockItemCombobox.tsx b/src/components/book-out/StockItemCombobox.tsx index d93d35a..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, } from "@/components/ui/command" import { Popover, @@ -19,29 +18,19 @@ 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; - className?: string; + stockItems: StockItemOption[]; + value: string; + onChange: (value: string) => void; } -export function StockItemCombobox({ - stockItems, - selectedItemName, - onSelectItem, - placeholder, - className, -}: StockItemComboboxProps) { +export function StockItemCombobox({ stockItems, value, onChange }: StockItemComboboxProps) { const [open, setOpen] = React.useState(false) return ( @@ -51,41 +40,38 @@ export function StockItemCombobox({ variant="outline" role="combobox" aria-expanded={open} - className={cn( - "w-[200px] justify-between", // Adjust width as needed - className - )} + 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 */} + - - - No items found. - - {stockItems.map((item) => ( - { - onSelectItem(value) - setOpen(false) - }} - > - - {item.name} - - ))} - - + + 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 6a78322..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/shared/StockItemCombobox" // Corrected import path // 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,13 +274,23 @@ export function PurchaseOrderForm({ setOpen, refreshData }: PurchaseOrderFormPro render={({ field }) => ( Item Name - handleStockItemSelect(index, selectedName)} - placeholder="Search item..." - className="w-full" // Make combobox take full width of its column - /> + )} diff --git a/src/components/shared/StockItemCombobox.tsx b/src/components/shared/StockItemCombobox.tsx deleted file mode 100644 index d93d35a..0000000 --- a/src/components/shared/StockItemCombobox.tsx +++ /dev/null @@ -1,93 +0,0 @@ -"use client" - -import * as React from "react" -import { Check, ChevronsUpDown } from "lucide-react" - -import { cn } from "@/lib/utils" -import { Button } from "@/components/ui/button" -import { - Command, - CommandEmpty, - CommandGroup, - CommandInput, - CommandItem, - CommandList, -} from "@/components/ui/command" -import { - Popover, - PopoverContent, - PopoverTrigger, -} from "@/components/ui/popover" - -// Define the type for a stock item -type StockItem = { - id: string - name: string - sku: string - costPrice: number -} - -interface StockItemComboboxProps { - stockItems: StockItem[]; - selectedItemName: string; - onSelectItem: (itemName: string) => void; - placeholder: string; - className?: string; -} - -export function StockItemCombobox({ - stockItems, - selectedItemName, - onSelectItem, - placeholder, - className, -}: StockItemComboboxProps) { - const [open, setOpen] = React.useState(false) - - return ( - - - - - {/* Adjust width as needed */} - - - - No items found. - - {stockItems.map((item) => ( - { - onSelectItem(value) - setOpen(false) - }} - > - - {item.name} - - ))} - - - - - - ) -} \ No newline at end of file