modified: src/screens/ActivePickListScreen.tsx
This commit is contained in:
@@ -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) => {
|
||||||
|
|||||||
@@ -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,7 +526,19 @@ export const ActivePickListScreen = () => {
|
|||||||
</Typography>
|
</Typography>
|
||||||
) : null}
|
) : null}
|
||||||
<Stack spacing={1}>
|
<Stack spacing={1}>
|
||||||
{visibleItems.map((item) => (
|
{/* NEW: empty-filter message */}
|
||||||
|
{visibleItemsFiltered.length === 0 ? (
|
||||||
|
visibleItems.length === 0 ? (
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
No items in this pick list
|
||||||
|
</Typography>
|
||||||
|
) : (
|
||||||
|
<Typography variant="body2" color="text.secondary">
|
||||||
|
No items match the filter
|
||||||
|
</Typography>
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
visibleItemsFiltered.map((item) => (
|
||||||
<PickItemRow
|
<PickItemRow
|
||||||
key={item.id}
|
key={item.id}
|
||||||
item={item}
|
item={item}
|
||||||
@@ -435,7 +549,8 @@ export const ActivePickListScreen = () => {
|
|||||||
onStatusChange={(status) => handleStatusChange(item.id, status)}
|
onStatusChange={(status) => handleStatusChange(item.id, status)}
|
||||||
onDelete={() => handleDeleteItem(item.id)}
|
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
|
||||||
|
|||||||
Reference in New Issue
Block a user