From e745ce437547ac9c168aef9dc44c1e467e816254 Mon Sep 17 00:00:00 2001 From: pyr0ball Date: Thu, 16 Apr 2026 14:23:38 -0700 Subject: [PATCH] feat: wire meal planner slot editor and meal type picker Slot click now opens an inline editor panel: - Pick from saved recipes via dropdown (pre-loaded on mount) - Or type a custom label - Clear slot button when a slot is already filled - Save/Cancel with loading state Add meal type opens a chip picker showing the types not yet active (breakfast / lunch / snack minus whatever is already on the plan). Selecting one calls the new PATCH /meal-plans/{plan_id} endpoint. Backend: - PATCH /meal-plans/{plan_id} with UpdatePlanRequest(meal_types) - store.update_meal_plan_types() UPDATE ... RETURNING * - 409 on IntegrityError in create_plan (already in place) --- app/api/endpoints/meal_plans.py | 23 ++++ app/db/store.py | 6 + app/models/schemas/meal_plan.py | 4 + frontend/src/components/MealPlanView.vue | 163 +++++++++++++++++++++-- frontend/src/services/api.ts | 5 + frontend/src/stores/mealPlan.ts | 10 +- 6 files changed, 202 insertions(+), 9 deletions(-) diff --git a/app/api/endpoints/meal_plans.py b/app/api/endpoints/meal_plans.py index ad647e4..5e9ed0e 100644 --- a/app/api/endpoints/meal_plans.py +++ b/app/api/endpoints/meal_plans.py @@ -19,6 +19,7 @@ from app.models.schemas.meal_plan import ( PrepTaskSummary, ShoppingListResponse, SlotSummary, + UpdatePlanRequest, UpdatePrepTaskRequest, UpsertSlotRequest, VALID_MEAL_TYPES, @@ -113,6 +114,28 @@ async def list_plans( return result +@router.patch("/{plan_id}", response_model=PlanSummary) +async def update_plan( + plan_id: int, + req: UpdatePlanRequest, + session: CloudUser = Depends(get_session), + store: Store = Depends(get_store), +) -> PlanSummary: + plan = await asyncio.to_thread(store.get_meal_plan, plan_id) + if plan is None: + raise HTTPException(status_code=404, detail="Plan not found.") + # Free tier stays dinner-only; paid+ may add meal types + if can_use("meal_plan_config", session.tier): + meal_types = [t for t in req.meal_types if t in VALID_MEAL_TYPES] or ["dinner"] + else: + meal_types = ["dinner"] + updated = await asyncio.to_thread(store.update_meal_plan_types, plan_id, meal_types) + if updated is None: + raise HTTPException(status_code=404, detail="Plan not found.") + slots = await asyncio.to_thread(store.get_plan_slots, plan_id) + return _plan_summary(updated, slots) + + @router.get("/{plan_id}", response_model=PlanSummary) async def get_plan( plan_id: int, diff --git a/app/db/store.py b/app/db/store.py index 7bc7cca..296cc6c 100644 --- a/app/db/store.py +++ b/app/db/store.py @@ -1119,6 +1119,12 @@ class Store: def get_meal_plan(self, plan_id: int) -> dict | None: return self._fetch_one("SELECT * FROM meal_plans WHERE id = ?", (plan_id,)) + def update_meal_plan_types(self, plan_id: int, meal_types: list[str]) -> dict | None: + return self._fetch_one( + "UPDATE meal_plans SET meal_types = ? WHERE id = ? RETURNING *", + (json.dumps(meal_types), plan_id), + ) + def list_meal_plans(self) -> list[dict]: return self._fetch_all("SELECT * FROM meal_plans ORDER BY week_start DESC") diff --git a/app/models/schemas/meal_plan.py b/app/models/schemas/meal_plan.py index 6e1a678..dca28c7 100644 --- a/app/models/schemas/meal_plan.py +++ b/app/models/schemas/meal_plan.py @@ -22,6 +22,10 @@ class CreatePlanRequest(BaseModel): return v +class UpdatePlanRequest(BaseModel): + meal_types: list[str] + + class UpsertSlotRequest(BaseModel): recipe_id: int | None = None servings: float = Field(2.0, gt=0) diff --git a/frontend/src/components/MealPlanView.vue b/frontend/src/components/MealPlanView.vue index 7ee4c1e..c1d4c23 100644 --- a/frontend/src/components/MealPlanView.vue +++ b/frontend/src/components/MealPlanView.vue @@ -29,6 +29,70 @@ @add-meal-type="onAddMealType" /> + +
+
+ + {{ DAY_LABELS[slotEditing.dayOfWeek] }} · {{ slotEditing.mealType }} + + +
+ + +
+ + +
+ + +
+ + +
+

Save recipes from the Recipes tab to pick them here.

+ +
+ + + +
+
+ + +
+ Add meal type +
+ +
+ +
+