From f9a093a39dc492a41c4d7c1540af2fddcd6cf7dc Mon Sep 17 00:00:00 2001 From: beatzaplenty Date: Tue, 25 Nov 2025 11:00:12 +1000 Subject: [PATCH] modified: public/templates/products_template.csv modified: src/components/ProductRow.test.tsx modified: src/components/ProductRow.tsx modified: src/screens/ImportExportScreen.tsx modified: src/screens/ManageProductsScreen.tsx modified: src/services/importExportService.ts --- public/templates/products_template.csv | 6 +- src/components/ProductRow.test.tsx | 23 +- src/components/ProductRow.tsx | 39 ++- src/screens/ImportExportScreen.tsx | 88 ++----- src/screens/ManageProductsScreen.tsx | 186 ++++++-------- src/services/importExportService.ts | 330 ++++++++----------------- 6 files changed, 224 insertions(+), 448 deletions(-) diff --git a/public/templates/products_template.csv b/public/templates/products_template.csv index 4a088f7..dfa901d 100644 --- a/public/templates/products_template.csv +++ b/public/templates/products_template.csv @@ -1,2 +1,4 @@ -id,name,category,unit_type,bulk_name,barcode,archived,created_at,updated_at -"Blue T-Shirt","Clothing","unit","carton","0123456789012",false,, +name,barcode,category +Running Shorts,0123456789,Clothing +Crop Top,9876543210,Clothing +Yoga Pants,,Bottoms diff --git a/src/components/ProductRow.test.tsx b/src/components/ProductRow.test.tsx index 1d87807..fb5b482 100644 --- a/src/components/ProductRow.test.tsx +++ b/src/components/ProductRow.test.tsx @@ -17,12 +17,21 @@ const product: Product = { const categories = ['Drinks', 'Snacks']; +// Provide a categoriesById Map for the new required prop. +// The test product uses category 'Drinks' (a name), so mapping name->name is fine. +// If you had an ID in the product, this map could be id->name. +const categoriesById = new Map([ + ['Drinks', 'Drinks'], + ['Snacks', 'Snacks'], +]); + describe('ProductRow', () => { it('shows name, category, and barcode without unit text in read-only mode', () => { render( , @@ -39,7 +48,7 @@ describe('ProductRow', () => { const onSave = vi.fn(); render( - , + , ); await user.click(screen.getByLabelText(/edit sparkling water/i)); @@ -59,12 +68,12 @@ describe('ProductRow', () => { it('surfaces validation errors from duplicate constraints', async () => { const user = userEvent.setup(); - const onSave = vi.fn().mockRejectedValueOnce(Object.assign(new Error('dup'), { name: 'DuplicateNameError' })) + const onSave = vi + .fn() + .mockRejectedValueOnce(Object.assign(new Error('dup'), { name: 'DuplicateNameError' })) .mockRejectedValueOnce(Object.assign(new Error('dup'), { name: 'DuplicateBarcodeError' })); - render( - , - ); + render(); await user.click(screen.getByLabelText(/edit sparkling water/i)); await user.clear(screen.getByLabelText(/name/i)); @@ -82,9 +91,7 @@ describe('ProductRow', () => { it('allows clearing and scanning a new barcode', async () => { const user = userEvent.setup(); - render( - , - ); + render(); await user.click(screen.getByLabelText(/edit sparkling water/i)); await user.click(screen.getByRole('button', { name: /clear/i })); diff --git a/src/components/ProductRow.tsx b/src/components/ProductRow.tsx index 1d300be..45283e1 100644 --- a/src/components/ProductRow.tsx +++ b/src/components/ProductRow.tsx @@ -21,12 +21,15 @@ import { BarcodeScannerView } from './BarcodeScannerView'; interface ProductRowProps { product: Product; + // list of category display names for the select categories: string[]; + // map of category id -> category name, used to resolve ids to names + categoriesById: Map; onSave: ( productId: string, updates: { name: string; - category: string; + category: string; // this is the *name* when passed back to parent barcode?: string; }, ) => Promise | void; @@ -39,22 +42,23 @@ interface ProductFormState { barcode: string; } -const getInitialFormState = (product: Product): ProductFormState => ({ +const getInitialFormState = (product: Product, categoriesById: Map): ProductFormState => ({ name: product.name, - category: product.category, + // If product.category is an id, resolve to name; otherwise assume it is already a name + category: categoriesById.get(product.category) ?? product.category ?? '', barcode: product.barcode ?? '', }); -export const ProductRow = ({ product, categories, onSave, onDelete }: ProductRowProps) => { +export const ProductRow = ({ product, categories, categoriesById, onSave, onDelete }: ProductRowProps) => { const [isEditing, setIsEditing] = useState(false); - const [formState, setFormState] = useState(() => getInitialFormState(product)); + const [formState, setFormState] = useState(() => getInitialFormState(product, categoriesById)); const [isScannerOpen, setIsScannerOpen] = useState(false); const [fieldErrors, setFieldErrors] = useState<{ name?: string; barcode?: string }>({}); useEffect(() => { - setFormState(getInitialFormState(product)); + setFormState(getInitialFormState(product, categoriesById)); setFieldErrors({}); - }, [product]); + }, [product, categoriesById]); const handleChange = (field: keyof ProductFormState) => (event: ChangeEvent) => { setFormState((prev) => ({ ...prev, [field]: event.target.value })); @@ -86,7 +90,7 @@ export const ProductRow = ({ product, categories, onSave, onDelete }: ProductRow const handleCancel = () => { setIsEditing(false); - setFormState(getInitialFormState(product)); + setFormState(getInitialFormState(product, categoriesById)); setFieldErrors({}); }; @@ -103,14 +107,7 @@ export const ProductRow = ({ product, categories, onSave, onDelete }: ProductRow error={Boolean(fieldErrors.name)} helperText={fieldErrors.name || undefined} /> - + {categories.map((cat) => (