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