diff --git a/Agents.md b/Agents.md index 3391438..0628625 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,10 +157,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 @@ -164,7 +167,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 @@ -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. - -### 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 +313,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 2c29468..6baae4b 100644 --- a/src/components/PickItemRow.tsx +++ b/src/components/PickItemRow.tsx @@ -1,8 +1,7 @@ -import { Chip, Stack, Typography } from '@mui/material'; -import { PickItem, PickItemStatus } from '../models/PickItem'; +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'; -import { useLongPress } from '../hooks/useLongPress'; -import { useSwipe } from '../hooks/useSwipe'; interface PickItemRowProps { item: PickItem; @@ -13,12 +12,6 @@ interface PickItemRowProps { onSwipeRight: () => void; } -const statusColor: Record = { - pending: 'default', - picked: 'success', - skipped: 'warning', -}; - export const PickItemRow = ({ item, product, @@ -39,8 +32,6 @@ export const PickItemRow = ({ justifyContent="space-between" spacing={1} sx={{ p: 1, borderRadius: 1, bgcolor: 'background.paper', boxShadow: 1 }} - {...longPressHandlers} - {...swipeHandlers} >
{product?.name ?? 'Unknown product'} 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 020494e..861e0b3 100644 --- a/src/screens/ActivePickListScreen.tsx +++ b/src/screens/ActivePickListScreen.tsx @@ -23,7 +23,7 @@ export const ActivePickListScreen = () => { const existing = await db.pickItems.get(itemId); if (!existing) return; await db.pickItems.update(itemId, { - quantity_units: existing.quantity_units + 1, + quantity: existing.quantity + 1, updated_at: Date.now(), }); }; @@ -41,7 +41,7 @@ export const ActivePickListScreen = () => { 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); }; diff --git a/src/screens/AddItemScreen.tsx b/src/screens/AddItemScreen.tsx index 3b1ac09..0985e7f 100644 --- a/src/screens/AddItemScreen.tsx +++ b/src/screens/AddItemScreen.tsx @@ -26,26 +26,18 @@ export const AddItemScreen = () => { const [quantity, setQuantity] = useState(1); const [isCarton, setIsCarton] = useState(false); 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; @@ -67,17 +59,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: quantity, - quantity_bulk: isCarton ? 1 : 0, - 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}`); }; @@ -124,22 +134,8 @@ export const AddItemScreen = () => { /> )} /> - { - const value = Number(event.target.value); - setQuantity(Number.isNaN(value) || value < 1 ? 1 : value); - }} - helperText={quantityHelperText} - fullWidth - /> - setIsCarton(event.target.checked)} />} - label={cartonLabel} - /> + +