11 KiB
Item Classification Implementation Guide
Overview
This implementation adds a classification system to the grocery app allowing users to categorize items by type, group, and store zone. The system supports both creating new items with classification and editing existing items.
Database Schema
New Table: item_classification
CREATE TABLE item_classification (
id INTEGER PRIMARY KEY REFERENCES grocery_list(id) ON DELETE CASCADE,
item_type VARCHAR(50) NOT NULL,
item_group VARCHAR(100) NOT NULL,
zone VARCHAR(100),
confidence DECIMAL(3,2) DEFAULT 1.0,
source VARCHAR(20) DEFAULT 'user',
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
Key Points:
- One-to-one relationship with
grocery_list(id is both PK and FK) - Cascade delete ensures classification is removed when item is deleted
- Classification values are NOT enforced by DB - controlled at app layer
confidence: 1.0 for user input, lower values reserved for future ML featuressource: 'user', 'ml', or 'default'
Architecture
Backend
Constants (backend/constants/classifications.js):
ITEM_TYPES: 11 predefined types (produce, meat, dairy, etc.)ITEM_GROUPS: Nested object mapping types to their valid groupsZONES: 10 Costco store zones- Validation helpers:
isValidItemType(),isValidItemGroup(),isValidZone()
Models (backend/models/list.model.js):
getClassification(itemId): Returns classification for an item or nullupsertClassification(itemId, classification): INSERT or UPDATE using ON CONFLICTupdateItem(id, itemName, quantity): Update item name/quantity
Controllers (backend/controllers/lists.controller.js):
getClassification(req, res): GET endpoint to fetch classificationupdateItemWithClassification(req, res): Validates and updates item + classification
Routes (backend/routes/list.routes.js):
GET /list/item/:id/classification- Fetch classification (all roles)PUT /list/item/:id- Update item + classification (editor/admin)
Frontend
Constants (frontend/src/constants/classifications.js):
- Mirrors backend constants for UI rendering
getItemTypeLabel(): Converts type keys to display names
API Methods (frontend/src/api/list.js):
getClassification(id): Fetch classification for itemupdateItemWithClassification(id, itemName, quantity, classification): Update item
Components:
-
EditItemModal (frontend/src/components/EditItemModal.jsx)
- Triggered by long-press (500ms) on any grocery item
- Prepopulates with existing item data + classification
- Cascading selects: item_type → item_group (filtered) → zone
- Validation: Requires item_group if item_type is selected
- Upserts classification with confidence=1.0, source='user'
-
ItemClassificationModal (frontend/src/components/ItemClassificationModal.jsx)
- Shown after image upload in new item flow
- Required fields: item_type, item_group
- Optional: zone
- User can skip classification entirely
-
GroceryListItem (frontend/src/components/GroceryListItem.jsx)
- Long-press detection with 500ms timer
- Supports both touch (mobile) and mouse (desktop) events
- Cancels long-press if finger/mouse moves >10px
Main Page (frontend/src/pages/GroceryList.jsx):
- Orchestrates all modal flows
- Add flow: Name → Similar check → Image upload → Classification → Save
- Edit flow: Long-press → Load classification → Edit → Save
User Flows
FEATURE 1: Edit Existing Item
Trigger: Long-press (500ms) on any item in the list
Steps:
- User long-presses an item
- System fetches existing classification (if any)
- EditItemModal opens with prepopulated data
- User can edit:
- Item name
- Quantity
- Classification (type, group, zone)
- On save:
- Updates
grocery_listif name/quantity changed - UPSERTS
item_classificationif classification provided - Sets confidence=1.0, source='user' for user-edited classification
- Updates
Validation:
- Item name required
- Quantity must be ≥ 1
- If item_type selected, item_group is required
- item_group options filtered by selected item_type
FEATURE 2: Add New Item with Classification
Enhanced flow:
- User enters item name
- System checks for similar items (80% threshold)
- User confirms/edits name
- User uploads image (or skips)
- NEW: ItemClassificationModal appears
- User selects type, group, zone (optional)
- Or skips classification
- Item saved to
grocery_list - If classification provided, saved to
item_classification
Data Flow Examples
Adding Item with Classification
// 1. Add item to grocery_list
const response = await addItem(itemName, quantity, imageFile);
// 2. Get item ID
const item = await getItemByName(itemName);
// 3. Add classification
await updateItemWithClassification(item.id, undefined, undefined, {
item_type: 'produce',
item_group: 'Fruits',
zone: 'Fresh Foods Right'
});
Editing Item Classification
// Update both item data and classification in one call
await updateItemWithClassification(itemId, newName, newQuantity, {
item_type: 'dairy',
item_group: 'Cheese',
zone: 'Dairy Cooler'
});
Backend Request/Response Shapes
GET /list/item/:id/classification
Response:
{
"item_type": "produce",
"item_group": "Fruits",
"zone": "Fresh Foods Right",
"confidence": 1.0,
"source": "user"
}
Or null if no classification exists.
PUT /list/item/:id
Request Body:
{
"itemName": "Organic Apples",
"quantity": 5,
"classification": {
"item_type": "produce",
"item_group": "Organic Produce",
"zone": "Fresh Foods Right"
}
}
Validation:
- Validates item_type against allowed values
- Validates item_group is valid for the selected item_type
- Validates zone against allowed zones
- Returns 400 with error message if invalid
Response:
{
"message": "Item updated successfully"
}
Setup Instructions
1. Run Database Migration
psql -U your_user -d your_database -f backend/migrations/create_item_classification_table.sql
2. Restart Backend
The backend automatically loads the new classification constants and routes.
3. Test Flows
Test Edit:
- Long-press any item in the list
- Verify modal opens with item data
- Select a type, then a group from filtered list
- Save and verify item updates
Test Add:
- Add a new item
- Upload image (or skip)
- Verify classification modal appears
- Complete classification or skip
- Verify item appears in list
Validation Rules Summary
-
Item Type → Item Group Dependency
- Must select item_type before item_group becomes available
- Item group dropdown shows only groups for selected type
-
Required Fields
- When creating: item_type and item_group are required
- When editing: Classification is optional (can edit name/quantity only)
-
No Free-Text
- All classification values are select dropdowns
- Backend validates against predefined constants
-
Graceful Handling
- Items without classification display normally
- Edit modal works for both classified and unclassified items
- Classification is always optional (can be skipped)
State Management (Frontend)
GroceryList.jsx state:
{
showEditModal: false,
editingItem: null, // Item + classification data
showClassificationModal: false,
classificationPendingItem: {
itemName,
quantity,
imageFile
}
}
Long-Press Implementation Details
Timing:
- Desktop (mouse): 500ms hold
- Mobile (touch): 500ms hold with <10px movement threshold
Event Handlers:
onTouchStart: Start timer, record positiononTouchMove: Cancel if movement >10pxonTouchEnd: Clear timeronMouseDown: Start timeronMouseUp: Clear timeronMouseLeave: Clear timer (prevent stuck state)
Future Enhancements
- ML Predictions: Use confidence <1.0 and source='ml' for auto-classification
- Bulk Edit: Select multiple items and apply same classification
- Smart Suggestions: Learn from user's classification patterns
- Zone Optimization: Suggest optimal shopping route based on zones
- Analytics: Most common types/groups, zone coverage
Troubleshooting
Issue: Classification not saving
- Check browser console for validation errors
- Verify item_type/item_group combination is valid
- Ensure item_classification table exists
Issue: Long-press not triggering
- Check that user has editor/admin role
- Verify onLongPress prop is passed to GroceryListItem
- Test on both mobile (touch) and desktop (mouse)
Issue: Item groups not filtering
- Verify item_type is selected first
- Check that ITEM_GROUPS constant has entries for the selected type
- Ensure state updates are triggering re-render
Files Modified/Created
Backend
- ✅
backend/constants/classifications.js(NEW) - ✅
backend/models/list.model.js(MODIFIED) - ✅
backend/controllers/lists.controller.js(MODIFIED) - ✅
backend/routes/list.routes.js(MODIFIED) - ✅
backend/migrations/create_item_classification_table.sql(NEW)
Frontend
- ✅
frontend/src/constants/classifications.js(NEW) - ✅
frontend/src/api/list.js(MODIFIED) - ✅
frontend/src/components/EditItemModal.jsx(NEW) - ✅
frontend/src/components/ItemClassificationModal.jsx(NEW) - ✅
frontend/src/components/GroceryListItem.jsx(MODIFIED) - ✅
frontend/src/pages/GroceryList.jsx(MODIFIED) - ✅
frontend/src/styles/EditItemModal.css(NEW) - ✅
frontend/src/styles/ItemClassificationModal.css(NEW)
API Summary
| Method | Endpoint | Auth | Description |
|---|---|---|---|
| GET | /list/item/:id/classification |
All roles | Get item classification |
| PUT | /list/item/:id |
Editor/Admin | Update item + classification |
Database Operations
Upsert Pattern:
INSERT INTO item_classification (id, item_type, item_group, zone, confidence, source)
VALUES ($1, $2, $3, $4, $5, $6)
ON CONFLICT (id)
DO UPDATE SET
item_type = EXCLUDED.item_type,
item_group = EXCLUDED.item_group,
zone = EXCLUDED.zone,
confidence = EXCLUDED.confidence,
source = EXCLUDED.source
RETURNING *;
This ensures we INSERT if no classification exists, or UPDATE if it does.