diff --git a/src/screens/ActivePickListScreen.test.tsx b/src/screens/ActivePickListScreen.test.tsx
index 2d3533c..3de488b 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();
@@ -545,6 +630,52 @@ describe('ActivePickListScreen product search', () => {
expect(screen.getByRole('radio', { name: /units/i })).toBeDisabled();
});
+ it('resets the filter when the selected packaging type is unavailable', async () => {
+ const user = userEvent.setup();
+
+ 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('hides picked items when show picked is unchecked', async () => {
pickItemsMock.mockReturnValue([
diff --git a/src/screens/ActivePickListScreen.tsx b/src/screens/ActivePickListScreen.tsx
index fab5cb0..afe93fc 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);
@@ -84,8 +86,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) {
@@ -124,9 +126,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(() => {
@@ -143,6 +156,16 @@ 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 = !showPicked || allItemsPicked || allItemsUnpicked;
const productIdsInList = useMemo(
@@ -183,6 +206,22 @@ export const ActivePickListScreen = () => {
}
}, [itemFilter, packagingFiltersDisabled]);
+ useEffect(() => {
+ if (packagingTypeCount <= 1) {
+ if (itemFilter !== 'all') {
+ setItemFilter('all');
+ }
+
+ return;
+ }
+
+ if (itemFilter === 'cartons' && !hasCartonItems) {
+ setItemFilter('units');
+ } else if (itemFilter === 'units' && !hasUnitItems) {
+ setItemFilter('cartons');
+ }
+ }, [itemFilter, packagingFiltersDisabled]);
+
const visibleItems = useMemo(() => {
let filteredItems = showPicked
? sortedItems
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;
});