Add packaging filter to active pick list

This commit is contained in:
beatz174-bit
2025-11-25 08:26:59 +10:00
parent f6aa5c9ae0
commit e7e9425aa1
2 changed files with 198 additions and 3 deletions
+134
View File
@@ -67,6 +67,8 @@ vi.mock('../context/DBProvider', () => ({
}));
describe('ActivePickListScreen product search', () => {
const getRadio = (testId: string) => within(screen.getByTestId(testId)).getByRole('radio');
beforeEach(() => {
addMock.mockReset();
updateMock.mockReset();
@@ -573,4 +575,136 @@ describe('ActivePickListScreen product search', () => {
expect(togglePicked).toBeDisabled();
expect(togglePicked).toBeChecked();
});
it('enables packaging filters when both packaging types are visible and filters items', 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: true,
status: 'pending',
created_at: 0,
updated_at: 0,
},
]);
const user = userEvent.setup();
render(
<MemoryRouter initialEntries={['/pick-lists/1']}>
<Routes>
<Route path="/pick-lists/:id" element={<ActivePickListScreen />} />
</Routes>
</MemoryRouter>,
);
const allRadio = getRadio('packaging-all');
const unitsRadio = getRadio('packaging-units');
const cartonsRadio = getRadio('packaging-cartons');
expect(allRadio).toBeChecked();
expect(unitsRadio).not.toBeDisabled();
expect(cartonsRadio).not.toBeDisabled();
await user.click(unitsRadio);
expect(unitsRadio).toBeChecked();
expect(screen.getByText('Cola')).toBeVisible();
expect(screen.queryByText('Chips')).not.toBeInTheDocument();
await user.click(cartonsRadio);
expect(cartonsRadio).toBeChecked();
expect(screen.getByText('Chips')).toBeVisible();
expect(screen.queryByText('Cola')).not.toBeInTheDocument();
});
it('disables units and cartons packaging options when only units are visible', () => {
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,
},
]);
render(
<MemoryRouter initialEntries={['/pick-lists/1']}>
<Routes>
<Route path="/pick-lists/:id" element={<ActivePickListScreen />} />
</Routes>
</MemoryRouter>,
);
expect(getRadio('packaging-all')).toBeChecked();
expect(getRadio('packaging-units')).toBeDisabled();
expect(getRadio('packaging-cartons')).toBeDisabled();
});
it('resets packaging filter to all and disables options when visible items become single packaging type', 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: true,
status: 'picked',
created_at: 0,
updated_at: 0,
},
]);
const user = userEvent.setup();
render(
<MemoryRouter initialEntries={['/pick-lists/1']}>
<Routes>
<Route path="/pick-lists/:id" element={<ActivePickListScreen />} />
</Routes>
</MemoryRouter>,
);
const cartonsRadio = getRadio('packaging-cartons');
expect(cartonsRadio).not.toBeDisabled();
await user.click(cartonsRadio);
expect(cartonsRadio).toBeChecked();
expect(screen.getByText('Chips')).toBeVisible();
expect(screen.queryByText('Cola')).not.toBeInTheDocument();
await user.click(screen.getByLabelText(/show picked/i));
expect(getRadio('packaging-all')).toBeChecked();
expect(getRadio('packaging-units')).toBeDisabled();
expect(getRadio('packaging-cartons')).toBeDisabled();
expect(screen.getByText('Cola')).toBeVisible();
expect(screen.queryByText('Chips')).not.toBeInTheDocument();
});
});
+64 -3
View File
@@ -10,6 +10,10 @@ import {
TextField,
Tooltip,
Typography,
FormControl,
FormLabel,
RadioGroup,
Radio,
} from '@mui/material';
import AddCircleOutlineIcon from '@mui/icons-material/AddCircleOutline';
import SearchIcon from '@mui/icons-material/Search';
@@ -37,6 +41,7 @@ export const ActivePickListScreen = () => {
const [showPicked, setShowPicked] = useState(true);
const [itemState, setItemState] = useState(items);
const [isBatchUpdating, setIsBatchUpdating] = useState(false);
const [packagingFilter, setPackagingFilter] = useState<'all' | 'units' | 'cartons'>('all');
useEffect(() => {
setItemState((current) => {
@@ -63,6 +68,21 @@ export const ActivePickListScreen = () => {
[itemState, showPicked],
);
const packagingInfo = useMemo(() => {
const visible = itemsAfterShowPicked ?? [];
const uniqueValues = new Set(visible.map((it) => !!it.is_carton));
return {
visibleCount: visible.length,
uniquePackagingCount: uniqueValues.size,
};
}, [itemsAfterShowPicked]);
useEffect(() => {
if (packagingInfo.visibleCount === 0 || packagingInfo.uniquePackagingCount === 1) {
setPackagingFilter('all');
}
}, [packagingInfo.visibleCount, packagingInfo.uniquePackagingCount]);
const allItemsPicked = useMemo(
() => itemState.length > 0 && itemState.every((item) => item.status === 'picked'),
[itemState],
@@ -165,9 +185,16 @@ export const ActivePickListScreen = () => {
}
}, [allItemsPicked, showPicked]);
// Sort the items that are actually visible (after showPicked)
// Sort the items that are actually visible (after showPicked and packaging filter)
const visibleItems = useMemo(() => {
const arr = [...itemsAfterShowPicked];
let arr = [...itemsAfterShowPicked];
if (packagingFilter === 'units') {
arr = arr.filter((item) => !item.is_carton);
} else if (packagingFilter === 'cartons') {
arr = arr.filter((item) => item.is_carton);
}
arr.sort((a, b) => {
const nameA = normalizeName(productMap.get(a.product_id)?.name ?? '');
const nameB = normalizeName(productMap.get(b.product_id)?.name ?? '');
@@ -178,7 +205,7 @@ export const ActivePickListScreen = () => {
return timeA - timeB;
});
return arr;
}, [itemsAfterShowPicked, productMap]);
}, [itemsAfterShowPicked, productMap, packagingFilter]);
const updateItemState = (itemId: string, updater: (item: PickItem) => PickItem) => {
setItemState((current) => current.map((item) => (item.id === itemId ? updater(item) : item)));
@@ -393,6 +420,40 @@ export const ActivePickListScreen = () => {
flexWrap="wrap"
rowGap={1}
>
<FormControl component="fieldset" sx={{ ml: { xs: 0, sm: 2 } }}>
<FormLabel component="legend" sx={{ fontSize: '0.875rem' }}>
Packaging
</FormLabel>
<RadioGroup
row
aria-label="packaging-filter"
name="packaging-filter"
value={packagingFilter}
onChange={(_, value) => setPackagingFilter(value as 'all' | 'units' | 'cartons')}
>
<FormControlLabel
value="all"
control={<Radio size="small" />}
label="All"
data-testid="packaging-all"
/>
<FormControlLabel
value="units"
control={<Radio size="small" />}
label="Units"
disabled={packagingInfo.visibleCount === 0 || packagingInfo.uniquePackagingCount === 1}
data-testid="packaging-units"
/>
<FormControlLabel
value="cartons"
control={<Radio size="small" />}
label="Cartons"
disabled={packagingInfo.visibleCount === 0 || packagingInfo.uniquePackagingCount === 1}
data-testid="packaging-cartons"
/>
</RadioGroup>
</FormControl>
<Stack direction="row" spacing={1} alignItems="center" sx={{ ml: { xs: 0, sm: 2 } }}>
<FormControlLabel
control={