Merge branch 'main' into codex/refactor-additemscreen-for-quantity-input

This commit is contained in:
beatz174-bit authored and GitHub committed 2025-11-23 12:09:30 +10:00
commit 6337f69b63
6 files changed
+120 -76

No files matched your search

+16 -20
View File
@@ -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
------------------------------------------------------------------------ ------------------------------------------------------------------------
+3 -12
View File
@@ -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>
+58
View File
@@ -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,
});
}),
);
});
} }
} }
+5 -2
View File
@@ -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;
} }
+2 -2
View File
@@ -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);
}; };
+36 -40
View File
@@ -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>