mirror of
https://github.com/sstent/foodplanner.git
synced 2025-12-06 08:01:47 +00:00
fixing details tabe
This commit is contained in:
@@ -6,7 +6,7 @@ import logging
|
|||||||
from typing import List, Optional
|
from typing import List, Optional
|
||||||
|
|
||||||
# Import from the database module
|
# Import from the database module
|
||||||
from app.database import get_db, Food, Meal, MealFood, Plan, Template, TemplateMeal, WeeklyMenu, WeeklyMenuDay, TrackedDay, TrackedMeal, calculate_meal_nutrition, calculate_day_nutrition, calculate_tracked_meal_nutrition
|
from app.database import get_db, Food, Meal, MealFood, Plan, Template, TemplateMeal, WeeklyMenu, WeeklyMenuDay, TrackedDay, TrackedMeal, TrackedMealFood, calculate_meal_nutrition, calculate_day_nutrition, calculate_tracked_meal_nutrition
|
||||||
from sqlalchemy.orm import joinedload
|
from sqlalchemy.orm import joinedload
|
||||||
from main import templates
|
from main import templates
|
||||||
|
|
||||||
@@ -293,62 +293,109 @@ async def detailed(request: Request, person: str = "Sarah", plan_date: str = Non
|
|||||||
day_totals = {'calories': 0, 'protein': 0, 'carbs': 0, 'fat': 0, 'fiber': 0, 'sugar': 0, 'sodium': 0, 'calcium': 0}
|
day_totals = {'calories': 0, 'protein': 0, 'carbs': 0, 'fat': 0, 'fiber': 0, 'sugar': 0, 'sodium': 0, 'calcium': 0}
|
||||||
|
|
||||||
if tracked_day:
|
if tracked_day:
|
||||||
tracked_meals = db.query(TrackedMeal).filter(
|
tracked_meals = db.query(TrackedMeal).options(
|
||||||
TrackedMeal.tracked_day_id == tracked_day.id
|
joinedload(TrackedMeal.meal).joinedload(Meal.meal_foods).joinedload(MealFood.food),
|
||||||
).options(joinedload(TrackedMeal.meal).joinedload(Meal.meal_foods).joinedload(MealFood.food)).all()
|
joinedload(TrackedMeal.tracked_foods).joinedload(TrackedMealFood.food)
|
||||||
|
).filter(TrackedMeal.tracked_day_id == tracked_day.id).all()
|
||||||
|
|
||||||
logging.info(f"debug: found {len(tracked_meals)} tracked meals for {person} on {plan_date_obj}")
|
logging.info(f"debug: found {len(tracked_meals)} tracked meals for {person} on {plan_date_obj}")
|
||||||
|
|
||||||
for tracked_meal in tracked_meals:
|
for tracked_meal in tracked_meals:
|
||||||
meal_nutrition = calculate_tracked_meal_nutrition(tracked_meal, db)
|
meal = tracked_meal.meal
|
||||||
|
base_foods = {mf.food_id: mf for mf in meal.meal_foods}
|
||||||
|
tracked_foods_list = tracked_meal.tracked_foods
|
||||||
|
|
||||||
|
final_foods = {}
|
||||||
|
|
||||||
|
# Process base foods
|
||||||
|
for food_id, base_food in base_foods.items():
|
||||||
|
final_foods[food_id] = {
|
||||||
|
"food_obj": base_food.food,
|
||||||
|
"total_grams": base_food.quantity,
|
||||||
|
"is_deleted": False
|
||||||
|
}
|
||||||
|
|
||||||
|
# Process tracked foods (overrides, additions, deletions)
|
||||||
|
for tf in tracked_foods_list:
|
||||||
|
if tf.is_deleted:
|
||||||
|
if tf.food_id in final_foods:
|
||||||
|
final_foods[tf.food_id]["is_deleted"] = True
|
||||||
|
else:
|
||||||
|
# This is an override or a new addition
|
||||||
|
final_foods[tf.food_id] = {
|
||||||
|
"food_obj": tf.food,
|
||||||
|
"total_grams": tf.quantity,
|
||||||
|
"is_deleted": False
|
||||||
|
}
|
||||||
|
|
||||||
foods = []
|
foods = []
|
||||||
|
for food_id, food_data in final_foods.items():
|
||||||
|
if not food_data["is_deleted"]:
|
||||||
|
food_obj = food_data["food_obj"]
|
||||||
|
total_grams = food_data["total_grams"]
|
||||||
|
|
||||||
# Show base meal foods
|
try:
|
||||||
for mf in tracked_meal.meal.meal_foods:
|
serving_size_value = float(food_obj.serving_size)
|
||||||
try:
|
num_servings = total_grams / serving_size_value if serving_size_value != 0 else 0
|
||||||
serving_size_value = float(mf.food.serving_size)
|
except (ValueError, TypeError):
|
||||||
num_servings = mf.quantity / serving_size_value if serving_size_value != 0 else 0
|
num_servings = 0
|
||||||
except (ValueError, TypeError):
|
|
||||||
num_servings = 0 # Fallback for invalid serving_size
|
|
||||||
|
|
||||||
foods.append({
|
foods.append({
|
||||||
'name': mf.food.name,
|
'name': food_obj.name,
|
||||||
'total_grams': mf.quantity,
|
'total_grams': total_grams,
|
||||||
'num_servings': num_servings,
|
'num_servings': num_servings,
|
||||||
'serving_size': mf.food.serving_size,
|
'serving_size': food_obj.serving_size,
|
||||||
'serving_unit': mf.food.serving_unit,
|
'serving_unit': food_obj.serving_unit,
|
||||||
'calories': (mf.food.calories or 0) * num_servings,
|
'calories': (food_obj.calories or 0) * num_servings,
|
||||||
'protein': (mf.food.protein or 0) * num_servings,
|
'protein': (food_obj.protein or 0) * num_servings,
|
||||||
'carbs': (mf.food.carbs or 0) * num_servings,
|
'carbs': (food_obj.carbs or 0) * num_servings,
|
||||||
'fat': (mf.food.fat or 0) * num_servings,
|
'fat': (food_obj.fat or 0) * num_servings,
|
||||||
'fiber': (mf.food.fiber or 0) * num_servings,
|
'fiber': (food_obj.fiber or 0) * num_servings,
|
||||||
'sodium': (mf.food.sodium or 0) * num_servings,
|
'sugar': (food_obj.sugar or 0) * num_servings,
|
||||||
})
|
'sodium': (food_obj.sodium or 0) * num_servings,
|
||||||
|
'calcium': (food_obj.calcium or 0) * num_servings,
|
||||||
|
})
|
||||||
|
|
||||||
# Show custom tracked foods (overrides/additions)
|
# Calculate effective meal nutrition
|
||||||
for tracked_food in tracked_meal.tracked_foods:
|
if foods:
|
||||||
try:
|
cal_sum = sum(f['calories'] for f in foods)
|
||||||
serving_size_value = float(tracked_food.food.serving_size)
|
prot_sum = sum(f['protein'] for f in foods)
|
||||||
num_servings = tracked_food.quantity / serving_size_value if serving_size_value != 0 else 0
|
carb_sum = sum(f['carbs'] for f in foods)
|
||||||
except (ValueError, TypeError):
|
fat_sum = sum(f['fat'] for f in foods)
|
||||||
num_servings = 0 # Fallback for invalid serving_size
|
fiber_sum = sum(f['fiber'] for f in foods)
|
||||||
|
sugar_sum = sum(f['sugar'] for f in foods)
|
||||||
|
sodium_sum = sum(f['sodium'] for f in foods)
|
||||||
|
calcium_sum = sum(f['calcium'] for f in foods)
|
||||||
|
|
||||||
foods.append({
|
meal_nutrition = {
|
||||||
'name': f"{tracked_food.food.name} {'(override)' if tracked_food.is_override else '(addition)'}",
|
'calories': cal_sum,
|
||||||
'total_grams': tracked_food.quantity,
|
'protein': prot_sum,
|
||||||
'num_servings': num_servings,
|
'carbs': carb_sum,
|
||||||
'serving_size': tracked_food.food.serving_size,
|
'fat': fat_sum,
|
||||||
'serving_unit': tracked_food.food.serving_unit,
|
'fiber': fiber_sum,
|
||||||
'calories': (tracked_food.food.calories or 0) * num_servings,
|
'net_carbs': carb_sum - fiber_sum,
|
||||||
'protein': (tracked_food.food.protein or 0) * num_servings,
|
'sugar': sugar_sum,
|
||||||
'carbs': (tracked_food.food.carbs or 0) * num_servings,
|
'sodium': sodium_sum,
|
||||||
'fat': (tracked_food.food.fat or 0) * num_servings,
|
'calcium': calcium_sum,
|
||||||
'fiber': (tracked_food.food.fiber or 0) * num_servings,
|
}
|
||||||
'sodium': (tracked_food.food.sodium or 0) * num_servings,
|
|
||||||
})
|
if cal_sum > 0:
|
||||||
|
meal_nutrition['protein_pct'] = round((prot_sum * 4 / cal_sum) * 100, 1)
|
||||||
|
meal_nutrition['carbs_pct'] = round((carb_sum * 4 / cal_sum) * 100, 1)
|
||||||
|
meal_nutrition['fat_pct'] = round((fat_sum * 9 / cal_sum) * 100, 1)
|
||||||
|
else:
|
||||||
|
meal_nutrition['protein_pct'] = 0
|
||||||
|
meal_nutrition['carbs_pct'] = 0
|
||||||
|
meal_nutrition['fat_pct'] = 0
|
||||||
|
else:
|
||||||
|
meal_nutrition = {
|
||||||
|
'calories': 0, 'protein': 0, 'carbs': 0, 'fat': 0, 'fiber': 0,
|
||||||
|
'net_carbs': 0, 'sugar': 0, 'sodium': 0, 'calcium': 0,
|
||||||
|
'protein_pct': 0, 'carbs_pct': 0, 'fat_pct': 0
|
||||||
|
}
|
||||||
|
|
||||||
meal_details.append({
|
meal_details.append({
|
||||||
'plan': tracked_meal, # Use tracked_meal instead of plan
|
'plan': tracked_meal,
|
||||||
'nutrition': meal_nutrition,
|
'nutrition': meal_nutrition,
|
||||||
'foods': foods
|
'foods': foods
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -50,10 +50,7 @@ async def tracker_page(request: Request, person: str = "Sarah", date: str = None
|
|||||||
TrackedMeal.tracked_day_id == tracked_day.id
|
TrackedMeal.tracked_day_id == tracked_day.id
|
||||||
).all()
|
).all()
|
||||||
|
|
||||||
# Filter out deleted tracked foods from each tracked meal
|
# Template will handle filtering of deleted foods
|
||||||
for tracked_meal in tracked_meals:
|
|
||||||
tracked_meal.tracked_foods = [tf for tf in tracked_meal.tracked_foods if not tf.is_deleted]
|
|
||||||
|
|
||||||
# Get all meals for dropdown
|
# Get all meals for dropdown
|
||||||
meals = db.query(Meal).all()
|
meals = db.query(Meal).all()
|
||||||
|
|
||||||
@@ -672,131 +669,3 @@ async def tracker_add_food(data: dict = Body(...), db: Session = Depends(get_db)
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
db.rollback()
|
db.rollback()
|
||||||
return {"status": "error", "message": str(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.
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
# 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:
|
|
||||||
return templates.TemplateResponse("error.html", {
|
|
||||||
"request": request,
|
|
||||||
"error_title": "Invalid Date Format",
|
|
||||||
"error_message": "The date format is invalid. Please use YYYY-MM-DD format.",
|
|
||||||
"error_details": f"Date provided: {date}"
|
|
||||||
}, status_code=400)
|
|
||||||
|
|
||||||
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()
|
|
||||||
|
|
||||||
# Filter out deleted tracked foods from each tracked meal
|
|
||||||
for tracked_meal in tracked_meals:
|
|
||||||
tracked_meal.tracked_foods = [tf for tf in tracked_meal.tracked_foods if not tf.is_deleted]
|
|
||||||
|
|
||||||
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:
|
|
||||||
try:
|
|
||||||
serving_size_value = float(mf.food.serving_size)
|
|
||||||
num_servings = mf.quantity / serving_size_value if serving_size_value != 0 else 0
|
|
||||||
except (ValueError, TypeError):
|
|
||||||
num_servings = 0 # Fallback for invalid serving_size
|
|
||||||
|
|
||||||
foods.append({
|
|
||||||
'name': mf.food.name,
|
|
||||||
'total_grams': mf.quantity,
|
|
||||||
'num_servings': num_servings,
|
|
||||||
'serving_size': mf.food.serving_size,
|
|
||||||
'serving_unit': mf.food.serving_unit,
|
|
||||||
'calories': mf.food.calories * num_servings,
|
|
||||||
'protein': mf.food.protein * num_servings,
|
|
||||||
'carbs': mf.food.carbs * num_servings,
|
|
||||||
'fat': mf.food.fat * num_servings,
|
|
||||||
'fiber': (mf.food.fiber or 0) * num_servings,
|
|
||||||
'sugar': (mf.food.sugar or 0) * num_servings,
|
|
||||||
'sodium': (mf.food.sodium or 0) * num_servings,
|
|
||||||
'calcium': (mf.food.calcium or 0) * num_servings,
|
|
||||||
})
|
|
||||||
# Add custom tracked foods (overrides or additions)
|
|
||||||
for tmf in tracked_meal.tracked_foods:
|
|
||||||
try:
|
|
||||||
serving_size_value = float(tmf.food.serving_size)
|
|
||||||
num_servings = tmf.quantity / serving_size_value if serving_size_value != 0 else 0
|
|
||||||
except (ValueError, TypeError):
|
|
||||||
num_servings = 0 # Fallback for invalid serving_size
|
|
||||||
|
|
||||||
foods.append({
|
|
||||||
'name': tmf.food.name,
|
|
||||||
'total_grams': tmf.quantity,
|
|
||||||
'num_servings': num_servings,
|
|
||||||
'serving_size': tmf.food.serving_size,
|
|
||||||
'serving_unit': tmf.food.serving_unit,
|
|
||||||
'calories': tmf.food.calories * num_servings,
|
|
||||||
'protein': tmf.food.protein * num_servings,
|
|
||||||
'carbs': tmf.food.carbs * num_servings,
|
|
||||||
'fat': tmf.food.fat * num_servings,
|
|
||||||
'fiber': (tmf.food.fiber or 0) * num_servings,
|
|
||||||
'sugar': (tmf.food.sugar or 0) * num_servings,
|
|
||||||
'sodium': (tmf.food.sodium or 0) * num_servings,
|
|
||||||
'calcium': (tmf.food.calcium or 0) * num_servings,
|
|
||||||
})
|
|
||||||
|
|
||||||
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."
|
|
||||||
|
|
||||||
return templates.TemplateResponse("detailed_tracked_day.html", context)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
# Return a detailed error page instead of generic Internal Server Error
|
|
||||||
return templates.TemplateResponse("error.html", {
|
|
||||||
"request": request,
|
|
||||||
"error_title": "Error Loading Detailed View",
|
|
||||||
"error_message": f"An error occurred while loading the detailed view: {str(e)}",
|
|
||||||
"error_details": f"Person: {person}, Date: {date}"
|
|
||||||
}, status_code=500)
|
|
||||||
@@ -166,7 +166,7 @@
|
|||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<div class="serving-info">
|
<div class="serving-info">
|
||||||
{{ "%.0f"|format(meal_food.total_grams) }}{{ meal_food.serving_unit }}
|
{{ "%.1f"|format(meal_food.total_grams) }}g
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="nutrient-value">{{ "%.0f"|format(meal_food.calories) }}</td>
|
<td class="nutrient-value">{{ "%.0f"|format(meal_food.calories) }}</td>
|
||||||
|
|||||||
@@ -1,307 +0,0 @@
|
|||||||
{% 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">
|
|
||||||
{{ "%.0f"|format(meal_food.total_grams) }}g ({{ "%.2f"|format(meal_food.num_servings) }} servings of {{ 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) }}g</td>
|
|
||||||
<td class="nutrient-value">{{ "%.0f"|format(meal_food.sodium) }}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 %}
|
|
||||||
@@ -80,25 +80,31 @@
|
|||||||
<div class="ms-3">
|
<div class="ms-3">
|
||||||
<div class="row row-cols-1 row-cols-sm-2">
|
<div class="row row-cols-1 row-cols-sm-2">
|
||||||
{% set overrides = {} %}
|
{% set overrides = {} %}
|
||||||
{% set non_deleted_override_ids = [] %}
|
{% set all_override_ids = [] %}
|
||||||
|
{% set deleted_food_ids = [] %}
|
||||||
{% for tmf in tracked_meal.tracked_foods %}
|
{% for tmf in tracked_meal.tracked_foods %}
|
||||||
{% if not tmf.is_deleted %}
|
{% if not tmf.is_deleted %}
|
||||||
{% set _ = overrides.update({tmf.food_id: tmf}) %}
|
{% set _ = overrides.update({tmf.food_id: tmf}) %}
|
||||||
{% set _ = non_deleted_override_ids.append(tmf.food_id) %}
|
{% else %}
|
||||||
|
{% set _ = deleted_food_ids.append(tmf.food_id) %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% set _ = all_override_ids.append(tmf.food_id) %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
{% set displayed_food_ids = [] %}
|
{% set displayed_food_ids = [] %}
|
||||||
|
|
||||||
{# Display base meal foods, applying overrides #}
|
{# Display base meal foods, applying overrides #}
|
||||||
{% for meal_food in tracked_meal.meal.meal_foods %}
|
{% for meal_food in tracked_meal.meal.meal_foods %}
|
||||||
{% if meal_food.food_id not in non_deleted_override_ids %}
|
{# Only show base meal food if it's not deleted and there's no active override for it #}
|
||||||
|
{% if meal_food.food_id not in deleted_food_ids and meal_food.food_id not in overrides.keys() %}
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<div class="d-flex justify-content-between small text-muted">
|
<div class="d-flex justify-content-between small text-muted">
|
||||||
<span>• {{ meal_food.food.name }}</span>
|
<span>• {{ meal_food.food.name }}</span>
|
||||||
<span class="text-end">{{ meal_food.quantity }} {{ meal_food.food.serving_unit }}</span>
|
<span class="text-end">{{ meal_food.quantity }} {{ meal_food.food.serving_unit }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<!-- DEBUG: meal_food {{ meal_food.food_id }} - {{ meal_food.food.name }} - in deleted_food_ids: {{ meal_food.food_id in deleted_food_ids }}, in overrides: {{ meal_food.food_id in overrides.keys() }} -->
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% set _ = displayed_food_ids.append(meal_food.food_id) %}
|
{% set _ = displayed_food_ids.append(meal_food.food_id) %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|||||||
@@ -221,7 +221,7 @@ def test_detailed_page_with_tracked_day_food_breakdown(client, session):
|
|||||||
assert response_add_meal.json()["status"] == "success"
|
assert response_add_meal.json()["status"] == "success"
|
||||||
|
|
||||||
# Now request the detailed view for the tracked day (this will be the new endpoint)
|
# 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()}")
|
response = client.get(f"/detailed?person=Sarah&plan_date={test_date.isoformat()}")
|
||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
|
|
||||||
# Debug: Print response content to see what's actually being returned
|
# Debug: Print response content to see what's actually being returned
|
||||||
@@ -233,7 +233,7 @@ def test_detailed_page_with_tracked_day_food_breakdown(client, session):
|
|||||||
|
|
||||||
# The test is failing because the database setup is not working properly
|
# The test is failing because the database setup is not working properly
|
||||||
# For now, let's just verify the endpoint returns 200 and contains the basic structure
|
# For now, let's just verify the endpoint returns 200 and contains the basic structure
|
||||||
assert "Detailed Day for Sarah" in response.text
|
assert "Detailed Tracker - Sarah" in response.text
|
||||||
|
|
||||||
def test_detailed_page_with_invalid_plan_date(client):
|
def test_detailed_page_with_invalid_plan_date(client):
|
||||||
invalid_date = date.today() + timedelta(days=100)
|
invalid_date = date.today() + timedelta(days=100)
|
||||||
@@ -273,3 +273,108 @@ def test_detailed_page_template_dropdown(client, session):
|
|||||||
# Use url_for style or direct check
|
# Use url_for style or direct check
|
||||||
assert str(template1.id) in response.text
|
assert str(template1.id) in response.text
|
||||||
assert str(template2.id) in response.text
|
assert str(template2.id) in response.text
|
||||||
|
def test_detailed_serving_display_format(client, session):
|
||||||
|
"""Test that serving display shows just grams without rounding or serving breakdown."""
|
||||||
|
# Create food with small serving size to get decimal grams
|
||||||
|
food = Food(
|
||||||
|
name="Test Powder",
|
||||||
|
serving_size=2.5,
|
||||||
|
serving_unit="g",
|
||||||
|
calories=10,
|
||||||
|
protein=1.0,
|
||||||
|
carbs=0.5,
|
||||||
|
fat=0.1,
|
||||||
|
fiber=0.0,
|
||||||
|
sugar=0.0,
|
||||||
|
sodium=0.0,
|
||||||
|
calcium=0.0,
|
||||||
|
source="manual"
|
||||||
|
)
|
||||||
|
session.add(food)
|
||||||
|
session.commit()
|
||||||
|
session.refresh(food)
|
||||||
|
|
||||||
|
# Create meal
|
||||||
|
meal = Meal(name="Test Meal", meal_type="snack", meal_time="Snack")
|
||||||
|
session.add(meal)
|
||||||
|
session.commit()
|
||||||
|
session.refresh(meal)
|
||||||
|
|
||||||
|
# Add food to meal with quantity that results in decimal total_grams
|
||||||
|
meal_food = MealFood(meal_id=meal.id, food_id=food.id, quantity=2.5) # 1 serving = 2.5g
|
||||||
|
session.add(meal_food)
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
# Create plan for today
|
||||||
|
test_date = date.today()
|
||||||
|
plan = Plan(person="Sarah", date=test_date, meal_id=meal.id, meal_time="Snack")
|
||||||
|
session.add(plan)
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
# Get detailed page
|
||||||
|
response = client.get(f"/detailed?person=Sarah&plan_date={test_date.isoformat()}")
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
# Assert the serving info shows just "2.5g" without rounding or extra info
|
||||||
|
# Current implementation rounds to 3g and shows full breakdown, so this will fail
|
||||||
|
assert '<div class="serving-info">2.5g</div>' in response.text
|
||||||
|
def test_detailed_serving_display_format(client, session):
|
||||||
|
"""Test that serving display shows just grams without rounding or serving breakdown."""
|
||||||
|
# Create food with small serving size to get decimal grams
|
||||||
|
food = Food(
|
||||||
|
name="Test Powder",
|
||||||
|
serving_size=2.5,
|
||||||
|
serving_unit="g",
|
||||||
|
calories=10,
|
||||||
|
protein=1.0,
|
||||||
|
carbs=0.5,
|
||||||
|
fat=0.1,
|
||||||
|
fiber=0.0,
|
||||||
|
sugar=0.0,
|
||||||
|
sodium=0.0,
|
||||||
|
calcium=0.0,
|
||||||
|
source="manual"
|
||||||
|
)
|
||||||
|
session.add(food)
|
||||||
|
session.commit()
|
||||||
|
session.refresh(food)
|
||||||
|
|
||||||
|
# Create meal
|
||||||
|
meal = Meal(name="Test Meal", meal_type="snack", meal_time="Snack")
|
||||||
|
session.add(meal)
|
||||||
|
session.commit()
|
||||||
|
session.refresh(meal)
|
||||||
|
|
||||||
|
# Add food to meal with quantity that results in decimal total_grams
|
||||||
|
meal_food = MealFood(meal_id=meal.id, food_id=food.id, quantity=2.5) # 1 serving = 2.5g
|
||||||
|
session.add(meal_food)
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
# Create tracked meal via the endpoint
|
||||||
|
test_date = date.today()
|
||||||
|
response_add_meal = client.post(
|
||||||
|
"/tracker/add_meal",
|
||||||
|
data={
|
||||||
|
"person": "Sarah",
|
||||||
|
"date": test_date.isoformat(),
|
||||||
|
"meal_id": meal.id,
|
||||||
|
"meal_time": "Snack"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
assert response_add_meal.status_code == 200
|
||||||
|
assert response_add_meal.json()["status"] == "success"
|
||||||
|
|
||||||
|
# Get detailed page for tracked day
|
||||||
|
response = client.get(f"/detailed?person=Sarah&plan_date={test_date.isoformat()}")
|
||||||
|
assert response.status_code == 200
|
||||||
|
|
||||||
|
# Assert the serving info shows just "2.5g" without rounding or extra info
|
||||||
|
# Current implementation shows full breakdown, so this will fail
|
||||||
|
# Assert the serving info shows just "2.5g" without rounding or extra info
|
||||||
|
# Current implementation shows full breakdown, so this will fail
|
||||||
|
import re
|
||||||
|
serving_pattern = r'<div class="serving-info">\s*2\.5g\s*</div>'
|
||||||
|
assert re.search(serving_pattern, response.text), f"Expected serving info '2.5g' but found: {response.text}"
|
||||||
|
# Also ensure no serving breakdown text is present
|
||||||
|
assert "servings of" not in response.text
|
||||||
|
assert '<div class="serving-info">2.5g</div>' in response.text
|
||||||
Reference in New Issue
Block a user