From 234d11832e10d3490647db235bd3e012b4d0a158 Mon Sep 17 00:00:00 2001 From: beatzaplenty Date: Tue, 25 Nov 2025 14:08:30 +1000 Subject: [PATCH] new file: src/components/ProductAutocomplete.tsx modified: src/screens/ActivePickListScreen.tsx deleted: src/screens/ActivePickListScreen.tsx.bak modified: src/screens/StartPickListScreen.tsx --- src/components/ProductAutocomplete.tsx | 97 +++++ src/screens/ActivePickListScreen.tsx | 391 ++++++++---------- src/screens/ActivePickListScreen.tsx.bak | 495 ----------------------- src/screens/StartPickListScreen.tsx | 12 +- 4 files changed, 270 insertions(+), 725 deletions(-) create mode 100644 src/components/ProductAutocomplete.tsx delete mode 100644 src/screens/ActivePickListScreen.tsx.bak diff --git a/src/components/ProductAutocomplete.tsx b/src/components/ProductAutocomplete.tsx new file mode 100644 index 0000000..044fce0 --- /dev/null +++ b/src/components/ProductAutocomplete.tsx @@ -0,0 +1,97 @@ +import { + Autocomplete, + IconButton, + InputAdornment, + TextField, + Tooltip, +} from '@mui/material'; +import AddCircleOutlineIcon from '@mui/icons-material/AddCircleOutline'; +import SearchIcon from '@mui/icons-material/Search'; +import { Link as RouterLink } from 'react-router-dom'; +import { useMemo, useState, useEffect } from 'react'; +import { useCategories } from '../hooks/dataHooks'; +import { Product } from '../models/Product'; + +interface ProductAutocompleteProps { + availableProducts: Product[]; + onSelect: (product: Product) => void; + placeholder?: string; +} + +export const ProductAutocomplete = ({ + availableProducts, + onSelect, + placeholder = 'Search products', +}: ProductAutocompleteProps) => { + const categories = useCategories(); + const categoriesById = useMemo(() => new Map(categories.map((c) => [c.id, c.name])), [categories]); + + const [selectedProduct, setSelectedProduct] = useState(null); + const [query, setQuery] = useState(''); + + // When a product is chosen, call onSelect and clear the selection. + useEffect(() => { + if (!selectedProduct) return; + onSelect(selectedProduct); + setSelectedProduct(null); + setQuery(''); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [selectedProduct]); + + // If the availableProducts change such that the selectedProduct is no longer available, clear it + useEffect(() => { + if (!selectedProduct) return; + if (!availableProducts.some((p) => p.id === selectedProduct.id)) { + setSelectedProduct(null); + } + }, [availableProducts, selectedProduct]); + + return ( + + `${option.name} (${categoriesById.get(option.category) ?? option.category ?? ''})` + } + isOptionEqualToValue={(option, value) => option.id === value.id} + value={selectedProduct} + onChange={(_, value) => setSelectedProduct(value ?? null)} + inputValue={query} + onInputChange={(_, value, reason) => { + if (reason === 'input') setQuery(value); + if (reason === 'clear') setQuery(''); + }} + filterOptions={(options) => options} + noOptionsText="No available products" + fullWidth + renderInput={(params) => ( + + + + + {params.InputProps.startAdornment} + + ), + endAdornment: ( + <> + {params.InputProps.endAdornment} + + + + + + + + + ), + }} + /> + )} + /> + ); +}; diff --git a/src/screens/ActivePickListScreen.tsx b/src/screens/ActivePickListScreen.tsx index c3fc4bf..79707aa 100644 --- a/src/screens/ActivePickListScreen.tsx +++ b/src/screens/ActivePickListScreen.tsx @@ -1,5 +1,5 @@ +// src/screens/ActivePickListScreen.tsx import { - Autocomplete, Button, Checkbox, Container, @@ -15,16 +15,21 @@ import { RadioGroup, Radio, } from '@mui/material'; -import AddCircleOutlineIcon from '@mui/icons-material/AddCircleOutline'; -import SearchIcon from '@mui/icons-material/Search'; import { Link as RouterLink, 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 { + useAreas, + useCategories, + 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'; +import { ProductAutocomplete } from '../components/ProductAutocomplete'; const normalizeName = (name: string) => name.trim().toLowerCase(); @@ -34,19 +39,21 @@ export const ActivePickListScreen = () => { const items = usePickItems(id); const products = useProducts(); const areas = useAreas(); + const categoriesList = useCategories(); const db = useDatabase(); const navigate = useNavigate(); - const [selectedProduct, setSelectedProduct] = useState(null); + const [query, setQuery] = useState(''); const [showPicked, setShowPicked] = useState(true); - const [itemState, setItemState] = useState(items); + const [itemState, setItemState] = useState(items ?? []); const [isBatchUpdating, setIsBatchUpdating] = useState(false); const [packagingFilter, setPackagingFilter] = useState<'all' | 'units' | 'cartons'>('all'); + // Keep local item state in sync with DB-driven `items` useEffect(() => { - setItemState((current) => { - const currentById = new Map(current.map((item) => [item.id, item])); - return items.map((incoming) => { + setItemState((current: PickItem[]) => { + const currentById = new Map(current.map((item: PickItem) => [item.id, item])); + return (items ?? []).map((incoming: PickItem) => { const local = currentById.get(incoming.id); if (!local) return incoming; @@ -90,10 +97,9 @@ export const ActivePickListScreen = () => { const productMap = useMemo(() => { const map = new Map(); - products.forEach((product) => { + products.forEach((product: Product) => { map.set(product.id, product); }); - return map; }, [products]); @@ -105,7 +111,7 @@ export const ActivePickListScreen = () => { const sortedProducts = useMemo(() => { const dedupedById = new Map(); - products.forEach((product) => { + products.forEach((product: Product) => { const existing = dedupedById.get(product.id); if (!existing || product.updated_at > existing.updated_at) { dedupedById.set(product.id, product); @@ -114,7 +120,7 @@ export const ActivePickListScreen = () => { const dedupedByName = new Map(); - dedupedById.forEach((product) => { + dedupedById.forEach((product: Product) => { const normalizedName = product.name.trim().toLowerCase(); const existing = dedupedByName.get(normalizedName); @@ -139,51 +145,168 @@ export const ActivePickListScreen = () => { }); }, [products]); + // Build a category id -> name map for display and name -> id map for resolution + const categoriesById = useMemo(() => new Map(categoriesList.map((c) => [c.id, c.name])), [categoriesList]); + const categoryNameToId = useMemo(() => new Map(categoriesList.map((c) => [c.name.trim().toLowerCase(), c.id])), [categoriesList]); + + // Filter products by pickList.categories (now stored as ids). Fallback: resolve name -> id const categoryFilteredProducts = useMemo(() => { if (!pickList?.categories || pickList.categories.length === 0) { return sortedProducts; } - const allowedCategories = new Set( - pickList.categories.map((category) => category.trim().toLowerCase()), - ); + const categoryIdsKnown = new Set(categoriesList.map((c) => c.id)); + const allowedCategoryIds = new Set(); - return sortedProducts.filter((product) => - allowedCategories.has(product.category.trim().toLowerCase()), - ); - }, [pickList?.categories, sortedProducts]); + pickList.categories.forEach((entry: string) => { + if (!entry) return; + if (categoryIdsKnown.has(entry)) { + // entry already an id + allowedCategoryIds.add(entry); + } else { + // maybe it's a name (legacy) — resolve + const resolved = categoryNameToId.get(entry.trim().toLowerCase()); + if (resolved) allowedCategoryIds.add(resolved); + } + }); - const productIdsInList = useMemo( - () => new Set(itemState.map((item) => item.product_id)), - [itemState], - ); + if (allowedCategoryIds.size === 0) return sortedProducts; + + return sortedProducts.filter((product: Product) => allowedCategoryIds.has(product.category)); + }, [pickList?.categories, sortedProducts, categoriesList, categoryNameToId]); + + const productIdsInList = useMemo(() => new Set(itemState.map((item) => item.product_id)), [itemState]); const filteredProducts = useMemo(() => { const normalizedQuery = query.trim().toLowerCase(); const availableProducts = categoryFilteredProducts.filter( - (product) => !productIdsInList.has(product.id), + (product: Product) => !productIdsInList.has(product.id), ); if (!normalizedQuery) return availableProducts; - return availableProducts.filter((product) => { - const searchableText = `${product.name} ${product.category} ${product.barcode ?? ''}`.toLowerCase(); + return availableProducts.filter((product: Product) => { + const catName = categoriesById.get(product.category) ?? product.category ?? ''; + const searchableText = `${product.name} ${catName} ${product.barcode ?? ''}`.toLowerCase(); return searchableText.includes(normalizedQuery); }); - }, [categoryFilteredProducts, productIdsInList, query]); + }, [categoryFilteredProducts, productIdsInList, query, categoriesById]); - useEffect(() => { - if (!selectedProduct) return; - if (!filteredProducts.some((product) => product.id === selectedProduct.id)) { - setSelectedProduct(null); - } - }, [filteredProducts, selectedProduct]); + // If a filtered list leaves a previously-selected product missing, callers (ProductAutocomplete) will handle clear. + // --- Handlers (restored) --- - useEffect(() => { - if (allItemsPicked && !showPicked) { - setShowPicked(true); + const handleIncrementQuantity = async (itemId: string) => { + const existing = await db.pickItems.get(itemId); + if (!existing) return; + + const nextQuantity = existing.quantity + 1; + setItemState((current) => current.map((item) => (item.id === itemId ? { ...item, quantity: nextQuantity, updated_at: Date.now() } : item))); + await db.pickItems.update(itemId, { + quantity: nextQuantity, + updated_at: Date.now(), + }); + }; + + const handleDecrementQuantity = async (itemId: string) => { + const existing = await db.pickItems.get(itemId); + if (!existing) return; + + const nextQuantity = Math.max(1, (existing.quantity || 1) - 1); + setItemState((current) => current.map((item) => (item.id === itemId ? { ...item, quantity: nextQuantity, updated_at: Date.now() } : item))); + await db.pickItems.update(itemId, { + quantity: nextQuantity, + updated_at: Date.now(), + }); + }; + + const handleToggleCarton = async (itemId: string) => { + const existing = await db.pickItems.get(itemId); + if (!existing) return; + + const nextCartonFlag = !existing.is_carton; + const nextQuantity = existing.quantity || 1; + + setItemState((current) => current.map((item) => (item.id === itemId ? { ...item, is_carton: nextCartonFlag, quantity: nextQuantity, updated_at: Date.now() } : item))); + await db.pickItems.update(itemId, { + is_carton: nextCartonFlag, + quantity: nextQuantity, + updated_at: Date.now(), + }); + }; + + const handleStatusChange = async (itemId: string, status: PickItem['status']) => { + const nextStatus = status === 'picked' ? 'picked' : 'pending'; + setItemState((current) => current.map((item) => (item.id === itemId ? { ...item, status: nextStatus, updated_at: Date.now() } : item))); + await db.pickItems.update(itemId, { status: nextStatus, updated_at: Date.now() }); + }; + + const handleDeleteItem = async (itemId: string) => { + setItemState((current) => current.filter((item) => item.id !== itemId)); + await db.pickItems.delete(itemId); + }; + + const handleMarkAllPicked = async () => { + if (!id) return; + + setShowPicked(true); + const timestamp = Date.now(); + setItemState((current) => current.map((item) => ({ ...item, status: 'picked', updated_at: timestamp }))); + setIsBatchUpdating(true); + + // if itemState is empty at invocation, fall back to DB items + const itemsToUpdate = itemState.length > 0 ? itemState : (items ?? []); + + try { + await Promise.all(itemsToUpdate.map((item: PickItem) => db.pickItems.update(item.id, { status: 'picked', updated_at: timestamp }))); + const refreshedItems = await db.pickItems.where('pick_list_id').equals(id as string).toArray(); + setItemState(refreshedItems); + } finally { + setIsBatchUpdating(false); } - }, [allItemsPicked, showPicked]); + }; + + const addOrUpdateItem = async (product: Product) => { + if (!id) return; + + const timestamp = Date.now(); + const existing = itemState.find((item) => item.product_id === product.id && item.is_carton === false); + + if (existing) { + setItemState((current) => + current.map((item) => + item.id === existing.id + ? { ...item, quantity: item.quantity + 1, updated_at: timestamp } + : item, + ), + ); + await db.pickItems.update(existing.id, { + quantity: existing.quantity + 1, + updated_at: timestamp, + }); + } else { + const newItem: PickItem = { + id: uuidv4(), + pick_list_id: id as string, + product_id: product.id, + quantity: 1, + is_carton: false, + status: 'pending', + created_at: timestamp, + updated_at: timestamp, + }; + + setItemState((current) => [...current, newItem]); + await db.pickItems.add({ + ...newItem, + }); + } + + setQuery(''); + }; + + const returnToLists = () => { + navigate('/pick-lists'); + }; // Sort the items that are actually visible (after showPicked and packaging filter) const visibleItems = useMemo(() => { @@ -207,137 +330,6 @@ export const ActivePickListScreen = () => { return arr; }, [itemsAfterShowPicked, productMap, packagingFilter]); - const updateItemState = (itemId: string, updater: (item: PickItem) => PickItem) => { - setItemState((current) => current.map((item) => (item.id === itemId ? updater(item) : item))); - }; - - const handleIncrementQuantity = async (itemId: string) => { - const existing = await db.pickItems.get(itemId); - if (!existing) return; - - const nextQuantity = existing.quantity + 1; - updateItemState(itemId, (item) => ({ ...item, quantity: nextQuantity, updated_at: Date.now() })); - await db.pickItems.update(itemId, { - quantity: nextQuantity, - updated_at: Date.now(), - }); - }; - - const handleDecrementQuantity = async (itemId: string) => { - const existing = await db.pickItems.get(itemId); - if (!existing) return; - - const nextQuantity = Math.max(1, (existing.quantity || 1) - 1); - - updateItemState(itemId, (item) => ({ ...item, quantity: nextQuantity, updated_at: Date.now() })); - await db.pickItems.update(itemId, { - quantity: nextQuantity, - updated_at: Date.now(), - }); - }; - - const handleToggleCarton = async (itemId: string) => { - const existing = await db.pickItems.get(itemId); - if (!existing) return; - - const nextCartonFlag = !existing.is_carton; - const nextQuantity = existing.quantity || 1; - - updateItemState(itemId, (item) => ({ - ...item, - is_carton: nextCartonFlag, - quantity: nextQuantity, - updated_at: Date.now(), - })); - await db.pickItems.update(itemId, { - is_carton: nextCartonFlag, - quantity: nextQuantity, - updated_at: Date.now(), - }); - }; - - const handleStatusChange = async (itemId: string, status: PickItem['status']) => { - const nextStatus = status === 'picked' ? 'picked' : 'pending'; - updateItemState(itemId, (item) => ({ ...item, status: nextStatus, updated_at: Date.now() })); - await db.pickItems.update(itemId, { status: nextStatus, updated_at: Date.now() }); - }; - - const handleDeleteItem = async (itemId: string) => { - setItemState((current) => current.filter((item) => item.id !== itemId)); - await db.pickItems.delete(itemId); - }; - - const handleMarkAllPicked = async () => { - if (!id) return; - - setShowPicked(true); - const timestamp = Date.now(); - setItemState((current) => - current.map((item) => ({ ...item, status: 'picked', updated_at: timestamp })), - ); - setIsBatchUpdating(true); - - const itemsToUpdate = itemState.length > 0 ? itemState : items; - - try { - await Promise.all( - itemsToUpdate.map((item) => - db.pickItems.update(item.id, { status: 'picked', updated_at: timestamp }), - ), - ); - const refreshedItems = await db.pickItems.where('pick_list_id').equals(id).toArray(); - setItemState(refreshedItems); - } finally { - setIsBatchUpdating(false); - } - }; - - const addOrUpdateItem = async (product: Product) => { - if (!id) return; - - const timestamp = Date.now(); - const existing = itemState.find( - (item) => item.product_id === product.id && item.is_carton === false, - ); - - if (existing) { - setItemState((current) => - current.map((item) => - item.id === existing.id - ? { ...item, quantity: item.quantity + 1, updated_at: timestamp } - : item, - ), - ); - await db.pickItems.update(existing.id, { - quantity: existing.quantity + 1, - updated_at: timestamp, - }); - } else { - const newItem: PickItem = { - id: uuidv4(), - pick_list_id: id, - product_id: product.id, - quantity: 1, - is_carton: false, - status: 'pending', - created_at: timestamp, - updated_at: timestamp, - }; - - setItemState((current) => [...current, newItem]); - await db.pickItems.add({ - ...newItem, - }); - } - - setSelectedProduct(null); - setQuery(''); - }; - - const returnToLists = () => { - navigate('/pick-lists'); - }; - return ( @@ -346,65 +338,14 @@ export const ActivePickListScreen = () => { 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(''); - } + { + void addOrUpdateItem(product); }} - filterOptions={(options) => options} - noOptionsText="No available products" - fullWidth - renderInput={(params) => ( - - - - - {params.InputProps.startAdornment} - - ), - endAdornment: ( - <> - {params.InputProps.endAdornment} - - - - - - - - - ), - }} - /> - )} /> + {filteredProducts.length === 0 ? ( No available products diff --git a/src/screens/ActivePickListScreen.tsx.bak b/src/screens/ActivePickListScreen.tsx.bak deleted file mode 100644 index e5725b9..0000000 --- a/src/screens/ActivePickListScreen.tsx.bak +++ /dev/null @@ -1,495 +0,0 @@ -import { - Autocomplete, - Button, - Checkbox, - Container, - FormControl, - FormControlLabel, - IconButton, - InputAdornment, - Radio, - RadioGroup, - Stack, - TextField, - Tooltip, - Typography, -} from '@mui/material'; -import AddCircleOutlineIcon from '@mui/icons-material/AddCircleOutline'; -import SearchIcon from '@mui/icons-material/Search'; -import { Link as RouterLink, 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'; - -const normalizeName = (name: string) => name.trim().toLowerCase(); - -export const ActivePickListScreen = () => { - const { id } = useParams(); - const pickList = usePickList(id); - const items = usePickItems(id); - const products = useProducts(); - const areas = useAreas(); - const db = useDatabase(); - const navigate = useNavigate(); - const [selectedProduct, setSelectedProduct] = useState(null); - const [query, setQuery] = useState(''); - const [itemFilter, setItemFilter] = useState<'all' | 'cartons' | 'units'>('all'); - const [showPicked, setShowPicked] = useState(true); - const [itemState, setItemState] = useState(items); - - useEffect(() => { - setItemState(items); - }, [items]); - - const itemsVisibleByStatus = useMemo( - () => (showPicked ? itemState : itemState.filter((item) => item.status !== 'picked')), - [itemState, showPicked], - ); - - const hasPickedItemsVisible = useMemo( - () => itemsVisibleByStatus.some((item) => item.status === 'picked'), - [itemsVisibleByStatus], - ); - const hasUnpickedItemsVisible = useMemo( - () => itemsVisibleByStatus.some((item) => item.status !== 'picked'), - [itemsVisibleByStatus], - ); - const hasCartonItems = useMemo( - () => itemsVisibleByStatus.some((item) => item.is_carton), - [itemsVisibleByStatus], - ); - const hasUnitItems = useMemo( - () => itemsVisibleByStatus.some((item) => !item.is_carton), - [itemsVisibleByStatus], - ); - const allItemsPicked = useMemo( - () => itemState.length > 0 && itemState.every((item) => item.status === 'picked'), - [itemState], - ); - const packagingFiltersDisabled = useMemo( - () => !showPicked || (hasPickedItemsVisible && hasUnpickedItemsVisible), - [hasPickedItemsVisible, hasUnpickedItemsVisible, showPicked], - ); - - const productMap = useMemo(() => { - const map = new Map(); - products.forEach((product) => { - map.set(product.id, product); - }); - - return map; - }, [products]); - - const areaName = useMemo( - () => areas.find((area) => area.id === pickList?.area_id)?.name ?? 'Area', - [areas, pickList?.area_id], - ); - - const sortedItems = useMemo(() => { - return [...itemsVisibleByStatus].sort((a, b) => { - const timeA = a.created_at ?? a.updated_at ?? 0; - const timeB = b.created_at ?? b.updated_at ?? 0; - - if (timeA !== timeB) { - return timeA - timeB; - } - - const nameA = normalizeName(productMap.get(a.product_id)?.name ?? ''); - const nameB = normalizeName(productMap.get(b.product_id)?.name ?? ''); - - return nameA.localeCompare(nameB, undefined, { sensitivity: 'base' }); - }); - }, [itemsVisibleByStatus, productMap]); - - const sortedProducts = useMemo(() => { - const dedupedById = new Map(); - - products.forEach((product) => { - const existing = dedupedById.get(product.id); - if (!existing || product.updated_at > existing.updated_at) { - dedupedById.set(product.id, product); - } - }); - - const dedupedByName = new Map(); - - dedupedById.forEach((product) => { - const normalizedName = product.name.trim().toLowerCase(); - const existing = dedupedByName.get(normalizedName); - - if (!existing || product.updated_at > existing.updated_at) { - dedupedByName.set(normalizedName, product); - } - }); - - return Array.from(dedupedByName.values()).sort((a, b) => { - const normalizedNameA = normalizeName(a.name); - const normalizedNameB = normalizeName(b.name); - - const nameComparison = normalizedNameA.localeCompare(normalizedNameB, undefined, { - sensitivity: 'base', - }); - - if (nameComparison !== 0) { - return nameComparison; - } - - return a.name.localeCompare(b.name, undefined, { sensitivity: 'base' }); - }); - }, [products]); - - const categoryFilteredProducts = useMemo(() => { - if (!pickList?.categories || pickList.categories.length === 0) { - return sortedProducts; - } - - const allowedCategories = new Set( - pickList.categories.map((category) => category.trim().toLowerCase()), - ); - - return sortedProducts.filter((product) => - allowedCategories.has(product.category.trim().toLowerCase()), - ); - }, [pickList?.categories, sortedProducts]); - - const appliedItemFilter = useMemo(() => { - if (packagingFiltersDisabled) { - return 'all'; - } - - if (itemFilter === 'cartons' && !hasCartonItems) { - return hasUnitItems ? 'units' : 'all'; - } - - if (itemFilter === 'units' && !hasUnitItems) { - return hasCartonItems ? 'cartons' : 'all'; - } - - return itemFilter; - }, [hasCartonItems, hasUnitItems, itemFilter, packagingFiltersDisabled]); - - const productIdsInList = useMemo( - () => new Set(itemState.map((item) => item.product_id)), - [itemState], - ); - - const filteredProducts = useMemo(() => { - const normalizedQuery = query.trim().toLowerCase(); - const availableProducts = categoryFilteredProducts.filter( - (product) => !productIdsInList.has(product.id), - ); - - if (!normalizedQuery) return availableProducts; - - return availableProducts.filter((product) => { - const searchableText = `${product.name} ${product.category} ${product.barcode ?? ''}`.toLowerCase(); - return searchableText.includes(normalizedQuery); - }); - }, [categoryFilteredProducts, productIdsInList, query]); - - useEffect(() => { - if (!selectedProduct) return; - if (!filteredProducts.some((product) => product.id === selectedProduct.id)) { - setSelectedProduct(null); - } - }, [filteredProducts, selectedProduct]); - - useEffect(() => { - if (allItemsPicked && !showPicked) { - setShowPicked(true); - } - }, [allItemsPicked, showPicked]); - - useEffect(() => { - setItemFilter((current) => (current === appliedItemFilter ? current : appliedItemFilter)); - }, [appliedItemFilter]); - - const visibleItems = useMemo(() => { - const filteredItems = showPicked - ? sortedItems - : sortedItems.filter((item) => item.status !== 'picked'); - - if (appliedItemFilter === 'cartons') { - return filteredItems.filter((item) => item.is_carton); - } - - if (appliedItemFilter === 'units') { - return filteredItems.filter((item) => !item.is_carton); - } - - return filteredItems; - }, [appliedItemFilter, showPicked, sortedItems]); - - const updateItemState = (itemId: string, updater: (item: PickItem) => PickItem) => { - setItemState((current) => current.map((item) => (item.id === itemId ? updater(item) : item))); - }; - - const handleIncrementQuantity = async (itemId: string) => { - const existing = await db.pickItems.get(itemId); - if (!existing) return; - - const nextQuantity = existing.quantity + 1; - updateItemState(itemId, (item) => ({ ...item, quantity: nextQuantity, updated_at: Date.now() })); - await db.pickItems.update(itemId, { - quantity: nextQuantity, - updated_at: Date.now(), - }); - }; - - const handleDecrementQuantity = async (itemId: string) => { - const existing = await db.pickItems.get(itemId); - if (!existing) return; - - const nextQuantity = Math.max(1, (existing.quantity || 1) - 1); - - updateItemState(itemId, (item) => ({ ...item, quantity: nextQuantity, updated_at: Date.now() })); - await db.pickItems.update(itemId, { - quantity: nextQuantity, - updated_at: Date.now(), - }); - }; - - const handleToggleCarton = async (itemId: string) => { - const existing = await db.pickItems.get(itemId); - if (!existing) return; - - const nextCartonFlag = !existing.is_carton; - const nextQuantity = existing.quantity || 1; - - updateItemState(itemId, (item) => ({ - ...item, - is_carton: nextCartonFlag, - quantity: nextQuantity, - updated_at: Date.now(), - })); - await db.pickItems.update(itemId, { - is_carton: nextCartonFlag, - quantity: nextQuantity, - updated_at: Date.now(), - }); - }; - - const handleStatusChange = async (itemId: string, status: PickItem['status']) => { - const nextStatus = status === 'picked' ? 'picked' : 'pending'; - updateItemState(itemId, (item) => ({ ...item, status: nextStatus, updated_at: Date.now() })); - await db.pickItems.update(itemId, { status: nextStatus, updated_at: Date.now() }); - }; - - const handleDeleteItem = async (itemId: string) => { - setItemState((current) => current.filter((item) => item.id !== itemId)); - await db.pickItems.delete(itemId); - }; - - const handleMarkAllPicked = async () => { - setShowPicked(true); - const timestamp = Date.now(); - setItemState((current) => - current.map((item) => ({ ...item, status: 'picked', updated_at: timestamp })), - ); - await Promise.all( - itemState.map((item) => - db.pickItems.update(item.id, { status: 'picked', updated_at: timestamp }), - ), - ); - }; - - const addOrUpdateItem = async (product: Product) => { - if (!id) return; - - const timestamp = Date.now(); - const existing = itemState.find( - (item) => item.product_id === product.id && item.is_carton === false, - ); - - if (existing) { - setItemState((current) => - current.map((item) => - item.id === existing.id - ? { ...item, quantity: item.quantity + 1, updated_at: timestamp } - : item, - ), - ); - await db.pickItems.update(existing.id, { - quantity: existing.quantity + 1, - updated_at: timestamp, - }); - } else { - const newItem: PickItem = { - id: uuidv4(), - pick_list_id: id, - product_id: product.id, - quantity: 1, - is_carton: false, - status: 'pending', - created_at: timestamp, - updated_at: timestamp, - }; - - setItemState((current) => [...current, newItem]); - await db.pickItems.add({ - ...newItem, - }); - } - - setSelectedProduct(null); - setQuery(''); - }; - - const returnToLists = () => { - navigate('/pick-lists'); - }; - - 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="No available products" - fullWidth - renderInput={(params) => ( - - - - - {params.InputProps.startAdornment} - - ), - endAdornment: ( - <> - {params.InputProps.endAdornment} - - - - - - - - - ), - }} - /> - )} - /> - {filteredProducts.length === 0 ? ( - - No available products - - ) : null} - - Selecting a product immediately adds it to the pick list. - - - - Filter list by packaging - - - setItemFilter(value as 'all' | 'cartons' | 'units')} - sx={{ flexGrow: 1 }} - > - } label="All" /> - } - label="Cartons" - disabled={packagingFiltersDisabled} - /> - } - label="Units" - disabled={packagingFiltersDisabled} - /> - - - setShowPicked(event.target.checked)} - disabled={allItemsPicked} - /> - } - label="Show picked" - /> - - - - - - - {pickList?.notes ? ( - - {pickList.notes} - - ) : null} - - {visibleItems.map((item) => ( - handleIncrementQuantity(item.id)} - onDecrementQuantity={() => handleDecrementQuantity(item.id)} - onToggleCarton={() => handleToggleCarton(item.id)} - onStatusChange={(status) => handleStatusChange(item.id, status)} - onDelete={() => handleDeleteItem(item.id)} - /> - ))} - - - - ); -}; diff --git a/src/screens/StartPickListScreen.tsx b/src/screens/StartPickListScreen.tsx index d6d9e51..4f3fe32 100644 --- a/src/screens/StartPickListScreen.tsx +++ b/src/screens/StartPickListScreen.tsx @@ -56,9 +56,10 @@ export const StartPickListScreen = () => { const pickListId = uuidv4(); const timestamp = Date.now(); - const selectedCategoryNames = categories + // NOTE: Persist category *ids* (not names) + const selectedCategoryIds = categories .filter((category) => selectedCategories.includes(category.id)) - .map((category) => category.name); + .map((category) => category.id); await db.transaction('rw', db.pickLists, db.pickItems, db.products, async () => { await db.pickLists.add({ @@ -66,17 +67,18 @@ export const StartPickListScreen = () => { area_id: areaId, created_at: timestamp, notes: notes.trim() || undefined, - categories: selectedCategoryNames, + categories: selectedCategoryIds, auto_add_new_products: autoAddNewProducts, }); - if (selectedCategoryNames.length === 0) { + if (selectedCategoryIds.length === 0) { return; } const products = await db.products.toArray(); + // Match product.category to selectedCategoryIds (product.category is an id) const productsInCategories = products.filter( - (product) => selectedCategoryNames.includes(product.category) && !product.archived, + (product) => selectedCategoryIds.includes(product.category) && !product.archived, ); const uniqueProducts: typeof productsInCategories = [];