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'; 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 allItemsPicked = useMemo( () => itemState.length > 0 && itemState.every((item) => item.status === 'picked'), [itemState], ); 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 visibleItemsByStatus = useMemo( () => (showPicked ? itemState : itemState.filter((item) => item.status !== 'picked')), [itemState, showPicked], ); const sortedItems = useMemo(() => { return [...visibleItemsByStatus].sort((a, b) => { const productA = productMap.get(a.product_id); const productB = productMap.get(b.product_id); const nameA = productA?.name.trim().toLowerCase() ?? ''; const nameB = productB?.name.trim().toLowerCase() ?? ''; const nameComparison = nameA.localeCompare(nameB, undefined, { sensitivity: 'base' }); if (nameComparison !== 0) { return nameComparison; } if (a.is_carton !== b.is_carton) { return a.is_carton ? 1 : -1; } const timeA = a.updated_at ?? a.created_at; const timeB = b.updated_at ?? b.created_at; return timeA - timeB; }); }, [visibleItemsByStatus, 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) => 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 hasCartonItems = useMemo( () => visibleItemsByStatus.some((item) => item.is_carton), [visibleItemsByStatus], ); const hasUnitItems = useMemo( () => visibleItemsByStatus.some((item) => !item.is_carton), [visibleItemsByStatus], ); const packagingTypeCount = Number(hasCartonItems) + Number(hasUnitItems); const singlePackagingType = packagingTypeCount === 1; 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(() => { if (packagingTypeCount <= 1) { if (itemFilter !== 'all') { setItemFilter('all'); } return; } if (itemFilter === 'cartons' && !hasCartonItems) { setItemFilter('units'); } else if (itemFilter === 'units' && !hasUnitItems) { setItemFilter('cartons'); } }, [itemFilter, hasCartonItems, hasUnitItems, packagingTypeCount]); const visibleItems = useMemo(() => { let filteredItems = showPicked ? sortedItems : sortedItems.filter((item) => item.status !== 'picked'); if (itemFilter === 'cartons') { return filteredItems.filter((item) => item.is_carton); } if (itemFilter === 'units') { return filteredItems.filter((item) => !item.is_carton); } return filteredItems; }, [itemFilter, 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(event.target.value as 'all' | 'cartons' | 'units') } sx={{ flexGrow: 1 }} > } label="All" /> } label="Cartons" disabled={singlePackagingType || !hasCartonItems} /> } label="Units" disabled={singlePackagingType || !hasUnitItems} /> 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)} /> ))} ); };