From aee6b23ee409bb6e760550e0cd1ffa2b1a92c86e Mon Sep 17 00:00:00 2001 From: sstent Date: Wed, 1 Oct 2025 07:00:38 -0700 Subject: [PATCH] fixed food details not loading on details tab --- app/api/routes/tracker.py | 108 +++++++++- templates/detailed_tracked_day.html | 307 ++++++++++++++++++++++++++++ tests/test_detailed.py | 61 ++++++ 3 files changed, 474 insertions(+), 2 deletions(-) create mode 100644 templates/detailed_tracked_day.html diff --git a/app/api/routes/tracker.py b/app/api/routes/tracker.py index 669c63e..79556f3 100644 --- a/app/api/routes/tracker.py +++ b/app/api/routes/tracker.py @@ -3,7 +3,7 @@ from fastapi.responses import HTMLResponse, RedirectResponse from sqlalchemy.orm import Session, joinedload from datetime import date, datetime, timedelta import logging -from typing import List, Optional +from typing import List, Optional, Union # Import from the database module from app.database import get_db, Meal, Template, TemplateMeal, TrackedDay, TrackedMeal, calculate_meal_nutrition, MealFood, TrackedMealFood, Food, calculate_day_nutrition_tracked @@ -715,4 +715,108 @@ async def tracker_add_food(data: dict = Body(...), db: Session = Depends(get_db) except Exception as e: db.rollback() logging.error(f"DEBUG: Error adding single food to tracker: {e}") - return {"status": "error", "message": str(e)} \ No newline at end of file + return {"status": "error", "message": str(e)} + +@router.get("/detailed_tracked_day", response_class=HTMLResponse, name="detailed_tracked_day") +async def detailed_tracked_day(request: Request, person: str = "Sarah", date: Optional[str] = None, db: Session = Depends(get_db)): + """ + Displays a detailed view of a tracked day, including all meals and their food breakdowns. + """ + logging.info(f"DEBUG: Detailed tracked day page requested with person={person}, date={date}") + + # If no date is provided, default to today's date + if not date: + current_date = date.today() + else: + try: + current_date = datetime.fromisoformat(date).date() + except ValueError: + logging.error(f"DEBUG: Invalid date format for date: {date}") + return templates.TemplateResponse("detailed.html", { + "request": request, "title": "Invalid Date", + "error": "Invalid date format. Please use YYYY-MM-DD.", + "day_totals": {}, + "person": person + }) + + tracked_day = db.query(TrackedDay).filter( + TrackedDay.person == person, + TrackedDay.date == current_date + ).first() + + if not tracked_day: + return templates.TemplateResponse("detailed_tracked_day.html", { + "request": request, "title": "No Tracked Day Found", + "error": "No tracked meals found for this day.", + "day_totals": {}, + "person": person, + "plan_date": current_date # Pass current_date for consistent template behavior + }) + + tracked_meals = db.query(TrackedMeal).options( + joinedload(TrackedMeal.meal).joinedload(Meal.meal_foods).joinedload(MealFood.food), + joinedload(TrackedMeal.tracked_foods).joinedload(TrackedMealFood.food) + ).filter( + TrackedMeal.tracked_day_id == tracked_day.id + ).all() + + day_totals = calculate_day_nutrition_tracked(tracked_meals, db) + + meal_details = [] + for tracked_meal in tracked_meals: + meal_nutrition = calculate_meal_nutrition(tracked_meal.meal, db) # Base meal nutrition + + foods = [] + # Add foods from the base meal definition + for mf in tracked_meal.meal.meal_foods: + foods.append({ + 'name': mf.food.name, + 'quantity': mf.quantity, + 'serving_size': mf.food.serving_size, + 'serving_unit': mf.food.serving_unit, + 'calories': mf.food.calories * mf.quantity, + 'protein': mf.food.protein * mf.quantity, + 'carbs': mf.food.carbs * mf.quantity, + 'fat': mf.food.fat * mf.quantity, + 'fiber': (mf.food.fiber or 0) * mf.quantity, + 'sugar': (mf.food.sugar or 0) * mf.quantity, + 'sodium': (mf.food.sodium or 0) * mf.quantity, + 'calcium': (mf.food.calcium or 0) * mf.quantity, + }) + # Add custom tracked foods (overrides or additions) + for tmf in tracked_meal.tracked_foods: + foods.append({ + 'name': tmf.food.name, + 'quantity': tmf.quantity, + 'serving_size': tmf.food.serving_size, + 'serving_unit': tmf.food.serving_unit, + 'calories': tmf.food.calories * tmf.quantity, + 'protein': tmf.food.protein * tmf.quantity, + 'carbs': tmf.food.carbs * tmf.quantity, + 'fat': tmf.food.fat * tmf.quantity, + 'fiber': (tmf.food.fiber or 0) * tmf.quantity, + 'sugar': (tmf.food.sugar or 0) * tmf.quantity, + 'sodium': (tmf.food.sodium or 0) * tmf.quantity, + 'calcium': (tmf.food.calcium or 0) * tmf.quantity, + }) + + meal_details.append({ + 'plan': {'meal': tracked_meal.meal, 'meal_time': tracked_meal.meal_time}, + 'nutrition': meal_nutrition, + 'foods': foods + }) + + context = { + "request": request, + "title": f"Detailed Day for {person} on {current_date.strftime('%B %d, %Y')}", + "meal_details": meal_details, + "day_totals": day_totals, + "person": person, + "plan_date": current_date # Renamed from current_date to plan_date for consistency with detailed.html + } + + if not meal_details: + context["message"] = "No meals tracked for this day." + + logging.info(f"DEBUG: Rendering tracked day details with context: {context}") + return templates.TemplateResponse("detailed_tracked_day.html", context) \ No newline at end of file diff --git a/templates/detailed_tracked_day.html b/templates/detailed_tracked_day.html new file mode 100644 index 0000000..a918584 --- /dev/null +++ b/templates/detailed_tracked_day.html @@ -0,0 +1,307 @@ +{% extends "base.html" %} +{% block content %} +
+
+

