From 6f12859c7193eb27cfb8a7779098057be84ef24c Mon Sep 17 00:00:00 2001 From: davidels1 <87242587+davidels1@users.noreply.github.com> Date: Mon, 24 Nov 2025 11:33:12 +0200 Subject: [PATCH] Refactor item selection to use stock item IDs Updated BookOutForm, StockItemCombobox, PurchaseOrderForm, and StockItemSearch to use stock item IDs instead of names for selection and form state management. Improved search robustness and matching logic, normalized cost price values, and enhanced field array updates for better React Hook Form integration. Added a dialog description to the purchase order modal for improved user guidance. --- src/SCRATCH | 0 src/components/book-out/BookOutForm.tsx | 63 ++++++++++--------- src/components/book-out/StockItemCombobox.tsx | 46 +++++++++----- .../purchase-orders/PurchaseOrderForm.tsx | 24 +++---- .../purchase-orders/StockItemSearch.tsx | 8 +-- src/pages/PurchaseOrders.tsx | 3 + 6 files changed, 85 insertions(+), 59 deletions(-) create mode 100644 src/SCRATCH 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. +