modified: src/screens/ActivePickListScreen.tsx

This commit is contained in:
2025-11-25 14:35:22 +10:00
parent 234d11832e
commit 2e69cd9d0c
2 changed files with 163 additions and 33 deletions
+33 -18
View File
@@ -68,7 +68,6 @@ export const PickItemRow = ({
const handleRowClick = () => { const handleRowClick = () => {
if (!isNarrowScreen || isControlsOpen) return; if (!isNarrowScreen || isControlsOpen) return;
setIsControlsOpen(true); setIsControlsOpen(true);
}; };
@@ -84,7 +83,6 @@ export const PickItemRow = ({
const handleRowKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => { const handleRowKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {
if (!isNarrowScreen) return; if (!isNarrowScreen) return;
if (event.key === 'Enter' || event.key === ' ') { if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault(); event.preventDefault();
setIsControlsOpen(true); setIsControlsOpen(true);
@@ -134,19 +132,18 @@ export const PickItemRow = ({
component="span" component="span"
variant="subtitle1" variant="subtitle1"
noWrap noWrap
sx={{ minWidth: 0, overflow: 'hidden', textOverflow: 'ellipsis', fontWeight: 600 }} sx={{
minWidth: 0,
overflow: 'hidden',
textOverflow: 'ellipsis',
fontWeight: 600,
whiteSpace: 'nowrap',
}}
> >
{product?.name ?? 'Unknown product'} {`${item.quantity} x ${product?.name ?? 'Unknown product'}`}
</Typography>
<Typography
component="span"
variant="subtitle1"
noWrap
sx={{ fontWeight: 600, whiteSpace: 'nowrap' }}
>
Qty: {item.quantity} {packagingLabel}
</Typography> </Typography>
</Stack> </Stack>
{isNarrowScreen && ( {isNarrowScreen && (
<Typography variant="caption" color="text.secondary" noWrap> <Typography variant="caption" color="text.secondary" noWrap>
Tap to adjust quantity and packaging Tap to adjust quantity and packaging
@@ -154,6 +151,7 @@ export const PickItemRow = ({
)} )}
</Stack> </Stack>
</Stack> </Stack>
{isNarrowScreen ? ( {isNarrowScreen ? (
<IconButton aria-label="Open item controls" onClick={openControls}> <IconButton aria-label="Open item controls" onClick={openControls}>
<MoreHoriz /> <MoreHoriz />
@@ -175,6 +173,7 @@ export const PickItemRow = ({
> >
<Inventory2 /> <Inventory2 />
</IconButton> </IconButton>
<IconButton <IconButton
aria-label="Decrease quantity" aria-label="Decrease quantity"
color="primary" color="primary"
@@ -185,6 +184,7 @@ export const PickItemRow = ({
> >
<Remove /> <Remove />
</IconButton> </IconButton>
<IconButton <IconButton
aria-label="Increase quantity" aria-label="Increase quantity"
color="primary" color="primary"
@@ -195,6 +195,7 @@ export const PickItemRow = ({
> >
<Add /> <Add />
</IconButton> </IconButton>
<IconButton <IconButton
color="error" color="error"
onClick={(event) => { onClick={(event) => {
@@ -220,21 +221,32 @@ export const PickItemRow = ({
id="item-controls-title" id="item-controls-title"
sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }} sx={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}
> >
<Typography variant="h6" noWrap sx={{ minWidth: 0, flex: 1 }}> <Typography
{product?.name ?? 'Unknown product'} component="span"
</Typography> variant="subtitle1"
<IconButton noWrap
aria-label="Close controls" sx={{
onClick={handleCloseControls} minWidth: 0,
overflow: 'hidden',
textOverflow: 'ellipsis',
fontWeight: 600,
whiteSpace: 'nowrap',
}}
> >
{`${item.quantity} x ${product?.name ?? 'Unknown product'}`}
</Typography>
<IconButton aria-label="Close controls" onClick={handleCloseControls}>
<Close /> <Close />
</IconButton> </IconButton>
</DialogTitle> </DialogTitle>
<DialogContent> <DialogContent>
<Stack spacing={2} alignItems="stretch"> <Stack spacing={2} alignItems="stretch">
<Typography variant="subtitle1" sx={{ fontWeight: 700 }}> <Typography variant="subtitle1" sx={{ fontWeight: 700 }}>
Quantity: {item.quantity} {packagingLabel} Quantity: {item.quantity} {packagingLabel}
</Typography> </Typography>
<Stack direction="row" spacing={1} justifyContent="center" flexWrap="wrap"> <Stack direction="row" spacing={1} justifyContent="center" flexWrap="wrap">
<IconButton <IconButton
aria-label={`Switch to ${item.is_carton ? 'unit' : 'carton'} packaging`} aria-label={`Switch to ${item.is_carton ? 'unit' : 'carton'} packaging`}
@@ -251,6 +263,7 @@ export const PickItemRow = ({
> >
<Inventory2 /> <Inventory2 />
</IconButton> </IconButton>
<IconButton <IconButton
aria-label="Decrease quantity" aria-label="Decrease quantity"
color="primary" color="primary"
@@ -261,6 +274,7 @@ export const PickItemRow = ({
> >
<Remove /> <Remove />
</IconButton> </IconButton>
<IconButton <IconButton
aria-label="Increase quantity" aria-label="Increase quantity"
color="primary" color="primary"
@@ -271,6 +285,7 @@ export const PickItemRow = ({
> >
<Add /> <Add />
</IconButton> </IconButton>
<IconButton <IconButton
color="error" color="error"
onClick={(event) => { onClick={(event) => {
+130 -15
View File
@@ -15,6 +15,7 @@ import {
RadioGroup, RadioGroup,
Radio, Radio,
} from '@mui/material'; } from '@mui/material';
import SearchIcon from '@mui/icons-material/Search';
import { Link as RouterLink, useParams, useNavigate } from 'react-router-dom'; import { Link as RouterLink, useParams, useNavigate } from 'react-router-dom';
import { useEffect, useMemo, useState } from 'react'; import { useEffect, useMemo, useState } from 'react';
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
@@ -49,6 +50,10 @@ export const ActivePickListScreen = () => {
const [isBatchUpdating, setIsBatchUpdating] = useState(false); const [isBatchUpdating, setIsBatchUpdating] = useState(false);
const [packagingFilter, setPackagingFilter] = useState<'all' | 'units' | 'cartons'>('all'); const [packagingFilter, setPackagingFilter] = useState<'all' | 'units' | 'cartons'>('all');
// NEW: search within the list and category filter for the visible list
const [listSearch, setListSearch] = useState('');
const [categoryFilter, setCategoryFilter] = useState<'all' | string>('all');
// Keep local item state in sync with DB-driven `items` // Keep local item state in sync with DB-driven `items`
useEffect(() => { useEffect(() => {
setItemState((current: PickItem[]) => { setItemState((current: PickItem[]) => {
@@ -179,9 +184,7 @@ export const ActivePickListScreen = () => {
const filteredProducts = useMemo(() => { const filteredProducts = useMemo(() => {
const normalizedQuery = query.trim().toLowerCase(); const normalizedQuery = query.trim().toLowerCase();
const availableProducts = categoryFilteredProducts.filter( const availableProducts = categoryFilteredProducts.filter((product: Product) => !productIdsInList.has(product.id));
(product: Product) => !productIdsInList.has(product.id),
);
if (!normalizedQuery) return availableProducts; if (!normalizedQuery) return availableProducts;
@@ -330,6 +333,62 @@ export const ActivePickListScreen = () => {
return arr; return arr;
}, [itemsAfterShowPicked, productMap, packagingFilter]); }, [itemsAfterShowPicked, productMap, packagingFilter]);
// --- NEW: build category options from pickList.categories (fallback to all categories) ---
const categoryOptions = useMemo(() => {
// helper maps
const idToName = new Map(categoriesList.map((c) => [c.id, c.name]));
const nameToId = new Map(categoriesList.map((c) => [c.name.trim().toLowerCase(), c.id]));
if (!pickList?.categories || pickList.categories.length === 0) {
// fallback: show all categories
return categoriesList
.map((c) => ({ id: c.id, name: c.name }))
.sort((a, b) => a.name.localeCompare(b.name, undefined, { sensitivity: 'base' }));
}
// Build from pickList.categories while resolving legacy names -> ids
const seen = new Map<string, string>();
pickList.categories.forEach((entry: string) => {
if (!entry) return;
if (idToName.has(entry)) {
seen.set(entry, idToName.get(entry)!);
} else {
const resolved = nameToId.get(entry.trim().toLowerCase());
if (resolved) {
seen.set(resolved, idToName.get(resolved)!);
}
}
});
// Convert to array and sort by name
return Array.from(seen.entries())
.map(([id, name]) => ({ id, name }))
.sort((a, b) => a.name.localeCompare(b.name, undefined, { sensitivity: 'base' }));
}, [pickList?.categories, categoriesList]);
// --- NEW: apply categoryFilter and listSearch to visibleItems ---
const visibleItemsFiltered = useMemo(() => {
const q = listSearch.trim().toLowerCase();
let arr = visibleItems;
if (categoryFilter !== 'all') {
arr = arr.filter((item) => {
const product = productMap.get(item.product_id);
return (product?.category ?? '') === categoryFilter;
});
}
if (!q) return arr;
return arr.filter((item) => {
const product = productMap.get(item.product_id);
const catName = categoriesById.get(product?.category ?? '') ?? product?.category ?? '';
const searchableText = `${product?.name ?? ''} ${catName} ${product?.barcode ?? ''}`.toLowerCase();
return searchableText.includes(q);
});
}, [visibleItems, productMap, listSearch, categoryFilter, categoriesById]);
return ( return (
<Container sx={{ py: 4 }}> <Container sx={{ py: 4 }}>
<Stack spacing={2} mb={2}> <Stack spacing={2} mb={2}>
@@ -416,6 +475,49 @@ export const ActivePickListScreen = () => {
</Button> </Button>
</Stack> </Stack>
</Stack> </Stack>
{/* NEW: Search List (left) and Category dropdown (right) on the same row */}
<Stack
direction="row"
alignItems="center"
justifyContent="space-between"
flexWrap="wrap"
rowGap={1}
sx={{ mt: 1 }}
>
<TextField
size="small"
placeholder="Search list"
variant="outlined"
value={listSearch}
onChange={(e) => setListSearch(e.target.value)}
sx={{ flexGrow: 1, minWidth: 160 }}
InputProps={{
startAdornment: (
<InputAdornment position="start">
<SearchIcon />
</InputAdornment>
),
}}
/>
<TextField
select
size="small"
SelectProps={{ native: true }}
label="Filter by category"
value={categoryFilter}
onChange={(e) => setCategoryFilter((e.target.value as any) ?? 'all')}
sx={{ width: { xs: '100%', sm: 240 }, ml: { xs: 0, sm: 2 }, mt: { xs: 1, sm: 0 } }}
>
<option value="all">All categories</option>
{categoryOptions.map((opt) => (
<option key={opt.id} value={opt.id}>
{opt.name}
</option>
))}
</TextField>
</Stack>
</Stack> </Stack>
</Stack> </Stack>
{pickList?.notes ? ( {pickList?.notes ? (
@@ -424,18 +526,31 @@ export const ActivePickListScreen = () => {
</Typography> </Typography>
) : null} ) : null}
<Stack spacing={1}> <Stack spacing={1}>
{visibleItems.map((item) => ( {/* NEW: empty-filter message */}
<PickItemRow {visibleItemsFiltered.length === 0 ? (
key={item.id} visibleItems.length === 0 ? (
item={item} <Typography variant="body2" color="text.secondary">
product={productMap.get(item.product_id)} No items in this pick list
onIncrementQuantity={() => handleIncrementQuantity(item.id)} </Typography>
onDecrementQuantity={() => handleDecrementQuantity(item.id)} ) : (
onToggleCarton={() => handleToggleCarton(item.id)} <Typography variant="body2" color="text.secondary">
onStatusChange={(status) => handleStatusChange(item.id, status)} No items match the filter
onDelete={() => handleDeleteItem(item.id)} </Typography>
/> )
))} ) : (
visibleItemsFiltered.map((item) => (
<PickItemRow
key={item.id}
item={item}
product={productMap.get(item.product_id)}
onIncrementQuantity={() => handleIncrementQuantity(item.id)}
onDecrementQuantity={() => handleDecrementQuantity(item.id)}
onToggleCarton={() => handleToggleCarton(item.id)}
onStatusChange={(status) => handleStatusChange(item.id, status)}
onDelete={() => handleDeleteItem(item.id)}
/>
))
)}
</Stack> </Stack>
<Button fullWidth sx={{ mt: 3 }} variant="outlined" onClick={returnToLists}> <Button fullWidth sx={{ mt: 3 }} variant="outlined" onClick={returnToLists}>
Save and Return Save and Return