diff --git a/src/screens/ActivePickListScreen.test.tsx b/src/screens/ActivePickListScreen.test.tsx index 661e545..e353ec8 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,90 @@ 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('deduplicates product options with the same name', async () => { + const duplicateNameProducts: Product[] = [ + ...defaultProducts, + { ...defaultProducts[0], id: 'prod-duplicate', barcode: '999', updated_at: 5 }, + ]; + + productsMock.mockReturnValue(duplicateNameProducts); + + 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..f12e1cb 100644 --- a/src/screens/ActivePickListScreen.tsx +++ b/src/screens/ActivePickListScreen.tsx @@ -48,15 +48,32 @@ export const ActivePickListScreen = () => { [areas, pickList?.area_id], ); + const sortedProducts = useMemo(() => { + const uniqueProducts = new Map(); + + products.forEach((product) => { + const normalizedName = product.name.trim().toLowerCase(); + const existing = uniqueProducts.get(normalizedName); + + if (!existing || product.updated_at > existing.updated_at) { + uniqueProducts.set(normalizedName, 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;