Merge branch 'main' into codex/fix-packaging-type-filter-functionality-8h0e1y
This commit is contained in:
@@ -181,6 +181,91 @@ describe('ActivePickListScreen product search', () => {
|
|||||||
expect(screen.getByText(/no available products/i)).toBeVisible();
|
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(
|
||||||
|
<MemoryRouter initialEntries={['/pick-lists/1']} future={{ v7_startTransition: true, v7_relativeSplatPath: true }}>
|
||||||
|
<Routes>
|
||||||
|
<Route path="/pick-lists/:id" element={<ActivePickListScreen />} />
|
||||||
|
</Routes>
|
||||||
|
</MemoryRouter>,
|
||||||
|
);
|
||||||
|
|
||||||
|
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(
|
||||||
|
<MemoryRouter initialEntries={['/pick-lists/1']} future={{ v7_startTransition: true, v7_relativeSplatPath: true }}>
|
||||||
|
<Routes>
|
||||||
|
<Route path="/pick-lists/:id" element={<ActivePickListScreen />} />
|
||||||
|
</Routes>
|
||||||
|
</MemoryRouter>,
|
||||||
|
);
|
||||||
|
|
||||||
|
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 () => {
|
it('adds a pick item when a product is selected', async () => {
|
||||||
const user = userEvent.setup();
|
const user = userEvent.setup();
|
||||||
|
|
||||||
@@ -545,6 +630,52 @@ describe('ActivePickListScreen product search', () => {
|
|||||||
expect(screen.getByRole('radio', { name: /units/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();
|
||||||
|
|
||||||
|
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(
|
||||||
|
<MemoryRouter initialEntries={['/pick-lists/1']} future={{ v7_startTransition: true, v7_relativeSplatPath: true }}>
|
||||||
|
<Routes>
|
||||||
|
<Route path="/pick-lists/:id" element={<ActivePickListScreen />} />
|
||||||
|
</Routes>
|
||||||
|
</MemoryRouter>,
|
||||||
|
);
|
||||||
|
|
||||||
|
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 () => {
|
it('hides picked items when show picked is unchecked', async () => {
|
||||||
pickItemsMock.mockReturnValue([
|
pickItemsMock.mockReturnValue([
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ import { PickItemRow } from '../components/PickItemRow';
|
|||||||
import { PickItem } from '../models/PickItem';
|
import { PickItem } from '../models/PickItem';
|
||||||
import { Product } from '../models/Product';
|
import { Product } from '../models/Product';
|
||||||
|
|
||||||
|
const normalizeName = (name: string) => name.trim().toLowerCase();
|
||||||
|
|
||||||
export const ActivePickListScreen = () => {
|
export const ActivePickListScreen = () => {
|
||||||
const { id } = useParams();
|
const { id } = useParams();
|
||||||
const pickList = usePickList(id);
|
const pickList = usePickList(id);
|
||||||
@@ -84,8 +86,8 @@ export const ActivePickListScreen = () => {
|
|||||||
const productA = productMap.get(a.product_id);
|
const productA = productMap.get(a.product_id);
|
||||||
const productB = productMap.get(b.product_id);
|
const productB = productMap.get(b.product_id);
|
||||||
|
|
||||||
const nameA = productA?.name.trim().toLowerCase() ?? '';
|
const nameA = productA ? normalizeName(productA.name) : '';
|
||||||
const nameB = productB?.name.trim().toLowerCase() ?? '';
|
const nameB = productB ? normalizeName(productB.name) : '';
|
||||||
|
|
||||||
const nameComparison = nameA.localeCompare(nameB, undefined, { sensitivity: 'base' });
|
const nameComparison = nameA.localeCompare(nameB, undefined, { sensitivity: 'base' });
|
||||||
if (nameComparison !== 0) {
|
if (nameComparison !== 0) {
|
||||||
@@ -124,9 +126,20 @@ export const ActivePickListScreen = () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return Array.from(dedupedByName.values()).sort((a, b) =>
|
return Array.from(dedupedByName.values()).sort((a, b) => {
|
||||||
a.name.localeCompare(b.name, undefined, { sensitivity: 'base' }),
|
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]);
|
}, [products]);
|
||||||
|
|
||||||
const categoryFilteredProducts = useMemo(() => {
|
const categoryFilteredProducts = useMemo(() => {
|
||||||
@@ -151,11 +164,9 @@ export const ActivePickListScreen = () => {
|
|||||||
() => itemsVisibleByStatus.some((item) => item.status !== 'picked'),
|
() => itemsVisibleByStatus.some((item) => item.status !== 'picked'),
|
||||||
[itemsVisibleByStatus],
|
[itemsVisibleByStatus],
|
||||||
);
|
);
|
||||||
|
const packagingTypeCount = Number(hasCartonItems) + Number(hasUnitItems);
|
||||||
const packagingFiltersDisabled =
|
const singlePackagingType = packagingTypeCount === 1;
|
||||||
!showPicked || itemsVisibleByStatus.length === 0 || !visibleHasPicked || !visibleHasUnpicked;
|
const packagingFiltersDisabled = !showPicked || allItemsPicked || allItemsUnpicked;
|
||||||
|
|
||||||
const appliedItemFilter = packagingFiltersDisabled ? 'all' : itemFilter;
|
|
||||||
|
|
||||||
const productIdsInList = useMemo(
|
const productIdsInList = useMemo(
|
||||||
() => new Set(itemState.map((item) => item.product_id)),
|
() => new Set(itemState.map((item) => item.product_id)),
|
||||||
@@ -195,6 +206,22 @@ export const ActivePickListScreen = () => {
|
|||||||
}
|
}
|
||||||
}, [itemFilter, packagingFiltersDisabled]);
|
}, [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(() => {
|
const visibleItems = useMemo(() => {
|
||||||
let filteredItems = showPicked
|
let filteredItems = showPicked
|
||||||
? sortedItems
|
? sortedItems
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
import { MemoryRouter } from 'react-router-dom';
|
import { MemoryRouter } from 'react-router-dom';
|
||||||
import { render, screen, within } from '@testing-library/react';
|
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';
|
import { PickListsScreen } from './PickListsScreen';
|
||||||
|
|
||||||
const areasMock = [
|
let areasMock = [
|
||||||
{ id: 'area-1', name: 'Front Counter', created_at: 0, updated_at: 0 },
|
{ 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-2', name: 'back room', created_at: 0, updated_at: 0 },
|
||||||
{ id: 'area-3', name: 'Cafe', 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-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-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 },
|
{ 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', () => {
|
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', () => {
|
it('sorts pick lists alphabetically by area name', () => {
|
||||||
render(
|
render(
|
||||||
<MemoryRouter future={{ v7_startTransition: true, v7_relativeSplatPath: true }}>
|
<MemoryRouter future={{ v7_startTransition: true, v7_relativeSplatPath: true }}>
|
||||||
@@ -51,4 +65,30 @@ describe('PickListsScreen sorting', () => {
|
|||||||
expect(within(listItems[1]).getByText('Cafe')).toBeVisible();
|
expect(within(listItems[1]).getByText('Cafe')).toBeVisible();
|
||||||
expect(within(listItems[2]).getByText('Front Counter')).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(
|
||||||
|
<MemoryRouter future={{ v7_startTransition: true, v7_relativeSplatPath: true }}>
|
||||||
|
<PickListsScreen />
|
||||||
|
</MemoryRouter>,
|
||||||
|
);
|
||||||
|
|
||||||
|
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();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -39,10 +39,14 @@ export const PickListsScreen = () => {
|
|||||||
|
|
||||||
const sortedLists = useMemo(() => {
|
const sortedLists = useMemo(() => {
|
||||||
const locale = new Intl.Collator(undefined, { sensitivity: 'base' });
|
const locale = new Intl.Collator(undefined, { sensitivity: 'base' });
|
||||||
|
const normalizeAreaName = (areaId: string) =>
|
||||||
|
(areaNameById.get(areaId) ?? 'Unknown area').trim();
|
||||||
|
|
||||||
return [...lists].sort((a, b) => {
|
return [...lists].sort((a, b) => {
|
||||||
const nameA = areaNameById.get(a.area_id) ?? 'Unknown area';
|
const nameComparison = locale.compare(
|
||||||
const nameB = areaNameById.get(b.area_id) ?? 'Unknown area';
|
normalizeAreaName(a.area_id),
|
||||||
const nameComparison = locale.compare(nameA, nameB);
|
normalizeAreaName(b.area_id),
|
||||||
|
);
|
||||||
if (nameComparison !== 0) return nameComparison;
|
if (nameComparison !== 0) return nameComparison;
|
||||||
return a.created_at - b.created_at;
|
return a.created_at - b.created_at;
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user