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.
+