Simplify pick item quantity handling
This commit is contained in:
@@ -199,7 +199,7 @@ Use a checkbox in each pick item row to switch between `"pending"` and `"picked"
|
|||||||
|
|
||||||
### Increment Controls
|
### Increment Controls
|
||||||
|
|
||||||
Provide explicit controls to increase `quantity_units` and `quantity_bulk` (no long-press). Avoid swipe gestures for status changes or deletion on pick item rows.
|
Provide explicit controls to adjust the pick item's `quantity` and to toggle between unit and carton counts (no long-press). Avoid swipe gestures for status changes or deletion on pick item rows.
|
||||||
|
|
||||||
### Barcode Scanning
|
### Barcode Scanning
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { Add, Delete } from '@mui/icons-material';
|
import { Add, Delete } from '@mui/icons-material';
|
||||||
import { Button, Checkbox, Stack, Typography } from '@mui/material';
|
import { Button, Checkbox, IconButton, Stack, Typography } from '@mui/material';
|
||||||
import { PickItem } from '../models/PickItem';
|
import { PickItem } from '../models/PickItem';
|
||||||
import { Product } from '../models/Product';
|
import { Product } from '../models/Product';
|
||||||
|
|
||||||
@@ -8,8 +8,8 @@ interface PickItemRowProps {
|
|||||||
product?: Product | null;
|
product?: Product | null;
|
||||||
onIncrementQuantity: () => void;
|
onIncrementQuantity: () => void;
|
||||||
onToggleCarton: () => void;
|
onToggleCarton: () => void;
|
||||||
onSwipeLeft: () => void;
|
onStatusChange: (status: PickItem['status']) => void;
|
||||||
onSwipeRight: () => void;
|
onDelete: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PickItemRow = ({
|
export const PickItemRow = ({
|
||||||
@@ -17,31 +17,56 @@ export const PickItemRow = ({
|
|||||||
product,
|
product,
|
||||||
onIncrementQuantity,
|
onIncrementQuantity,
|
||||||
onToggleCarton,
|
onToggleCarton,
|
||||||
onSwipeLeft,
|
onStatusChange,
|
||||||
onSwipeRight,
|
onDelete,
|
||||||
}: PickItemRowProps) => {
|
}: PickItemRowProps) => {
|
||||||
const longPressHandlers = useLongPress({ onLongPress: onToggleCarton, onClick: onIncrementQuantity });
|
const packagingLabel = item.is_carton
|
||||||
const swipeHandlers = useSwipe({ onSwipeLeft, onSwipeRight });
|
? product?.bulk_name ?? 'Carton'
|
||||||
|
: product?.unit_type ?? 'Unit';
|
||||||
|
|
||||||
const isCarton = item.quantity_bulk > 0;
|
const toggleStatus = (checked: boolean) => {
|
||||||
|
onStatusChange(checked ? 'picked' : 'pending');
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack
|
<Stack
|
||||||
direction="row"
|
direction="row"
|
||||||
alignItems="center"
|
alignItems="center"
|
||||||
justifyContent="space-between"
|
justifyContent="space-between"
|
||||||
spacing={1}
|
spacing={1.5}
|
||||||
sx={{ p: 1, borderRadius: 1, bgcolor: 'background.paper', boxShadow: 1 }}
|
sx={{ p: 1, borderRadius: 1, bgcolor: 'background.paper', boxShadow: 1 }}
|
||||||
>
|
>
|
||||||
<div>
|
<Stack direction="row" spacing={1} alignItems="center" flex={1} minWidth={0}>
|
||||||
<Typography variant="subtitle1">{product?.name ?? 'Unknown product'}</Typography>
|
<Checkbox
|
||||||
<Typography variant="caption" color="text.secondary">
|
edge="start"
|
||||||
Qty: {item.quantity_units}
|
checked={item.status === 'picked'}
|
||||||
</Typography>
|
onChange={(event) => toggleStatus(event.target.checked)}
|
||||||
</div>
|
inputProps={{ 'aria-label': 'Toggle picked status' }}
|
||||||
|
/>
|
||||||
|
<Stack spacing={0.25} minWidth={0} flex={1}>
|
||||||
|
<Typography variant="subtitle1" noWrap>
|
||||||
|
{product?.name ?? 'Unknown product'}
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="caption" color="text.secondary" noWrap>
|
||||||
|
Qty: {item.quantity} {packagingLabel}
|
||||||
|
</Typography>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
<Stack direction="row" spacing={1} alignItems="center">
|
<Stack direction="row" spacing={1} alignItems="center">
|
||||||
{isCarton ? <Chip label="Carton" color="primary" size="small" /> : null}
|
<Button
|
||||||
<Chip label={item.status} color={statusColor[item.status]} size="small" />
|
variant={item.is_carton ? 'contained' : 'outlined'}
|
||||||
|
size="small"
|
||||||
|
onClick={onToggleCarton}
|
||||||
|
>
|
||||||
|
{item.is_carton ? 'Carton' : 'Unit'}
|
||||||
|
</Button>
|
||||||
|
<Button variant="contained" size="small" startIcon={<Add />} onClick={onIncrementQuantity}>
|
||||||
|
Add 1
|
||||||
|
</Button>
|
||||||
|
<IconButton color="error" onClick={onDelete} aria-label="Delete item">
|
||||||
|
<Delete />
|
||||||
|
</IconButton>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Stack>
|
</Stack>
|
||||||
);
|
);
|
||||||
|
|||||||
+56
-2
@@ -112,8 +112,12 @@ export class StockFillDB extends Dexie {
|
|||||||
|
|
||||||
if (hasNewFields) return undefined;
|
if (hasNewFields) return undefined;
|
||||||
|
|
||||||
const legacyUnits = Number((item as PickItem).quantity_units ?? 0);
|
const legacyUnits = Number(
|
||||||
const legacyBulk = Number((item as PickItem).quantity_bulk ?? 0);
|
(item as PickItem & { quantity_units?: number }).quantity_units ?? 0,
|
||||||
|
);
|
||||||
|
const legacyBulk = Number(
|
||||||
|
(item as PickItem & { quantity_bulk?: number }).quantity_bulk ?? 0,
|
||||||
|
);
|
||||||
|
|
||||||
if (legacyUnits > 0 && legacyBulk > 0) {
|
if (legacyUnits > 0 && legacyBulk > 0) {
|
||||||
await tx.table('pickItems').update(item.id, {
|
await tx.table('pickItems').update(item.id, {
|
||||||
@@ -148,6 +152,56 @@ export class StockFillDB extends Dexie {
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.version(6)
|
||||||
|
.stores({
|
||||||
|
products: 'id, name, category, &barcode, archived, created_at, updated_at',
|
||||||
|
areas: 'id, name, created_at, updated_at',
|
||||||
|
pickLists: 'id, area_id, created_at, completed_at',
|
||||||
|
pickItems:
|
||||||
|
'id, pick_list_id, product_id, status, is_carton, quantity, created_at, updated_at',
|
||||||
|
categories: 'id, name, created_at, updated_at',
|
||||||
|
})
|
||||||
|
.upgrade(async (tx) => {
|
||||||
|
const items = await tx.table('pickItems').toArray();
|
||||||
|
const now = Date.now();
|
||||||
|
|
||||||
|
await Promise.all(
|
||||||
|
items.map(async (item) => {
|
||||||
|
const legacyUnits = Number((item as PickItem & { quantity_units?: number }).quantity_units ?? 0);
|
||||||
|
const legacyBulk = Number((item as PickItem & { quantity_bulk?: number }).quantity_bulk ?? 0);
|
||||||
|
const hasQuantity = typeof (item as PickItem).quantity === 'number';
|
||||||
|
const hasCartonFlag = typeof (item as PickItem).is_carton === 'boolean';
|
||||||
|
|
||||||
|
const updateData: Partial<PickItem> & {
|
||||||
|
quantity_units?: undefined;
|
||||||
|
quantity_bulk?: undefined;
|
||||||
|
} = {};
|
||||||
|
|
||||||
|
if (!hasQuantity) {
|
||||||
|
updateData.quantity = legacyBulk > 0 ? legacyBulk : legacyUnits;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasCartonFlag) {
|
||||||
|
updateData.is_carton = legacyBulk > 0 && legacyUnits === 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('quantity_units' in item) {
|
||||||
|
updateData.quantity_units = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('quantity_bulk' in item) {
|
||||||
|
updateData.quantity_bulk = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Object.keys(updateData).length === 0) return undefined;
|
||||||
|
|
||||||
|
updateData.updated_at = now;
|
||||||
|
|
||||||
|
return tx.table('pickItems').update(item.id, updateData);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,4 @@ export interface PickItem {
|
|||||||
status: PickItemStatus;
|
status: PickItemStatus;
|
||||||
created_at: number;
|
created_at: number;
|
||||||
updated_at: number;
|
updated_at: number;
|
||||||
// Legacy fields retained for backward compatibility with pre-v5 data.
|
|
||||||
quantity_units?: number;
|
|
||||||
quantity_bulk?: number;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { useMemo } from 'react';
|
|||||||
import { useAreas, usePickItems, usePickList, useProducts } from '../hooks/dataHooks';
|
import { useAreas, usePickItems, usePickList, useProducts } from '../hooks/dataHooks';
|
||||||
import { useDatabase } from '../context/DBProvider';
|
import { useDatabase } from '../context/DBProvider';
|
||||||
import { PickItemRow } from '../components/PickItemRow';
|
import { PickItemRow } from '../components/PickItemRow';
|
||||||
|
import { PickItem } from '../models/PickItem';
|
||||||
|
|
||||||
export const ActivePickListScreen = () => {
|
export const ActivePickListScreen = () => {
|
||||||
const { id } = useParams();
|
const { id } = useParams();
|
||||||
@@ -31,14 +32,17 @@ export const ActivePickListScreen = () => {
|
|||||||
const handleToggleCarton = async (itemId: string) => {
|
const handleToggleCarton = async (itemId: string) => {
|
||||||
const existing = await db.pickItems.get(itemId);
|
const existing = await db.pickItems.get(itemId);
|
||||||
if (!existing) return;
|
if (!existing) return;
|
||||||
|
|
||||||
await db.pickItems.update(itemId, {
|
await db.pickItems.update(itemId, {
|
||||||
quantity_bulk: existing.quantity_bulk > 0 ? 0 : 1,
|
is_carton: !existing.is_carton,
|
||||||
|
quantity: existing.quantity || 1,
|
||||||
updated_at: Date.now(),
|
updated_at: Date.now(),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSwipeLeft = async (itemId: string) => {
|
const handleStatusChange = async (itemId: string, status: PickItem['status']) => {
|
||||||
await db.pickItems.update(itemId, { status: 'picked', updated_at: Date.now() });
|
const nextStatus = status === 'picked' ? 'picked' : 'pending';
|
||||||
|
await db.pickItems.update(itemId, { status: nextStatus, updated_at: Date.now() });
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDeleteItem = async (itemId: string) => {
|
const handleDeleteItem = async (itemId: string) => {
|
||||||
@@ -70,8 +74,8 @@ export const ActivePickListScreen = () => {
|
|||||||
product={products.find((p) => p.id === item.product_id)}
|
product={products.find((p) => p.id === item.product_id)}
|
||||||
onIncrementQuantity={() => handleIncrementQuantity(item.id)}
|
onIncrementQuantity={() => handleIncrementQuantity(item.id)}
|
||||||
onToggleCarton={() => handleToggleCarton(item.id)}
|
onToggleCarton={() => handleToggleCarton(item.id)}
|
||||||
onSwipeLeft={() => handleSwipeLeft(item.id)}
|
onStatusChange={(status) => handleStatusChange(item.id, status)}
|
||||||
onSwipeRight={() => handleSwipeRight(item.id)}
|
onDelete={() => handleDeleteItem(item.id)}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|||||||
@@ -1,20 +1,11 @@
|
|||||||
import {
|
import { Autocomplete, Button, Container, InputAdornment, Stack, TextField, Typography } from '@mui/material';
|
||||||
Autocomplete,
|
|
||||||
Button,
|
|
||||||
Checkbox,
|
|
||||||
Container,
|
|
||||||
FormControlLabel,
|
|
||||||
InputAdornment,
|
|
||||||
Stack,
|
|
||||||
TextField,
|
|
||||||
Typography,
|
|
||||||
} from '@mui/material';
|
|
||||||
import SearchIcon from '@mui/icons-material/Search';
|
import SearchIcon from '@mui/icons-material/Search';
|
||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
import { useNavigate, useParams } from 'react-router-dom';
|
import { useNavigate, useParams } from 'react-router-dom';
|
||||||
import { v4 as uuidv4 } from 'uuid';
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
import { usePickItems, useProducts } from '../hooks/dataHooks';
|
import { usePickItems, useProducts } from '../hooks/dataHooks';
|
||||||
import { useDatabase } from '../context/DBProvider';
|
import { useDatabase } from '../context/DBProvider';
|
||||||
|
import { NumericStepper } from '../components/NumericStepper';
|
||||||
|
|
||||||
export const AddItemScreen = () => {
|
export const AddItemScreen = () => {
|
||||||
const { id } = useParams();
|
const { id } = useParams();
|
||||||
@@ -23,8 +14,8 @@ export const AddItemScreen = () => {
|
|||||||
const products = useProducts();
|
const products = useProducts();
|
||||||
const [selectedProduct, setSelectedProduct] = useState<typeof products[number] | null>(null);
|
const [selectedProduct, setSelectedProduct] = useState<typeof products[number] | null>(null);
|
||||||
const [query, setQuery] = useState('');
|
const [query, setQuery] = useState('');
|
||||||
const [quantity, setQuantity] = useState(1);
|
const [units, setUnits] = useState(0);
|
||||||
const [isCarton, setIsCarton] = useState(false);
|
const [cartons, setCartons] = useState(0);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const unitLabel = selectedProduct?.unit_type ?? 'Units';
|
const unitLabel = selectedProduct?.unit_type ?? 'Units';
|
||||||
const cartonLabel = selectedProduct?.bulk_name ?? 'Cartons';
|
const cartonLabel = selectedProduct?.bulk_name ?? 'Cartons';
|
||||||
@@ -47,15 +38,10 @@ export const AddItemScreen = () => {
|
|||||||
}, [filteredProducts, selectedProduct]);
|
}, [filteredProducts, selectedProduct]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setQuantity(1);
|
setUnits(0);
|
||||||
setIsCarton(false);
|
setCartons(0);
|
||||||
}, [selectedProduct]);
|
}, [selectedProduct]);
|
||||||
|
|
||||||
const quantityHelperText = selectedProduct?.unit_type
|
|
||||||
? `Enter ${selectedProduct.unit_type} to pick`
|
|
||||||
: 'Enter the quantity to pick';
|
|
||||||
const cartonLabel = selectedProduct?.bulk_name ? `Carton (${selectedProduct.bulk_name})` : 'Carton';
|
|
||||||
|
|
||||||
const addItem = async () => {
|
const addItem = async () => {
|
||||||
const productId = selectedProduct?.id;
|
const productId = selectedProduct?.id;
|
||||||
|
|
||||||
@@ -87,7 +73,7 @@ export const AddItemScreen = () => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
await Promise.all([addOrUpdateItem(false, units), addOrUpdateItem(true, bulk)]);
|
await Promise.all([addOrUpdateItem(false, units), addOrUpdateItem(true, cartons)]);
|
||||||
navigate(`/pick-lists/${id}`);
|
navigate(`/pick-lists/${id}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -135,7 +121,7 @@ export const AddItemScreen = () => {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<NumericStepper label={unitLabel} value={units} onChange={setUnits} />
|
<NumericStepper label={unitLabel} value={units} onChange={setUnits} />
|
||||||
<NumericStepper label={cartonLabel} value={bulk} onChange={setBulk} />
|
<NumericStepper label={cartonLabel} value={cartons} onChange={setCartons} />
|
||||||
<Button variant="contained" disabled={!selectedProduct} onClick={addItem}>
|
<Button variant="contained" disabled={!selectedProduct} onClick={addItem}>
|
||||||
Add to List
|
Add to List
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
Reference in New Issue
Block a user