kiwi/app/models/schemas/meal_plan.py
pyr0ball e745ce4375 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)
2026-04-16 14:23:38 -07:00

100 lines
2.3 KiB
Python

# app/models/schemas/meal_plan.py
"""Pydantic schemas for meal planning endpoints."""
from __future__ import annotations
from datetime import date as _date
from pydantic import BaseModel, Field, field_validator
VALID_MEAL_TYPES = {"breakfast", "lunch", "dinner", "snack"}
class CreatePlanRequest(BaseModel):
week_start: _date
meal_types: list[str] = Field(default_factory=lambda: ["dinner"])
@field_validator("week_start")
@classmethod
def must_be_monday(cls, v: _date) -> _date:
if v.weekday() != 0:
raise ValueError("week_start must be a Monday (weekday 0)")
return v
class UpdatePlanRequest(BaseModel):
meal_types: list[str]
class UpsertSlotRequest(BaseModel):
recipe_id: int | None = None
servings: float = Field(2.0, gt=0)
custom_label: str | None = None
class SlotSummary(BaseModel):
id: int
plan_id: int
day_of_week: int
meal_type: str
recipe_id: int | None
recipe_title: str | None
servings: float
custom_label: str | None
class PlanSummary(BaseModel):
id: int
week_start: str
meal_types: list[str]
slots: list[SlotSummary]
created_at: str
class RetailerLink(BaseModel):
retailer: str
label: str
url: str
class GapItem(BaseModel):
ingredient_name: str
needed_raw: str | None # e.g. "2 cups" from recipe text
have_quantity: float | None # from pantry
have_unit: str | None
covered: bool # True = pantry has it
retailer_links: list[RetailerLink] = Field(default_factory=list)
class ShoppingListResponse(BaseModel):
plan_id: int
gap_items: list[GapItem]
covered_items: list[GapItem]
disclosure: str | None = None # affiliate disclosure text when links present
class PrepTaskSummary(BaseModel):
id: int
recipe_id: int | None
task_label: str
duration_minutes: int | None
sequence_order: int
equipment: str | None
is_parallel: bool
notes: str | None
user_edited: bool
class PrepSessionSummary(BaseModel):
id: int
plan_id: int
scheduled_date: str
status: str
tasks: list[PrepTaskSummary]
class UpdatePrepTaskRequest(BaseModel):
duration_minutes: int | None = None
sequence_order: int | None = None
notes: str | None = None
equipment: str | None = None