fixed food details not loading on details tab

This commit is contained in:
2025-10-01 07:00:38 -07:00
parent e3b4c49161
commit aee6b23ee4
3 changed files with 474 additions and 2 deletions

View File

@@ -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
@@ -716,3 +716,107 @@ async def tracker_add_food(data: dict = Body(...), db: Session = Depends(get_db)
db.rollback()
logging.error(f"DEBUG: Error adding single food to tracker: {e}")
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)

View File

@@ -0,0 +1,307 @@
{% extends "base.html" %}
{% block content %}
<div class="row mb-3">
<div class="col-md-6">
<h4 class="mb-0">{{ title }}</h4>
<div id="daySelector">
<form action="{{ url_for('detailed_tracked_day') }}" method="get" class="d-flex">
<input type="hidden" name="person" value="{{ person }}">
<input type="date" class="form-control me-2" name="date" value="{{ plan_date.isoformat() if plan_date else '' }}" required>
<button class="btn btn-primary" type="submit">
<i class="bi bi-search"></i> View Day
</button>
</form>
</div>
</div>
<div class="col-md-6 text-end">
<div class="dropdown mt-2">
<button class="btn btn-secondary dropdown-toggle" type="button" id="templateDropdown" data-bs-toggle="dropdown" aria-expanded="false">
View Template
</button>
<ul class="dropdown-menu" aria-labelledby="templateDropdown">
{% for t in templates %}
<li><a class="dropdown-item" href="/detailed?person={{ person }}{% if plan_date %}&plan_date={{ plan_date.isoformat() }}{% endif %}&template_id={{ t.id }}">{{ t.name }}</a></li>
{% endfor %}
</ul>
</div>
</div>
</div>
<style>
.meal-card {
margin-bottom: 1.5rem;
border: 1px solid #dee2e6;
border-radius: 0.5rem;
overflow: hidden;
}
.meal-header {
background: linear-gradient(135deg, #6c757d 0%, #495057 100%);
color: white;
padding: 0.75rem 1rem;
font-weight: 600;
display: flex;
justify-content: space-between;
align-items: center;
}
.meal-table {
margin-bottom: 0;
font-size: 0.9em;
}
.meal-table th {
background-color: #f8f9fa;
border-top: none;
font-size: 0.8em;
font-weight: 600;
padding: 0.5rem 0.75rem;
vertical-align: middle;
}
.meal-table td {
padding: 0.5rem 0.75rem;
vertical-align: middle;
border-top: 1px solid #f1f3f4;
}
.meal-table tbody tr:hover {
background-color: #f8f9fa;
}
.food-name {
font-weight: 500;
color: #495057;
}
.serving-info {
font-size: 0.8em;
color: #6c757d;
}
.nutrient-value {
font-weight: 500;
text-align: center;
}
.meal-totals {
background-color: #e3f2fd;
border-top: 2px solid #1976d2;
}
.meal-totals td {
font-weight: 600;
background-color: #e3f2fd;
}
.day-totals {
background: linear-gradient(135deg, #28a745 0%, #20c997 100%);
color: white;
padding: 1rem;
border-radius: 0.5rem;
margin-top: 1.5rem;
}
.day-totals-table {
color: white;
margin-bottom: 0;
}
.day-totals-table th,
.day-totals-table td {
border-color: rgba(255, 255, 255, 0.3);
text-align: center;
font-weight: 600;
}
.day-totals-table th {
background-color: rgba(255, 255, 255, 0.1);
}
.macro-pct {
font-size: 0.8em;
opacity: 0.9;
}
</style>
{% for meal_detail in meal_details %}
<div class="meal-card">
<div class="meal-header">
<span>
<i class="bi bi-egg-fried"></i> {{ meal_detail.plan.meal.name }} - {{ meal_detail.plan.meal.meal_type.title() }}
{% if meal_detail.plan.meal_time %}
<small class="text-muted">({{ meal_detail.plan.meal_time }})</small>
{% endif %}
</span>
<span class="badge bg-light text-dark">{{ "%.0f"|format(meal_detail.nutrition.calories) }} cal</span>
</div>
<table class="table meal-table">
<thead>
<tr>
<th style="width: 35%;">Food Item</th>
<th style="width: 15%;">Serving</th>
<th style="width: 8%;">Cal</th>
<th style="width: 8%;">Protein</th>
<th style="width: 8%;">Carbs</th>
<th style="width: 8%;">Fat</th>
<th style="width: 8%;">Fiber</th>
<th style="width: 10%;">Sodium</th>
</tr>
</thead>
<tbody>
{% for meal_food in meal_detail.foods %}
<tr>
<td>
<div class="food-name">{{ meal_food.name }}</div>
</td>
<td>
<div class="serving-info">
{{ "%.1f"|format(meal_food.quantity) }} × {{ meal_food.serving_size }}{{ meal_food.serving_unit }}
</div>
</td>
<td class="nutrient-value">{{ "%.0f"|format(meal_food.calories) }}</td>
<td class="nutrient-value">{{ "%.1f"|format(meal_food.protein) }}g</td>
<td class="nutrient-value">{{ "%.1f"|format(meal_food.carbs) }}g</td>
<td class="nutrient-value">{{ "%.1f"|format(meal_food.fat) }}g</td>
<td class="nutrient-value">{{ "%.1f"|format(meal_food['fiber']|float) }}g</td>
<td class="nutrient-value">{{ "%.0f"|format(meal_food['sodium']|float) }}mg</td>
</tr>
{% endfor %}
<!-- Meal Totals Row -->
<tr class="meal-totals">
<td><strong><i class="bi bi-calculator"></i> Meal Totals</strong></td>
<td class="text-center"><small>-</small></td>
<td class="nutrient-value">{{ "%.0f"|format(meal_detail.nutrition.calories) }}</td>
<td class="nutrient-value">
{{ "%.1f"|format(meal_detail.nutrition.protein) }}g
<div class="macro-pct">({{ meal_detail.nutrition.protein_pct or 0 }}%)</div>
</td>
<td class="nutrient-value">
{{ "%.1f"|format(meal_detail.nutrition.carbs) }}g
<div class="macro-pct">({{ meal_detail.nutrition.carbs_pct or 0 }}%)</div>
</td>
<td class="nutrient-value">
{{ "%.1f"|format(meal_detail.nutrition.fat) }}g
<div class="macro-pct">({{ meal_detail.nutrition.fat_pct or 0 }}%)</div>
</td>
<td class="nutrient-value">{{ "%.1f"|format(meal_detail.nutrition.fiber) }}g</td>
<td class="nutrient-value">{{ "%.0f"|format(meal_detail.nutrition.sodium) }}mg</td>
</tr>
</tbody>
</table>
<!-- Additional Nutrients Row -->
<div class="px-3 py-2" style="background-color: #f8f9fa; border-top: 1px solid #dee2e6; font-size: 0.85em;">
<div class="row text-center">
<div class="col-3">
<strong>Net Carbs:</strong> {{ "%.1f"|format(meal_detail.nutrition.net_carbs or 0) }}g
</div>
<div class="col-3">
<strong>Sugar:</strong> {{ "%.1f"|format(meal_detail.nutrition.sugar) }}g
</div>
<div class="col-3">
<strong>Calcium:</strong> {{ "%.0f"|format(meal_detail.nutrition.calcium) }}mg
</div>
<div class="col-3">
<strong>Ratio:</strong>
<span class="text-muted">
{{ meal_detail.nutrition.protein_pct or 0 }}:{{ meal_detail.nutrition.carbs_pct or 0 }}:{{ meal_detail.nutrition.fat_pct or 0 }}
</span>
</div>
</div>
</div>
</div>
{% endfor %}
<!-- Day Totals -->
{% if day_totals and day_totals.calories is defined and day_totals.calories > 0 %}
<div class="day-totals">
<h5 class="mb-3 text-center">
<i class="bi bi-calendar-check"></i> Daily Totals - {{ "%.0f"|format(day_totals.calories) }} Total Calories
</h5>
<table class="table day-totals-table">
<thead>
<tr>
<th>Calories</th>
<th>Protein</th>
<th>Carbs</th>
<th>Fat</th>
<th>Fiber</th>
<th>Net Carbs</th>
<th>Sodium</th>
<th>Calcium</th>
</tr>
</thead>
<tbody>
<tr>
<td>
<div style="font-size: 1.2em;">{{ "%.0f"|format(day_totals.calories) }}</div>
</td>
<td>
<div style="font-size: 1.1em;">{{ "%.1f"|format(day_totals.protein) }}g</div>
<div class="macro-pct">({{ day_totals.protein_pct or 0 }}%)</div>
</td>
<td>
<div style="font-size: 1.1em;">{{ "%.1f"|format(day_totals.carbs) }}g</div>
<div class="macro-pct">({{ day_totals.carbs_pct or 0 }}%)</div>
</td>
<td>
<div style="font-size: 1.1em;">{{ "%.1f"|format(day_totals.fat) }}g</div>
<div class="macro-pct">({{ day_totals.fat_pct or 0 }}%)</div>
</td>
<td>{{ "%.1f"|format(day_totals.fiber) }}g</td>
<td>{{ "%.1f"|format(day_totals.net_carbs or 0) }}g</td>
<td>{{ "%.0f"|format(day_totals.sodium) }}mg</td>
<td>{{ "%.0f"|format(day_totals.calcium) }}mg</td>
</tr>
</tbody>
</table>
<div class="text-center mt-2">
<small>
<strong>Daily Macro Ratio:</strong>
{{ day_totals.protein_pct or 0 }}% Protein : {{ day_totals.carbs_pct or 0 }}% Carbs : {{ day_totals.fat_pct or 0 }}% Fat
</small>
</div>
</div>
{% endif %}
{% if error %}
<div class="alert alert-danger mt-3">
<i class="bi bi-exclamation-triangle-fill"></i> <strong>Error:</strong> {{ error }}
</div>
{% elif not meal_details %}
<div class="alert alert-info mt-3">
<i class="bi bi-info-circle"></i>
{% if view_mode == 'template' %}
This template has no meals.
{% else %}
No meals tracked for this day.
{% endif %}
</div>
{% endif %}
<script>
document.addEventListener('DOMContentLoaded', function() {
// Handle view mode switching
const dayViewRadio = document.getElementById('dayView');
const templateViewRadio = document.getElementById('templateView');
const daySelector = document.getElementById('daySelector');
const templateSelector = document.getElementById('templateSelector');
// This script block is largely irrelevant for detailed_tracked_day.html as it focuses on a single day.
// However, keeping it for now to avoid breaking other functionalities if this template is used elsewhere.
// The relevant part would be the form action for "View Day".
// Set default date to today if no date is selected
const dateInput = document.querySelector('input[name="date"]');
if (dateInput && !dateInput.value) {
const today = new Date().toISOString().split('T')[0];
dateInput.value = today;
}
});
</script>
{% endblock %}

View File

@@ -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)