From 3a84537d4255263027b036a104b9f9448cd807de Mon Sep 17 00:00:00 2001 From: beatz174-bit Date: Fri, 21 Nov 2025 19:12:49 +1000 Subject: [PATCH 1/2] Prevent deletion of in-use records --- src/screens/ManageAreasScreen.test.tsx | 51 ++++++++++++++++++ src/screens/ManageAreasScreen.tsx | 15 ++++++ src/screens/ManageCategoriesScreen.test.tsx | 59 +++++++++++++++++++++ src/screens/ManageProductsScreen.test.tsx | 37 +++++++++++-- src/screens/ManageProductsScreen.tsx | 23 ++++++-- 5 files changed, 178 insertions(+), 7 deletions(-) create mode 100644 src/screens/ManageAreasScreen.test.tsx create mode 100644 src/screens/ManageCategoriesScreen.test.tsx diff --git a/src/screens/ManageAreasScreen.test.tsx b/src/screens/ManageAreasScreen.test.tsx new file mode 100644 index 0000000..d9689d1 --- /dev/null +++ b/src/screens/ManageAreasScreen.test.tsx @@ -0,0 +1,51 @@ +import { MemoryRouter } 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 { ManageAreasScreen } from './ManageAreasScreen'; + +const areasMock = [{ id: 'area-1', name: 'Front Counter', created_at: 0, updated_at: 0 }]; + +vi.mock('../hooks/dataHooks', () => ({ + useAreas: () => areasMock, +})); + +const areaDeleteMock = vi.fn(); +const pickListCountMock = vi.fn(); + +vi.mock('../context/DBProvider', () => ({ + useDatabase: () => ({ + areas: { + add: vi.fn(), + update: vi.fn(), + delete: areaDeleteMock, + }, + pickLists: { + where: () => ({ + equals: () => ({ + count: pickListCountMock, + }), + }), + }, + }), +})); + +describe('ManageAreasScreen deletion safeguards', () => { + it('prevents deleting an area that is used by pick lists', async () => { + pickListCountMock.mockResolvedValueOnce(1); + + const user = userEvent.setup(); + render( + + + , + ); + + await user.click(screen.getByRole('button', { name: /delete front counter/i })); + + expect(areaDeleteMock).not.toHaveBeenCalled(); + expect( + await screen.findByText(/cannot delete this area while 1 pick list\(s\) use it/i), + ).toBeVisible(); + }); +}); diff --git a/src/screens/ManageAreasScreen.tsx b/src/screens/ManageAreasScreen.tsx index 61cd5db..d84e4d8 100644 --- a/src/screens/ManageAreasScreen.tsx +++ b/src/screens/ManageAreasScreen.tsx @@ -1,4 +1,6 @@ import { + Alert, + AlertColor, Button, Container, IconButton, @@ -24,6 +26,7 @@ export const ManageAreasScreen = () => { const [name, setName] = useState(''); const [editingAreaId, setEditingAreaId] = useState(null); const [editName, setEditName] = useState(''); + const [feedback, setFeedback] = useState<{ text: string; severity: AlertColor } | null>(null); const addArea = async () => { if (!name) return; @@ -34,6 +37,7 @@ export const ManageAreasScreen = () => { const startEditing = (areaId: string, currentName: string) => { setEditingAreaId(areaId); setEditName(currentName); + setFeedback(null); }; const saveArea = async () => { @@ -41,6 +45,7 @@ export const ManageAreasScreen = () => { await db.areas.update(editingAreaId, { name: editName, updated_at: Date.now() }); setEditingAreaId(null); setEditName(''); + setFeedback({ text: 'Area updated.', severity: 'success' }); }; const cancelEditing = () => { @@ -49,10 +54,19 @@ export const ManageAreasScreen = () => { }; const deleteArea = async (areaId: string) => { + const usageCount = await db.pickLists.where('area_id').equals(areaId).count(); + if (usageCount > 0) { + setFeedback({ + text: `Cannot delete this area while ${usageCount} pick list(s) use it. Remove those lists first.`, + severity: 'error', + }); + return; + } await db.areas.delete(areaId); if (editingAreaId === areaId) { cancelEditing(); } + setFeedback({ text: 'Area deleted.', severity: 'success' }); }; return ( @@ -61,6 +75,7 @@ export const ManageAreasScreen = () => { Manage Areas + {feedback ? {feedback.text} : null} setName(event.target.value)} /> From d50c2a3dc8508a895b222bc35b703f34eaea9905 Mon Sep 17 00:00:00 2001 From: beatz174-bit Date: Fri, 21 Nov 2025 19:13:16 +1000 Subject: [PATCH 2/2] Prevent adding duplicate products to pick lists --- src/screens/AddItemScreen.tsx | 36 ++++++++++++++++++++++++++--------- 1 file changed, 27 insertions(+), 9 deletions(-) diff --git a/src/screens/AddItemScreen.tsx b/src/screens/AddItemScreen.tsx index eaaf84f..fb3fe4d 100644 --- a/src/screens/AddItemScreen.tsx +++ b/src/screens/AddItemScreen.tsx @@ -8,16 +8,17 @@ import { InputAdornment, } from '@mui/material'; import SearchIcon from '@mui/icons-material/Search'; -import { useMemo, useState } from 'react'; +import { useEffect, useMemo, useState } from 'react'; import { useNavigate, useParams } from 'react-router-dom'; import { v4 as uuidv4 } from 'uuid'; import { NumericStepper } from '../components/NumericStepper'; -import { useProducts } from '../hooks/dataHooks'; +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 [productId, setProductId] = useState(''); const [query, setQuery] = useState(''); @@ -25,16 +26,33 @@ export const AddItemScreen = () => { const [bulk, setBulk] = useState(0); const navigate = useNavigate(); - const filteredProducts = useMemo( - () => - products.filter((product) => - `${product.name} ${product.category}`.toLowerCase().includes(query.toLowerCase()), - ), - [products, query], + const existingProductIds = useMemo( + () => new Set(items.map((item) => item.product_id)), + [items], ); + const availableProducts = useMemo( + () => products.filter((product) => !existingProductIds.has(product.id)), + [existingProductIds, products], + ); + + const filteredProducts = useMemo( + () => + availableProducts.filter((product) => + `${product.name} ${product.category}`.toLowerCase().includes(query.toLowerCase()), + ), + [availableProducts, query], + ); + + useEffect(() => { + if (!productId) return; + if (!filteredProducts.some((product) => product.id === productId)) { + setProductId(''); + } + }, [filteredProducts, productId]); + const addItem = async () => { - if (!id || !productId) return; + if (!id || !productId || existingProductIds.has(productId)) return; await db.pickItems.add({ id: uuidv4(), pick_list_id: id,