From 97e953c87be4ccb25cb8361ae0a554d10adb5e31 Mon Sep 17 00:00:00 2001 From: beatz174-bit Date: Mon, 24 Nov 2025 16:08:41 +1000 Subject: [PATCH 1/4] Fix pick list item sorting --- src/screens/ActivePickListScreen.test.tsx | 85 +++++++++++++++++++++++ src/screens/ActivePickListScreen.tsx | 23 ++++-- src/screens/PickListsScreen.test.tsx | 46 +++++++++++- src/screens/PickListsScreen.tsx | 10 ++- 4 files changed, 153 insertions(+), 11 deletions(-) diff --git a/src/screens/ActivePickListScreen.test.tsx b/src/screens/ActivePickListScreen.test.tsx index f959f9c..6bdc8e6 100644 --- a/src/screens/ActivePickListScreen.test.tsx +++ b/src/screens/ActivePickListScreen.test.tsx @@ -181,6 +181,91 @@ describe('ActivePickListScreen product search', () => { expect(screen.getByText(/no available products/i)).toBeVisible(); }); + it('sorts available products alphabetically, ignoring whitespace and casing', async () => { + productsMock.mockReturnValue([ + { ...defaultProducts[0], id: 'prod-1', name: ' cola ' }, + { ...defaultProducts[1], id: 'prod-2', name: 'apple chips' }, + { ...defaultProducts[2], id: 'prod-3', name: 'Banana Bites' }, + ]); + + 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'); + + const optionLabels = options.map((option) => option.textContent?.trim()); + expect(optionLabels).toEqual([ + expect.stringMatching(/apple chips/i), + expect.stringMatching(/banana bites/i), + expect.stringMatching(/cola/i), + ]); + }); + + it('sorts pick items alphabetically by product name, ignoring whitespace and casing', () => { + productsMock.mockReturnValue([ + { ...defaultProducts[0], id: 'prod-1', name: ' cola ' }, + { ...defaultProducts[1], id: 'prod-2', name: 'apple chips' }, + { ...defaultProducts[2], id: 'prod-3', name: 'Banana Bites' }, + ]); + + pickItemsMock.mockReturnValue([ + { + id: 'item-1', + pick_list_id: 'list-1', + product_id: 'prod-1', + quantity: 1, + is_carton: false, + status: 'pending', + created_at: 0, + updated_at: 0, + }, + { + id: 'item-2', + pick_list_id: 'list-1', + product_id: 'prod-2', + quantity: 1, + is_carton: false, + status: 'pending', + created_at: 0, + updated_at: 0, + }, + { + id: 'item-3', + pick_list_id: 'list-1', + product_id: 'prod-3', + quantity: 1, + is_carton: false, + status: 'pending', + created_at: 0, + updated_at: 0, + }, + ]); + + render( + + + } /> + + , + ); + + const itemRows = screen.getAllByTestId('pick-item-title-row'); + expect(itemRows[0]).toHaveTextContent(/apple chips/i); + expect(itemRows[1]).toHaveTextContent(/banana bites/i); + expect(itemRows[2]).toHaveTextContent(/cola/i); + }); + it('adds a pick item when a product is selected', async () => { const user = userEvent.setup(); diff --git a/src/screens/ActivePickListScreen.tsx b/src/screens/ActivePickListScreen.tsx index f5ffe3c..b857315 100644 --- a/src/screens/ActivePickListScreen.tsx +++ b/src/screens/ActivePickListScreen.tsx @@ -25,6 +25,8 @@ import { PickItemRow } from '../components/PickItemRow'; import { PickItem } from '../models/PickItem'; import { Product } from '../models/Product'; +const normalizeName = (name: string) => name.trim().toLowerCase(); + export const ActivePickListScreen = () => { const { id } = useParams(); const pickList = usePickList(id); @@ -81,8 +83,8 @@ export const ActivePickListScreen = () => { const productA = productMap.get(a.product_id); const productB = productMap.get(b.product_id); - const nameA = productA?.name.trim().toLowerCase() ?? ''; - const nameB = productB?.name.trim().toLowerCase() ?? ''; + const nameA = productA ? normalizeName(productA.name) : ''; + const nameB = productB ? normalizeName(productB.name) : ''; const nameComparison = nameA.localeCompare(nameB, undefined, { sensitivity: 'base' }); if (nameComparison !== 0) { @@ -121,9 +123,20 @@ export const ActivePickListScreen = () => { } }); - return Array.from(dedupedByName.values()).sort((a, b) => - a.name.localeCompare(b.name, undefined, { sensitivity: 'base' }), - ); + return Array.from(dedupedByName.values()).sort((a, b) => { + const normalizedNameA = normalizeName(a.name); + const normalizedNameB = normalizeName(b.name); + + const nameComparison = normalizedNameA.localeCompare(normalizedNameB, undefined, { + sensitivity: 'base', + }); + + if (nameComparison !== 0) { + return nameComparison; + } + + return a.name.localeCompare(b.name, undefined, { sensitivity: 'base' }); + }); }, [products]); const categoryFilteredProducts = useMemo(() => { diff --git a/src/screens/PickListsScreen.test.tsx b/src/screens/PickListsScreen.test.tsx index 0f646a4..fa4c4ff 100644 --- a/src/screens/PickListsScreen.test.tsx +++ b/src/screens/PickListsScreen.test.tsx @@ -1,15 +1,15 @@ import { MemoryRouter } from 'react-router-dom'; import { render, screen, within } from '@testing-library/react'; -import { describe, expect, it, vi } from 'vitest'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; import { PickListsScreen } from './PickListsScreen'; -const areasMock = [ +let 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 = [ +let pickListsMock = [ { id: 'list-2', area_id: 'area-2', created_at: 3, categories: [], auto_add_new_products: false }, { id: 'list-3', area_id: 'area-3', created_at: 4, categories: [], auto_add_new_products: false }, { id: 'list-1', area_id: 'area-1', created_at: 5, categories: [], auto_add_new_products: false }, @@ -38,6 +38,20 @@ vi.mock('../context/DBProvider', () => ({ })); describe('PickListsScreen sorting', () => { + beforeEach(() => { + 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 }, + ]; + + pickListsMock = [ + { id: 'list-2', area_id: 'area-2', created_at: 3, categories: [], auto_add_new_products: false }, + { id: 'list-3', area_id: 'area-3', created_at: 4, categories: [], auto_add_new_products: false }, + { id: 'list-1', area_id: 'area-1', created_at: 5, categories: [], auto_add_new_products: false }, + ]; + }); + it('sorts pick lists alphabetically by area name', () => { render( @@ -51,4 +65,30 @@ describe('PickListsScreen sorting', () => { expect(within(listItems[1]).getByText('Cafe')).toBeVisible(); expect(within(listItems[2]).getByText('Front Counter')).toBeVisible(); }); + + it('ignores casing and whitespace when ordering pick lists', () => { + areasMock = [ + { id: 'area-1', name: ' front counter', created_at: 0, updated_at: 0 }, + { id: 'area-2', name: ' Cafe ', created_at: 0, updated_at: 0 }, + { id: 'area-3', name: 'Back room', created_at: 0, updated_at: 0 }, + ]; + + pickListsMock = [ + { id: 'list-1', area_id: 'area-1', created_at: 5, categories: [], auto_add_new_products: false }, + { id: 'list-2', area_id: 'area-2', created_at: 3, categories: [], auto_add_new_products: false }, + { id: 'list-3', area_id: 'area-3', created_at: 4, categories: [], auto_add_new_products: false }, + ]; + + render( + + + , + ); + + const listItems = screen.getAllByRole('listitem'); + + expect(within(listItems[0]).getByText(/back room/i)).toBeVisible(); + expect(within(listItems[1]).getByText(/cafe/i)).toBeVisible(); + expect(within(listItems[2]).getByText(/front counter/i)).toBeVisible(); + }); }); diff --git a/src/screens/PickListsScreen.tsx b/src/screens/PickListsScreen.tsx index d04fb42..ade79fd 100644 --- a/src/screens/PickListsScreen.tsx +++ b/src/screens/PickListsScreen.tsx @@ -39,10 +39,14 @@ export const PickListsScreen = () => { const sortedLists = useMemo(() => { const locale = new Intl.Collator(undefined, { sensitivity: 'base' }); + const normalizeAreaName = (areaId: string) => + (areaNameById.get(areaId) ?? 'Unknown area').trim(); + 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); + const nameComparison = locale.compare( + normalizeAreaName(a.area_id), + normalizeAreaName(b.area_id), + ); if (nameComparison !== 0) return nameComparison; return a.created_at - b.created_at; }); From a4dbe1679a71cb13b50be9fe1e06af0b968cd7fc Mon Sep 17 00:00:00 2001 From: beatz174-bit Date: Mon, 24 Nov 2025 16:09:11 +1000 Subject: [PATCH 2/4] Fix packaging filter availability logic --- src/screens/ActivePickListScreen.test.tsx | 80 +++++++++++++++++++++++ src/screens/ActivePickListScreen.tsx | 29 +++++--- 2 files changed, 99 insertions(+), 10 deletions(-) diff --git a/src/screens/ActivePickListScreen.test.tsx b/src/screens/ActivePickListScreen.test.tsx index f959f9c..8822731 100644 --- a/src/screens/ActivePickListScreen.test.tsx +++ b/src/screens/ActivePickListScreen.test.tsx @@ -492,6 +492,50 @@ describe('ActivePickListScreen product search', () => { expect(unitsRadio).toBeEnabled(); }); + it('resets the packaging filter when show picked is unchecked', async () => { + pickItemsMock.mockReturnValue([ + { + id: 'item-1', + pick_list_id: 'list-1', + product_id: 'prod-1', + quantity: 1, + is_carton: true, + status: 'picked', + created_at: 0, + updated_at: 0, + }, + { + id: 'item-2', + pick_list_id: 'list-1', + product_id: 'prod-2', + quantity: 1, + is_carton: false, + status: 'pending', + created_at: 0, + updated_at: 0, + }, + ]); + + const user = userEvent.setup(); + + render( + + + } /> + + , + ); + + await user.click(screen.getByRole('radio', { name: /cartons/i })); + expect(screen.getByRole('radio', { name: /cartons/i })).toBeChecked(); + + await user.click(screen.getByLabelText(/show picked/i)); + + await waitFor(() => expect(screen.getByRole('radio', { name: /all/i })).toBeChecked()); + expect(screen.getByRole('radio', { name: /cartons/i })).toBeDisabled(); + expect(screen.getByRole('radio', { name: /units/i })).toBeDisabled(); + }); + it('resets the filter when the selected packaging type is unavailable', async () => { const user = userEvent.setup(); @@ -686,6 +730,42 @@ describe('ActivePickListScreen product search', () => { expect(screen.getByRole('radio', { name: /units/i })).toBeDisabled(); }); + it('disables packaging filters when every item is picked', () => { + pickItemsMock.mockReturnValue([ + { + id: 'item-1', + pick_list_id: 'list-1', + product_id: 'prod-1', + quantity: 1, + is_carton: true, + status: 'picked', + created_at: 0, + updated_at: 0, + }, + { + id: 'item-2', + pick_list_id: 'list-1', + product_id: 'prod-2', + quantity: 1, + is_carton: false, + status: 'picked', + created_at: 0, + updated_at: 0, + }, + ]); + + render( + + + } /> + + , + ); + + expect(screen.getByRole('radio', { name: /cartons/i })).toBeDisabled(); + expect(screen.getByRole('radio', { name: /units/i })).toBeDisabled(); + }); + it('disables show picked toggle when all items are picked', async () => { pickItemsMock.mockReturnValue([ { diff --git a/src/screens/ActivePickListScreen.tsx b/src/screens/ActivePickListScreen.tsx index f5ffe3c..b42362e 100644 --- a/src/screens/ActivePickListScreen.tsx +++ b/src/screens/ActivePickListScreen.tsx @@ -48,19 +48,22 @@ export const ActivePickListScreen = () => { [itemState, showPicked], ); - const allItemsPicked = useMemo( - () => itemState.length > 0 && itemState.every((item) => item.status === 'picked'), + const hasPickedItemsInList = useMemo( + () => itemState.some((item) => item.status === 'picked'), [itemState], ); - const hasUnpickedItems = useMemo( - () => itemsVisibleByStatus.some((item) => item.status !== 'picked'), - [itemsVisibleByStatus], + const hasUnpickedItemsInList = useMemo( + () => itemState.some((item) => item.status !== 'picked'), + [itemState], ); - const hasPickedItems = useMemo( - () => itemsVisibleByStatus.some((item) => item.status === 'picked'), - [itemsVisibleByStatus], + const allItemsPicked = useMemo( + () => itemState.length > 0 && !hasUnpickedItemsInList, + [hasUnpickedItemsInList, itemState.length], + ); + const allItemsUnpicked = useMemo( + () => itemState.length > 0 && !hasPickedItemsInList, + [hasPickedItemsInList, itemState.length], ); - const hasMixedPickStatuses = hasPickedItems && hasUnpickedItems; const productMap = useMemo(() => { const map = new Map(); @@ -150,7 +153,7 @@ export const ActivePickListScreen = () => { ); const packagingTypeCount = Number(hasCartonItems) + Number(hasUnitItems); const singlePackagingType = packagingTypeCount === 1; - const packagingFiltersDisabled = !hasMixedPickStatuses; + const packagingFiltersDisabled = !showPicked || allItemsPicked || allItemsUnpicked; const productIdsInList = useMemo( () => new Set(itemState.map((item) => item.product_id)), @@ -184,6 +187,12 @@ export const ActivePickListScreen = () => { } }, [allItemsPicked, showPicked]); + useEffect(() => { + if (packagingFiltersDisabled && itemFilter !== 'all') { + setItemFilter('all'); + } + }, [itemFilter, packagingFiltersDisabled]); + useEffect(() => { if (packagingTypeCount <= 1) { if (itemFilter !== 'all') { From f2261dddd9d5d4a4d0f3267d1651a88561f8b4f4 Mon Sep 17 00:00:00 2001 From: beatz174-bit Date: Mon, 24 Nov 2025 16:19:49 +1000 Subject: [PATCH 3/4] Adjust packaging filter availability logic --- src/screens/ActivePickListScreen.test.tsx | 87 +++++++++++++++-------- src/screens/ActivePickListScreen.tsx | 53 +++++--------- 2 files changed, 75 insertions(+), 65 deletions(-) diff --git a/src/screens/ActivePickListScreen.test.tsx b/src/screens/ActivePickListScreen.test.tsx index f959f9c..2d3533c 100644 --- a/src/screens/ActivePickListScreen.test.tsx +++ b/src/screens/ActivePickListScreen.test.tsx @@ -415,7 +415,7 @@ describe('ActivePickListScreen product search', () => { expect(itemLabels).toEqual(['Apple Juice', 'Chips', 'Cola', 'Cola']); }); - it('disables packaging filters when only one packaging type is present', () => { + it('keeps packaging filters enabled when items share a single packaging type but mixed statuses', () => { pickItemsMock.mockReturnValue([ { id: 'item-1', @@ -427,6 +427,16 @@ describe('ActivePickListScreen product search', () => { created_at: 0, updated_at: 0, }, + { + id: 'item-2', + pick_list_id: 'list-1', + product_id: 'prod-2', + quantity: 1, + is_carton: false, + status: 'picked', + created_at: 0, + updated_at: 0, + }, ]); render( @@ -437,9 +447,8 @@ describe('ActivePickListScreen product search', () => { , ); - expect(screen.getByRole('radio', { name: /all/i })).toBeChecked(); - expect(screen.getByRole('radio', { name: /cartons/i })).toBeDisabled(); - expect(screen.getByRole('radio', { name: /units/i })).toBeDisabled(); + expect(screen.getByRole('radio', { name: /cartons/i })).toBeEnabled(); + expect(screen.getByRole('radio', { name: /units/i })).toBeEnabled(); }); it('enables and disables packaging filters based on the visible item statuses', async () => { @@ -492,9 +501,7 @@ describe('ActivePickListScreen product search', () => { expect(unitsRadio).toBeEnabled(); }); - it('resets the filter when the selected packaging type is unavailable', async () => { - const user = userEvent.setup(); - + it('resets the packaging filter when show picked is unchecked', async () => { pickItemsMock.mockReturnValue([ { id: 'item-1', @@ -518,7 +525,9 @@ describe('ActivePickListScreen product search', () => { }, ]); - const { rerender } = render( + const user = userEvent.setup(); + + render( } /> @@ -529,32 +538,14 @@ describe('ActivePickListScreen product search', () => { await user.click(screen.getByRole('radio', { name: /cartons/i })); expect(screen.getByRole('radio', { name: /cartons/i })).toBeChecked(); - pickItemsMock.mockReturnValue([ - { - id: 'item-2', - pick_list_id: 'list-1', - product_id: 'prod-2', - quantity: 1, - is_carton: false, - status: 'pending', - created_at: 0, - updated_at: 0, - }, - ]); + await user.click(screen.getByLabelText(/show picked/i)); - rerender( - - - } /> - - , - ); - - await waitFor(() => expect(screen.getByRole('radio', { name: /cartons/i })).toBeDisabled()); - expect(screen.getByRole('radio', { name: /all/i })).toBeChecked(); + await waitFor(() => expect(screen.getByRole('radio', { name: /all/i })).toBeChecked()); + expect(screen.getByRole('radio', { name: /cartons/i })).toBeDisabled(); expect(screen.getByRole('radio', { name: /units/i })).toBeDisabled(); }); + it('hides picked items when show picked is unchecked', async () => { pickItemsMock.mockReturnValue([ { @@ -686,6 +677,42 @@ describe('ActivePickListScreen product search', () => { expect(screen.getByRole('radio', { name: /units/i })).toBeDisabled(); }); + it('disables packaging filters when every item is picked', () => { + pickItemsMock.mockReturnValue([ + { + id: 'item-1', + pick_list_id: 'list-1', + product_id: 'prod-1', + quantity: 1, + is_carton: true, + status: 'picked', + created_at: 0, + updated_at: 0, + }, + { + id: 'item-2', + pick_list_id: 'list-1', + product_id: 'prod-2', + quantity: 1, + is_carton: false, + status: 'picked', + created_at: 0, + updated_at: 0, + }, + ]); + + render( + + + } /> + + , + ); + + expect(screen.getByRole('radio', { name: /cartons/i })).toBeDisabled(); + expect(screen.getByRole('radio', { name: /units/i })).toBeDisabled(); + }); + it('disables show picked toggle when all items are picked', async () => { pickItemsMock.mockReturnValue([ { diff --git a/src/screens/ActivePickListScreen.tsx b/src/screens/ActivePickListScreen.tsx index f5ffe3c..fab5cb0 100644 --- a/src/screens/ActivePickListScreen.tsx +++ b/src/screens/ActivePickListScreen.tsx @@ -48,19 +48,22 @@ export const ActivePickListScreen = () => { [itemState, showPicked], ); - const allItemsPicked = useMemo( - () => itemState.length > 0 && itemState.every((item) => item.status === 'picked'), + const hasPickedItemsInList = useMemo( + () => itemState.some((item) => item.status === 'picked'), [itemState], ); - const hasUnpickedItems = useMemo( - () => itemsVisibleByStatus.some((item) => item.status !== 'picked'), - [itemsVisibleByStatus], + const hasUnpickedItemsInList = useMemo( + () => itemState.some((item) => item.status !== 'picked'), + [itemState], ); - const hasPickedItems = useMemo( - () => itemsVisibleByStatus.some((item) => item.status === 'picked'), - [itemsVisibleByStatus], + const allItemsPicked = useMemo( + () => itemState.length > 0 && !hasUnpickedItemsInList, + [hasUnpickedItemsInList, itemState.length], + ); + const allItemsUnpicked = useMemo( + () => itemState.length > 0 && !hasPickedItemsInList, + [hasPickedItemsInList, itemState.length], ); - const hasMixedPickStatuses = hasPickedItems && hasUnpickedItems; const productMap = useMemo(() => { const map = new Map(); @@ -140,17 +143,7 @@ export const ActivePickListScreen = () => { ); }, [pickList?.categories, sortedProducts]); - const hasCartonItems = useMemo( - () => itemsVisibleByStatus.some((item) => item.is_carton), - [itemsVisibleByStatus], - ); - const hasUnitItems = useMemo( - () => itemsVisibleByStatus.some((item) => !item.is_carton), - [itemsVisibleByStatus], - ); - const packagingTypeCount = Number(hasCartonItems) + Number(hasUnitItems); - const singlePackagingType = packagingTypeCount === 1; - const packagingFiltersDisabled = !hasMixedPickStatuses; + const packagingFiltersDisabled = !showPicked || allItemsPicked || allItemsUnpicked; const productIdsInList = useMemo( () => new Set(itemState.map((item) => item.product_id)), @@ -185,20 +178,10 @@ export const ActivePickListScreen = () => { }, [allItemsPicked, showPicked]); useEffect(() => { - if (packagingTypeCount <= 1) { - if (itemFilter !== 'all') { - setItemFilter('all'); - } - - return; + if (packagingFiltersDisabled && itemFilter !== 'all') { + setItemFilter('all'); } - - if (itemFilter === 'cartons' && !hasCartonItems) { - setItemFilter('units'); - } else if (itemFilter === 'units' && !hasUnitItems) { - setItemFilter('cartons'); - } - }, [itemFilter, hasCartonItems, hasUnitItems, packagingTypeCount]); + }, [itemFilter, packagingFiltersDisabled]); const visibleItems = useMemo(() => { let filteredItems = showPicked @@ -434,13 +417,13 @@ export const ActivePickListScreen = () => { value="cartons" control={} label="Cartons" - disabled={packagingFiltersDisabled || !hasCartonItems} + disabled={packagingFiltersDisabled} /> } label="Units" - disabled={packagingFiltersDisabled || !hasUnitItems} + disabled={packagingFiltersDisabled} /> From 370b6aef551af1a67bde7a4150e7e1b8d6f9323c Mon Sep 17 00:00:00 2001 From: beatz174-bit Date: Mon, 24 Nov 2025 16:44:10 +1000 Subject: [PATCH 4/4] Fix packaging filter behavior --- src/screens/ActivePickListScreen.test.tsx | 129 +++++++++++++++++----- src/screens/ActivePickListScreen.tsx | 65 +++++------ 2 files changed, 129 insertions(+), 65 deletions(-) diff --git a/src/screens/ActivePickListScreen.test.tsx b/src/screens/ActivePickListScreen.test.tsx index f959f9c..893f253 100644 --- a/src/screens/ActivePickListScreen.test.tsx +++ b/src/screens/ActivePickListScreen.test.tsx @@ -415,7 +415,7 @@ describe('ActivePickListScreen product search', () => { expect(itemLabels).toEqual(['Apple Juice', 'Chips', 'Cola', 'Cola']); }); - it('disables packaging filters when only one packaging type is present', () => { + it('keeps packaging filters enabled when items share a single packaging type but mixed statuses', () => { pickItemsMock.mockReturnValue([ { id: 'item-1', @@ -427,6 +427,16 @@ describe('ActivePickListScreen product search', () => { created_at: 0, updated_at: 0, }, + { + id: 'item-2', + pick_list_id: 'list-1', + product_id: 'prod-2', + quantity: 1, + is_carton: false, + status: 'picked', + created_at: 0, + updated_at: 0, + }, ]); render( @@ -437,9 +447,8 @@ describe('ActivePickListScreen product search', () => { , ); - expect(screen.getByRole('radio', { name: /all/i })).toBeChecked(); - expect(screen.getByRole('radio', { name: /cartons/i })).toBeDisabled(); - expect(screen.getByRole('radio', { name: /units/i })).toBeDisabled(); + expect(screen.getByRole('radio', { name: /cartons/i })).toBeEnabled(); + expect(screen.getByRole('radio', { name: /units/i })).toBeEnabled(); }); it('enables and disables packaging filters based on the visible item statuses', async () => { @@ -492,9 +501,7 @@ describe('ActivePickListScreen product search', () => { expect(unitsRadio).toBeEnabled(); }); - it('resets the filter when the selected packaging type is unavailable', async () => { - const user = userEvent.setup(); - + it('resets the packaging filter when show picked is unchecked', async () => { pickItemsMock.mockReturnValue([ { id: 'item-1', @@ -518,7 +525,9 @@ describe('ActivePickListScreen product search', () => { }, ]); - const { rerender } = render( + const user = userEvent.setup(); + + render( } /> @@ -529,32 +538,14 @@ describe('ActivePickListScreen product search', () => { await user.click(screen.getByRole('radio', { name: /cartons/i })); expect(screen.getByRole('radio', { name: /cartons/i })).toBeChecked(); - pickItemsMock.mockReturnValue([ - { - id: 'item-2', - pick_list_id: 'list-1', - product_id: 'prod-2', - quantity: 1, - is_carton: false, - status: 'pending', - created_at: 0, - updated_at: 0, - }, - ]); + await user.click(screen.getByLabelText(/show picked/i)); - rerender( - - - } /> - - , - ); - - await waitFor(() => expect(screen.getByRole('radio', { name: /cartons/i })).toBeDisabled()); - expect(screen.getByRole('radio', { name: /all/i })).toBeChecked(); + await waitFor(() => expect(screen.getByRole('radio', { name: /all/i })).toBeChecked()); + expect(screen.getByRole('radio', { name: /cartons/i })).toBeDisabled(); expect(screen.getByRole('radio', { name: /units/i })).toBeDisabled(); }); + it('hides picked items when show picked is unchecked', async () => { pickItemsMock.mockReturnValue([ { @@ -650,6 +641,48 @@ describe('ActivePickListScreen product search', () => { expect(screen.queryByText('Chips')).not.toBeInTheDocument(); }); + it('retains a chosen packaging filter when statuses are mixed but packaging types are not', async () => { + pickItemsMock.mockReturnValue([ + { + id: 'item-1', + pick_list_id: 'list-1', + product_id: 'prod-1', + quantity: 1, + is_carton: false, + status: 'pending', + created_at: 0, + updated_at: 0, + }, + { + id: 'item-2', + pick_list_id: 'list-1', + product_id: 'prod-2', + quantity: 1, + is_carton: false, + status: 'picked', + created_at: 0, + updated_at: 0, + }, + ]); + + const user = userEvent.setup(); + + render( + + + } /> + + , + ); + + const unitsRadio = screen.getByRole('radio', { name: /units/i }); + await user.click(unitsRadio); + + expect(unitsRadio).toBeChecked(); + expect(screen.getByText('Cola')).toBeVisible(); + expect(screen.getByText('Chips')).toBeVisible(); + }); + it('disables packaging filters when all items share the same status', () => { pickItemsMock.mockReturnValue([ { @@ -686,6 +719,42 @@ describe('ActivePickListScreen product search', () => { expect(screen.getByRole('radio', { name: /units/i })).toBeDisabled(); }); + it('disables packaging filters when every item is picked', () => { + pickItemsMock.mockReturnValue([ + { + id: 'item-1', + pick_list_id: 'list-1', + product_id: 'prod-1', + quantity: 1, + is_carton: true, + status: 'picked', + created_at: 0, + updated_at: 0, + }, + { + id: 'item-2', + pick_list_id: 'list-1', + product_id: 'prod-2', + quantity: 1, + is_carton: false, + status: 'picked', + created_at: 0, + updated_at: 0, + }, + ]); + + render( + + + } /> + + , + ); + + expect(screen.getByRole('radio', { name: /cartons/i })).toBeDisabled(); + expect(screen.getByRole('radio', { name: /units/i })).toBeDisabled(); + }); + it('disables show picked toggle when all items are picked', async () => { pickItemsMock.mockReturnValue([ { diff --git a/src/screens/ActivePickListScreen.tsx b/src/screens/ActivePickListScreen.tsx index f5ffe3c..2b022bc 100644 --- a/src/screens/ActivePickListScreen.tsx +++ b/src/screens/ActivePickListScreen.tsx @@ -48,19 +48,22 @@ export const ActivePickListScreen = () => { [itemState, showPicked], ); - const allItemsPicked = useMemo( - () => itemState.length > 0 && itemState.every((item) => item.status === 'picked'), + const hasPickedItemsInList = useMemo( + () => itemState.some((item) => item.status === 'picked'), [itemState], ); - const hasUnpickedItems = useMemo( - () => itemsVisibleByStatus.some((item) => item.status !== 'picked'), - [itemsVisibleByStatus], + const hasUnpickedItemsInList = useMemo( + () => itemState.some((item) => item.status !== 'picked'), + [itemState], ); - const hasPickedItems = useMemo( - () => itemsVisibleByStatus.some((item) => item.status === 'picked'), - [itemsVisibleByStatus], + const allItemsPicked = useMemo( + () => itemState.length > 0 && !hasUnpickedItemsInList, + [hasUnpickedItemsInList, itemState.length], + ); + const allItemsUnpicked = useMemo( + () => itemState.length > 0 && !hasPickedItemsInList, + [hasPickedItemsInList, itemState.length], ); - const hasMixedPickStatuses = hasPickedItems && hasUnpickedItems; const productMap = useMemo(() => { const map = new Map(); @@ -140,17 +143,19 @@ export const ActivePickListScreen = () => { ); }, [pickList?.categories, sortedProducts]); - const hasCartonItems = useMemo( - () => itemsVisibleByStatus.some((item) => item.is_carton), + const visibleHasPicked = useMemo( + () => itemsVisibleByStatus.some((item) => item.status === 'picked'), [itemsVisibleByStatus], ); - const hasUnitItems = useMemo( - () => itemsVisibleByStatus.some((item) => !item.is_carton), + const visibleHasUnpicked = useMemo( + () => itemsVisibleByStatus.some((item) => item.status !== 'picked'), [itemsVisibleByStatus], ); - const packagingTypeCount = Number(hasCartonItems) + Number(hasUnitItems); - const singlePackagingType = packagingTypeCount === 1; - const packagingFiltersDisabled = !hasMixedPickStatuses; + + const packagingFiltersDisabled = + !showPicked || itemsVisibleByStatus.length === 0 || !visibleHasPicked || !visibleHasUnpicked; + + const appliedItemFilter = packagingFiltersDisabled ? 'all' : itemFilter; const productIdsInList = useMemo( () => new Set(itemState.map((item) => item.product_id)), @@ -185,36 +190,26 @@ export const ActivePickListScreen = () => { }, [allItemsPicked, showPicked]); useEffect(() => { - if (packagingTypeCount <= 1) { - if (itemFilter !== 'all') { - setItemFilter('all'); - } - - return; + if (packagingFiltersDisabled && itemFilter !== 'all') { + setItemFilter('all'); } - - if (itemFilter === 'cartons' && !hasCartonItems) { - setItemFilter('units'); - } else if (itemFilter === 'units' && !hasUnitItems) { - setItemFilter('cartons'); - } - }, [itemFilter, hasCartonItems, hasUnitItems, packagingTypeCount]); + }, [itemFilter, packagingFiltersDisabled]); const visibleItems = useMemo(() => { let filteredItems = showPicked ? sortedItems : sortedItems.filter((item) => item.status !== 'picked'); - if (itemFilter === 'cartons') { + if (appliedItemFilter === 'cartons') { return filteredItems.filter((item) => item.is_carton); } - if (itemFilter === 'units') { + if (appliedItemFilter === 'units') { return filteredItems.filter((item) => !item.is_carton); } return filteredItems; - }, [itemFilter, showPicked, sortedItems]); + }, [appliedItemFilter, showPicked, sortedItems]); const updateItemState = (itemId: string, updater: (item: PickItem) => PickItem) => { setItemState((current) => current.map((item) => (item.id === itemId ? updater(item) : item))); @@ -423,7 +418,7 @@ export const ActivePickListScreen = () => { > setItemFilter(event.target.value as 'all' | 'cartons' | 'units') } @@ -434,13 +429,13 @@ export const ActivePickListScreen = () => { value="cartons" control={} label="Cartons" - disabled={packagingFiltersDisabled || !hasCartonItems} + disabled={packagingFiltersDisabled} /> } label="Units" - disabled={packagingFiltersDisabled || !hasUnitItems} + disabled={packagingFiltersDisabled} />