diff --git a/src/App.tsx b/src/App.tsx
index 2bf67d6..8ad21a7 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -6,7 +6,6 @@ import { HomeScreen } from './screens/HomeScreen';
import { StartPickListScreen } from './screens/StartPickListScreen';
import { PickListsScreen } from './screens/PickListsScreen';
import { ActivePickListScreen } from './screens/ActivePickListScreen';
-import { AddItemScreen } from './screens/AddItemScreen';
import { ManageProductsScreen } from './screens/ManageProductsScreen';
import { ManageAreasScreen } from './screens/ManageAreasScreen';
import { ManageCategoriesScreen } from './screens/ManageCategoriesScreen';
@@ -31,7 +30,6 @@ const AppRoutes = () => {
} />
} />
} />
- } />
} />
} />
} />
diff --git a/src/screens/ActivePickListScreen.test.tsx b/src/screens/ActivePickListScreen.test.tsx
new file mode 100644
index 0000000..69cd8be
--- /dev/null
+++ b/src/screens/ActivePickListScreen.test.tsx
@@ -0,0 +1,136 @@
+import { MemoryRouter, Route, Routes } from 'react-router-dom';
+import { render, screen, within } from '@testing-library/react';
+import userEvent from '@testing-library/user-event';
+import { describe, expect, it, vi, beforeEach } from 'vitest';
+import { ActivePickListScreen } from './ActivePickListScreen';
+import { PickItem } from '../models/PickItem';
+
+const addMock = vi.fn();
+const updateMock = vi.fn();
+const pickItemsMock = vi.fn();
+
+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,
+ },
+ ],
+ usePickList: () => ({ id: 'list-1', area_id: 'area-1', created_at: 0 }),
+ useAreas: () => [{ id: 'area-1', name: 'Front Counter', created_at: 0, updated_at: 0 }],
+}));
+
+vi.mock('../context/DBProvider', () => ({
+ useDatabase: () => ({
+ pickItems: {
+ add: addMock,
+ update: updateMock,
+ get: vi.fn(),
+ delete: vi.fn(),
+ },
+ }),
+}));
+
+describe('ActivePickListScreen product search', () => {
+ beforeEach(() => {
+ addMock.mockReset();
+ updateMock.mockReset();
+ pickItemsMock.mockReturnValue([]);
+ });
+
+ it('filters the product list based on the search query', async () => {
+ const user = userEvent.setup();
+
+ render(
+
+
+ } />
+
+ ,
+ );
+
+ const combobox = screen.getByRole('combobox');
+ await user.type(combobox, 'cola');
+
+ expect(await screen.findByRole('option', { name: /cola \(drinks\)/i })).toBeVisible();
+ expect(screen.queryByRole('option', { name: /chips \(snacks\)/i })).not.toBeInTheDocument();
+ });
+
+ it('adds a pick item when a product is selected', async () => {
+ const user = userEvent.setup();
+
+ render(
+
+
+ } />
+
+ ,
+ );
+
+ const combobox = screen.getByRole('combobox');
+ await user.click(combobox);
+
+ const listbox = await screen.findByRole('listbox');
+ await user.click(within(listbox).getByRole('option', { name: /cola \(drinks\)/i }));
+
+ expect(addMock).toHaveBeenCalledTimes(1);
+ expect(addMock.mock.calls[0][0]).toMatchObject({
+ product_id: 'prod-1',
+ quantity: 1,
+ is_carton: false,
+ });
+ });
+
+ it('updates an existing pick item when the same packaging is selected', async () => {
+ pickItemsMock.mockReturnValue([
+ {
+ id: 'item-1',
+ pick_list_id: 'list-1',
+ product_id: 'prod-1',
+ quantity: 2,
+ is_carton: false,
+ status: 'pending',
+ created_at: 0,
+ updated_at: 0,
+ },
+ ]);
+
+ const user = userEvent.setup();
+
+ render(
+
+
+ } />
+
+ ,
+ );
+
+ const combobox = screen.getByRole('combobox');
+ await user.click(combobox);
+
+ const listbox = await screen.findByRole('listbox');
+ await user.click(within(listbox).getByRole('option', { name: /cola \(drinks\)/i }));
+
+ expect(updateMock).toHaveBeenCalledWith('item-1', expect.objectContaining({ quantity: 3 }));
+ expect(addMock).not.toHaveBeenCalled();
+ });
+});
diff --git a/src/screens/ActivePickListScreen.tsx b/src/screens/ActivePickListScreen.tsx
index ecc461e..74e96b4 100644
--- a/src/screens/ActivePickListScreen.tsx
+++ b/src/screens/ActivePickListScreen.tsx
@@ -1,10 +1,23 @@
-import { Button, Container, Stack, Typography } from '@mui/material';
-import { useParams, useNavigate, Link as RouterLink } from 'react-router-dom';
-import { useMemo } from 'react';
+import {
+ Autocomplete,
+ Button,
+ Checkbox,
+ Container,
+ FormControlLabel,
+ InputAdornment,
+ Stack,
+ TextField,
+ Typography,
+} from '@mui/material';
+import SearchIcon from '@mui/icons-material/Search';
+import { useParams, useNavigate } from 'react-router-dom';
+import { useEffect, useMemo, useState } from 'react';
+import { v4 as uuidv4 } from 'uuid';
import { useAreas, usePickItems, usePickList, useProducts } from '../hooks/dataHooks';
import { useDatabase } from '../context/DBProvider';
import { PickItemRow } from '../components/PickItemRow';
import { PickItem } from '../models/PickItem';
+import { Product } from '../models/Product';
export const ActivePickListScreen = () => {
const { id } = useParams();
@@ -14,12 +27,38 @@ export const ActivePickListScreen = () => {
const areas = useAreas();
const db = useDatabase();
const navigate = useNavigate();
+ const [selectedProduct, setSelectedProduct] = useState(null);
+ const [query, setQuery] = useState('');
+ const [quantity, setQuantity] = useState(1);
+ const [isCarton, setIsCarton] = useState(false);
const areaName = useMemo(
() => areas.find((area) => area.id === pickList?.area_id)?.name ?? 'Area',
[areas, pickList?.area_id],
);
+ const filteredProducts = useMemo(() => {
+ const normalizedQuery = query.trim().toLowerCase();
+ if (!normalizedQuery) return products;
+
+ return products.filter((product) => {
+ const searchableText = `${product.name} ${product.category} ${product.barcode ?? ''}`.toLowerCase();
+ return searchableText.includes(normalizedQuery);
+ });
+ }, [products, query]);
+
+ useEffect(() => {
+ if (!selectedProduct) return;
+ if (!filteredProducts.some((product) => product.id === selectedProduct.id)) {
+ setSelectedProduct(null);
+ }
+ }, [filteredProducts, selectedProduct]);
+
+ useEffect(() => {
+ setQuantity(1);
+ setIsCarton(false);
+ }, [selectedProduct]);
+
const handleIncrementQuantity = async (itemId: string) => {
const existing = await db.pickItems.get(itemId);
if (!existing) return;
@@ -61,17 +100,121 @@ export const ActivePickListScreen = () => {
await db.pickItems.delete(itemId);
};
+ const addOrUpdateItem = async (product: Product) => {
+ if (!id || quantity <= 0) return;
+
+ const existing = items.find(
+ (item) => item.product_id === product.id && item.is_carton === isCarton,
+ );
+
+ if (existing) {
+ await db.pickItems.update(existing.id, {
+ quantity: existing.quantity + quantity,
+ updated_at: Date.now(),
+ });
+ } else {
+ await db.pickItems.add({
+ id: uuidv4(),
+ pick_list_id: id,
+ product_id: product.id,
+ quantity,
+ is_carton: isCarton,
+ status: 'pending',
+ created_at: Date.now(),
+ updated_at: Date.now(),
+ });
+ }
+
+ setSelectedProduct(null);
+ setQuery('');
+ setQuantity(1);
+ setIsCarton(false);
+ };
+
const returnToLists = () => {
navigate('/pick-lists');
};
+ const packagingLabel = isCarton
+ ? selectedProduct?.bulk_name ?? 'Cartons'
+ : selectedProduct?.unit_type ?? 'Units';
+ const quantityHelperText = selectedProduct
+ ? `Enter ${packagingLabel.toLowerCase()} to pick`
+ : 'Select a product to add it to the list';
+ const cartonCheckboxLabel = selectedProduct?.bulk_name
+ ? `Carton (${selectedProduct.bulk_name})`
+ : 'Carton pick';
+
return (
-
+
{areaName} List
-
+
+
+ Add products to this list
+
+ `${option.name} (${option.category})`}
+ isOptionEqualToValue={(option, value) => option.id === value.id}
+ value={selectedProduct}
+ onChange={(_, value) => {
+ setSelectedProduct(value);
+ if (value) {
+ void addOrUpdateItem(value);
+ }
+ }}
+ inputValue={query}
+ onInputChange={(_, value, reason) => {
+ if (reason === 'input') {
+ setQuery(value);
+ }
+
+ if (reason === 'clear') {
+ setQuery('');
+ }
+ }}
+ filterOptions={(options) => options}
+ noOptionsText={query.trim() ? 'No matching products' : 'No products available'}
+ fullWidth
+ renderInput={(params) => (
+
+
+
+
+ {params.InputProps.startAdornment}
+ >
+ ),
+ }}
+ />
+ )}
+ />
+
+ setQuantity(Math.max(1, Number(event.target.value)))}
+ inputProps={{ min: 1 }}
+ helperText={quantityHelperText}
+ fullWidth
+ />
+ setIsCarton(event.target.checked)} />}
+ label={cartonCheckboxLabel}
+ disabled={!selectedProduct}
+ />
+
+
+ Selecting a product immediately adds it to the pick list.
+
+
{pickList?.notes ? (
diff --git a/src/screens/AddItemScreen.test.tsx b/src/screens/AddItemScreen.test.tsx
deleted file mode 100644
index c11b174..0000000
--- a/src/screens/AddItemScreen.test.tsx
+++ /dev/null
@@ -1,62 +0,0 @@
-import { MemoryRouter, Route, Routes } from 'react-router-dom';
-import { render, screen } from '@testing-library/react';
-import userEvent from '@testing-library/user-event';
-import { describe, expect, it, vi } from 'vitest';
-import { AddItemScreen } from './AddItemScreen';
-
-const addMock = vi.fn();
-
-vi.mock('../hooks/dataHooks', () => ({
- usePickItems: () => [],
- 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,
- },
- ],
-}));
-
-vi.mock('../context/DBProvider', () => ({
- useDatabase: () => ({
- pickItems: {
- add: addMock,
- },
- }),
-}));
-
-describe('AddItemScreen product search', () => {
- it('filters the product list based on the search query', async () => {
- const user = userEvent.setup();
-
- render(
-
-
- } />
-
- ,
- );
-
- await user.type(screen.getByPlaceholderText(/search products/i), 'cola');
-
- expect(await screen.findByRole('option', { name: /cola \(drinks\)/i })).toBeVisible();
- expect(screen.queryByRole('option', { name: /chips \(snacks\)/i })).not.toBeInTheDocument();
- });
-});
diff --git a/src/screens/AddItemScreen.tsx b/src/screens/AddItemScreen.tsx
deleted file mode 100644
index b578d67..0000000
--- a/src/screens/AddItemScreen.tsx
+++ /dev/null
@@ -1,166 +0,0 @@
-import {
- Autocomplete,
- Button,
- Checkbox,
- Container,
- FormControlLabel,
- InputAdornment,
- Stack,
- TextField,
- Typography,
-} from '@mui/material';
-import SearchIcon from '@mui/icons-material/Search';
-import { useEffect, useMemo, useState } from 'react';
-import { useNavigate, useParams } from 'react-router-dom';
-import { v4 as uuidv4 } from 'uuid';
-import { usePickItems, useProducts } from '../hooks/dataHooks';
-import { useDatabase } from '../context/DBProvider';
-
-export const AddItemScreen = () => {
- const { id } = useParams();
- const db = useDatabase();
- const items = usePickItems(id);
- const products = useProducts();
- const [selectedProduct, setSelectedProduct] = useState(null);
- const [query, setQuery] = useState('');
- const [quantity, setQuantity] = useState(1);
- const [isCarton, setIsCarton] = useState(false);
- const navigate = useNavigate();
- const unitLabel = selectedProduct?.unit_type ?? 'Units';
- const cartonLabel = selectedProduct?.bulk_name ?? 'Cartons';
-
- const filteredProducts = useMemo(() => {
- const normalizedQuery = query.trim().toLowerCase();
- if (!normalizedQuery) return products;
-
- return products.filter((product) => {
- const searchableText = `${product.name} ${product.category} ${product.barcode ?? ''}`.toLowerCase();
- return searchableText.includes(normalizedQuery);
- });
- }, [products, query]);
-
- useEffect(() => {
- if (!selectedProduct) return;
- if (!filteredProducts.some((product) => product.id === selectedProduct.id)) {
- setSelectedProduct(null);
- }
- }, [filteredProducts, selectedProduct]);
-
- useEffect(() => {
- setQuantity(1);
- setIsCarton(false);
- }, [selectedProduct]);
-
- const packagingLabel = isCarton ? cartonLabel : unitLabel;
- const quantityHelperText = selectedProduct
- ? `Enter ${packagingLabel.toLowerCase()} to pick`
- : 'Enter the quantity to pick';
- const cartonCheckboxLabel = selectedProduct?.bulk_name
- ? `Carton (${selectedProduct.bulk_name})`
- : 'Carton pick';
-
- const addItem = async () => {
- const productId = selectedProduct?.id;
-
- if (!id || !productId || quantity <= 0) return;
-
- const existing = items.find((item) => item.product_id === productId && item.is_carton === isCarton);
-
- if (existing) {
- await db.pickItems.update(existing.id, {
- quantity: existing.quantity + quantity,
- updated_at: Date.now(),
- });
- navigate(`/pick-lists/${id}`);
- return;
- }
-
- await db.pickItems.add({
- id: uuidv4(),
- pick_list_id: id,
- product_id: productId,
- quantity,
- is_carton: isCarton,
- status: 'pending',
- created_at: Date.now(),
- updated_at: Date.now(),
- });
- navigate(`/pick-lists/${id}`);
- };
-
- return (
-
-
- Add Item
-
-
- `${option.name} (${option.category})`}
- isOptionEqualToValue={(option, value) => option.id === value.id}
- value={selectedProduct}
- onChange={(_, value) => setSelectedProduct(value)}
- inputValue={query}
- onInputChange={(_, value, reason) => {
- if (reason === 'input') {
- setQuery(value);
- }
-
- if (reason === 'clear') {
- setQuery('');
- }
- }}
- filterOptions={(options) => options}
- noOptionsText={query.trim() ? 'No matching products' : 'No products available'}
- fullWidth
- renderInput={(params) => (
-
-
-
-
- {params.InputProps.startAdornment}
- >
- ),
- }}
- />
- )}
- />
- {selectedProduct && (
-
-
- Selected product
-
-
- {selectedProduct.name}
-
-
- {selectedProduct.category}
-
-
- )}
- setQuantity(Math.max(1, Number(event.target.value)))}
- inputProps={{ min: 1 }}
- helperText={quantityHelperText}
- />
- setIsCarton(event.target.checked)} />}
- label={cartonCheckboxLabel}
- disabled={!selectedProduct}
- />
-
-
-
- );
-};