From 8782d61718788e3018bac55aad415aa06aa1fc00 Mon Sep 17 00:00:00 2001 From: beatz174-bit Date: Sun, 23 Nov 2025 14:58:07 +1000 Subject: [PATCH 1/4] Sort pick list product search options alphabetically --- src/screens/ActivePickListScreen.test.tsx | 35 +++++++++++++++++++++++ src/screens/ActivePickListScreen.tsx | 14 +++++++-- 2 files changed, 46 insertions(+), 3 deletions(-) diff --git a/src/screens/ActivePickListScreen.test.tsx b/src/screens/ActivePickListScreen.test.tsx index 661e545..9e359ef 100644 --- a/src/screens/ActivePickListScreen.test.tsx +++ b/src/screens/ActivePickListScreen.test.tsx @@ -34,6 +34,17 @@ vi.mock('../hooks/dataHooks', () => ({ created_at: 0, updated_at: 0, }, + { + id: 'prod-3', + name: 'Apple Juice', + category: 'Drinks', + unit_type: 'unit', + bulk_name: 'box', + barcode: '333', + archived: false, + created_at: 0, + updated_at: 0, + }, ], usePickList: () => ({ id: 'list-1', area_id: 'area-1', created_at: 0 }), useAreas: () => [{ id: 'area-1', name: 'Front Counter', created_at: 0, updated_at: 0 }], @@ -100,6 +111,30 @@ describe('ActivePickListScreen product search', () => { }); }); + it('sorts the product options alphabetically', async () => { + const user = userEvent.setup(); + + render( + + + } /> + + , + ); + + const combobox = screen.getByRole('combobox'); + await user.click(combobox); + + const listbox = await screen.findByRole('listbox'); + const options = within(listbox).getAllByRole('option'); + + expect(options.map((option) => option.textContent)).toEqual([ + 'Apple Juice (Drinks)', + 'Chips (Snacks)', + 'Cola (Drinks)', + ]); + }); + it('updates an existing pick item when the same packaging is selected', async () => { pickItemsMock.mockReturnValue([ { diff --git a/src/screens/ActivePickListScreen.tsx b/src/screens/ActivePickListScreen.tsx index 69bacd8..b80cf79 100644 --- a/src/screens/ActivePickListScreen.tsx +++ b/src/screens/ActivePickListScreen.tsx @@ -48,15 +48,23 @@ export const ActivePickListScreen = () => { [areas, pickList?.area_id], ); + const sortedProducts = useMemo( + () => + [...products].sort((a, b) => + a.name.localeCompare(b.name, undefined, { sensitivity: 'base' }), + ), + [products], + ); + const filteredProducts = useMemo(() => { const normalizedQuery = query.trim().toLowerCase(); - if (!normalizedQuery) return products; + if (!normalizedQuery) return sortedProducts; - return products.filter((product) => { + return sortedProducts.filter((product) => { const searchableText = `${product.name} ${product.category} ${product.barcode ?? ''}`.toLowerCase(); return searchableText.includes(normalizedQuery); }); - }, [products, query]); + }, [sortedProducts, query]); useEffect(() => { if (!selectedProduct) return; From 32b2e3eadaab15815d02db79ed000062ef23f71f Mon Sep 17 00:00:00 2001 From: beatz174-bit Date: Sun, 23 Nov 2025 15:00:05 +1000 Subject: [PATCH 2/4] Sort pick lists alphabetically --- src/screens/PickListsScreen.test.tsx | 54 ++++++++++++++++++++++++++++ src/screens/PickListsScreen.tsx | 21 ++++++++--- 2 files changed, 71 insertions(+), 4 deletions(-) create mode 100644 src/screens/PickListsScreen.test.tsx diff --git a/src/screens/PickListsScreen.test.tsx b/src/screens/PickListsScreen.test.tsx new file mode 100644 index 0000000..b253ca8 --- /dev/null +++ b/src/screens/PickListsScreen.test.tsx @@ -0,0 +1,54 @@ +import { MemoryRouter } from 'react-router-dom'; +import { render, screen, within } from '@testing-library/react'; +import { describe, expect, it, vi } from 'vitest'; +import { PickListsScreen } from './PickListsScreen'; + +const areasMock = [ + { id: 'area-1', name: 'Front Counter', created_at: 0, updated_at: 0 }, + { id: 'area-2', name: 'back room', created_at: 0, updated_at: 0 }, + { id: 'area-3', name: 'Cafe', created_at: 0, updated_at: 0 }, +]; + +const pickListsMock = [ + { id: 'list-2', area_id: 'area-2', created_at: 3 }, + { id: 'list-3', area_id: 'area-3', created_at: 4 }, + { id: 'list-1', area_id: 'area-1', created_at: 5 }, +]; + +vi.mock('../hooks/dataHooks', () => ({ + usePickLists: () => pickListsMock, + useAreas: () => areasMock, +})); + +vi.mock('../context/DBProvider', () => ({ + useDatabase: () => ({ + pickItems: { + where: () => ({ + equals: () => ({ delete: vi.fn() }), + }), + }, + pickLists: { + update: vi.fn(), + delete: vi.fn(), + where: () => ({ + equals: () => ({ delete: vi.fn() }), + }), + }, + }), +})); + +describe('PickListsScreen sorting', () => { + it('sorts pick lists alphabetically by area name', () => { + render( + + + , + ); + + const listItems = screen.getAllByRole('listitem'); + + expect(within(listItems[0]).getByText('back room')).toBeVisible(); + expect(within(listItems[1]).getByText('Cafe')).toBeVisible(); + expect(within(listItems[2]).getByText('Front Counter')).toBeVisible(); + }); +}); diff --git a/src/screens/PickListsScreen.tsx b/src/screens/PickListsScreen.tsx index 805c45d..5cad426 100644 --- a/src/screens/PickListsScreen.tsx +++ b/src/screens/PickListsScreen.tsx @@ -31,11 +31,24 @@ export const PickListsScreen = () => { const [areaId, setAreaId] = useState(''); const [notes, setNotes] = useState(''); - const sortedLists = useMemo(() => { - return [...lists].sort((a, b) => a.created_at - b.created_at); - }, [lists]); + const areaNameById = useMemo(() => { + const map = new Map(); + areas.forEach((area) => map.set(area.id, area.name)); + return map; + }, [areas]); - const getAreaName = (areaId: string) => areas.find((a) => a.id === areaId)?.name ?? 'Unknown area'; + const sortedLists = useMemo(() => { + const locale = new Intl.Collator(undefined, { sensitivity: 'base' }); + return [...lists].sort((a, b) => { + const nameA = areaNameById.get(a.area_id) ?? 'Unknown area'; + const nameB = areaNameById.get(b.area_id) ?? 'Unknown area'; + const nameComparison = locale.compare(nameA, nameB); + if (nameComparison !== 0) return nameComparison; + return a.created_at - b.created_at; + }); + }, [areaNameById, lists]); + + const getAreaName = (areaId: string) => areaNameById.get(areaId) ?? 'Unknown area'; const openEdit = (list: PickList) => { setEditingList(list); From cc4f20e1ea2732283a7d9c5f300c99eaf6ae4ea6 Mon Sep 17 00:00:00 2001 From: beatz174-bit Date: Sun, 23 Nov 2025 15:05:47 +1000 Subject: [PATCH 3/4] Deduplicate pick list product options --- src/screens/ActivePickListScreen.test.tsx | 116 +++++++++++++++++----- src/screens/ActivePickListScreen.tsx | 18 +++- 2 files changed, 107 insertions(+), 27 deletions(-) diff --git a/src/screens/ActivePickListScreen.test.tsx b/src/screens/ActivePickListScreen.test.tsx index 661e545..40d6653 100644 --- a/src/screens/ActivePickListScreen.test.tsx +++ b/src/screens/ActivePickListScreen.test.tsx @@ -4,37 +4,52 @@ import userEvent from '@testing-library/user-event'; import { describe, expect, it, vi, beforeEach } from 'vitest'; import { ActivePickListScreen } from './ActivePickListScreen'; import { PickItem } from '../models/PickItem'; +import { Product } from '../models/Product'; const addMock = vi.fn(); const updateMock = vi.fn(); const pickItemsMock = vi.fn(); +const productsMock = vi.fn(); + +const defaultProducts: Product[] = [ + { + id: 'prod-1', + name: 'Cola', + category: 'Drinks', + unit_type: 'unit', + bulk_name: 'box', + barcode: '111', + archived: false, + created_at: 0, + updated_at: 0, + }, + { + id: 'prod-2', + name: 'Chips', + category: 'Snacks', + unit_type: 'unit', + bulk_name: 'box', + barcode: '222', + archived: false, + created_at: 0, + updated_at: 0, + }, + { + id: 'prod-3', + name: 'Apple Juice', + category: 'Drinks', + unit_type: 'unit', + bulk_name: 'box', + barcode: '333', + archived: false, + created_at: 0, + updated_at: 0, + }, +]; vi.mock('../hooks/dataHooks', () => ({ usePickItems: () => pickItemsMock(), - useProducts: () => [ - { - id: 'prod-1', - name: 'Cola', - category: 'Drinks', - unit_type: 'unit', - bulk_name: 'box', - barcode: '111', - archived: false, - created_at: 0, - updated_at: 0, - }, - { - id: 'prod-2', - name: 'Chips', - category: 'Snacks', - unit_type: 'unit', - bulk_name: 'box', - barcode: '222', - archived: false, - created_at: 0, - updated_at: 0, - }, - ], + useProducts: () => productsMock(), usePickList: () => ({ id: 'list-1', area_id: 'area-1', created_at: 0 }), useAreas: () => [{ id: 'area-1', name: 'Front Counter', created_at: 0, updated_at: 0 }], })); @@ -55,6 +70,7 @@ describe('ActivePickListScreen product search', () => { addMock.mockReset(); updateMock.mockReset(); pickItemsMock.mockReturnValue([]); + productsMock.mockReturnValue(defaultProducts); }); it('filters the product list based on the search query', async () => { @@ -100,6 +116,58 @@ describe('ActivePickListScreen product search', () => { }); }); + it('sorts the product options alphabetically', async () => { + const user = userEvent.setup(); + + render( + + + } /> + + , + ); + + const combobox = screen.getByRole('combobox'); + await user.click(combobox); + + const listbox = await screen.findByRole('listbox'); + const options = within(listbox).getAllByRole('option'); + + expect(options.map((option) => option.textContent)).toEqual([ + 'Apple Juice (Drinks)', + 'Chips (Snacks)', + 'Cola (Drinks)', + ]); + }); + + it('deduplicates product options with the same id', async () => { + const duplicateProducts = [...defaultProducts, { ...defaultProducts[0] }]; + productsMock.mockReturnValue(duplicateProducts); + + const user = userEvent.setup(); + + render( + + + } /> + + , + ); + + const combobox = screen.getByRole('combobox'); + await user.click(combobox); + + const listbox = await screen.findByRole('listbox'); + const options = within(listbox).getAllByRole('option'); + + expect(options).toHaveLength(3); + expect(options.map((option) => option.textContent)).toEqual([ + 'Apple Juice (Drinks)', + 'Chips (Snacks)', + 'Cola (Drinks)', + ]); + }); + it('updates an existing pick item when the same packaging is selected', async () => { pickItemsMock.mockReturnValue([ { diff --git a/src/screens/ActivePickListScreen.tsx b/src/screens/ActivePickListScreen.tsx index 69bacd8..61b827a 100644 --- a/src/screens/ActivePickListScreen.tsx +++ b/src/screens/ActivePickListScreen.tsx @@ -48,15 +48,27 @@ export const ActivePickListScreen = () => { [areas, pickList?.area_id], ); + const sortedProducts = useMemo(() => { + const uniqueProducts = new Map(); + + products.forEach((product) => { + uniqueProducts.set(product.id, product); + }); + + return Array.from(uniqueProducts.values()).sort((a, b) => + a.name.localeCompare(b.name, undefined, { sensitivity: 'base' }), + ); + }, [products]); + const filteredProducts = useMemo(() => { const normalizedQuery = query.trim().toLowerCase(); - if (!normalizedQuery) return products; + if (!normalizedQuery) return sortedProducts; - return products.filter((product) => { + return sortedProducts.filter((product) => { const searchableText = `${product.name} ${product.category} ${product.barcode ?? ''}`.toLowerCase(); return searchableText.includes(normalizedQuery); }); - }, [products, query]); + }, [sortedProducts, query]); useEffect(() => { if (!selectedProduct) return; From 666048a10baa8d843b602a3185a620b2188dbc9f Mon Sep 17 00:00:00 2001 From: beatz174-bit Date: Sun, 23 Nov 2025 15:06:19 +1000 Subject: [PATCH 4/4] Fix pick list item sorting --- src/screens/ActivePickListScreen.test.tsx | 59 +++++++++++++++++++++++ src/screens/ActivePickListScreen.tsx | 28 ++++++++++- 2 files changed, 85 insertions(+), 2 deletions(-) diff --git a/src/screens/ActivePickListScreen.test.tsx b/src/screens/ActivePickListScreen.test.tsx index 9e359ef..ee65dd2 100644 --- a/src/screens/ActivePickListScreen.test.tsx +++ b/src/screens/ActivePickListScreen.test.tsx @@ -169,6 +169,65 @@ describe('ActivePickListScreen product search', () => { expect(addMock).not.toHaveBeenCalled(); }); + it('sorts pick list items by product name and packaging', () => { + pickItemsMock.mockReturnValue([ + { + id: 'item-1', + pick_list_id: 'list-1', + product_id: 'prod-1', + quantity: 2, + is_carton: false, + status: 'pending', + created_at: 0, + updated_at: 0, + }, + { + id: 'item-2', + pick_list_id: 'list-1', + product_id: 'prod-3', + quantity: 1, + is_carton: true, + status: 'pending', + created_at: 0, + updated_at: 0, + }, + { + id: 'item-3', + pick_list_id: 'list-1', + product_id: 'prod-2', + quantity: 1, + is_carton: false, + status: 'pending', + created_at: 0, + updated_at: 0, + }, + { + id: 'item-4', + pick_list_id: 'list-1', + product_id: 'prod-1', + quantity: 1, + is_carton: true, + status: 'pending', + created_at: 0, + updated_at: 0, + }, + ]); + + render( + + + } /> + + , + ); + + const itemLabels = screen + .getAllByText(/Apple Juice|Chips|Cola/) + .map((element) => element.textContent); + + expect(itemLabels).toEqual(['Apple Juice', 'Chips', 'Cola', 'Cola']); + }); + it('hides picked items when show picked is unchecked', async () => { pickItemsMock.mockReturnValue([ { diff --git a/src/screens/ActivePickListScreen.tsx b/src/screens/ActivePickListScreen.tsx index b80cf79..6c243ce 100644 --- a/src/screens/ActivePickListScreen.tsx +++ b/src/screens/ActivePickListScreen.tsx @@ -48,6 +48,11 @@ export const ActivePickListScreen = () => { [areas, pickList?.area_id], ); + const productMap = useMemo( + () => new Map(products.map((product) => [product.id, product])), + [products], + ); + const sortedProducts = useMemo( () => [...products].sort((a, b) => @@ -56,6 +61,23 @@ export const ActivePickListScreen = () => { [products], ); + const sortedItems = useMemo(() => { + return [...items].sort((a, b) => { + const productA = productMap.get(a.product_id); + const productB = productMap.get(b.product_id); + + const nameA = productA?.name.toLowerCase() ?? ''; + const nameB = productB?.name.toLowerCase() ?? ''; + + const nameComparison = nameA.localeCompare(nameB); + if (nameComparison !== 0) return nameComparison; + + if (a.is_carton !== b.is_carton) return Number(a.is_carton) - Number(b.is_carton); + + return (a.created_at ?? 0) - (b.created_at ?? 0); + }); + }, [items, productMap]); + const filteredProducts = useMemo(() => { const normalizedQuery = query.trim().toLowerCase(); if (!normalizedQuery) return sortedProducts; @@ -80,7 +102,9 @@ export const ActivePickListScreen = () => { }, [allItemsPicked, showPicked]); const visibleItems = useMemo(() => { - let filteredItems = showPicked ? items : items.filter((item) => item.status !== 'picked'); + let filteredItems = showPicked + ? sortedItems + : sortedItems.filter((item) => item.status !== 'picked'); if (itemFilter === 'cartons') { return filteredItems.filter((item) => item.is_carton); @@ -299,7 +323,7 @@ export const ActivePickListScreen = () => { p.id === item.product_id)} + product={productMap.get(item.product_id)} onIncrementQuantity={() => handleIncrementQuantity(item.id)} onDecrementQuantity={() => handleDecrementQuantity(item.id)} onToggleCarton={() => handleToggleCarton(item.id)}