updated the tracker to have more features

This commit is contained in:
2025-09-30 08:57:38 -07:00
parent 2c87d4c630
commit b6184414c0
4 changed files with 830 additions and 152 deletions

View File

@@ -6,15 +6,11 @@ import logging
from typing import List, Optional
# Import from the database module
from app.database import get_db, Meal, Template, TemplateMeal, TrackedDay, TrackedMeal, calculate_meal_nutrition
from app.database import get_db, Meal, Template, TemplateMeal, TrackedDay, TrackedMeal, calculate_meal_nutrition, MealFood, TrackedMealFood, Food, calculate_day_nutrition_tracked
from main import templates
router = APIRouter()
# Import from the database module
from app.database import get_db, Meal, Template, TemplateMeal, TrackedDay, TrackedMeal, calculate_meal_nutrition, calculate_day_nutrition_tracked
from main import templates
# Tracker tab - Main page
@router.get("/tracker", response_class=HTMLResponse)
async def tracker_page(request: Request, person: str = "Sarah", date: str = None, db: Session = Depends(get_db)):
@@ -46,8 +42,10 @@ async def tracker_page(request: Request, person: str = "Sarah", date: str = None
db.refresh(tracked_day)
logging.info(f"DEBUG: Created new tracked day for {person} on {current_date}")
# Get tracked meals for this day
tracked_meals = db.query(TrackedMeal).filter(
# Get tracked meals for this day with eager loading of meal foods
tracked_meals = db.query(TrackedMeal).options(
joinedload(TrackedMeal.meal).joinedload(Meal.meal_foods).joinedload(MealFood.food)
).filter(
TrackedMeal.tracked_day_id == tracked_day.id
).all()
@@ -56,6 +54,9 @@ async def tracker_page(request: Request, person: str = "Sarah", date: str = None
# Get all templates for template dropdown
templates_list = db.query(Template).all()
# Get all foods for dropdown
foods = db.query(Food).all()
# Calculate day totals
day_totals = calculate_day_nutrition_tracked(tracked_meals, db)
@@ -72,7 +73,8 @@ async def tracker_page(request: Request, person: str = "Sarah", date: str = None
"is_modified": tracked_day.is_modified,
"day_totals": day_totals,
"meals": meals,
"templates": templates_list
"templates": templates_list,
"foods": foods
})
# Tracker API Routes
@@ -272,6 +274,36 @@ async def tracker_apply_template(request: Request, db: Session = Depends(get_db)
logging.error(f"DEBUG: Error applying template: {e}")
return {"status": "error", "message": str(e)}
@router.post("/tracker/update_tracked_food")
async def update_tracked_food(request: Request, data: dict = Body(...), db: Session = Depends(get_db)):
"""Update quantity of a custom food in a tracked meal"""
try:
tracked_food_id = data.get("tracked_food_id")
quantity = float(data.get("quantity", 1.0))
logging.info(f"DEBUG: Updating tracked food {tracked_food_id} quantity to {quantity}")
tracked_food = db.query(TrackedMealFood).filter(TrackedMealFood.id == tracked_food_id).first()
if not tracked_food:
return {"status": "error", "message": "Tracked food not found"}
# Update quantity
tracked_food.quantity = quantity
# Mark the tracked day as modified
tracked_day = tracked_food.tracked_meal.tracked_day
tracked_day.is_modified = True
db.commit()
logging.info(f"DEBUG: Successfully updated tracked food quantity")
return {"status": "success"}
except Exception as e:
db.rollback()
logging.error(f"DEBUG: Error updating tracked food: {e}")
return {"status": "error", "message": str(e)}
@router.post("/tracker/reset_to_plan")
async def tracker_reset_to_plan(request: Request, db: Session = Depends(get_db)):
"""Reset tracked day back to original plan"""
@@ -311,4 +343,128 @@ async def tracker_reset_to_plan(request: Request, db: Session = Depends(get_db))
except Exception as e:
db.rollback()
logging.error(f"DEBUG: Error resetting to plan: {e}")
return {"status": "error", "message": str(e)}
@router.post("/tracker/save_as_new_meal")
async def save_as_new_meal(data: dict = Body(...), db: Session = Depends(get_db)):
"""Save an edited tracked meal as a new meal/variant"""
try:
tracked_meal_id = data.get("tracked_meal_id")
new_meal_name = data.get("new_meal_name")
foods_data = data.get("foods", [])
if not new_meal_name:
raise HTTPException(status_code=400, detail="New meal name is required")
tracked_meal = db.query(TrackedMeal).options(
joinedload(TrackedMeal.meal).joinedload(Meal.meal_foods).joinedload(MealFood.food),
joinedload(TrackedMeal.tracked_foods).joinedload(TrackedMealFood.food)
).filter(TrackedMeal.id == tracked_meal_id).first()
if not tracked_meal:
raise HTTPException(status_code=404, detail="Tracked meal not found")
# Create a new meal
new_meal = Meal(name=new_meal_name, meal_type="custom", meal_time=tracked_meal.meal_time)
db.add(new_meal)
db.flush() # Flush to get the new meal ID
# Add foods to the new meal
for food_data in foods_data:
meal_food = MealFood(
meal_id=new_meal.id,
food_id=food_data["food_id"],
quantity=food_data["quantity"]
)
db.add(meal_food)
# Update the original tracked meal to point to the new meal
tracked_meal.meal_id = new_meal.id
tracked_meal.quantity = 1.0 # Reset quantity to 1.0 as the new meal contains the correct quantities
# Clear custom tracked foods from the original tracked meal
for tf in tracked_meal.tracked_foods:
db.delete(tf)
# Mark the tracked day as modified
tracked_meal.tracked_day.is_modified = True
db.commit()
db.refresh(new_meal)
db.refresh(tracked_meal)
return {"status": "success", "new_meal_id": new_meal.id}
except HTTPException as he:
db.rollback()
logging.error(f"DEBUG: HTTP Error saving as new meal: {he.detail}")
return {"status": "error", "message": he.detail}
except Exception as e:
db.rollback()
logging.error(f"DEBUG: Error saving as new meal: {e}")
return {"status": "error", "message": str(e)}
@router.post("/tracker/add_food")
async def tracker_add_food(data: dict = Body(...), db: Session = Depends(get_db)):
"""Add a single food item to the tracker"""
try:
person = data.get("person")
date_str = data.get("date")
food_id = data.get("food_id")
quantity = float(data.get("quantity", 1.0))
meal_time = data.get("meal_time")
logging.info(f"DEBUG: Adding single food to tracker - person={person}, date={date_str}, food_id={food_id}, quantity={quantity}, meal_time={meal_time}")
# Parse date
from datetime import datetime
date = datetime.fromisoformat(date_str).date()
# Get or create tracked day
tracked_day = db.query(TrackedDay).filter(
TrackedDay.person == person,
TrackedDay.date == date
).first()
if not tracked_day:
tracked_day = TrackedDay(person=person, date=date, is_modified=True)
db.add(tracked_day)
db.commit()
db.refresh(tracked_day)
# Get the food
food = db.query(Food).filter(Food.id == food_id).first()
if not food:
return {"status": "error", "message": "Food not found"}
# Create a new Meal for this single food entry
# This allows it to be treated like any other meal in the tracker view
new_meal = Meal(name=food.name, meal_type="single_food", meal_time=meal_time)
db.add(new_meal)
db.flush() # Flush to get the new meal ID
# Link the food to the new meal
meal_food = MealFood(meal_id=new_meal.id, food_id=food.id, quantity=quantity)
db.add(meal_food)
# Create tracked meal entry
tracked_meal = TrackedMeal(
tracked_day_id=tracked_day.id,
meal_id=new_meal.id,
meal_time=meal_time,
quantity=1.0 # Quantity for single food meals is always 1.0, actual food quantity is in MealFood
)
db.add(tracked_meal)
# Mark day as modified
tracked_day.is_modified = True
db.commit()
logging.info(f"DEBUG: Successfully added single food to tracker")
return {"status": "success"}
except Exception as e:
db.rollback()
logging.error(f"DEBUG: Error adding single food to tracker: {e}")
return {"status": "error", "message": str(e)}

View File

@@ -138,6 +138,21 @@ class TrackedMeal(Base):
tracked_day = relationship("TrackedDay", back_populates="tracked_meals")
meal = relationship("Meal")
tracked_foods = relationship("TrackedMealFood", back_populates="tracked_meal", cascade="all, delete-orphan")
class TrackedMealFood(Base):
"""Custom food entries for a tracked meal (overrides or additions)"""
__tablename__ = "tracked_meal_foods"
id = Column(Integer, primary_key=True, index=True)
tracked_meal_id = Column(Integer, ForeignKey("tracked_meals.id"))
food_id = Column(Integer, ForeignKey("foods.id"))
quantity = Column(Float, default=1.0) # Custom quantity for this tracked instance
is_override = Column(Boolean, default=False) # True if overriding original meal food, False if addition
tracked_meal = relationship("TrackedMeal", back_populates="tracked_foods")
food = relationship("Food")
# Pydantic models
class FoodCreate(BaseModel):
@@ -259,10 +274,17 @@ class WeeklyMenuDetail(BaseModel):
model_config = ConfigDict(from_attributes=True)
class TrackedMealFoodExport(BaseModel):
food_id: int
quantity: float
is_override: bool
class TrackedMealExport(BaseModel):
meal_id: int
meal_time: str
quantity: float
tracked_foods: List[TrackedMealFoodExport] = []
class TrackedDayExport(BaseModel):
id: int
@@ -353,6 +375,49 @@ def calculate_day_nutrition(plans, db: Session):
return day_totals
def calculate_tracked_meal_nutrition(tracked_meal, db: Session):
"""Calculate nutrition for a tracked meal, including custom foods"""
totals = {
'calories': 0, 'protein': 0, 'carbs': 0, 'fat': 0,
'fiber': 0, 'sugar': 0, 'sodium': 0, 'calcium': 0
}
# Base meal nutrition scaled by quantity
base_nutrition = calculate_meal_nutrition(tracked_meal.meal, db)
quantity = tracked_meal.quantity
for key in totals:
if key in base_nutrition:
totals[key] += base_nutrition[key] * quantity
# Add custom tracked foods
for tracked_food in tracked_meal.tracked_foods:
food = tracked_food.food
food_quantity = tracked_food.quantity
totals['calories'] += food.calories * food_quantity
totals['protein'] += food.protein * food_quantity
totals['carbs'] += food.carbs * food_quantity
totals['fat'] += food.fat * food_quantity
totals['fiber'] += (food.fiber or 0) * food_quantity
totals['sugar'] += (food.sugar or 0) * food_quantity
totals['sodium'] += (food.sodium or 0) * food_quantity
totals['calcium'] += (food.calcium or 0) * food_quantity
# Calculate percentages
total_cals = totals['calories']
if total_cals > 0:
totals['protein_pct'] = round((totals['protein'] * 4 / total_cals) * 100, 1)
totals['carbs_pct'] = round((totals['carbs'] * 4 / total_cals) * 100, 1)
totals['fat_pct'] = round((totals['fat'] * 9 / total_cals) * 100, 1)
totals['net_carbs'] = totals['carbs'] - totals['fiber']
else:
totals['protein_pct'] = 0
totals['carbs_pct'] = 0
totals['fat_pct'] = 0
totals['net_carbs'] = 0
return totals
def calculate_day_nutrition_tracked(tracked_meals, db: Session):
"""Calculate total nutrition for tracked meals"""
day_totals = {
@@ -361,17 +426,10 @@ def calculate_day_nutrition_tracked(tracked_meals, db: Session):
}
for tracked_meal in tracked_meals:
meal_nutrition = calculate_meal_nutrition(tracked_meal.meal, db)
quantity = tracked_meal.quantity
day_totals['calories'] += meal_nutrition['calories'] * quantity
day_totals['protein'] += meal_nutrition['protein'] * quantity
day_totals['carbs'] += meal_nutrition['carbs'] * quantity
day_totals['fat'] += meal_nutrition['fat'] * quantity
day_totals['fiber'] += (meal_nutrition.get('fiber', 0) or 0) * quantity
day_totals['sugar'] += (meal_nutrition.get('sugar', 0) or 0) * quantity
day_totals['sodium'] += (meal_nutrition.get('sodium', 0) or 0) * quantity
day_totals['calcium'] += (meal_nutrition.get('calcium', 0) or 0) * quantity
meal_nutrition = calculate_tracked_meal_nutrition(tracked_meal, db)
for key in day_totals:
if key in meal_nutrition:
day_totals[key] += meal_nutrition[key]
# Calculate percentages
total_cals = day_totals['calories']