diff --git a/src/SCRATCH b/src/SCRATCH new file mode 100644 index 0000000..e69de29 diff --git a/src/components/book-out/BookOutForm.tsx b/src/components/book-out/BookOutForm.tsx index 4295611..7450726 100644 --- a/src/components/book-out/BookOutForm.tsx +++ b/src/components/book-out/BookOutForm.tsx @@ -2,7 +2,7 @@ import * as React from "react" import { zodResolver } from "@hookform/resolvers/zod" -import { useForm } from "react-hook-form" +import { useForm, useFieldArray } from "react-hook-form" import * as z from "zod" import { useState, useEffect, useCallback } from "react" @@ -114,45 +114,47 @@ export function BookOutForm({ setOpen, refreshData }: BookOutFormProps) { }, }) + const { control } = form + const { fields, append, remove, update } = useFieldArray({ + control, + name: "items", + }) + const addItem = () => { - const currentItems = form.getValues("items"); - form.setValue("items", [...currentItems, { + append({ stockItemId: "", itemName: "", quantity: 1, sku: "", - }]); + }) } const removeItem = (index: number) => { - const currentItems = form.getValues("items"); - const newItems = currentItems.filter((_, i) => i !== index); - form.setValue("items", newItems); + remove(index) } const handleItemChange = (index: number, field: keyof BookOutItemSchemaType, value: any) => { - const currentItems = form.getValues("items"); - const newItems = [...currentItems]; - const currentItem = newItems[index] as BookOutItemSchemaType; - + const currentItem = form.getValues("items")[index] as BookOutItemSchemaType + const nextItem = { ...currentItem } if (field === "quantity") { - currentItem[field] = Number(value) || 0; + nextItem[field] = Number(value) || 0 } else { - currentItem[field] = value; + ;(nextItem as any)[field] = value } - newItems[index] = currentItem; - form.setValue("items", newItems); + update(index, nextItem) } - const handleStockItemSelect = (index: number, selectedItemName: string) => { - const selectedStockItem = stockItems.find(item => item.name === selectedItemName); + const handleStockItemSelect = (index: number, selectedItemId: string) => { + const selectedStockItem = stockItems.find(item => item.id === selectedItemId) if (selectedStockItem) { - handleItemChange(index, "stockItemId", selectedStockItem.id); - handleItemChange(index, "itemName", selectedStockItem.name); - handleItemChange(index, "sku", selectedStockItem.sku); - if (form.getValues("items")[index].quantity === 0) { - handleItemChange(index, "quantity", 1); + const currentItem = form.getValues("items")[index] as BookOutItemSchemaType + const nextItem: BookOutItemSchemaType = { + ...currentItem, + itemName: selectedStockItem.name, + sku: selectedStockItem.sku, + quantity: currentItem.quantity && currentItem.quantity > 0 ? currentItem.quantity : 1, } + update(index, nextItem) } } @@ -229,19 +231,22 @@ export function BookOutForm({ setOpen, refreshData }: BookOutFormProps) {

Items to Book Out

- {form.getValues("items").map((item, index) => ( -
+ {fields.map((item, index) => ( +
( - Item Name + Item handleStockItemSelect(index, selectedItemName)} - placeholder="Search items..." + selectedItemId={field.value} + onSelectItem={(selectedItemId) => { + field.onChange(selectedItemId); + handleStockItemSelect(index, selectedItemId); + }} + placeholder="Search by name or SKU..." /> diff --git a/src/components/book-out/StockItemCombobox.tsx b/src/components/book-out/StockItemCombobox.tsx index 4a5bbe8..b935891 100644 --- a/src/components/book-out/StockItemCombobox.tsx +++ b/src/components/book-out/StockItemCombobox.tsx @@ -29,15 +29,38 @@ type StockItem = { interface StockItemComboboxProps { stockItems: StockItem[]; - selectedItemName: string; - onSelectItem: (itemName: string) => void; + selectedItemId: string; + onSelectItem: (itemId: string) => void; placeholder: string; } -export function StockItemCombobox({ stockItems, selectedItemName, onSelectItem, placeholder }: StockItemComboboxProps) { +export function StockItemCombobox({ stockItems, selectedItemId, onSelectItem, placeholder }: StockItemComboboxProps) { const [open, setOpen] = React.useState(false) const [search, setSearch] = React.useState("") + // Normalize strings for more robust matching (ignore case and non-alphanumerics) + const normalize = (s: string) => s.toLowerCase().replace(/[^a-z0-9]/g, "") + const query = normalize(search) + const filteredItems = React.useMemo(() => { + if (!query) return stockItems + return stockItems + .map((item) => { + const nameNorm = normalize(item.name) + const skuNorm = normalize(item.sku) + return { item, nameNorm, skuNorm } + }) + .filter(({ nameNorm, skuNorm }) => nameNorm.includes(query) || skuNorm.includes(query)) + .sort((a, b) => { + const aSkuMatch = a.skuNorm.includes(query) ? 1 : 0 + const bSkuMatch = b.skuNorm.includes(query) ? 1 : 0 + if (aSkuMatch !== bSkuMatch) return bSkuMatch - aSkuMatch // prioritize SKU matches + const aPos = a.nameNorm.indexOf(query) + const bPos = b.nameNorm.indexOf(query) + return aPos - bPos // earlier match in name comes first + }) + .map(({ item }) => item) + }, [stockItems, query]) + return ( @@ -47,7 +70,7 @@ export function StockItemCombobox({ stockItems, selectedItemName, onSelectItem, aria-expanded={open} className="w-full justify-between" > - {selectedItemName ? stockItems.find(item => item.name === selectedItemName)?.name : placeholder} + {selectedItemId ? stockItems.find(item => item.id === selectedItemId)?.name : placeholder} @@ -61,24 +84,19 @@ export function StockItemCombobox({ stockItems, selectedItemName, onSelectItem, {/* Use CommandList for scrolling */} No items found. - {stockItems - .filter(item => - item.name.toLowerCase().includes(search.toLowerCase()) || - item.sku.toLowerCase().includes(search.toLowerCase()) - ) - .map((item) => ( + {filteredItems.map((item) => ( { - onSelectItem(value); + value={`${item.name} ${item.sku}`} + onSelect={() => { + onSelectItem(item.id); setOpen(false); }} >
diff --git a/src/components/purchase-orders/PurchaseOrderForm.tsx b/src/components/purchase-orders/PurchaseOrderForm.tsx index 965e4a2..290988c 100644 --- a/src/components/purchase-orders/PurchaseOrderForm.tsx +++ b/src/components/purchase-orders/PurchaseOrderForm.tsx @@ -132,8 +132,9 @@ export function PurchaseOrderForm({ setOpen, refreshData }: PurchaseOrderFormPro id: item.id, name: item.name, sku: item.sku, - costPrice: item.cost_price, // ZAR cost price - costPriceGBP: item.cost_price_gbp || 0, // GBP cost price + // Normalize costs to numbers to avoid UI rendering errors + costPrice: Number(item.cost_price ?? 0), // ZAR cost price + costPriceGBP: Number(item.cost_price_gbp ?? 0), // GBP cost price })); setStockItems(formattedStockItems); } catch (error) { @@ -174,7 +175,7 @@ export function PurchaseOrderForm({ setOpen, refreshData }: PurchaseOrderFormPro } }, [orderNumber, form]); - const { fields, append, remove } = useFieldArray({ + const { fields, append, remove, update } = useFieldArray({ control: form.control, name: "items", }); @@ -198,9 +199,7 @@ export function PurchaseOrderForm({ setOpen, refreshData }: PurchaseOrderFormPro // 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; + const currentItem = form.getValues(`items.${index}`) as ItemSchemaType; if (field === "quantity") { currentItem[field] = parseInt(value, 10) || 0; @@ -209,13 +208,14 @@ export function PurchaseOrderForm({ setOpen, refreshData }: PurchaseOrderFormPro } else { currentItem[field] = value; } - newItems[index] = currentItem; - form.setValue("items", newItems); + + // Use useFieldArray.update to keep internal ids in sync + update(index, currentItem); } - // Function to handle stock item selection from dropdown - const handleStockItemSelect = (index: number, selectedItemName: string) => { - const selectedStockItem = stockItems.find(item => item.name === selectedItemName); + // Function to handle stock item selection by ID (robust against duplicate/special names) + const handleStockItemSelect = (index: number, selectedItemId: string) => { + const selectedStockItem = stockItems.find(item => item.id === selectedItemId); if (selectedStockItem) { // Update the form fields with the selected stock item's details handleItemChange(index, "stockItemId", selectedStockItem.id); @@ -393,7 +393,7 @@ export function PurchaseOrderForm({ setOpen, refreshData }: PurchaseOrderFormPro handleStockItemSelect(index, selectedItemName)} + onSelectItem={(selectedItemId) => handleStockItemSelect(index, selectedItemId)} placeholder="Search by SKU or name..." /> diff --git a/src/components/purchase-orders/StockItemSearch.tsx b/src/components/purchase-orders/StockItemSearch.tsx index de75bae..1f73fa2 100644 --- a/src/components/purchase-orders/StockItemSearch.tsx +++ b/src/components/purchase-orders/StockItemSearch.tsx @@ -31,7 +31,7 @@ type StockItem = { interface StockItemSearchProps { stockItems: StockItem[]; selectedItemName: string; - onSelectItem: (itemName: string) => void; + onSelectItem: (itemId: string) => void; placeholder: string; } @@ -80,8 +80,8 @@ export function StockItemSearch({ stockItems, selectedItemName, onSelectItem, pl { - onSelectItem(value); + onSelect={() => { + onSelectItem(item.id); setOpen(false); }} > @@ -94,7 +94,7 @@ export function StockItemSearch({ stockItems, selectedItemName, onSelectItem, pl
{item.name}
- SKU: {item.sku} | GBP: £{item.costPriceGBP.toFixed(2)} | ZAR: R{item.costPrice.toFixed(2)} + SKU: {item.sku} | GBP: £{Number(item.costPriceGBP ?? 0).toFixed(2)} | ZAR: R{Number(item.costPrice ?? 0).toFixed(2)}
diff --git a/src/pages/PurchaseOrders.tsx b/src/pages/PurchaseOrders.tsx index a905e53..72d477d 100644 --- a/src/pages/PurchaseOrders.tsx +++ b/src/pages/PurchaseOrders.tsx @@ -103,6 +103,9 @@ const PurchaseOrders = () => { New Purchase Order + + Select a supplier and add one or more items to create a purchase order. +