summaryrefslogtreecommitdiff
path: root/routers/menus.py
diff options
context:
space:
mode:
Diffstat (limited to 'routers/menus.py')
-rw-r--r--routers/menus.py191
1 files changed, 149 insertions, 42 deletions
diff --git a/routers/menus.py b/routers/menus.py
index 49c4654..9ec1d1d 100644
--- a/routers/menus.py
+++ b/routers/menus.py
@@ -1,4 +1,4 @@
-from fastapi import APIRouter, Depends, HTTPException
+from fastapi import APIRouter, Depends, HTTPException, Body
from sqlalchemy.orm import Session
from datetime import datetime, timedelta, date
import json
@@ -12,11 +12,15 @@ from config import settings
router = APIRouter(prefix="/api/menus", tags=["menus"])
+def _current_monday():
+ today = datetime.utcnow().date()
+ return today - timedelta(days=today.weekday())
+
+
@router.get("/current")
async def get_current_menu(db: Session = Depends(get_db)):
"""Get menu plan for current week."""
- today = datetime.utcnow().date()
- monday = today - timedelta(days=today.weekday())
+ monday = _current_monday()
menu = db.query(MenuPlan).filter(MenuPlan.week_start == monday).first()
if not menu:
@@ -37,67 +41,64 @@ async def generate_menu(request: dict = None, db: Session = Depends(get_db)):
try:
if isinstance(week_start, str):
week_start = datetime.fromisoformat(week_start).date()
- else:
- week_start = week_start
except Exception:
raise HTTPException(status_code=400, detail="Invalid week_start date")
else:
- today = datetime.utcnow().date()
- week_start = today - timedelta(days=today.weekday())
+ week_start = _current_monday()
# Build pantry context and generate menu
try:
pantry_context = pantry_service.build_pantry_context(db)
- ai_result = await ai_service.generate_weekly_menu(pantry_context)
+ user_notes = request.get("user_notes") if request and isinstance(request, dict) else None
+ ai_result = await ai_service.generate_weekly_menu(pantry_context, user_notes)
except ValueError as e:
raise HTTPException(status_code=503, detail=str(e))
- except (ConnectionError, Exception) as e:
+ except Exception as e:
raise HTTPException(status_code=503, detail=f"Ollama service error: {str(e)}")
- # Save recipes and build plan dict
- week_plan = ai_result.get("week_plan", {})
- plan_dict = {}
-
- for day, meals in week_plan.items():
- plan_dict[day] = {}
- for meal_type, meal_data in meals.items():
- if isinstance(meal_data, dict) and "name" in meal_data:
- meal_name = meal_data["name"]
+ # Save recipes and build plan list (flat array of IDs)
+ recipes_ai = ai_result.get("recipes", [])
+ plan_ids = []
- # Check if recipe exists by name
- existing_recipe = db.query(Recipe).filter(Recipe.name == meal_name).first()
- if existing_recipe:
- recipe_id = existing_recipe.id
- else:
- # Create new recipe
- new_recipe = Recipe(
- name=meal_name,
- meal_type=meal_type,
- ingredients=json.dumps(meal_data.get("ingredients", [])),
- instructions=meal_data.get("instructions", ""),
- estimated_time_minutes=meal_data.get("time_minutes", 30),
- servings=meal_data.get("serves", 2),
- source="ai",
- )
- db.add(new_recipe)
- db.flush()
- recipe_id = new_recipe.id
+ valid_recipes = [r for r in recipes_ai if isinstance(r, dict) and "name" in r]
+ all_names = [r["name"] for r in valid_recipes]
+ existing = db.query(Recipe).filter(Recipe.name.in_(all_names)).all()
+ existing_by_name = {r.name: r for r in existing}
- plan_dict[day][meal_type] = recipe_id
+ for recipe_data in valid_recipes:
+ meal_name = recipe_data["name"]
+ if meal_name in existing_by_name:
+ recipe_id = existing_by_name[meal_name].id
+ else:
+ new_recipe = Recipe(
+ name=meal_name,
+ meal_type=recipe_data.get("meal_type", "dinner"),
+ ingredients=json.dumps(recipe_data.get("ingredients", [])),
+ instructions=recipe_data.get("instructions", ""),
+ estimated_time_minutes=recipe_data.get("time_minutes", 30),
+ servings=recipe_data.get("serves", 2),
+ source="ai",
+ )
+ db.add(new_recipe)
+ db.flush()
+ recipe_id = new_recipe.id
+ plan_ids.append(recipe_id)
- # Upsert MenuPlan
+ ai_notes = ai_result.get("notes", "")
existing_plan = db.query(MenuPlan).filter(MenuPlan.week_start == week_start).first()
if existing_plan:
- existing_plan.plan = json.dumps(plan_dict)
+ existing_plan.plan = json.dumps(plan_ids)
existing_plan.generated_at = datetime.utcnow()
existing_plan.model_used = settings.model_name
+ existing_plan.notes = ai_notes
menu = existing_plan
else:
menu = MenuPlan(
week_start=week_start,
- plan=json.dumps(plan_dict),
+ plan=json.dumps(plan_ids),
generated_at=datetime.utcnow(),
model_used=settings.model_name,
+ notes=ai_notes,
)
db.add(menu)
@@ -106,8 +107,8 @@ async def generate_menu(request: dict = None, db: Session = Depends(get_db)):
return {
"menu_plan": MenuPlanRead.from_orm(menu),
- "week_plan": week_plan,
- "notes": ai_result.get("notes", ""),
+ "recipes": recipes_ai,
+ "notes": menu.notes or "",
}
@@ -127,6 +128,112 @@ async def get_menu(week_start: date, db: Session = Depends(get_db)):
return MenuPlanRead.from_orm(menu)
+@router.delete("/current")
+async def delete_current_menu(db: Session = Depends(get_db)):
+ """Delete the menu plan for the current week."""
+ monday = _current_monday()
+ menu = db.query(MenuPlan).filter(MenuPlan.week_start == monday).first()
+ if not menu:
+ raise HTTPException(status_code=404, detail="No menu plan for current week")
+ db.delete(menu)
+ db.commit()
+ return {"status": "deleted"}
+
+
+@router.delete("/current/recipes/{recipe_id}")
+async def remove_recipe_from_menu(recipe_id: int, db: Session = Depends(get_db)):
+ """Remove a single recipe from the current week's plan."""
+ monday = _current_monday()
+ menu = db.query(MenuPlan).filter(MenuPlan.week_start == monday).first()
+ if not menu:
+ raise HTTPException(status_code=404, detail="No menu plan for current week")
+
+ try:
+ plan_ids = json.loads(menu.plan)
+ except json.JSONDecodeError:
+ raise HTTPException(status_code=500, detail="Invalid menu plan data")
+
+ if recipe_id not in plan_ids:
+ raise HTTPException(status_code=404, detail="Recipe not in current menu plan")
+
+ plan_ids.remove(recipe_id)
+ menu.plan = json.dumps(plan_ids)
+ db.commit()
+ return {"status": "removed", "plan": plan_ids}
+
+
+@router.post("/current/recipes/{recipe_id}/swap")
+async def swap_recipe_in_menu(recipe_id: int, request: dict = Body(default={}), db: Session = Depends(get_db)):
+ """Swap a single recipe in the current week's plan with a new AI suggestion."""
+ monday = _current_monday()
+ menu = db.query(MenuPlan).filter(MenuPlan.week_start == monday).first()
+ if not menu:
+ raise HTTPException(status_code=404, detail="No menu plan for current week")
+
+ try:
+ plan_ids = json.loads(menu.plan)
+ except json.JSONDecodeError:
+ raise HTTPException(status_code=500, detail="Invalid menu plan data")
+
+ # Find the recipe being swapped
+ old_recipe = db.query(Recipe).filter(Recipe.id == recipe_id).first()
+ if not old_recipe:
+ raise HTTPException(status_code=404, detail="Recipe not found")
+
+ meal_type = old_recipe.meal_type
+
+ # Get names of all other recipes in the plan to avoid repeats
+ other_ids = [rid for rid in plan_ids if rid != recipe_id]
+ other_recipes = db.query(Recipe).filter(Recipe.id.in_(other_ids)).all() if other_ids else []
+ existing_names = [r.name for r in other_recipes]
+
+ # Generate replacement
+ user_notes = request.get("user_notes") if request else None
+ try:
+ pantry_context = pantry_service.build_pantry_context(db)
+ ai_result = await ai_service.generate_replacement_recipe(meal_type, existing_names, pantry_context, user_notes)
+ except ValueError as e:
+ raise HTTPException(status_code=503, detail=str(e))
+ except Exception as e:
+ raise HTTPException(status_code=503, detail=f"Ollama service error: {str(e)}")
+
+ recipe_data = ai_result.get("recipe", {})
+ if not recipe_data or "name" not in recipe_data:
+ raise HTTPException(status_code=503, detail="AI returned invalid recipe data")
+
+ # Save new recipe
+ new_recipe = Recipe(
+ name=recipe_data["name"],
+ meal_type=recipe_data.get("meal_type", meal_type),
+ ingredients=json.dumps(recipe_data.get("ingredients", [])),
+ instructions=recipe_data.get("instructions", ""),
+ estimated_time_minutes=recipe_data.get("time_minutes", 30),
+ servings=recipe_data.get("serves", 2),
+ source="ai",
+ )
+ db.add(new_recipe)
+ db.flush()
+
+ # Swap the ID in the plan
+ idx = plan_ids.index(recipe_id)
+ plan_ids[idx] = new_recipe.id
+ menu.plan = json.dumps(plan_ids)
+ db.commit()
+ db.refresh(new_recipe)
+
+ return {
+ "recipe": {
+ "id": new_recipe.id,
+ "name": new_recipe.name,
+ "meal_type": new_recipe.meal_type,
+ "ingredients": recipe_data.get("ingredients", []),
+ "instructions": new_recipe.instructions,
+ "estimated_time_minutes": new_recipe.estimated_time_minutes,
+ "servings": new_recipe.servings,
+ }
+ }
+
+
@router.delete("/{id}")
async def delete_menu(id: int, db: Session = Depends(get_db)):
"""Delete a menu plan."""