costco-grocery-list/docs/classification-implementation.md

337 lines
11 KiB
Markdown

# 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`
```sql
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 features
- `source`: 'user', 'ml', or 'default'
## Architecture
### Backend
**Constants** ([backend/constants/classifications.js](backend/constants/classifications.js)):
- `ITEM_TYPES`: 11 predefined types (produce, meat, dairy, etc.)
- `ITEM_GROUPS`: Nested object mapping types to their valid groups
- `ZONES`: 10 Costco store zones
- Validation helpers: `isValidItemType()`, `isValidItemGroup()`, `isValidZone()`
**Models** ([backend/models/list.model.js](backend/models/list.model.js)):
- `getClassification(itemId)`: Returns classification for an item or null
- `upsertClassification(itemId, classification)`: INSERT or UPDATE using ON CONFLICT
- `updateItem(id, itemName, quantity)`: Update item name/quantity
**Controllers** ([backend/controllers/lists.controller.js](backend/controllers/lists.controller.js)):
- `getClassification(req, res)`: GET endpoint to fetch classification
- `updateItemWithClassification(req, res)`: Validates and updates item + classification
**Routes** ([backend/routes/list.routes.js](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](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](frontend/src/api/list.js)):
- `getClassification(id)`: Fetch classification for item
- `updateItemWithClassification(id, itemName, quantity, classification)`: Update item
**Components:**
1. **EditItemModal** ([frontend/src/components/EditItemModal.jsx](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'
2. **ItemClassificationModal** ([frontend/src/components/ItemClassificationModal.jsx](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
3. **GroceryListItem** ([frontend/src/components/GroceryListItem.jsx](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](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:**
1. User long-presses an item
2. System fetches existing classification (if any)
3. EditItemModal opens with prepopulated data
4. User can edit:
- Item name
- Quantity
- Classification (type, group, zone)
5. On save:
- Updates `grocery_list` if name/quantity changed
- UPSERTS `item_classification` if classification provided
- Sets confidence=1.0, source='user' for user-edited classification
**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:**
1. User enters item name
2. System checks for similar items (80% threshold)
3. User confirms/edits name
4. User uploads image (or skips)
5. **NEW:** ItemClassificationModal appears
- User selects type, group, zone (optional)
- Or skips classification
6. Item saved to `grocery_list`
7. If classification provided, saved to `item_classification`
## Data Flow Examples
### Adding Item with Classification
```javascript
// 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
```javascript
// 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:**
```json
{
"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:**
```json
{
"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:**
```json
{
"message": "Item updated successfully"
}
```
## Setup Instructions
### 1. Run Database Migration
```bash
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:**
1. Long-press any item in the list
2. Verify modal opens with item data
3. Select a type, then a group from filtered list
4. Save and verify item updates
**Test Add:**
1. Add a new item
2. Upload image (or skip)
3. Verify classification modal appears
4. Complete classification or skip
5. Verify item appears in list
## Validation Rules Summary
1. **Item Type → Item Group Dependency**
- Must select item_type before item_group becomes available
- Item group dropdown shows only groups for selected type
2. **Required Fields**
- When creating: item_type and item_group are required
- When editing: Classification is optional (can edit name/quantity only)
3. **No Free-Text**
- All classification values are select dropdowns
- Backend validates against predefined constants
4. **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:**
```javascript
{
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 position
- `onTouchMove`: Cancel if movement >10px
- `onTouchEnd`: Clear timer
- `onMouseDown`: Start timer
- `onMouseUp`: Clear timer
- `onMouseLeave`: Clear timer (prevent stuck state)
## Future Enhancements
1. **ML Predictions**: Use confidence <1.0 and source='ml' for auto-classification
2. **Bulk Edit**: Select multiple items and apply same classification
3. **Smart Suggestions**: Learn from user's classification patterns
4. **Zone Optimization**: Suggest optimal shopping route based on zones
5. **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:**
```sql
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.