mirror of
https://github.com/sstent/foodplanner.git
synced 2026-03-27 23:55:24 +00:00
adding new bug fixes - changed the way meals are tracked and logged to make them copies not references
This commit is contained in:
101
alembic/versions/31fdce040eea_snapshot_existing_meals.py
Normal file
101
alembic/versions/31fdce040eea_snapshot_existing_meals.py
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
"""snapshot_existing_meals
|
||||||
|
|
||||||
|
Revision ID: 31fdce040eea
|
||||||
|
Revises: 4522e2de4143
|
||||||
|
Create Date: 2026-01-10 13:30:49.977264
|
||||||
|
|
||||||
|
"""
|
||||||
|
from typing import Sequence, Union
|
||||||
|
from sqlalchemy import orm, text
|
||||||
|
from app.database import Base
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = '31fdce040eea'
|
||||||
|
down_revision: Union[str, None] = '4522e2de4143'
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
bind = op.get_bind()
|
||||||
|
session = orm.Session(bind=bind)
|
||||||
|
|
||||||
|
# Use reflection or raw SQL to avoid importing app models directly
|
||||||
|
# This ensures the migration remains valid even if app models change later
|
||||||
|
|
||||||
|
# 1. Get all tracked meals that are NOT already snapshots
|
||||||
|
# We join tracked_meals with meals to check the meal_type
|
||||||
|
sql = text("""
|
||||||
|
SELECT tm.id as tracked_meal_id, tm.meal_id, m.name, m.meal_time
|
||||||
|
FROM tracked_meals tm
|
||||||
|
JOIN meals m ON tm.meal_id = m.id
|
||||||
|
WHERE m.meal_type != 'tracked_snapshot'
|
||||||
|
""")
|
||||||
|
|
||||||
|
tracked_meals_to_snapshot = session.execute(sql).fetchall()
|
||||||
|
|
||||||
|
print(f"Found {len(tracked_meals_to_snapshot)} tracked meals to snapshot.")
|
||||||
|
|
||||||
|
for row in tracked_meals_to_snapshot:
|
||||||
|
tm_id = row.tracked_meal_id
|
||||||
|
original_meal_id = row.meal_id
|
||||||
|
original_name = row.name
|
||||||
|
original_meal_time = row.meal_time
|
||||||
|
|
||||||
|
# 2. Create a new snapshot meal
|
||||||
|
# We can't easily use ORM since we don't have the classes, so we use raw SQL
|
||||||
|
insert_meal_sql = text("""
|
||||||
|
INSERT INTO meals (name, meal_type, meal_time)
|
||||||
|
VALUES (:name, 'tracked_snapshot', :meal_time)
|
||||||
|
""")
|
||||||
|
|
||||||
|
# execution_options={"autocommit": True} might be needed for some drivers,
|
||||||
|
# but session.execute usually handles it.
|
||||||
|
# For SQLite, we can get the last inserted id via cursor, but SQLAlchemy does this via result.lastrowid
|
||||||
|
|
||||||
|
result = session.execute(insert_meal_sql, {
|
||||||
|
"name": original_name,
|
||||||
|
"meal_time": original_meal_time
|
||||||
|
})
|
||||||
|
new_meal_id = result.lastrowid
|
||||||
|
|
||||||
|
# 3. Copy ingredients from original meal to new snapshot
|
||||||
|
# Get ingredients
|
||||||
|
get_foods_sql = text("""
|
||||||
|
SELECT food_id, quantity
|
||||||
|
FROM meal_foods
|
||||||
|
WHERE meal_id = :meal_id
|
||||||
|
""")
|
||||||
|
foods = session.execute(get_foods_sql, {"meal_id": original_meal_id}).fetchall()
|
||||||
|
|
||||||
|
if foods:
|
||||||
|
insert_food_sql = text("""
|
||||||
|
INSERT INTO meal_foods (meal_id, food_id, quantity)
|
||||||
|
VALUES (:meal_id, :food_id, :quantity)
|
||||||
|
""")
|
||||||
|
|
||||||
|
for food in foods:
|
||||||
|
session.execute(insert_food_sql, {
|
||||||
|
"meal_id": new_meal_id,
|
||||||
|
"food_id": food.food_id,
|
||||||
|
"quantity": food.quantity
|
||||||
|
})
|
||||||
|
|
||||||
|
# 4. Update the stored tracked_meal to point to the new snapshot
|
||||||
|
update_tm_sql = text("""
|
||||||
|
UPDATE tracked_meals
|
||||||
|
SET meal_id = :new_meal_id
|
||||||
|
WHERE id = :tm_id
|
||||||
|
""")
|
||||||
|
session.execute(update_tm_sql, {"new_meal_id": new_meal_id, "tm_id": tm_id})
|
||||||
|
|
||||||
|
session.commit()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
pass
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
from fastapi import APIRouter, Depends, HTTPException, Request, Form, Body, File, UploadFile
|
from fastapi import APIRouter, Depends, HTTPException, Request, Form, Body, File, UploadFile, Response
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
from typing import List, Optional
|
from typing import List, Optional
|
||||||
from datetime import date, datetime
|
from datetime import date, datetime
|
||||||
@@ -12,7 +12,7 @@ import re
|
|||||||
import json
|
import json
|
||||||
|
|
||||||
from app.database import get_db, Food, Meal, Plan, Template, WeeklyMenu, TrackedDay, MealFood, TemplateMeal, WeeklyMenuDay, TrackedMeal
|
from app.database import get_db, Food, Meal, Plan, Template, WeeklyMenu, TrackedDay, MealFood, TemplateMeal, WeeklyMenuDay, TrackedMeal
|
||||||
from app.database import FoodCreate, FoodResponse, MealCreate, TrackedDayCreate, TrackedMealCreate, AllData, FoodExport, MealFoodExport, MealExport, PlanExport, TemplateMealExport, TemplateExport, TemplateMealDetail, TemplateDetail, WeeklyMenuDayExport, WeeklyMenuDayDetail, WeeklyMenuExport, WeeklyMenuDetail, TrackedMealExport, TrackedDayExport
|
from app.database import FoodCreate, FoodResponse, MealCreate, TrackedDayCreate, TrackedMealCreate, AllData, FoodExport, MealFoodExport, MealExport, PlanExport, TemplateMealExport, TemplateExport, TemplateMealDetail, TemplateDetail, WeeklyMenuDayExport, WeeklyMenuDayDetail, WeeklyMenuExport, WeeklyMenuDetail, TrackedMealExport, TrackedDayExport, TrackedMealFoodExport
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
@@ -66,96 +66,117 @@ def validate_import_data(data: AllData):
|
|||||||
detail=f"Invalid tracked meal: meal_id {tracked_meal.meal_id} not found.",
|
detail=f"Invalid tracked meal: meal_id {tracked_meal.meal_id} not found.",
|
||||||
)
|
)
|
||||||
|
|
||||||
@router.get("/export/all", response_model=AllData)
|
@router.get("/export/all")
|
||||||
async def export_all_data(db: Session = Depends(get_db)):
|
async def export_all_data(db: Session = Depends(get_db)):
|
||||||
"""Export all data from the database as a single JSON file."""
|
"""Export all data from the database as a single JSON file."""
|
||||||
foods = db.query(Food).all()
|
|
||||||
meals = db.query(Meal).all()
|
|
||||||
plans = db.query(Plan).all()
|
|
||||||
templates = db.query(Template).all()
|
|
||||||
weekly_menus = db.query(WeeklyMenu).all()
|
|
||||||
tracked_days = db.query(TrackedDay).all()
|
|
||||||
|
|
||||||
# Manual serialization to handle nested relationships
|
try:
|
||||||
|
# ... (rest of the code)
|
||||||
# Meals with MealFoods
|
foods = db.query(Food).all()
|
||||||
meals_export = []
|
meals = db.query(Meal).all()
|
||||||
for meal in meals:
|
plans = db.query(Plan).all()
|
||||||
meal_foods_export = [
|
templates = db.query(Template).all()
|
||||||
MealFoodExport(food_id=mf.food_id, quantity=mf.quantity)
|
weekly_menus = db.query(WeeklyMenu).all()
|
||||||
for mf in meal.meal_foods
|
tracked_days = db.query(TrackedDay).all()
|
||||||
]
|
|
||||||
meals_export.append(
|
# Manual serialization to handle nested relationships
|
||||||
MealExport(
|
|
||||||
id=meal.id,
|
# Meals with MealFoods
|
||||||
name=meal.name,
|
meals_export = []
|
||||||
meal_type=meal.meal_type,
|
for meal in meals:
|
||||||
meal_time=meal.meal_time,
|
meal_foods_export = [
|
||||||
meal_foods=meal_foods_export,
|
MealFoodExport(food_id=mf.food_id, quantity=mf.quantity)
|
||||||
|
for mf in meal.meal_foods
|
||||||
|
]
|
||||||
|
meals_export.append(
|
||||||
|
MealExport(
|
||||||
|
id=meal.id,
|
||||||
|
name=meal.name,
|
||||||
|
meal_type=meal.meal_type,
|
||||||
|
meal_time=meal.meal_time,
|
||||||
|
meal_foods=meal_foods_export,
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Templates with TemplateMeals
|
||||||
|
templates_export = []
|
||||||
|
for template in templates:
|
||||||
|
template_meals_export = [
|
||||||
|
TemplateMealExport(meal_id=tm.meal_id, meal_time=tm.meal_time)
|
||||||
|
for tm in template.template_meals
|
||||||
|
]
|
||||||
|
templates_export.append(
|
||||||
|
TemplateExport(
|
||||||
|
id=template.id,
|
||||||
|
name=template.name,
|
||||||
|
template_meals=template_meals_export,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Weekly Menus with WeeklyMenuDays
|
||||||
|
weekly_menus_export = []
|
||||||
|
for weekly_menu in weekly_menus:
|
||||||
|
weekly_menu_days_export = [
|
||||||
|
WeeklyMenuDayExport(
|
||||||
|
day_of_week=wmd.day_of_week, template_id=wmd.template_id
|
||||||
|
)
|
||||||
|
for wmd in weekly_menu.weekly_menu_days
|
||||||
|
]
|
||||||
|
weekly_menus_export.append(
|
||||||
|
WeeklyMenuExport(
|
||||||
|
id=weekly_menu.id,
|
||||||
|
name=weekly_menu.name,
|
||||||
|
weekly_menu_days=weekly_menu_days_export,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
# Tracked Days with TrackedMeals
|
||||||
|
tracked_days_export = []
|
||||||
|
for tracked_day in tracked_days:
|
||||||
|
tracked_meals_export = [
|
||||||
|
TrackedMealExport(
|
||||||
|
meal_id=tm.meal_id,
|
||||||
|
meal_time=tm.meal_time,
|
||||||
|
tracked_foods=[
|
||||||
|
TrackedMealFoodExport(
|
||||||
|
food_id=tmf.food_id,
|
||||||
|
quantity=tmf.quantity,
|
||||||
|
is_override=tmf.is_override
|
||||||
|
) for tmf in tm.tracked_foods
|
||||||
|
]
|
||||||
|
)
|
||||||
|
for tm in tracked_day.tracked_meals
|
||||||
|
]
|
||||||
|
tracked_days_export.append(
|
||||||
|
TrackedDayExport(
|
||||||
|
id=tracked_day.id,
|
||||||
|
person=tracked_day.person,
|
||||||
|
date=tracked_day.date,
|
||||||
|
is_modified=tracked_day.is_modified,
|
||||||
|
tracked_meals=tracked_meals_export,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
data = AllData(
|
||||||
|
foods=[FoodExport.from_orm(f) for f in foods],
|
||||||
|
meals=meals_export,
|
||||||
|
plans=[PlanExport.from_orm(p) for p in plans],
|
||||||
|
templates=templates_export,
|
||||||
|
weekly_menus=weekly_menus_export,
|
||||||
|
tracked_days=tracked_days_export,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Templates with TemplateMeals
|
json_content = data.model_dump_json()
|
||||||
templates_export = []
|
|
||||||
for template in templates:
|
return Response(
|
||||||
template_meals_export = [
|
content=json_content,
|
||||||
TemplateMealExport(meal_id=tm.meal_id, meal_time=tm.meal_time)
|
media_type="application/json",
|
||||||
for tm in template.template_meals
|
headers={"Content-Disposition": "attachment; filename=meal_planner_backup.json"}
|
||||||
]
|
|
||||||
templates_export.append(
|
|
||||||
TemplateExport(
|
|
||||||
id=template.id,
|
|
||||||
name=template.name,
|
|
||||||
template_meals=template_meals_export,
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
except Exception as e:
|
||||||
# Weekly Menus with WeeklyMenuDays
|
import traceback
|
||||||
weekly_menus_export = []
|
logging.error(f"Error exporting data: {e}\n{traceback.format_exc()}")
|
||||||
for weekly_menu in weekly_menus:
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
weekly_menu_days_export = [
|
|
||||||
WeeklyMenuDayExport(
|
|
||||||
day_of_week=wmd.day_of_week, template_id=wmd.template_id
|
|
||||||
)
|
|
||||||
for wmd in weekly_menu.weekly_menu_days
|
|
||||||
]
|
|
||||||
weekly_menus_export.append(
|
|
||||||
WeeklyMenuExport(
|
|
||||||
id=weekly_menu.id,
|
|
||||||
name=weekly_menu.name,
|
|
||||||
weekly_menu_days=weekly_menu_days_export,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
# Tracked Days with TrackedMeals
|
|
||||||
tracked_days_export = []
|
|
||||||
for tracked_day in tracked_days:
|
|
||||||
tracked_meals_export = [
|
|
||||||
TrackedMealExport(
|
|
||||||
meal_id=tm.meal_id,
|
|
||||||
meal_time=tm.meal_time,
|
|
||||||
quantity=tm.quantity,
|
|
||||||
)
|
|
||||||
for tm in tracked_day.tracked_meals
|
|
||||||
]
|
|
||||||
tracked_days_export.append(
|
|
||||||
TrackedDayExport(
|
|
||||||
id=tracked_day.id,
|
|
||||||
person=tracked_day.person,
|
|
||||||
date=tracked_day.date,
|
|
||||||
is_modified=tracked_day.is_modified,
|
|
||||||
tracked_meals=tracked_meals_export,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
return AllData(
|
|
||||||
foods=[FoodExport.from_orm(f) for f in foods],
|
|
||||||
meals=meals_export,
|
|
||||||
plans=[PlanExport.from_orm(p) for p in plans],
|
|
||||||
templates=templates_export,
|
|
||||||
weekly_menus=weekly_menus_export,
|
|
||||||
tracked_days=tracked_days_export,
|
|
||||||
)
|
|
||||||
|
|
||||||
@router.post("/import/all")
|
@router.post("/import/all")
|
||||||
async def import_all_data(file: UploadFile = File(...), db: Session = Depends(get_db)):
|
async def import_all_data(file: UploadFile = File(...), db: Session = Depends(get_db)):
|
||||||
@@ -259,7 +280,6 @@ async def import_all_data(file: UploadFile = File(...), db: Session = Depends(ge
|
|||||||
tracked_day_id=tracked_day.id,
|
tracked_day_id=tracked_day.id,
|
||||||
meal_id=tm_data.meal_id,
|
meal_id=tm_data.meal_id,
|
||||||
meal_time=tm_data.meal_time,
|
meal_time=tm_data.meal_time,
|
||||||
quantity=tm_data.quantity,
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
db.commit()
|
db.commit()
|
||||||
|
|||||||
@@ -180,10 +180,10 @@ async def get_meal_foods(meal_id: int, db: Session = Depends(get_db)):
|
|||||||
|
|
||||||
@router.post("/meals/{meal_id}/add_food")
|
@router.post("/meals/{meal_id}/add_food")
|
||||||
async def add_food_to_meal(meal_id: int, food_id: int = Form(...),
|
async def add_food_to_meal(meal_id: int, food_id: int = Form(...),
|
||||||
grams: float = Form(...), db: Session = Depends(get_db)):
|
quantity: float = Form(...), db: Session = Depends(get_db)):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
meal_food = MealFood(meal_id=meal_id, food_id=food_id, quantity=grams)
|
meal_food = MealFood(meal_id=meal_id, food_id=food_id, quantity=quantity)
|
||||||
db.add(meal_food)
|
db.add(meal_food)
|
||||||
db.commit()
|
db.commit()
|
||||||
return {"status": "success"}
|
return {"status": "success"}
|
||||||
@@ -210,14 +210,14 @@ async def remove_food_from_meal(meal_food_id: int, db: Session = Depends(get_db)
|
|||||||
return {"status": "error", "message": str(e)}
|
return {"status": "error", "message": str(e)}
|
||||||
|
|
||||||
@router.post("/meals/update_food_quantity")
|
@router.post("/meals/update_food_quantity")
|
||||||
async def update_meal_food_quantity(meal_food_id: int = Form(...), grams: float = Form(...), db: Session = Depends(get_db)):
|
async def update_meal_food_quantity(meal_food_id: int = Form(...), quantity: float = Form(...), db: Session = Depends(get_db)):
|
||||||
"""Update the quantity of a food in a meal"""
|
"""Update the quantity of a food in a meal"""
|
||||||
try:
|
try:
|
||||||
meal_food = db.query(MealFood).filter(MealFood.id == meal_food_id).first()
|
meal_food = db.query(MealFood).filter(MealFood.id == meal_food_id).first()
|
||||||
if not meal_food:
|
if not meal_food:
|
||||||
return {"status": "error", "message": "Meal food not found"}
|
return {"status": "error", "message": "Meal food not found"}
|
||||||
|
|
||||||
meal_food.quantity = grams
|
meal_food.quantity = quantity
|
||||||
db.commit()
|
db.commit()
|
||||||
return {"status": "success"}
|
return {"status": "success"}
|
||||||
except ValueError as ve:
|
except ValueError as ve:
|
||||||
|
|||||||
@@ -75,8 +75,8 @@ async def tracker_page(request: Request, person: str = "Sarah", date: str = None
|
|||||||
).all()
|
).all()
|
||||||
|
|
||||||
# Template will handle filtering of deleted foods
|
# Template will handle filtering of deleted foods
|
||||||
# Get all meals for dropdown
|
# Get all meals for dropdown (exclude snapshots)
|
||||||
meals = db.query(Meal).all()
|
meals = db.query(Meal).filter(Meal.meal_type != "tracked_snapshot").all()
|
||||||
|
|
||||||
# Get all templates for template dropdown
|
# Get all templates for template dropdown
|
||||||
templates_list = db.query(Template).all()
|
templates_list = db.query(Template).all()
|
||||||
@@ -138,10 +138,34 @@ async def tracker_add_meal(request: Request, db: Session = Depends(get_db)):
|
|||||||
db.commit()
|
db.commit()
|
||||||
db.refresh(tracked_day)
|
db.refresh(tracked_day)
|
||||||
|
|
||||||
# Create tracked meal
|
# 1. Fetch the original meal
|
||||||
|
original_meal = db.query(Meal).filter(Meal.id == int(meal_id)).first()
|
||||||
|
if not original_meal:
|
||||||
|
return {"status": "error", "message": "Meal not found"}
|
||||||
|
|
||||||
|
# 2. Create a snapshot copy of the meal
|
||||||
|
snapshot_meal = Meal(
|
||||||
|
name=original_meal.name,
|
||||||
|
meal_type="tracked_snapshot",
|
||||||
|
meal_time=original_meal.meal_time
|
||||||
|
)
|
||||||
|
db.add(snapshot_meal)
|
||||||
|
db.flush() # get ID
|
||||||
|
|
||||||
|
# 3. Copy ingredients (MealFood)
|
||||||
|
meal_foods = db.query(MealFood).filter(MealFood.meal_id == original_meal.id).all()
|
||||||
|
for mf in meal_foods:
|
||||||
|
snapshot_food = MealFood(
|
||||||
|
meal_id=snapshot_meal.id,
|
||||||
|
food_id=mf.food_id,
|
||||||
|
quantity=mf.quantity
|
||||||
|
)
|
||||||
|
db.add(snapshot_food)
|
||||||
|
|
||||||
|
# 4. Create tracked meal pointing to the SNAPSHOT
|
||||||
tracked_meal = TrackedMeal(
|
tracked_meal = TrackedMeal(
|
||||||
tracked_day_id=tracked_day.id,
|
tracked_day_id=tracked_day.id,
|
||||||
meal_id=int(meal_id),
|
meal_id=snapshot_meal.id,
|
||||||
meal_time=meal_time
|
meal_time=meal_time
|
||||||
)
|
)
|
||||||
db.add(tracked_meal)
|
db.add(tracked_meal)
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ from sqlalchemy.orm import sessionmaker, Session, relationship, declarative_base
|
|||||||
from sqlalchemy.orm import joinedload
|
from sqlalchemy.orm import joinedload
|
||||||
from pydantic import BaseModel, ConfigDict
|
from pydantic import BaseModel, ConfigDict
|
||||||
|
|
||||||
from typing import List, Optional
|
from typing import List, Optional, Union
|
||||||
from datetime import date, datetime
|
from datetime import date, datetime
|
||||||
import os
|
import os
|
||||||
import logging
|
import logging
|
||||||
@@ -173,7 +173,7 @@ class TrackedMealFood(Base):
|
|||||||
# Pydantic models
|
# Pydantic models
|
||||||
class FoodCreate(BaseModel):
|
class FoodCreate(BaseModel):
|
||||||
name: str
|
name: str
|
||||||
serving_size: str
|
serving_size: Union[float, str]
|
||||||
serving_unit: str
|
serving_unit: str
|
||||||
calories: float
|
calories: float
|
||||||
protein: float
|
protein: float
|
||||||
@@ -189,7 +189,7 @@ class FoodCreate(BaseModel):
|
|||||||
class FoodResponse(BaseModel):
|
class FoodResponse(BaseModel):
|
||||||
id: int
|
id: int
|
||||||
name: str
|
name: str
|
||||||
serving_size: str
|
serving_size: Union[float, str]
|
||||||
serving_unit: str
|
serving_unit: str
|
||||||
calories: float
|
calories: float
|
||||||
protein: float
|
protein: float
|
||||||
|
|||||||
@@ -5,6 +5,10 @@ services:
|
|||||||
- "8999:8999"
|
- "8999:8999"
|
||||||
environment:
|
environment:
|
||||||
- DATABASE_URL=sqlite:////app/data/meal_planner.db
|
- DATABASE_URL=sqlite:////app/data/meal_planner.db
|
||||||
|
- PYTHONUNBUFFERED=1
|
||||||
volumes:
|
volumes:
|
||||||
- ./alembic:/app/alembic
|
- ./alembic:/app/alembic
|
||||||
- ./data:/app/data
|
- ./data:/app/data
|
||||||
|
- ./app:/app/app
|
||||||
|
- ./templates:/app/templates
|
||||||
|
- ./main.py:/app/main.py
|
||||||
BIN
meal_planner_2026-01-06_17-20-51.db:Zone.Identifier
Normal file
BIN
meal_planner_2026-01-06_17-20-51.db:Zone.Identifier
Normal file
Binary file not shown.
@@ -117,6 +117,7 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<th style="width: 40%">Food</th>
|
<th style="width: 40%">Food</th>
|
||||||
<th class="text-end">Carbs</th>
|
<th class="text-end">Carbs</th>
|
||||||
|
<th class="text-end">Fiber</th>
|
||||||
<th class="text-end">Net Carbs</th>
|
<th class="text-end">Net Carbs</th>
|
||||||
<th class="text-end">Fat</th>
|
<th class="text-end">Fat</th>
|
||||||
<th class="text-end">Protein</th>
|
<th class="text-end">Protein</th>
|
||||||
@@ -153,6 +154,7 @@
|
|||||||
}})</span>
|
}})</span>
|
||||||
</td>
|
</td>
|
||||||
<td class="text-end">{{ "%.1f"|format(row_carbs) }}g</td>
|
<td class="text-end">{{ "%.1f"|format(row_carbs) }}g</td>
|
||||||
|
<td class="text-end">{{ "%.1f"|format(row_fiber) }}g</td>
|
||||||
<td class="text-end">{{ "%.1f"|format(row_carbs - row_fiber) }}g</td>
|
<td class="text-end">{{ "%.1f"|format(row_carbs - row_fiber) }}g</td>
|
||||||
<td class="text-end">{{ "%.1f"|format(row_fat) }}g</td>
|
<td class="text-end">{{ "%.1f"|format(row_fat) }}g</td>
|
||||||
<td class="text-end">{{ "%.1f"|format(row_protein) }}g</td>
|
<td class="text-end">{{ "%.1f"|format(row_protein) }}g</td>
|
||||||
@@ -190,6 +192,7 @@
|
|||||||
<span class="text-muted ms-1">({{ qty|round(1) }} g)</span>
|
<span class="text-muted ms-1">({{ qty|round(1) }} g)</span>
|
||||||
</td>
|
</td>
|
||||||
<td class="text-end">{{ "%.1f"|format(row_carbs) }}g</td>
|
<td class="text-end">{{ "%.1f"|format(row_carbs) }}g</td>
|
||||||
|
<td class="text-end">{{ "%.1f"|format(row_fiber) }}g</td>
|
||||||
<td class="text-end">{{ "%.1f"|format(row_carbs - row_fiber) }}g</td>
|
<td class="text-end">{{ "%.1f"|format(row_carbs - row_fiber) }}g</td>
|
||||||
<td class="text-end">{{ "%.1f"|format(row_fat) }}g</td>
|
<td class="text-end">{{ "%.1f"|format(row_fat) }}g</td>
|
||||||
<td class="text-end">{{ "%.1f"|format(row_protein) }}g</td>
|
<td class="text-end">{{ "%.1f"|format(row_protein) }}g</td>
|
||||||
@@ -206,6 +209,7 @@
|
|||||||
<tr class="table-secondary fw-bold">
|
<tr class="table-secondary fw-bold">
|
||||||
<td>Total</td>
|
<td>Total</td>
|
||||||
<td class="text-end">{{ "%.1f"|format(meal_totals.carbs) }}g</td>
|
<td class="text-end">{{ "%.1f"|format(meal_totals.carbs) }}g</td>
|
||||||
|
<td class="text-end">{{ "%.1f"|format(meal_totals.fiber) }}g</td>
|
||||||
<td class="text-end">{{ "%.1f"|format(meal_totals.carbs - meal_totals.fiber)
|
<td class="text-end">{{ "%.1f"|format(meal_totals.carbs - meal_totals.fiber)
|
||||||
}}g</td>
|
}}g</td>
|
||||||
<td class="text-end">{{ "%.1f"|format(meal_totals.fat) }}g</td>
|
<td class="text-end">{{ "%.1f"|format(meal_totals.fat) }}g</td>
|
||||||
@@ -231,58 +235,103 @@
|
|||||||
|
|
||||||
<!-- Right Column - Nutrition Totals -->
|
<!-- Right Column - Nutrition Totals -->
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
<div class="card sticky-top" style="top: 20px;">
|
<div class="sticky-top" style="top: 20px;">
|
||||||
<div class="card-header">
|
<!-- Daily Totals Card -->
|
||||||
<h5 class="mb-0">Daily Totals</h5>
|
<div class="card mb-3">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="mb-0">Daily Totals</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row text-center">
|
||||||
|
<div class="col-6 mb-3">
|
||||||
|
<div class="border rounded p-2">
|
||||||
|
<strong class="h4 text-primary">{{ "%.0f"|format(day_totals.calories) }}</strong>
|
||||||
|
<div class="small text-muted">Calories</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-6 mb-3">
|
||||||
|
<div class="border rounded p-2">
|
||||||
|
<strong class="h4 text-success">{{ "%.1f"|format(day_totals.protein) }}g</strong>
|
||||||
|
<div class="small text-muted">Protein ({{ day_totals.protein_pct }}%)</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-6 mb-3">
|
||||||
|
<div class="border rounded p-2">
|
||||||
|
<strong>{{ "%.1f"|format(day_totals.carbs) }}g</strong>
|
||||||
|
<div class="small text-muted">Carbs ({{ day_totals.carbs_pct }}%)</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-6 mb-3">
|
||||||
|
<div class="border rounded p-2">
|
||||||
|
<strong class="h4 text-danger">{{ "%.1f"|format(day_totals.fat) }}g</strong>
|
||||||
|
<div class="small text-muted">Fat ({{ day_totals.fat_pct }}%)</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-6 mb-3">
|
||||||
|
<div class="border rounded p-2">
|
||||||
|
<strong>{{ "%.1f"|format(day_totals.fiber) }}g</strong>
|
||||||
|
<div class="small text-muted">Fiber</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-6 mb-3">
|
||||||
|
<div class="border rounded p-2">
|
||||||
|
<strong class="h4 text-warning">{{ "%.1f"|format(day_totals.net_carbs) }}g</strong>
|
||||||
|
<div class="small text-muted">Net Carbs</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-6 mb-3">
|
||||||
|
<div class="border rounded p-2">
|
||||||
|
<strong>{{ "%.0f"|format(day_totals.sugar) }}g</strong>
|
||||||
|
<div class="small text-muted">Sugar</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-6 mb-3">
|
||||||
|
<div class="border rounded p-2">
|
||||||
|
<strong>{{ "%.0f"|format(day_totals.sodium) }}mg</strong>
|
||||||
|
<div class="small text-muted">Sodium</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
|
||||||
<div class="row text-center">
|
<!-- Macro Balance Card -->
|
||||||
<div class="col-6 mb-3">
|
<div class="card">
|
||||||
<div class="border rounded p-2">
|
<div class="card-header">
|
||||||
<strong class="h4 text-primary">{{ "%.0f"|format(day_totals.calories) }}</strong>
|
<h5 class="mb-0">Macro Balance</h5>
|
||||||
<div class="small text-muted">Calories</div>
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="mb-3">
|
||||||
|
<div class="d-flex justify-content-between small mb-1">
|
||||||
|
<span class="text-success fw-bold">Protein</span>
|
||||||
|
<span>{{ day_totals.protein_pct }}%</span>
|
||||||
|
</div>
|
||||||
|
<div class="progress" style="height: 10px;">
|
||||||
|
<div class="progress-bar bg-success" role="progressbar"
|
||||||
|
style="width: {{ day_totals.protein_pct }}%"
|
||||||
|
aria-valuenow="{{ day_totals.protein_pct }}" aria-valuemin="0" aria-valuemax="100">
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-6 mb-3">
|
<div class="mb-3">
|
||||||
<div class="border rounded p-2">
|
<div class="d-flex justify-content-between small mb-1">
|
||||||
<strong class="h4 text-success">{{ "%.1f"|format(day_totals.protein) }}g</strong>
|
<span class="fw-bold" style="color: #0d6efd;">Carbs</span>
|
||||||
<div class="small text-muted">Protein ({{ day_totals.protein_pct }}%)</div>
|
<span>{{ day_totals.carbs_pct }}%</span>
|
||||||
|
</div>
|
||||||
|
<div class="progress" style="height: 10px;">
|
||||||
|
<div class="progress-bar" role="progressbar" style="width: {{ day_totals.carbs_pct }}%"
|
||||||
|
aria-valuenow="{{ day_totals.carbs_pct }}" aria-valuemin="0" aria-valuemax="100"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-6 mb-3">
|
<div class="mb-0">
|
||||||
<div class="border rounded p-2">
|
<div class="d-flex justify-content-between small mb-1">
|
||||||
<strong>{{ "%.1f"|format(day_totals.carbs) }}g</strong>
|
<span class="text-danger fw-bold">Fat</span>
|
||||||
<div class="small text-muted">Carbs ({{ day_totals.carbs_pct }}%)</div>
|
<span>{{ day_totals.fat_pct }}%</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="progress" style="height: 10px;">
|
||||||
<div class="col-6 mb-3">
|
<div class="progress-bar bg-danger" role="progressbar"
|
||||||
<div class="border rounded p-2">
|
style="width: {{ day_totals.fat_pct }}%" aria-valuenow="{{ day_totals.fat_pct }}"
|
||||||
<strong class="h4 text-danger">{{ "%.1f"|format(day_totals.fat) }}g</strong>
|
aria-valuemin="0" aria-valuemax="100"></div>
|
||||||
<div class="small text-muted">Fat ({{ day_totals.fat_pct }}%)</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-6 mb-3">
|
|
||||||
<div class="border rounded p-2">
|
|
||||||
<strong>{{ "%.1f"|format(day_totals.fiber) }}g</strong>
|
|
||||||
<div class="small text-muted">Fiber</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-6 mb-3">
|
|
||||||
<div class="border rounded p-2">
|
|
||||||
<strong class="h4 text-warning">{{ "%.1f"|format(day_totals.net_carbs) }}g</strong>
|
|
||||||
<div class="small text-muted">Net Carbs</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-6 mb-3">
|
|
||||||
<div class="border rounded p-2">
|
|
||||||
<strong>{{ "%.0f"|format(day_totals.sugar) }}g</strong>
|
|
||||||
<div class="small text-muted">Sugar</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-6 mb-3">
|
|
||||||
<div class="border rounded p-2">
|
|
||||||
<strong>{{ "%.0f"|format(day_totals.sodium) }}mg</strong>
|
|
||||||
<div class="small text-muted">Sodium</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user