From 3a7914995609646f31cd9d75a14db46c0a0e366a Mon Sep 17 00:00:00 2001 From: beatzaplenty Date: Mon, 24 Nov 2025 21:10:17 +1000 Subject: [PATCH] modified: src/screens/ActivePickListScreen.tsx new file: src/screens/ActivePickListScreen.tsx.bak --- src/screens/ActivePickListScreen.tsx | 105 ++--- src/screens/ActivePickListScreen.tsx.bak | 495 +++++++++++++++++++++++ 2 files changed, 552 insertions(+), 48 deletions(-) create mode 100644 src/screens/ActivePickListScreen.tsx.bak diff --git a/src/screens/ActivePickListScreen.tsx b/src/screens/ActivePickListScreen.tsx index e5725b9..0a187ad 100644 --- a/src/screens/ActivePickListScreen.tsx +++ b/src/screens/ActivePickListScreen.tsx @@ -45,32 +45,46 @@ export const ActivePickListScreen = () => { setItemState(items); }, [items]); - const itemsVisibleByStatus = useMemo( + // Reworked visible/filter logic: + // - Keep `itemFilter` as the single source-of-truth for the radio selection. + // - Compute items after applying `showPicked` and `itemFilter`, and derive + // disabled flags from the actual visible items to avoid races / feedback loops. + const itemsAfterShowPicked = useMemo( () => (showPicked ? itemState : itemState.filter((item) => item.status !== 'picked')), [itemState, showPicked], ); + const hasCartonItemsOverall = useMemo( + () => itemsAfterShowPicked.some((item) => item.is_carton), + [itemsAfterShowPicked], + ); + const hasUnitItemsOverall = useMemo( + () => itemsAfterShowPicked.some((item) => !item.is_carton), + [itemsAfterShowPicked], + ); + + const itemsAfterPackagingFilter = useMemo(() => { + if (itemFilter === 'cartons') return itemsAfterShowPicked.filter((item) => item.is_carton); + if (itemFilter === 'units') return itemsAfterShowPicked.filter((item) => !item.is_carton); + return itemsAfterShowPicked; + }, [itemsAfterShowPicked, itemFilter]); + const hasPickedItemsVisible = useMemo( - () => itemsVisibleByStatus.some((item) => item.status === 'picked'), - [itemsVisibleByStatus], + () => itemsAfterPackagingFilter.some((item) => item.status === 'picked'), + [itemsAfterPackagingFilter], ); 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], + () => itemsAfterPackagingFilter.some((item) => item.status !== 'picked'), + [itemsAfterPackagingFilter], ); + const allItemsPicked = useMemo( () => itemState.length > 0 && itemState.every((item) => item.status === 'picked'), [itemState], ); + const packagingFiltersDisabled = useMemo( + // packaging filters disabled when showPicked is false, or visible items contain mixed statuses () => !showPicked || (hasPickedItemsVisible && hasUnpickedItemsVisible), [hasPickedItemsVisible, hasUnpickedItemsVisible, showPicked], ); @@ -89,8 +103,9 @@ export const ActivePickListScreen = () => { [areas, pickList?.area_id], ); + // Keep a sorted list for available items after applying showPicked (used for other UX) const sortedItems = useMemo(() => { - return [...itemsVisibleByStatus].sort((a, b) => { + return [...itemsAfterShowPicked].sort((a, b) => { const timeA = a.created_at ?? a.updated_at ?? 0; const timeB = b.created_at ?? b.updated_at ?? 0; @@ -103,7 +118,7 @@ export const ActivePickListScreen = () => { return nameA.localeCompare(nameB, undefined, { sensitivity: 'base' }); }); - }, [itemsVisibleByStatus, productMap]); + }, [itemsAfterShowPicked, productMap]); const sortedProducts = useMemo(() => { const dedupedById = new Map(); @@ -156,22 +171,6 @@ export const ActivePickListScreen = () => { ); }, [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], @@ -204,25 +203,34 @@ export const ActivePickListScreen = () => { } }, [allItemsPicked, showPicked]); + // Sanitize itemFilter whenever availability changes or packaging is disabled. useEffect(() => { - setItemFilter((current) => (current === appliedItemFilter ? current : appliedItemFilter)); - }, [appliedItemFilter]); + if (packagingFiltersDisabled) { + if (itemFilter !== 'all') setItemFilter('all'); + return; + } + if (itemFilter === 'cartons' && !hasCartonItemsOverall) { + setItemFilter(hasUnitItemsOverall ? 'units' : 'all'); + } else if (itemFilter === 'units' && !hasUnitItemsOverall) { + setItemFilter(hasCartonItemsOverall ? 'cartons' : 'all'); + } + }, [packagingFiltersDisabled, itemFilter, hasCartonItemsOverall, hasUnitItemsOverall]); + + // Sort the items that are actually visible (after showPicked + packaging filter) 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]); + // itemsAfterPackagingFilter is already computed after showPicked & packaging + const arr = [...itemsAfterPackagingFilter]; + arr.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' }); + }); + return arr; + }, [itemsAfterPackagingFilter, productMap]); const updateItemState = (itemId: string, updater: (item: PickItem) => PickItem) => { setItemState((current) => current.map((item) => (item.id === itemId ? updater(item) : item))); @@ -431,7 +439,8 @@ export const ActivePickListScreen = () => { > setItemFilter(value as 'all' | 'cartons' | 'units')} sx={{ flexGrow: 1 }} > diff --git a/src/screens/ActivePickListScreen.tsx.bak b/src/screens/ActivePickListScreen.tsx.bak new file mode 100644 index 0000000..e5725b9 --- /dev/null +++ b/src/screens/ActivePickListScreen.tsx.bak @@ -0,0 +1,495 @@ +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)} + /> + ))} + + + + ); +};