From 6452b41e0c18673d043e4328c935a32ea610092e Mon Sep 17 00:00:00 2001 From: beatz174-bit Date: Sun, 23 Nov 2025 11:59:18 +1000 Subject: [PATCH 1/3] Update interaction guidance for checkbox model --- Agents.md | 25 +++++-------- src/components/PickItemRow.tsx | 53 +++++++++++++++------------- src/screens/ActivePickListScreen.tsx | 14 ++++---- 3 files changed, 42 insertions(+), 50 deletions(-) diff --git a/Agents.md b/Agents.md index 3391438..42d24ef 100644 --- a/Agents.md +++ b/Agents.md @@ -152,10 +152,8 @@ Dexie tables must be implemented exactly as follows: ### ActivePickListScreen - List of PickItems\ -- Tap = +1 unit\ -- Long-press = +1 bulk\ -- Swipe left = mark picked\ -- Swipe right = delete\ +- Use the checkbox in each row to toggle between pending and picked without removing the item\ +- Row controls provide explicit +1 unit and +1 bulk actions (no long-press or swipe)\ - Add Item button\ - Complete List button @@ -188,23 +186,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. - -### Swipe Left - -Mark item as `"picked"`. - -### Swipe Right - -Delete item. +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. ### Barcode Scanning @@ -318,6 +308,7 @@ Expose port 8080: - Product creation must be minimal friction\ - Auto-save pick lists\ - 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 ------------------------------------------------------------------------ diff --git a/src/components/PickItemRow.tsx b/src/components/PickItemRow.tsx index 4089c5c..5e3be08 100644 --- a/src/components/PickItemRow.tsx +++ b/src/components/PickItemRow.tsx @@ -1,34 +1,24 @@ -import { Chip, Stack, Typography } from '@mui/material'; -import { PickItem, PickItemStatus } from '../models/PickItem'; +import { Add } from '@mui/icons-material'; +import { Button, Checkbox, Stack, Typography } from '@mui/material'; +import { PickItem } from '../models/PickItem'; import { Product } from '../models/Product'; -import { useLongPress } from '../hooks/useLongPress'; -import { useSwipe } from '../hooks/useSwipe'; interface PickItemRowProps { item: PickItem; product?: Product | null; onIncrementUnit: () => void; onIncrementBulk: () => void; - onSwipeLeft: () => void; - onSwipeRight: () => void; + onToggleStatus: () => void; } -const statusColor: Record = { - pending: 'default', - picked: 'success', - skipped: 'warning', -}; - export const PickItemRow = ({ item, product, onIncrementUnit, onIncrementBulk, - onSwipeLeft, - onSwipeRight, + onToggleStatus, }: PickItemRowProps) => { - const longPressHandlers = useLongPress({ onLongPress: onIncrementBulk, onClick: onIncrementUnit }); - const swipeHandlers = useSwipe({ onSwipeLeft, onSwipeRight }); + const isPicked = item.status === 'picked'; return ( -
- {product?.name ?? 'Unknown product'} - - {item.quantity_units} units / {item.quantity_bulk} bulk - -
- + + + + + {isPicked ? '✔️' : null} {product?.name ?? 'Unknown product'} + + + {item.quantity_units} units / {item.quantity_bulk} bulk + + + + + + +
); }; diff --git a/src/screens/ActivePickListScreen.tsx b/src/screens/ActivePickListScreen.tsx index 3a4b04d..9d44e18 100644 --- a/src/screens/ActivePickListScreen.tsx +++ b/src/screens/ActivePickListScreen.tsx @@ -37,12 +37,11 @@ export const ActivePickListScreen = () => { }); }; - const handleSwipeLeft = async (itemId: string) => { - await db.pickItems.update(itemId, { status: 'picked', updated_at: Date.now() }); - }; - - const handleSwipeRight = async (itemId: string) => { - await db.pickItems.delete(itemId); + const handleToggleStatus = async (itemId: string) => { + const existing = await db.pickItems.get(itemId); + if (!existing) return; + const nextStatus = existing.status === 'picked' ? 'pending' : 'picked'; + await db.pickItems.update(itemId, { status: nextStatus, updated_at: Date.now() }); }; const returnToLists = () => { @@ -70,8 +69,7 @@ export const ActivePickListScreen = () => { product={products.find((p) => p.id === item.product_id)} onIncrementUnit={() => handleIncrementUnit(item.id)} onIncrementBulk={() => handleIncrementBulk(item.id)} - onSwipeLeft={() => handleSwipeLeft(item.id)} - onSwipeRight={() => handleSwipeRight(item.id)} + onToggleStatus={() => handleToggleStatus(item.id)} /> ))} From 0dce13233ff2a1eecfe5dfe2065942a7e79d86dc Mon Sep 17 00:00:00 2001 From: beatz174-bit Date: Sun, 23 Nov 2025 12:03:22 +1000 Subject: [PATCH 2/3] Add carton-aware pick item quantities and migration --- Agents.md | 19 +++++---- src/components/PickItemRow.tsx | 11 +++-- src/db/index.ts | 58 ++++++++++++++++++++++++++ src/models/PickItem.ts | 7 +++- src/screens/ActivePickListScreen.tsx | 16 ++----- src/screens/AddItemScreen.tsx | 62 ++++++++++++++++------------ 6 files changed, 119 insertions(+), 54 deletions(-) diff --git a/Agents.md b/Agents.md index 3391438..5896d38 100644 --- a/Agents.md +++ b/Agents.md @@ -113,12 +113,17 @@ Dexie tables must be implemented exactly as follows: id: string pick_list_id: string product_id: string - quantity_units: number - quantity_bulk: number + quantity: number + is_carton: boolean status: "pending" | "picked" | "skipped" created_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 @@ -152,8 +157,8 @@ Dexie tables must be implemented exactly as follows: ### ActivePickListScreen - List of PickItems\ -- Tap = +1 unit\ -- Long-press = +1 bulk\ +- Tap = +1 of the item's unit type\ +- Long-press = +1 of the item's unit type\ - Swipe left = mark picked\ - Swipe right = delete\ - Add Item button\ @@ -164,7 +169,7 @@ Dexie tables must be implemented exactly as follows: - Search\ - Category filter\ - Scan barcode\ -- Increment units & bulk\ +- Increment units & cartons separately (saved as distinct PickItems)\ - Add to pick list ### ManageProductsScreen @@ -192,11 +197,11 @@ Dexie tables must be implemented exactly as follows: ### Tap -Increase `quantity_units` by **1**. +Increase `quantity` by **1** for the tapped item's unit type. ### Long Press -Increase `quantity_bulk` by **1** using a shared `useLongPress()` hook. +Increase `quantity` by **1** using a shared `useLongPress()` hook. ### Swipe Left diff --git a/src/components/PickItemRow.tsx b/src/components/PickItemRow.tsx index 4089c5c..eb51b6e 100644 --- a/src/components/PickItemRow.tsx +++ b/src/components/PickItemRow.tsx @@ -7,8 +7,7 @@ import { useSwipe } from '../hooks/useSwipe'; interface PickItemRowProps { item: PickItem; product?: Product | null; - onIncrementUnit: () => void; - onIncrementBulk: () => void; + onIncrement: () => void; onSwipeLeft: () => void; onSwipeRight: () => void; } @@ -22,13 +21,13 @@ const statusColor: Record = { export const PickItemRow = ({ item, product, - onIncrementUnit, - onIncrementBulk, + onIncrement, onSwipeLeft, onSwipeRight, }: PickItemRowProps) => { - const longPressHandlers = useLongPress({ onLongPress: onIncrementBulk, onClick: onIncrementUnit }); + const longPressHandlers = useLongPress({ onLongPress: onIncrement, onClick: onIncrement }); const swipeHandlers = useSwipe({ onSwipeLeft, onSwipeRight }); + const unitLabel = item.is_carton ? product?.bulk_name ?? 'cartons' : product?.unit_type ?? 'units'; return ( {product?.name ?? 'Unknown product'} - {item.quantity_units} units / {item.quantity_bulk} bulk + {item.quantity} {unitLabel} diff --git a/src/db/index.ts b/src/db/index.ts index 8526a51..4b07329 100644 --- a/src/db/index.ts +++ b/src/db/index.ts @@ -90,6 +90,64 @@ export class StockFillDB extends Dexie { pickItems: 'id, pick_list_id, product_id, status, 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, + }); + }), + ); + }); } } diff --git a/src/models/PickItem.ts b/src/models/PickItem.ts index 7e51e76..ab0ca96 100644 --- a/src/models/PickItem.ts +++ b/src/models/PickItem.ts @@ -4,9 +4,12 @@ export interface PickItem { id: string; pick_list_id: string; product_id: string; - quantity_units: number; - quantity_bulk: number; + quantity: number; + is_carton: boolean; status: PickItemStatus; created_at: number; updated_at: number; + // Legacy fields retained for backward compatibility with pre-v5 data. + quantity_units?: number; + quantity_bulk?: number; } diff --git a/src/screens/ActivePickListScreen.tsx b/src/screens/ActivePickListScreen.tsx index 3a4b04d..295ce96 100644 --- a/src/screens/ActivePickListScreen.tsx +++ b/src/screens/ActivePickListScreen.tsx @@ -19,20 +19,11 @@ export const ActivePickListScreen = () => { [areas, pickList?.area_id], ); - const handleIncrementUnit = async (itemId: string) => { + const handleIncrement = async (itemId: string) => { const existing = await db.pickItems.get(itemId); if (!existing) return; await db.pickItems.update(itemId, { - quantity_units: existing.quantity_units + 1, - updated_at: Date.now(), - }); - }; - - const handleIncrementBulk = async (itemId: string) => { - const existing = await db.pickItems.get(itemId); - if (!existing) return; - await db.pickItems.update(itemId, { - quantity_bulk: existing.quantity_bulk + 1, + quantity: existing.quantity + 1, updated_at: Date.now(), }); }; @@ -68,8 +59,7 @@ export const ActivePickListScreen = () => { key={item.id} item={item} product={products.find((p) => p.id === item.product_id)} - onIncrementUnit={() => handleIncrementUnit(item.id)} - onIncrementBulk={() => handleIncrementBulk(item.id)} + onIncrement={() => handleIncrement(item.id)} onSwipeLeft={() => handleSwipeLeft(item.id)} onSwipeRight={() => handleSwipeRight(item.id)} /> diff --git a/src/screens/AddItemScreen.tsx b/src/screens/AddItemScreen.tsx index 308aa40..71e2f87 100644 --- a/src/screens/AddItemScreen.tsx +++ b/src/screens/AddItemScreen.tsx @@ -25,26 +25,18 @@ export const AddItemScreen = () => { const [units, setUnits] = useState(1); const [bulk, setBulk] = useState(0); const navigate = useNavigate(); - - const existingProductIds = useMemo( - () => new Set(items.map((item) => item.product_id)), - [items], - ); - - const availableProducts = useMemo( - () => products.filter((product) => !existingProductIds.has(product.id)), - [existingProductIds, products], - ); + const unitLabel = selectedProduct?.unit_type ?? 'Units'; + const cartonLabel = selectedProduct?.bulk_name ?? 'Cartons'; const filteredProducts = useMemo(() => { 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(); return searchableText.includes(normalizedQuery); }); - }, [availableProducts, query]); + }, [products, query]); useEffect(() => { if (!selectedProduct) return; @@ -56,17 +48,35 @@ export const AddItemScreen = () => { const addItem = async () => { const productId = selectedProduct?.id; - if (!id || !productId || existingProductIds.has(productId)) return; - await db.pickItems.add({ - id: uuidv4(), - pick_list_id: id, - product_id: productId, - quantity_units: units, - quantity_bulk: bulk, - status: 'pending', - created_at: Date.now(), - updated_at: Date.now(), - }); + if (!id || !productId) return; + + const addOrUpdateItem = async (is_carton: boolean, quantity: number) => { + if (quantity <= 0) return; + const existing = items.find( + (item) => item.product_id === productId && item.is_carton === is_carton, + ); + + if (existing) { + 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}`); }; @@ -113,8 +123,8 @@ export const AddItemScreen = () => { /> )} /> - - + + From dc2aeaa3dddb45961ee882a3f8318e1fda151f4e Mon Sep 17 00:00:00 2001 From: beatz174-bit Date: Sun, 23 Nov 2025 12:03:54 +1000 Subject: [PATCH 3/3] Add delete control for pick list items --- src/components/PickItemRow.tsx | 9 +++++++-- src/screens/ActivePickListScreen.tsx | 5 +++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/src/components/PickItemRow.tsx b/src/components/PickItemRow.tsx index 5e3be08..6046528 100644 --- a/src/components/PickItemRow.tsx +++ b/src/components/PickItemRow.tsx @@ -1,4 +1,4 @@ -import { Add } from '@mui/icons-material'; +import { Add, Delete } from '@mui/icons-material'; import { Button, Checkbox, Stack, Typography } from '@mui/material'; import { PickItem } from '../models/PickItem'; import { Product } from '../models/Product'; @@ -9,6 +9,7 @@ interface PickItemRowProps { onIncrementUnit: () => void; onIncrementBulk: () => void; onToggleStatus: () => void; + onDelete: () => void; } export const PickItemRow = ({ @@ -17,6 +18,7 @@ export const PickItemRow = ({ onIncrementUnit, onIncrementBulk, onToggleStatus, + onDelete, }: PickItemRowProps) => { const isPicked = item.status === 'picked'; @@ -42,13 +44,16 @@ export const PickItemRow = ({ - + + ); diff --git a/src/screens/ActivePickListScreen.tsx b/src/screens/ActivePickListScreen.tsx index 9d44e18..5e30ec3 100644 --- a/src/screens/ActivePickListScreen.tsx +++ b/src/screens/ActivePickListScreen.tsx @@ -44,6 +44,10 @@ export const ActivePickListScreen = () => { await db.pickItems.update(itemId, { status: nextStatus, updated_at: Date.now() }); }; + const handleDeleteItem = async (itemId: string) => { + await db.pickItems.delete(itemId); + }; + const returnToLists = () => { navigate('/pick-lists'); }; @@ -70,6 +74,7 @@ export const ActivePickListScreen = () => { onIncrementUnit={() => handleIncrementUnit(item.id)} onIncrementBulk={() => handleIncrementBulk(item.id)} onToggleStatus={() => handleToggleStatus(item.id)} + onDelete={() => handleDeleteItem(item.id)} /> ))}