Merge branch 'main' into codex/refactor-additemscreen-for-quantity-input
This commit is contained in:
6 files changed
+120
-76
No files matched your search
@@ -113,12 +113,17 @@ Dexie tables must be implemented exactly as follows:
|
|||||||
id: string
|
id: string
|
||||||
pick_list_id: string
|
pick_list_id: string
|
||||||
product_id: string
|
product_id: string
|
||||||
quantity_units: number
|
quantity: number
|
||||||
quantity_bulk: number
|
is_carton: boolean
|
||||||
status: "pending" | "picked" | "skipped"
|
status: "pending" | "picked" | "skipped"
|
||||||
created_at: number
|
created_at: number
|
||||||
updated_at: number
|
updated_at: number
|
||||||
|
|
||||||
|
Pick items record a single packaging type per row: set `is_carton` to `true` when counting
|
||||||
|
cartons (using the product's `bulk_name`) or `false` for single units (using `unit_type`). If
|
||||||
|
both units and cartons are needed for the same product, store them as two PickItem records so
|
||||||
|
quantities remain distinct.
|
||||||
|
|
||||||
------------------------------------------------------------------------
|
------------------------------------------------------------------------
|
||||||
|
|
||||||
## 4. UI & UX Rules
|
## 4. UI & UX Rules
|
||||||
@@ -152,10 +157,8 @@ Dexie tables must be implemented exactly as follows:
|
|||||||
### ActivePickListScreen
|
### ActivePickListScreen
|
||||||
|
|
||||||
- List of PickItems\
|
- List of PickItems\
|
||||||
- Tap = +1 unit\
|
- Use the checkbox in each row to toggle between pending and picked without removing the item\
|
||||||
- Long-press = +1 bulk\
|
- Row controls provide explicit +1 unit and +1 bulk actions (no long-press or swipe)\
|
||||||
- Swipe left = mark picked\
|
|
||||||
- Swipe right = delete\
|
|
||||||
- Add Item button\
|
- Add Item button\
|
||||||
- Complete List button
|
- Complete List button
|
||||||
|
|
||||||
@@ -164,7 +167,7 @@ Dexie tables must be implemented exactly as follows:
|
|||||||
- Search\
|
- Search\
|
||||||
- Category filter\
|
- Category filter\
|
||||||
- Scan barcode\
|
- Scan barcode\
|
||||||
- Increment units & bulk\
|
- Increment units & cartons separately (saved as distinct PickItems)\
|
||||||
- Add to pick list
|
- Add to pick list
|
||||||
|
|
||||||
### ManageProductsScreen
|
### ManageProductsScreen
|
||||||
@@ -188,23 +191,15 @@ Dexie tables must be implemented exactly as follows:
|
|||||||
|
|
||||||
------------------------------------------------------------------------
|
------------------------------------------------------------------------
|
||||||
|
|
||||||
## 6. Gestures & Interaction Rules
|
## 6. Interaction Rules
|
||||||
|
|
||||||
### Tap
|
### Checkbox Toggle
|
||||||
|
|
||||||
Increase `quantity_units` by **1**.
|
Use a checkbox in each pick item row to switch between `"pending"` and `"picked"` without removing the item from the list. Picked rows must remain visible with clear status cues (e.g., checkmarks/strikethrough).
|
||||||
|
|
||||||
### Long Press
|
### Increment Controls
|
||||||
|
|
||||||
Increase `quantity_bulk` by **1** using a shared `useLongPress()` hook.
|
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.
|
||||||
|
|
||||||
### Swipe Left
|
|
||||||
|
|
||||||
Mark item as `"picked"`.
|
|
||||||
|
|
||||||
### Swipe Right
|
|
||||||
|
|
||||||
Delete item.
|
|
||||||
|
|
||||||
### Barcode Scanning
|
### Barcode Scanning
|
||||||
|
|
||||||
@@ -318,6 +313,7 @@ Expose port 8080:
|
|||||||
- Product creation must be minimal friction\
|
- Product creation must be minimal friction\
|
||||||
- Auto-save pick lists\
|
- Auto-save pick lists\
|
||||||
- Smooth animations on long press
|
- Smooth animations on long press
|
||||||
|
- Pick list rows use checkboxes to toggle items between pending and picked; no swipe or long-press gestures should be required to update status, and picked rows stay visible with clear status cues
|
||||||
|
|
||||||
------------------------------------------------------------------------
|
------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import { Chip, Stack, Typography } from '@mui/material';
|
import { Add, Delete } from '@mui/icons-material';
|
||||||
import { PickItem, PickItemStatus } from '../models/PickItem';
|
import { Button, Checkbox, Stack, Typography } from '@mui/material';
|
||||||
|
import { PickItem } from '../models/PickItem';
|
||||||
import { Product } from '../models/Product';
|
import { Product } from '../models/Product';
|
||||||
import { useLongPress } from '../hooks/useLongPress';
|
|
||||||
import { useSwipe } from '../hooks/useSwipe';
|
|
||||||
|
|
||||||
interface PickItemRowProps {
|
interface PickItemRowProps {
|
||||||
item: PickItem;
|
item: PickItem;
|
||||||
@@ -13,12 +12,6 @@ interface PickItemRowProps {
|
|||||||
onSwipeRight: () => void;
|
onSwipeRight: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const statusColor: Record<PickItemStatus, 'default' | 'success' | 'warning'> = {
|
|
||||||
pending: 'default',
|
|
||||||
picked: 'success',
|
|
||||||
skipped: 'warning',
|
|
||||||
};
|
|
||||||
|
|
||||||
export const PickItemRow = ({
|
export const PickItemRow = ({
|
||||||
item,
|
item,
|
||||||
product,
|
product,
|
||||||
@@ -39,8 +32,6 @@ export const PickItemRow = ({
|
|||||||
justifyContent="space-between"
|
justifyContent="space-between"
|
||||||
spacing={1}
|
spacing={1}
|
||||||
sx={{ p: 1, borderRadius: 1, bgcolor: 'background.paper', boxShadow: 1 }}
|
sx={{ p: 1, borderRadius: 1, bgcolor: 'background.paper', boxShadow: 1 }}
|
||||||
{...longPressHandlers}
|
|
||||||
{...swipeHandlers}
|
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<Typography variant="subtitle1">{product?.name ?? 'Unknown product'}</Typography>
|
<Typography variant="subtitle1">{product?.name ?? 'Unknown product'}</Typography>
|
||||||
|
|||||||
@@ -90,6 +90,64 @@ export class StockFillDB extends Dexie {
|
|||||||
pickItems: 'id, pick_list_id, product_id, status, created_at, updated_at',
|
pickItems: 'id, pick_list_id, product_id, status, created_at, updated_at',
|
||||||
categories: 'id, name, created_at, updated_at',
|
categories: 'id, name, created_at, updated_at',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.version(5)
|
||||||
|
.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 hasNewFields =
|
||||||
|
typeof (item as PickItem).quantity === 'number' &&
|
||||||
|
typeof (item as PickItem).is_carton === 'boolean';
|
||||||
|
|
||||||
|
if (hasNewFields) return undefined;
|
||||||
|
|
||||||
|
const legacyUnits = Number((item as PickItem).quantity_units ?? 0);
|
||||||
|
const legacyBulk = Number((item as PickItem).quantity_bulk ?? 0);
|
||||||
|
|
||||||
|
if (legacyUnits > 0 && legacyBulk > 0) {
|
||||||
|
await tx.table('pickItems').update(item.id, {
|
||||||
|
quantity: legacyUnits,
|
||||||
|
is_carton: false,
|
||||||
|
updated_at: now,
|
||||||
|
});
|
||||||
|
|
||||||
|
return tx.table('pickItems').add({
|
||||||
|
...item,
|
||||||
|
id: uuidv4(),
|
||||||
|
quantity: legacyBulk,
|
||||||
|
is_carton: true,
|
||||||
|
created_at: (item as PickItem).created_at ?? now,
|
||||||
|
updated_at: now,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (legacyBulk > 0) {
|
||||||
|
return tx.table('pickItems').update(item.id, {
|
||||||
|
quantity: legacyBulk,
|
||||||
|
is_carton: true,
|
||||||
|
updated_at: now,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return tx.table('pickItems').update(item.id, {
|
||||||
|
quantity: legacyUnits,
|
||||||
|
is_carton: false,
|
||||||
|
updated_at: now,
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,9 +4,12 @@ export interface PickItem {
|
|||||||
id: string;
|
id: string;
|
||||||
pick_list_id: string;
|
pick_list_id: string;
|
||||||
product_id: string;
|
product_id: string;
|
||||||
quantity_units: number;
|
quantity: number;
|
||||||
quantity_bulk: number;
|
is_carton: boolean;
|
||||||
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;
|
||||||
}
|
}
|
||||||
@@ -23,7 +23,7 @@ export const ActivePickListScreen = () => {
|
|||||||
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_units: existing.quantity_units + 1,
|
quantity: existing.quantity + 1,
|
||||||
updated_at: Date.now(),
|
updated_at: Date.now(),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -41,7 +41,7 @@ export const ActivePickListScreen = () => {
|
|||||||
await db.pickItems.update(itemId, { status: 'picked', updated_at: Date.now() });
|
await db.pickItems.update(itemId, { status: 'picked', updated_at: Date.now() });
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSwipeRight = async (itemId: string) => {
|
const handleDeleteItem = async (itemId: string) => {
|
||||||
await db.pickItems.delete(itemId);
|
await db.pickItems.delete(itemId);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -26,26 +26,18 @@ export const AddItemScreen = () => {
|
|||||||
const [quantity, setQuantity] = useState(1);
|
const [quantity, setQuantity] = useState(1);
|
||||||
const [isCarton, setIsCarton] = useState(false);
|
const [isCarton, setIsCarton] = useState(false);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const unitLabel = selectedProduct?.unit_type ?? 'Units';
|
||||||
const existingProductIds = useMemo(
|
const cartonLabel = selectedProduct?.bulk_name ?? 'Cartons';
|
||||||
() => new Set(items.map((item) => item.product_id)),
|
|
||||||
[items],
|
|
||||||
);
|
|
||||||
|
|
||||||
const availableProducts = useMemo(
|
|
||||||
() => products.filter((product) => !existingProductIds.has(product.id)),
|
|
||||||
[existingProductIds, products],
|
|
||||||
);
|
|
||||||
|
|
||||||
const filteredProducts = useMemo(() => {
|
const filteredProducts = useMemo(() => {
|
||||||
const normalizedQuery = query.trim().toLowerCase();
|
const normalizedQuery = query.trim().toLowerCase();
|
||||||
if (!normalizedQuery) return availableProducts;
|
if (!normalizedQuery) return products;
|
||||||
|
|
||||||
return availableProducts.filter((product) => {
|
return products.filter((product) => {
|
||||||
const searchableText = `${product.name} ${product.category} ${product.barcode ?? ''}`.toLowerCase();
|
const searchableText = `${product.name} ${product.category} ${product.barcode ?? ''}`.toLowerCase();
|
||||||
return searchableText.includes(normalizedQuery);
|
return searchableText.includes(normalizedQuery);
|
||||||
});
|
});
|
||||||
}, [availableProducts, query]);
|
}, [products, query]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!selectedProduct) return;
|
if (!selectedProduct) return;
|
||||||
@@ -67,17 +59,35 @@ export const AddItemScreen = () => {
|
|||||||
const addItem = async () => {
|
const addItem = async () => {
|
||||||
const productId = selectedProduct?.id;
|
const productId = selectedProduct?.id;
|
||||||
|
|
||||||
if (!id || !productId || existingProductIds.has(productId)) return;
|
if (!id || !productId) return;
|
||||||
await db.pickItems.add({
|
|
||||||
id: uuidv4(),
|
const addOrUpdateItem = async (is_carton: boolean, quantity: number) => {
|
||||||
pick_list_id: id,
|
if (quantity <= 0) return;
|
||||||
product_id: productId,
|
const existing = items.find(
|
||||||
quantity_units: quantity,
|
(item) => item.product_id === productId && item.is_carton === is_carton,
|
||||||
quantity_bulk: isCarton ? 1 : 0,
|
);
|
||||||
status: 'pending',
|
|
||||||
created_at: Date.now(),
|
if (existing) {
|
||||||
updated_at: Date.now(),
|
await db.pickItems.update(existing.id, {
|
||||||
});
|
quantity: existing.quantity + quantity,
|
||||||
|
updated_at: Date.now(),
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await db.pickItems.add({
|
||||||
|
id: uuidv4(),
|
||||||
|
pick_list_id: id,
|
||||||
|
product_id: productId,
|
||||||
|
quantity,
|
||||||
|
is_carton,
|
||||||
|
status: 'pending',
|
||||||
|
created_at: Date.now(),
|
||||||
|
updated_at: Date.now(),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
await Promise.all([addOrUpdateItem(false, units), addOrUpdateItem(true, bulk)]);
|
||||||
navigate(`/pick-lists/${id}`);
|
navigate(`/pick-lists/${id}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -124,22 +134,8 @@ export const AddItemScreen = () => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<TextField
|
<NumericStepper label={unitLabel} value={units} onChange={setUnits} />
|
||||||
label="Quantity"
|
<NumericStepper label={cartonLabel} value={bulk} onChange={setBulk} />
|
||||||
type="number"
|
|
||||||
inputProps={{ min: 1 }}
|
|
||||||
value={quantity}
|
|
||||||
onChange={(event) => {
|
|
||||||
const value = Number(event.target.value);
|
|
||||||
setQuantity(Number.isNaN(value) || value < 1 ? 1 : value);
|
|
||||||
}}
|
|
||||||
helperText={quantityHelperText}
|
|
||||||
fullWidth
|
|
||||||
/>
|
|
||||||
<FormControlLabel
|
|
||||||
control={<Checkbox checked={isCarton} onChange={(event) => setIsCarton(event.target.checked)} />}
|
|
||||||
label={cartonLabel}
|
|
||||||
/>
|
|
||||||
<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