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,
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)} />