Merge pull request #32 from beatz174-bit/codex/add-quantity-and-is_carton-fields-to-pickitem

Add carton-aware pick item quantities and migration
This commit is contained in:
beatz174-bit
2025-11-23 12:07:10 +10:00
committed by GitHub
5 changed files with 109 additions and 42 deletions
+8 -3
View File
@@ -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
@@ -162,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
+58
View File
@@ -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,
});
}),
);
});
}
}
+5 -2
View File
@@ -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;
}
+2 -11
View File
@@ -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(),
});
};
+28 -18
View File
@@ -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;
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_units: units,
quantity_bulk: bulk,
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 = () => {
/>
)}
/>
<NumericStepper label="Units" value={units} onChange={setUnits} />
<NumericStepper label="Bulk" value={bulk} onChange={setBulk} />
<NumericStepper label={unitLabel} value={units} onChange={setUnits} />
<NumericStepper label={cartonLabel} value={bulk} onChange={setBulk} />
<Button variant="contained" disabled={!selectedProduct} onClick={addItem}>
Add to List
</Button>