diff --git a/README.md b/README.md
index c09996c..8f50dc0 100644
--- a/README.md
+++ b/README.md
@@ -14,7 +14,7 @@ This project was born out of frustration with meal kit services (Blue Apron →
- **Grocery Integration**: Scrapes Lucky California weekly ads and sales
- **Family Approval Workflow**: Email proposals with approve/deny; one denial swaps the meal
-- **Shopping List Generation**: Weekly list grouped by store aisles, highlighting sales
+- **Shopping List Generation**: Weekly list grouped by store aisles, highlighting sales, with interactive checkboxes to track purchased items
- **Pantry Integration**: Specify home items to incorporate into suggestions
- **Web UI**: Modern interface for the whole family
- **Learning**: Feedback-based meal recommendations
diff --git a/backend/app/api/shopping_list.py b/backend/app/api/shopping_list.py
index d1dc56d..2de3919 100644
--- a/backend/app/api/shopping_list.py
+++ b/backend/app/api/shopping_list.py
@@ -10,10 +10,20 @@ from app.schemas import ShoppingListResponse, ShoppingListItem
from datetime import date
from typing import List
from collections import defaultdict
+from uuid import UUID
router = APIRouter()
+def _parse_uuid(value):
+ if not value:
+ return None
+ try:
+ return UUID(value) if isinstance(value, str) else value
+ except ValueError:
+ return None
+
+
@router.get("", response_model=ShoppingListResponse)
def get_shopping_list(db: Session = Depends(get_db)):
profile = db.query(FamilyProfile).first()
@@ -51,8 +61,9 @@ def get_shopping_list(db: Session = Depends(get_db)):
recipe = db.query(Recipe).filter(Recipe.id == item.recipe_id).first()
if recipe and recipe.ingredients:
for ing in recipe.ingredients:
- if ing.get('ingredient_id'):
- all_ingredient_ids.add(ing['ingredient_id'])
+ ing_id = _parse_uuid(ing.get('ingredient_id'))
+ if ing_id:
+ all_ingredient_ids.add(ing_id)
if all_ingredient_ids:
ingredients = db.query(Ingredient).filter(
@@ -71,19 +82,14 @@ def get_shopping_list(db: Session = Depends(get_db)):
aggregated = defaultdict(lambda: {"quantity": 0.0, "unit": None, "name": ""})
- # Build ingredient name lookup from IDs
- ingredient_names = {}
- if all_ingredient_ids:
- ingredient_names = {str(i.id): i.name for i in ingredients}
-
for plan_item in current_plan.items:
recipe = db.query(Recipe).filter(Recipe.id == plan_item.recipe_id).first()
if recipe and recipe.ingredients:
for ing in recipe.ingredients:
- ing_id = ing.get('ingredient_id')
+ ing_id = _parse_uuid(ing.get('ingredient_id'))
name = ing.get('name')
- if not name and ing_id:
- name = ingredient_names.get(str(ing_id))
+ if not name and ing_id and ing_id in ingredient_map:
+ name = ingredient_map[ing_id].name
if not name:
name = 'Unknown'
quantity = ing.get('quantity', 1.0) or 1.0
@@ -104,7 +110,7 @@ def get_shopping_list(db: Session = Depends(get_db)):
in_pantry = ingredient_id and ingredient_id in pantry_items
ing_obj = ingredient_map.get(ingredient_id) if ingredient_id else None
- price = ing_obj.typical_price if ing_obj else None
+ price = float(ing_obj.typical_price) if ing_obj and ing_obj.typical_price is not None else None
sale_price = None
is_on_sale = False
diff --git a/frontend/src/pages/ShoppingList.tsx b/frontend/src/pages/ShoppingList.tsx
index 72039af..d2d3868 100644
--- a/frontend/src/pages/ShoppingList.tsx
+++ b/frontend/src/pages/ShoppingList.tsx
@@ -1,5 +1,6 @@
+import { useState, useEffect } from 'react'
import { useQuery } from '@tanstack/react-query'
-import { Printer, ShoppingCart, Package, Tag, Receipt } from 'lucide-react'
+import { Printer, ShoppingCart, Package, Tag, Receipt, RotateCcw } from 'lucide-react'
import { mealPlannerApi } from '../api'
import type { ShoppingList } from '../types'
import { Button } from '../components/ui/Button'
@@ -40,12 +41,61 @@ function SkeletonCard() {
)
}
+function storageKey(week: string) {
+ return `shopping-list-checked-${week}`
+}
+
export default function ShoppingListPage() {
const { data: shoppingList, isLoading } = useQuery