From 81569e1124c20ab1df74772998540cce10f3586f Mon Sep 17 00:00:00 2001 From: beatz174-bit Date: Sun, 23 Nov 2025 15:38:41 +1000 Subject: [PATCH 1/2] Add category selection when starting pick lists --- e2e/picklist.spec.ts | 25 ++-- src/screens/StartPickListScreen.test.tsx | 139 +++++++++++++++++++++++ src/screens/StartPickListScreen.tsx | 120 +++++++++++++++++-- 3 files changed, 263 insertions(+), 21 deletions(-) create mode 100644 src/screens/StartPickListScreen.test.tsx diff --git a/e2e/picklist.spec.ts b/e2e/picklist.spec.ts index c23f29c..421dd9b 100644 --- a/e2e/picklist.spec.ts +++ b/e2e/picklist.spec.ts @@ -1,37 +1,36 @@ import { expect, test } from '@playwright/test'; const areaName = 'Drinks'; -const firstProduct = 'Mount Franklin 600ml'; -const secondProduct = 'Mars Bar'; +const chocolateProduct = 'Mars Bar'; +const chipsProduct = 'Smiths Salt n Vinegar 90g'; +const additionalProduct = 'Pump 750'; test.describe('Active pick list', () => { - test('allows creating a pick list and adding products without crashing', async ({ page }) => { + test('creates a pick list with category-prefilled items and adds more products', async ({ page }) => { await page.goto('/'); await expect(page.getByRole('heading', { name: 'StockFill' })).toBeVisible(); await page.getByRole('link', { name: 'Create Pick List' }).click(); await page.getByLabel('Area').click(); await page.getByRole('option', { name: areaName }).first().click(); + await page.getByLabel('Category (optional)').click(); + await page.getByRole('option', { name: 'Chocolates' }).click(); + await page.getByRole('checkbox', { name: 'Chips' }).click(); await page.getByRole('button', { name: 'Save Pick List' }).click(); await expect(page.getByRole('heading', { name: `${areaName} List` })).toBeVisible(); + await expect(page.getByText(chocolateProduct).first()).toBeVisible(); + await expect(page.getByText(chipsProduct).first()).toBeVisible(); const searchInput = page.getByPlaceholder('Search products'); await searchInput.click(); - await searchInput.fill(firstProduct); + await searchInput.fill(additionalProduct); await page - .getByRole('option', { name: new RegExp(`${firstProduct} \\(${areaName}\\)`, 'i') }) + .getByRole('option', { name: new RegExp(`${additionalProduct} \\(${areaName}\\)`, 'i') }) .first() .click(); - await expect(page.getByText(firstProduct).first()).toBeVisible(); - await expect(page.getByText(/Qty: 1 unit/i)).toBeVisible(); - - await searchInput.click(); - await searchInput.fill(secondProduct); - await page.getByRole('option', { name: new RegExp(secondProduct, 'i') }).first().click(); - - await expect(page.getByText(secondProduct).first()).toBeVisible(); + await expect(page.getByText(additionalProduct).first()).toBeVisible(); await expect(page.getByRole('button', { name: 'Save and Return' })).toBeEnabled(); }); }); diff --git a/src/screens/StartPickListScreen.test.tsx b/src/screens/StartPickListScreen.test.tsx new file mode 100644 index 0000000..6f442ad --- /dev/null +++ b/src/screens/StartPickListScreen.test.tsx @@ -0,0 +1,139 @@ +import { MemoryRouter } from 'react-router-dom'; +import { render, screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { StartPickListScreen } from './StartPickListScreen'; + +const navigateMock = vi.fn(); + +vi.mock('react-router-dom', async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + useNavigate: () => navigateMock, + }; +}); + +const areasMock = [{ id: 'area-1', name: 'Front Counter', created_at: 0, updated_at: 0 }]; +const categoriesMock = [ + { id: 'cat-1', name: 'Drinks', created_at: 0, updated_at: 0 }, + { id: 'cat-2', name: 'Snacks', created_at: 0, updated_at: 0 }, +]; + +const productsMock = [ + { + id: 'prod-1', + name: 'Soda', + category: 'Drinks', + unit_type: 'unit', + bulk_name: 'box', + archived: false, + created_at: 0, + updated_at: 0, + }, + { + id: 'prod-2', + name: 'Chips', + category: 'Snacks', + unit_type: 'unit', + bulk_name: 'box', + archived: false, + created_at: 0, + updated_at: 0, + }, + { + id: 'prod-3', + name: 'Old Soda', + category: 'Drinks', + unit_type: 'unit', + bulk_name: 'box', + archived: true, + created_at: 0, + updated_at: 0, + }, +]; + +const pickListAddMock = vi.fn(); +const pickItemsBulkAddMock = vi.fn(); +const transactionMock = vi.fn(); +const productsToArrayMock = vi.fn(); + +vi.mock('../hooks/dataHooks', () => ({ + useAreas: () => areasMock, + useCategories: () => categoriesMock, +})); + +vi.mock('../context/DBProvider', () => ({ + useDatabase: () => ({ + pickLists: { add: pickListAddMock }, + pickItems: { bulkAdd: pickItemsBulkAddMock }, + products: { toArray: productsToArrayMock }, + transaction: transactionMock, + }), +})); + +vi.mock('uuid', () => ({ + v4: () => 'generated-id', +})); + +beforeEach(() => { + navigateMock.mockReset(); + pickListAddMock.mockReset(); + pickItemsBulkAddMock.mockReset(); + transactionMock.mockReset(); + productsToArrayMock.mockReset(); + productsToArrayMock.mockResolvedValue(productsMock); + transactionMock.mockImplementation(async (_mode: string, ...args: unknown[]) => { + const callback = args[args.length - 1] as () => Promise; + await callback(); + }); +}); + +describe('StartPickListScreen', () => { + it('shows category selection controls', () => { + render( + + + , + ); + + expect(screen.getByLabelText(/category \(optional\)/i)).toBeVisible(); + categoriesMock.forEach((category) => { + expect(screen.getByRole('checkbox', { name: category.name })).toBeInTheDocument(); + }); + }); + + it('prefills a new pick list with products from selected categories', async () => { + const user = userEvent.setup(); + + render( + + + , + ); + + await user.click(screen.getByLabelText(/area/i)); + await user.click(screen.getByRole('option', { name: /front counter/i })); + + await user.click(screen.getByLabelText(/category \(optional\)/i)); + await user.click(screen.getByRole('option', { name: /drinks/i })); + + await user.click(screen.getByRole('checkbox', { name: /snacks/i })); + + await user.click(screen.getByRole('button', { name: /save pick list/i })); + + await waitFor(() => expect(pickListAddMock).toHaveBeenCalled()); + await waitFor(() => expect(pickItemsBulkAddMock).toHaveBeenCalled()); + + const pickItems = pickItemsBulkAddMock.mock.calls[0][0]; + + expect(pickItems).toHaveLength(2); + expect(pickItems.map((item: any) => item.product_id).sort()).toEqual(['prod-1', 'prod-2']); + pickItems.forEach((item: any) => { + expect(item.pick_list_id).toBe('generated-id'); + expect(item.is_carton).toBe(false); + expect(item.quantity).toBe(1); + expect(item.status).toBe('pending'); + }); + }); +}); diff --git a/src/screens/StartPickListScreen.tsx b/src/screens/StartPickListScreen.tsx index f137c1f..147510e 100644 --- a/src/screens/StartPickListScreen.tsx +++ b/src/screens/StartPickListScreen.tsx @@ -1,25 +1,98 @@ -import { Button, Container, MenuItem, Stack, TextField, Typography } from '@mui/material'; -import { useState } from 'react'; +import { + Button, + Checkbox, + Container, + FormControlLabel, + FormGroup, + MenuItem, + Stack, + TextField, + Typography, +} from '@mui/material'; +import { useMemo, useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { v4 as uuidv4 } from 'uuid'; -import { useAreas } from '../hooks/dataHooks'; +import { useAreas, useCategories } from '../hooks/dataHooks'; import { useDatabase } from '../context/DBProvider'; export const StartPickListScreen = () => { const areas = useAreas(); + const categories = useCategories(); const db = useDatabase(); const navigate = useNavigate(); const [areaId, setAreaId] = useState(''); const [notes, setNotes] = useState(''); + const [selectedCategories, setSelectedCategories] = useState([]); + const [quickCategoryId, setQuickCategoryId] = useState(''); + + const sortedCategories = useMemo( + () => + [...categories].sort((a, b) => + a.name.localeCompare(b.name, undefined, { sensitivity: 'base' }), + ), + [categories], + ); + + const handleToggleCategory = (categoryId: string) => { + setSelectedCategories((current) => + current.includes(categoryId) + ? current.filter((id) => id !== categoryId) + : [...current, categoryId], + ); + }; + + const handleSelectCategory = (categoryId: string) => { + setQuickCategoryId(categoryId); + if (!categoryId) return; + + setSelectedCategories((current) => + current.includes(categoryId) ? current : [...current, categoryId], + ); + setQuickCategoryId(''); + }; const start = async () => { if (!areaId) return; const pickListId = uuidv4(); - await db.pickLists.add({ - id: pickListId, - area_id: areaId, - created_at: Date.now(), - notes: notes.trim() || undefined, + const timestamp = Date.now(); + + const selectedCategoryNames = categories + .filter((category) => selectedCategories.includes(category.id)) + .map((category) => category.name); + + await db.transaction('rw', db.pickLists, db.pickItems, db.products, async () => { + await db.pickLists.add({ + id: pickListId, + area_id: areaId, + created_at: timestamp, + notes: notes.trim() || undefined, + }); + + if (selectedCategoryNames.length === 0) { + return; + } + + const products = await db.products.toArray(); + const productsInCategories = products.filter( + (product) => selectedCategoryNames.includes(product.category) && !product.archived, + ); + + if (productsInCategories.length === 0) { + return; + } + + await db.pickItems.bulkAdd( + productsInCategories.map((product) => ({ + id: uuidv4(), + pick_list_id: pickListId, + product_id: product.id, + quantity: 1, + is_carton: false, + status: 'pending', + created_at: timestamp, + updated_at: timestamp, + })), + ); }); navigate(`/pick-lists/${pickListId}`); }; @@ -46,6 +119,37 @@ export const StartPickListScreen = () => { ))} + handleSelectCategory(event.target.value as string)} + > + None + {sortedCategories.map((category) => ( + + {category.name} + + ))} + + + Add categories to prefill products + + {sortedCategories.map((category) => ( + handleToggleCategory(category.id)} + /> + } + label={category.name} + /> + ))} + + Date: Sun, 23 Nov 2025 15:42:14 +1000 Subject: [PATCH 2/2] Remove category dropdown from pick list creation --- e2e/picklist.spec.ts | 3 +- src/screens/StartPickListScreen.test.tsx | 5 +--- src/screens/StartPickListScreen.tsx | 36 ++++-------------------- 3 files changed, 8 insertions(+), 36 deletions(-) diff --git a/e2e/picklist.spec.ts b/e2e/picklist.spec.ts index 421dd9b..945c423 100644 --- a/e2e/picklist.spec.ts +++ b/e2e/picklist.spec.ts @@ -13,8 +13,7 @@ test.describe('Active pick list', () => { await page.getByRole('link', { name: 'Create Pick List' }).click(); await page.getByLabel('Area').click(); await page.getByRole('option', { name: areaName }).first().click(); - await page.getByLabel('Category (optional)').click(); - await page.getByRole('option', { name: 'Chocolates' }).click(); + await page.getByRole('checkbox', { name: 'Chocolates' }).click(); await page.getByRole('checkbox', { name: 'Chips' }).click(); await page.getByRole('button', { name: 'Save Pick List' }).click(); diff --git a/src/screens/StartPickListScreen.test.tsx b/src/screens/StartPickListScreen.test.tsx index 6f442ad..58f6454 100644 --- a/src/screens/StartPickListScreen.test.tsx +++ b/src/screens/StartPickListScreen.test.tsx @@ -97,7 +97,6 @@ describe('StartPickListScreen', () => { , ); - expect(screen.getByLabelText(/category \(optional\)/i)).toBeVisible(); categoriesMock.forEach((category) => { expect(screen.getByRole('checkbox', { name: category.name })).toBeInTheDocument(); }); @@ -115,9 +114,7 @@ describe('StartPickListScreen', () => { await user.click(screen.getByLabelText(/area/i)); await user.click(screen.getByRole('option', { name: /front counter/i })); - await user.click(screen.getByLabelText(/category \(optional\)/i)); - await user.click(screen.getByRole('option', { name: /drinks/i })); - + await user.click(screen.getByRole('checkbox', { name: /drinks/i })); await user.click(screen.getByRole('checkbox', { name: /snacks/i })); await user.click(screen.getByRole('button', { name: /save pick list/i })); diff --git a/src/screens/StartPickListScreen.tsx b/src/screens/StartPickListScreen.tsx index 147510e..a4fa143 100644 --- a/src/screens/StartPickListScreen.tsx +++ b/src/screens/StartPickListScreen.tsx @@ -23,7 +23,6 @@ export const StartPickListScreen = () => { const [areaId, setAreaId] = useState(''); const [notes, setNotes] = useState(''); const [selectedCategories, setSelectedCategories] = useState([]); - const [quickCategoryId, setQuickCategoryId] = useState(''); const sortedCategories = useMemo( () => @@ -41,16 +40,6 @@ export const StartPickListScreen = () => { ); }; - const handleSelectCategory = (categoryId: string) => { - setQuickCategoryId(categoryId); - if (!categoryId) return; - - setSelectedCategories((current) => - current.includes(categoryId) ? current : [...current, categoryId], - ); - setQuickCategoryId(''); - }; - const start = async () => { if (!areaId) return; const pickListId = uuidv4(); @@ -120,19 +109,13 @@ export const StartPickListScreen = () => { ))} handleSelectCategory(event.target.value as string)} - > - None - {sortedCategories.map((category) => ( - - {category.name} - - ))} - + label="Notes (optional)" + value={notes} + onChange={(event) => setNotes(event.target.value)} + multiline + minRows={2} + /> Add categories to prefill products @@ -150,13 +133,6 @@ export const StartPickListScreen = () => { ))} - setNotes(event.target.value)} - multiline - minRows={2} - />