{{ title }}

+
+
+ + + +
+
+
+
+ + +
+
+ + + +{% for meal_detail in meal_details %} +
+
+ + {{ meal_detail.plan.meal.name }} - {{ meal_detail.plan.meal.meal_type.title() }} + {% if meal_detail.plan.meal_time %} + ({{ meal_detail.plan.meal_time }}) + {% endif %} + + {{ "%.0f"|format(meal_detail.nutrition.calories) }} cal +
+ + + + + + + + + + + + + + + + {% for meal_food in meal_detail.foods %} + + + + + + + + + + + {% endfor %} + + + + + + + + + + + + +
Food ItemServingCalProteinCarbsFatFiberSodium
+
{{ meal_food.name }}
+
+
+ {{ "%.1f"|format(meal_food.quantity) }} × {{ meal_food.serving_size }}{{ meal_food.serving_unit }} +
+
{{ "%.0f"|format(meal_food.calories) }}{{ "%.1f"|format(meal_food.protein) }}g{{ "%.1f"|format(meal_food.carbs) }}g{{ "%.1f"|format(meal_food.fat) }}g{{ "%.1f"|format(meal_food['fiber']|float) }}g{{ "%.0f"|format(meal_food['sodium']|float) }}mg
Meal Totals-{{ "%.0f"|format(meal_detail.nutrition.calories) }} + {{ "%.1f"|format(meal_detail.nutrition.protein) }}g +
({{ meal_detail.nutrition.protein_pct or 0 }}%)
+
+ {{ "%.1f"|format(meal_detail.nutrition.carbs) }}g +
({{ meal_detail.nutrition.carbs_pct or 0 }}%)
+
+ {{ "%.1f"|format(meal_detail.nutrition.fat) }}g +
({{ meal_detail.nutrition.fat_pct or 0 }}%)
+
{{ "%.1f"|format(meal_detail.nutrition.fiber) }}g{{ "%.0f"|format(meal_detail.nutrition.sodium) }}mg
+ + +
+
+
+ Net Carbs: {{ "%.1f"|format(meal_detail.nutrition.net_carbs or 0) }}g +
+
+ Sugar: {{ "%.1f"|format(meal_detail.nutrition.sugar) }}g +
+
+ Calcium: {{ "%.0f"|format(meal_detail.nutrition.calcium) }}mg +
+
+ Ratio: + + {{ meal_detail.nutrition.protein_pct or 0 }}:{{ meal_detail.nutrition.carbs_pct or 0 }}:{{ meal_detail.nutrition.fat_pct or 0 }} + +
+
+
+
+{% endfor %} + + +{% if day_totals and day_totals.calories is defined and day_totals.calories > 0 %} +
+
+ Daily Totals - {{ "%.0f"|format(day_totals.calories) }} Total Calories +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +
CaloriesProteinCarbsFatFiberNet CarbsSodiumCalcium
+
{{ "%.0f"|format(day_totals.calories) }}
+
+
{{ "%.1f"|format(day_totals.protein) }}g
+
({{ day_totals.protein_pct or 0 }}%)
+
+
{{ "%.1f"|format(day_totals.carbs) }}g
+
({{ day_totals.carbs_pct or 0 }}%)
+
+
{{ "%.1f"|format(day_totals.fat) }}g
+
({{ day_totals.fat_pct or 0 }}%)
+
{{ "%.1f"|format(day_totals.fiber) }}g{{ "%.1f"|format(day_totals.net_carbs or 0) }}g{{ "%.0f"|format(day_totals.sodium) }}mg{{ "%.0f"|format(day_totals.calcium) }}mg
+ +
+ + Daily Macro Ratio: + {{ day_totals.protein_pct or 0 }}% Protein : {{ day_totals.carbs_pct or 0 }}% Carbs : {{ day_totals.fat_pct or 0 }}% Fat + +
+
+{% endif %} + +{% if error %} +
+ Error: {{ error }} +
+{% elif not meal_details %} +
+ + {% if view_mode == 'template' %} + This template has no meals. + {% else %} + No meals tracked for this day. + {% endif %} +
+{% endif %} + + +{% endblock %} \ No newline at end of file diff --git a/tests/test_detailed.py b/tests/test_detailed.py index 6a91ae5..e2460da 100644 --- a/tests/test_detailed.py +++ b/tests/test_detailed.py @@ -173,6 +173,67 @@ def test_detailed_page_with_template_id(client, session): assert "Morning Boost Template" in response.text assert "Banana Smoothie" in response.text +def test_detailed_page_with_tracked_day_food_breakdown(client, session): + # Create mock data for a tracked day + food1 = Food( + name="Chicken Breast", + serving_size="100", + serving_unit="g", + calories=165, protein=31, carbs=0, fat=3.6, + fiber=0, sugar=0, sodium=74, calcium=11, + source="manual" + ) + food2 = Food( + name="Broccoli", + serving_size="100", + serving_unit="g", + calories=55, protein=3.7, carbs=11.2, fat=0.6, + fiber=5.1, sugar=2.2, sodium=33, calcium=47, + source="manual" + ) + session.add_all([food1, food2]) + session.commit() + session.refresh(food1) + session.refresh(food2) + + meal = Meal(name="Chicken and Broccoli", meal_type="dinner", meal_time="Dinner") + session.add(meal) + session.commit() + session.refresh(meal) + + meal_food1 = MealFood(meal_id=meal.id, food_id=food1.id, quantity=1.5) # 150g chicken + meal_food2 = MealFood(meal_id=meal.id, food_id=food2.id, quantity=2.0) # 200g broccoli + session.add_all([meal_food1, meal_food2]) + session.commit() + + test_date = date.today() + + # Simulate adding a tracked meal + response_add_meal = client.post( + "/tracker/add_meal", + data={ + "person": "Sarah", + "date": test_date.isoformat(), + "meal_id": meal.id, + "meal_time": "Dinner" + } + ) + assert response_add_meal.status_code == 200 + assert response_add_meal.json()["status"] == "success" + + # Now request the detailed view for the tracked day (this will be the new endpoint) + response = client.get(f"/detailed_tracked_day?person=Sarah&date={test_date.isoformat()}") + assert response.status_code == 200 + + # Assert that the meal and individual food items are present + assert "Detailed Day for Sarah" in response.text + assert "Chicken and Broccoli" in response.text + assert "Chicken Breast" in response.text + assert "Broccoli" in response.text + assert "1.5 × 100g" in response.text # Check quantity and unit for chicken + assert "2.0 × 100g" in response.text # Check quantity and unit for broccoli + assert "248" in response.text # Check calories for chicken (1.5 * 165 = 247.5, rounded to 248) + assert "110" in response.text # Check calories for broccoli (2.0 * 55 = 110) def test_detailed_page_with_invalid_plan_date(client): invalid_date = date.today() + timedelta(days=100)