from fastapi import APIRouter, Depends, HTTPException from sqlalchemy.orm import Session, joinedload from app.database import get_db from app.models import ( MealPlan, MealPlanItem, MealPlanVote, Recipe, FamilyProfile, FamilyMember, ApprovalToken, MealPlanStatus, MealPlanItemStatus, MealType, ApprovalTokenStatus ) from app.schemas import ( MealPlanResponse, MealPlanCreate, MealPlanItemResponse, VoteRequest, VoteResponse ) from uuid import UUID from typing import List, Optional from datetime import datetime, timedelta router = APIRouter() @router.get("/planned", response_model=Optional[MealPlanResponse]) def get_planned_meals(db: Session = Depends(get_db)): profile = db.query(FamilyProfile).first() if not profile: raise HTTPException(status_code=404, detail="Family profile not found") meal_plan = db.query(MealPlan).filter( MealPlan.family_profile_id == profile.id ).order_by(MealPlan.week_start_date.desc()).first() if not meal_plan: return None return meal_plan @router.post("/", response_model=MealPlanResponse) def create_meal_plan(meal_plan_data: MealPlanCreate, db: Session = Depends(get_db)): profile = db.query(FamilyProfile).first() if not profile: raise HTTPException(status_code=404, detail="Family profile not found") existing = db.query(MealPlan).filter( MealPlan.family_profile_id == profile.id, MealPlan.week_start_date == meal_plan_data.week_start_date ).first() if existing: raise HTTPException(status_code=400, detail="Meal plan for this week already exists") db_meal_plan = MealPlan( family_profile_id=profile.id, week_start_date=meal_plan_data.week_start_date, status=MealPlanStatus[meal_plan_data.status.value.upper()], approval_deadline=meal_plan_data.approval_deadline, notes=meal_plan_data.notes ) db.add(db_meal_plan) db.flush() for item_data in meal_plan_data.items: db_item = MealPlanItem( meal_plan_id=db_meal_plan.id, recipe_id=item_data.recipe_id, day_of_week=item_data.day_of_week, meal_type=MealType[item_data.meal_type.value.upper()], estimated_cost=item_data.estimated_cost ) db.add(db_item) db.commit() db.refresh(db_meal_plan) return db_meal_plan @router.get("/{meal_plan_id}", response_model=MealPlanResponse) def get_meal_plan(meal_plan_id: UUID, db: Session = Depends(get_db)): meal_plan = db.query(MealPlan).filter(MealPlan.id == meal_plan_id).first() if not meal_plan: raise HTTPException(status_code=404, detail="Meal plan not found") return meal_plan @router.post("/{meal_plan_id}/lock") def lock_meal_plan(meal_plan_id: UUID, db: Session = Depends(get_db)): meal_plan = db.query(MealPlan).filter(MealPlan.id == meal_plan_id).first() if not meal_plan: raise HTTPException(status_code=404, detail="Meal plan not found") meal_plan.status = MealPlanStatus.LOCKED db.commit() return {"message": "Meal plan locked", "status": meal_plan.status.value} @router.get("/items/{item_id}/vote/{token}") def get_vote_page(item_id: UUID, token: str, db: Session = Depends(get_db)): approval_token = db.query(ApprovalToken).filter(ApprovalToken.token == token).first() if not approval_token: raise HTTPException(status_code=404, detail="Invalid token") if approval_token.meal_plan_item_id != item_id: raise HTTPException(status_code=400, detail="Token not valid for this meal") if approval_token.status != ApprovalTokenStatus.ACTIVE: raise HTTPException(status_code=400, detail="Token has already been used or expired") if approval_token.expires_at < datetime.now(): raise HTTPException(status_code=400, detail="Token has expired") item = db.query(MealPlanItem).filter(MealPlanItem.id == item_id).first() if not item: raise HTTPException(status_code=404, detail="Meal plan item not found") return { "item_id": str(item_id), "family_member_id": str(approval_token.family_member_id), "meal_plan_item": item } @router.post("/items/{item_id}/vote/{token}", response_model=VoteResponse) def submit_vote(item_id: UUID, token: str, vote_data: VoteRequest, db: Session = Depends(get_db)): approval_token = db.query(ApprovalToken).filter(ApprovalToken.token == token).first() if not approval_token: raise HTTPException(status_code=404, detail="Invalid token") if approval_token.meal_plan_item_id != item_id: raise HTTPException(status_code=400, detail="Token not valid for this meal") if approval_token.status != ApprovalTokenStatus.ACTIVE: raise HTTPException(status_code=400, detail="Token has already been used or expired") if approval_token.expires_at < datetime.now(): approval_token.status = ApprovalTokenStatus.EXPIRED db.commit() raise HTTPException(status_code=400, detail="Token has expired") existing_vote = db.query(MealPlanVote).filter( MealPlanVote.meal_plan_item_id == item_id, MealPlanVote.family_member_id == approval_token.family_member_id ).first() if existing_vote: raise HTTPException(status_code=400, detail="You have already voted on this meal") vote = MealPlanVote( meal_plan_item_id=item_id, family_member_id=approval_token.family_member_id, vote=vote_data.vote ) db.add(vote) approval_token.status = ApprovalTokenStatus.USED approval_token.used_at = datetime.now() item = db.query(MealPlanItem).filter(MealPlanItem.id == item_id).first() if not vote_data.vote and vote_data.denial_reason: item.approval_status = MealPlanItemStatus.DENIED item.denial_reason = vote_data.denial_reason item.denial_details = vote_data.denial_details db.commit() db.refresh(vote) return vote @router.get("/items/{item_id}", response_model=MealPlanItemResponse) def get_meal_item(item_id: UUID, db: Session = Depends(get_db)): item = db.query(MealPlanItem).options(joinedload(MealPlanItem.recipe)).filter( MealPlanItem.id == item_id ).first() if not item: raise HTTPException(status_code=404, detail="Meal plan item not found") return item @router.post("/items/{item_id}/swap") def swap_meal_item(item_id: UUID, new_recipe_id: UUID, db: Session = Depends(get_db)): item = db.query(MealPlanItem).filter(MealPlanItem.id == item_id).first() if not item: raise HTTPException(status_code=404, detail="Meal plan item not found") new_recipe = db.query(Recipe).filter(Recipe.id == new_recipe_id).first() if not new_recipe: raise HTTPException(status_code=404, detail="New recipe not found") item.recipe_id = new_recipe_id item.approval_status = MealPlanItemStatus.PENDING item.denial_reason = None item.denial_details = None db.commit() return {"message": "Meal swapped", "item": item}