mirror of
https://github.com/sstent/foodplanner.git
synced 2025-12-06 08:01:47 +00:00
updated the tracker to have more features
This commit is contained in:
@@ -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()
|
||||
|
||||
@@ -57,6 +55,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"""
|
||||
@@ -312,3 +344,127 @@ async def tracker_reset_to_plan(request: Request, db: Session = Depends(get_db))
|
||||
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)}
|
||||
@@ -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']
|
||||
|
||||
@@ -37,6 +37,9 @@
|
||||
<button class="btn btn-primary" onclick="applyTemplate()">
|
||||
<i class="bi bi-upload"></i> Apply Template
|
||||
</button>
|
||||
<button class="btn btn-info text-white" onclick="addSingleFood()">
|
||||
<i class="bi bi-plus-circle"></i> Add Single Food
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Meal Times -->
|
||||
@@ -57,17 +60,40 @@
|
||||
|
||||
{% if meals_for_time %}
|
||||
{% for tracked_meal in meals_for_time %}
|
||||
<div class="d-flex justify-content-between align-items-center mb-2 p-2 bg-light rounded">
|
||||
<div class="mb-3 p-3 bg-light rounded">
|
||||
<div class="d-flex justify-content-between align-items-center mb-2">
|
||||
<div>
|
||||
<strong>{{ tracked_meal.meal.name }}</strong>
|
||||
{% if tracked_meal.quantity != 1.0 %}
|
||||
<span class="text-muted">({{ "%.1f"|format(tracked_meal.quantity) }}x)</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div>
|
||||
<button class="btn btn-sm btn-outline-secondary me-1" onclick="editTrackedMeal({{ tracked_meal.id }})" title="Edit Meal">
|
||||
<i class="bi bi-pencil"></i>
|
||||
</button>
|
||||
<button class="btn btn-sm btn-outline-danger" onclick="removeMeal({{ tracked_meal.id }})">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Food Breakdown -->
|
||||
<div class="ms-3">
|
||||
{% for meal_food in tracked_meal.meal.meal_foods %}
|
||||
{% set effective_quantity = meal_food.quantity * tracked_meal.quantity %}
|
||||
<div class="small text-muted mb-1">
|
||||
• {{ meal_food.food.name }}: {{ "%.1f"|format(effective_quantity) }} {{ meal_food.food.serving_unit }}
|
||||
{% if meal_food.food.serving_size %}
|
||||
({{ meal_food.food.serving_size }})
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% if not tracked_meal.meal.meal_foods %}
|
||||
<div class="small text-muted">No foods in this meal</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<p class="text-muted mb-0">No meals tracked</p>
|
||||
@@ -241,23 +267,94 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Edit Tracked Meal Modal -->
|
||||
<div class="modal fade" id="editTrackedMealModal" tabindex="-1" aria-labelledby="editTrackedMealModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="editTrackedMealModalLabel">Edit Tracked Meal</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<input type="hidden" id="editTrackedMealId" value="">
|
||||
<div id="editMealFoodsList">
|
||||
<!-- Foods will be loaded here -->
|
||||
</div>
|
||||
<div class="mt-3">
|
||||
<button class="btn btn-outline-primary" onclick="addFoodToTrackedMeal()">
|
||||
<i class="bi bi-plus"></i> Add Food
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-primary" onclick="saveTrackedMeal()">Save Changes</button>
|
||||
<button type="button" class="btn btn-success" onclick="saveAsNewMeal()">Save as New Meal</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Add Single Food Modal -->
|
||||
<div class="modal fade" id="addSingleFoodModal" tabindex="-1" aria-labelledby="addSingleFoodModalLabel" aria-hidden="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title" id="addSingleFoodModalLabel">Add Single Food</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form id="addSingleFoodForm">
|
||||
<input type="hidden" name="person" value="{{ person }}">
|
||||
<input type="hidden" name="date" value="{{ current_date.isoformat() }}">
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Select Food</label>
|
||||
<select class="form-control" name="food_id" required>
|
||||
<option value="">Choose food...</option>
|
||||
{% for food in foods %}
|
||||
<option value="{{ food.id }}">{{ food.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Quantity</label>
|
||||
<input type="number" step="0.1" class="form-control" name="quantity" value="1.0" min="0.1" required>
|
||||
</div>
|
||||
<div class="mb-3">
|
||||
<label class="form-label">Meal Time</label>
|
||||
<select class="form-control" name="meal_time" required>
|
||||
{% for meal_time in meal_times %}
|
||||
<option value="{{ meal_time }}">{{ meal_time }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-primary" onclick="submitAddSingleFood()">Add Food</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Date navigation
|
||||
function navigateDate(date) {
|
||||
// Date navigation
|
||||
function navigateDate(date) {
|
||||
const url = new URL(window.location);
|
||||
url.searchParams.set('date', date);
|
||||
window.location.href = url.toString();
|
||||
}
|
||||
}
|
||||
|
||||
// Add meal to specific time
|
||||
function addMealToTime(mealTime) {
|
||||
// Add meal to specific time
|
||||
function addMealToTime(mealTime) {
|
||||
document.getElementById('mealTimeDisplay').textContent = mealTime;
|
||||
document.getElementById('addMealTime').value = mealTime;
|
||||
new bootstrap.Modal(document.getElementById('addMealModal')).show();
|
||||
}
|
||||
}
|
||||
|
||||
// Submit add meal form
|
||||
async function submitAddMeal() {
|
||||
// Submit add meal form
|
||||
async function submitAddMeal() {
|
||||
const form = document.getElementById('addMealForm');
|
||||
const formData = new FormData(form);
|
||||
|
||||
@@ -278,10 +375,10 @@ async function submitAddMeal() {
|
||||
} catch (error) {
|
||||
alert('Error: ' + error.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Remove meal
|
||||
async function removeMeal(trackedMealId) {
|
||||
// Remove meal
|
||||
async function removeMeal(trackedMealId) {
|
||||
if (confirm('Remove this meal from the tracker?')) {
|
||||
try {
|
||||
const response = await fetch(`/tracker/remove_meal/${trackedMealId}`, {
|
||||
@@ -299,15 +396,15 @@ async function removeMeal(trackedMealId) {
|
||||
alert('Error: ' + error.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Save as template
|
||||
function saveAsTemplate() {
|
||||
// Save as template
|
||||
function saveAsTemplate() {
|
||||
new bootstrap.Modal(document.getElementById('saveTemplateModal')).show();
|
||||
}
|
||||
}
|
||||
|
||||
// Submit save template
|
||||
async function submitSaveTemplate() {
|
||||
// Submit save template
|
||||
async function submitSaveTemplate() {
|
||||
const form = document.getElementById('saveTemplateForm');
|
||||
const formData = new FormData(form);
|
||||
|
||||
@@ -328,15 +425,15 @@ async function submitSaveTemplate() {
|
||||
} catch (error) {
|
||||
alert('Error: ' + error.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Apply template
|
||||
function applyTemplate() {
|
||||
// Apply template
|
||||
function applyTemplate() {
|
||||
new bootstrap.Modal(document.getElementById('applyTemplateModal')).show();
|
||||
}
|
||||
}
|
||||
|
||||
// Submit apply template
|
||||
async function submitApplyTemplate() {
|
||||
// Submit apply template
|
||||
async function submitApplyTemplate() {
|
||||
const form = document.getElementById('applyTemplateForm');
|
||||
const formData = new FormData(form);
|
||||
|
||||
@@ -357,10 +454,10 @@ async function submitApplyTemplate() {
|
||||
} catch (error) {
|
||||
alert('Error: ' + error.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Reset to plan
|
||||
async function resetToPlan() {
|
||||
// Reset to plan
|
||||
async function resetToPlan() {
|
||||
if (confirm('Reset this day back to the original plan? All custom changes will be lost.')) {
|
||||
const formData = new FormData();
|
||||
formData.append('person', '{{ person }}');
|
||||
@@ -383,6 +480,192 @@ async function resetToPlan() {
|
||||
alert('Error: ' + error.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Edit tracked meal
|
||||
function editTrackedMeal(trackedMealId) {
|
||||
document.getElementById('editTrackedMealId').value = trackedMealId;
|
||||
loadTrackedMealFoods(trackedMealId);
|
||||
new bootstrap.Modal(document.getElementById('editTrackedMealModal')).show();
|
||||
}
|
||||
|
||||
// Load foods for editing
|
||||
async function loadTrackedMealFoods(trackedMealId) {
|
||||
try {
|
||||
const response = await fetch(`/tracker/get_tracked_meal_foods/${trackedMealId}`);
|
||||
const data = await response.json();
|
||||
|
||||
const container = document.getElementById('editMealFoodsList');
|
||||
container.innerHTML = '';
|
||||
|
||||
if (data.status === 'success') {
|
||||
data.meal_foods.forEach(food => {
|
||||
const foodDiv = document.createElement('div');
|
||||
foodDiv.className = 'mb-2 p-2 border rounded';
|
||||
foodDiv.innerHTML = `
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div>
|
||||
<strong>${food.food_name}</strong>
|
||||
<small class="text-muted">(${food.serving_size} ${food.serving_unit})</small>
|
||||
</div>
|
||||
<div>
|
||||
<input type="number" class="form-control w-25 d-inline me-2" value="${food.quantity}" data-food-id="${food.id}" min="0" step="0.1">
|
||||
<button class="btn btn-sm btn-outline-danger" onclick="removeTrackedFood(${food.id})">
|
||||
<i class="bi bi-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
container.appendChild(foodDiv);
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading meal foods:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Add food to tracked meal
|
||||
function addFoodToTrackedMeal() {
|
||||
// Implementation for adding new food - would need a food selection modal
|
||||
alert('Add food functionality to be implemented');
|
||||
}
|
||||
|
||||
// Remove tracked food
|
||||
async function removeTrackedFood(trackedFoodId) {
|
||||
if (confirm('Remove this food from the tracked meal?')) {
|
||||
try {
|
||||
const response = await fetch(`/tracker/remove_tracked_food/${trackedFoodId}`, {
|
||||
method: 'DELETE'
|
||||
});
|
||||
const result = await response.json();
|
||||
|
||||
if (result.status === 'success') {
|
||||
loadTrackedMealFoods(document.getElementById('editTrackedMealId').value);
|
||||
} else {
|
||||
alert('Error: ' + result.message);
|
||||
}
|
||||
} catch (error) {
|
||||
alert('Error: ' + error.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Save tracked meal changes
|
||||
async function saveTrackedMeal() {
|
||||
const trackedMealId = document.getElementById('editTrackedMealId').value;
|
||||
const inputs = document.querySelectorAll('#editMealFoodsList input[type="number"]');
|
||||
|
||||
const updates = [];
|
||||
inputs.forEach(input => {
|
||||
updates.push({
|
||||
tracked_food_id: input.dataset.foodId,
|
||||
quantity: parseFloat(input.value)
|
||||
});
|
||||
});
|
||||
|
||||
try {
|
||||
const response = await fetch('/tracker/update_tracked_meal', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ tracked_meal_id: trackedMealId, updates: updates })
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.status === 'success') {
|
||||
bootstrap.Modal.getInstance(document.getElementById('editTrackedMealModal')).hide();
|
||||
location.reload();
|
||||
} else {
|
||||
alert('Error: ' + result.message);
|
||||
}
|
||||
} catch (error) {
|
||||
alert('Error: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Save as new meal
|
||||
async function saveAsNewMeal() {
|
||||
const mealName = prompt('Enter name for new meal:');
|
||||
if (!mealName) return;
|
||||
|
||||
const trackedMealId = document.getElementById('editTrackedMealId').value;
|
||||
const inputs = document.querySelectorAll('#editMealFoodsList input[type="number"]');
|
||||
|
||||
const foods = [];
|
||||
inputs.forEach(input => {
|
||||
foods.push({
|
||||
food_id: input.dataset.foodId,
|
||||
quantity: parseFloat(input.value)
|
||||
});
|
||||
});
|
||||
|
||||
try {
|
||||
const response = await fetch('/tracker/save_as_new_meal', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
tracked_meal_id: trackedMealId,
|
||||
new_meal_name: mealName,
|
||||
foods: foods
|
||||
})
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.status === 'success') {
|
||||
bootstrap.Modal.getInstance(document.getElementById('editTrackedMealModal')).hide();
|
||||
alert('New meal saved successfully!');
|
||||
location.reload();
|
||||
} else {
|
||||
alert('Error: ' + result.message);
|
||||
}
|
||||
} catch (error) {
|
||||
alert('Error: ' + error.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Update quantity on input change (real-time update)
|
||||
document.addEventListener('input', function(e) {
|
||||
if (e.target.type === 'number' && e.target.dataset.foodId) {
|
||||
const trackedFoodId = e.target.dataset.foodId;
|
||||
const quantity = parseFloat(e.target.value);
|
||||
|
||||
fetch('/tracker/update_tracked_food', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ tracked_food_id: trackedFoodId, quantity: quantity })
|
||||
}).catch(error => console.error('Error updating quantity:', error));
|
||||
}
|
||||
});
|
||||
|
||||
// Show add single food modal
|
||||
function addSingleFood() {
|
||||
new bootstrap.Modal(document.getElementById('addSingleFoodModal')).show();
|
||||
}
|
||||
|
||||
// Submit add single food form
|
||||
async function submitAddSingleFood() {
|
||||
const form = document.getElementById('addSingleFoodForm');
|
||||
const formData = new FormData(form);
|
||||
|
||||
try {
|
||||
const response = await fetch('/tracker/add_food', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(Object.fromEntries(formData)), // Convert FormData to JSON
|
||||
headers: { 'Content-Type': 'application/json' }
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.status === 'success') {
|
||||
bootstrap.Modal.getInstance(document.getElementById('addSingleFoodModal')).hide();
|
||||
location.reload();
|
||||
} else {
|
||||
alert('Error: ' + result.message);
|
||||
}
|
||||
} catch (error) {
|
||||
alert('Error: ' + error.message);
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -3,6 +3,10 @@ Tests for Tracker CRUD operations
|
||||
"""
|
||||
import pytest
|
||||
from datetime import date, timedelta
|
||||
from app.database import (
|
||||
TrackedDay, TrackedMeal, TrackedMealFood, Meal, MealFood, Food,
|
||||
Template, calculate_day_nutrition_tracked
|
||||
)
|
||||
|
||||
|
||||
class TestTrackerRoutes:
|
||||
@@ -49,7 +53,6 @@ class TestTrackerRoutes:
|
||||
|
||||
def test_tracker_remove_meal(self, client, sample_tracked_day, db_session):
|
||||
"""Test DELETE /tracker/remove_meal/{tracked_meal_id}"""
|
||||
from main import TrackedMeal
|
||||
|
||||
tracked_meal = db_session.query(TrackedMeal).filter(
|
||||
TrackedMeal.tracked_day_id == sample_tracked_day.id
|
||||
@@ -122,7 +125,6 @@ class TestTrackerTemplates:
|
||||
|
||||
def test_tracker_apply_empty_template(self, client, db_session):
|
||||
"""Test applying template with no meals"""
|
||||
from main import Template
|
||||
|
||||
empty_template = Template(name="Empty Tracker Template")
|
||||
db_session.add(empty_template)
|
||||
@@ -171,7 +173,6 @@ class TestTrackerNutrition:
|
||||
|
||||
def test_calculate_tracked_day_nutrition(self, client, sample_tracked_day, db_session):
|
||||
"""Test tracked day nutrition calculation"""
|
||||
from main import calculate_day_nutrition_tracked, TrackedMeal
|
||||
|
||||
tracked_meals = db_session.query(TrackedMeal).filter(
|
||||
TrackedMeal.tracked_day_id == sample_tracked_day.id
|
||||
@@ -187,7 +188,6 @@ class TestTrackerNutrition:
|
||||
|
||||
def test_tracked_day_with_quantity_multiplier(self, client, sample_meal, db_session):
|
||||
"""Test nutrition calculation with quantity multiplier"""
|
||||
from main import TrackedDay, TrackedMeal, calculate_day_nutrition_tracked
|
||||
|
||||
# Create tracked day with meal at 2x quantity
|
||||
tracked_day = TrackedDay(
|
||||
@@ -213,3 +213,184 @@ class TestTrackerNutrition:
|
||||
|
||||
# Should be double the base meal nutrition
|
||||
assert nutrition["calories"] > 0
|
||||
|
||||
|
||||
class TestTrackerView:
|
||||
"""Test tracker view rendering"""
|
||||
|
||||
def test_tracker_page_shows_food_breakdown(self, client, sample_meal, sample_food, db_session):
|
||||
"""Test that tracker page shows food breakdown for tracked meals"""
|
||||
|
||||
# Create sample tracked day and meal
|
||||
tracked_day = TrackedDay(person="Sarah", date=date.today(), is_modified=True)
|
||||
db_session.add(tracked_day)
|
||||
db_session.commit()
|
||||
db_session.refresh(tracked_day)
|
||||
|
||||
# Add the meal to tracker (assuming meal has the food)
|
||||
tracked_meal = TrackedMeal(
|
||||
tracked_day_id=tracked_day.id,
|
||||
meal_id=sample_meal.id,
|
||||
meal_time="Breakfast",
|
||||
quantity=1.0
|
||||
)
|
||||
db_session.add(tracked_meal)
|
||||
db_session.commit()
|
||||
|
||||
# Get tracker page
|
||||
response = client.get(f"/tracker?person=Sarah")
|
||||
assert response.status_code == 200
|
||||
|
||||
# Check if food name appears in the response (breakdown should show it)
|
||||
assert sample_food.name.encode() in response.content
|
||||
|
||||
|
||||
class TestTrackerEdit:
|
||||
"""Test editing tracked meals"""
|
||||
|
||||
def test_update_tracked_food_quantity(self, client, sample_meal, sample_food, db_session):
|
||||
"""Test updating quantity of a custom food in a tracked meal"""
|
||||
|
||||
# Create sample tracked day and meal
|
||||
tracked_day = TrackedDay(person="Sarah", date=date.today(), is_modified=True)
|
||||
db_session.add(tracked_day)
|
||||
db_session.commit()
|
||||
db_session.refresh(tracked_day)
|
||||
|
||||
tracked_meal = TrackedMeal(
|
||||
tracked_day_id=tracked_day.id,
|
||||
meal_id=sample_meal.id,
|
||||
meal_time="Breakfast",
|
||||
quantity=1.0
|
||||
)
|
||||
db_session.add(tracked_meal)
|
||||
db_session.commit()
|
||||
db_session.refresh(tracked_meal)
|
||||
|
||||
# Add a custom tracked food
|
||||
tracked_food = TrackedMealFood(
|
||||
tracked_meal_id=tracked_meal.id,
|
||||
food_id=sample_food.id,
|
||||
quantity=2.0,
|
||||
is_override=True
|
||||
)
|
||||
db_session.add(tracked_food)
|
||||
db_session.commit()
|
||||
db_session.refresh(tracked_food)
|
||||
|
||||
original_quantity = tracked_food.quantity
|
||||
|
||||
# Update the food quantity via API
|
||||
response = client.post("/tracker/update_tracked_food", json={
|
||||
"tracked_food_id": tracked_food.id,
|
||||
"quantity": 3.0
|
||||
})
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["status"] == "success"
|
||||
|
||||
# Verify the update
|
||||
db_session.commit()
|
||||
updated_food = db_session.query(TrackedMealFood).get(tracked_food.id)
|
||||
assert updated_food.quantity == 3.0
|
||||
assert updated_food.quantity != original_quantity
|
||||
|
||||
|
||||
class TestTrackerSaveAsNewMeal:
|
||||
"""Test saving an edited tracked meal as a new meal"""
|
||||
|
||||
def test_save_as_new_meal(self, client, sample_meal, sample_food, db_session):
|
||||
"""Test POST /tracker/save_as_new_meal"""
|
||||
|
||||
# Create a tracked day and meal with custom foods
|
||||
tracked_day = TrackedDay(person="Sarah", date=date.today(), is_modified=True)
|
||||
db_session.add(tracked_day)
|
||||
db_session.commit()
|
||||
db_session.refresh(tracked_day)
|
||||
|
||||
tracked_meal = TrackedMeal(
|
||||
tracked_day_id=tracked_day.id,
|
||||
meal_id=sample_meal.id,
|
||||
meal_time="Breakfast",
|
||||
quantity=1.0
|
||||
)
|
||||
db_session.add(tracked_meal)
|
||||
db_session.commit()
|
||||
db_session.refresh(tracked_meal)
|
||||
|
||||
# Add a custom food to the tracked meal
|
||||
tracked_food = TrackedMealFood(
|
||||
tracked_meal_id=tracked_meal.id,
|
||||
food_id=sample_food.id,
|
||||
quantity=2.5,
|
||||
is_override=False # This is an addition, not an override for this test
|
||||
)
|
||||
db_session.add(tracked_food)
|
||||
db_session.commit()
|
||||
db_session.refresh(tracked_food)
|
||||
|
||||
new_meal_name = "My Custom Breakfast"
|
||||
|
||||
response = client.post("/tracker/save_as_new_meal", json={
|
||||
"tracked_meal_id": tracked_meal.id,
|
||||
"new_meal_name": new_meal_name,
|
||||
"foods": [
|
||||
{"food_id": sample_food.id, "quantity": 3.0}
|
||||
]
|
||||
})
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["status"] == "success"
|
||||
assert "new_meal_id" in data
|
||||
|
||||
# Verify a new meal was created
|
||||
new_meal = db_session.query(Meal).filter(Meal.name == new_meal_name).first()
|
||||
assert new_meal is not None
|
||||
assert len(new_meal.meal_foods) == 1 # Only the custom food should be here
|
||||
|
||||
# Verify the original tracked meal now points to the new meal
|
||||
db_session.commit()
|
||||
updated_tracked_meal = db_session.query(TrackedMeal).get(tracked_meal.id)
|
||||
assert updated_tracked_meal.meal_id == new_meal.id
|
||||
assert len(updated_tracked_meal.tracked_foods) == 0 # Custom foods should be moved to the new meal
|
||||
|
||||
|
||||
class TestTrackerAddFood:
|
||||
"""Test adding a single food directly to the tracker"""
|
||||
|
||||
def test_add_food_to_tracker(self, client, sample_food, db_session):
|
||||
"""Test POST /tracker/add_food"""
|
||||
|
||||
# Create a tracked day
|
||||
tracked_day = TrackedDay(person="Sarah", date=date.today(), is_modified=False)
|
||||
db_session.add(tracked_day)
|
||||
db_session.commit()
|
||||
db_session.refresh(tracked_day)
|
||||
|
||||
# Add food directly to tracker
|
||||
response = client.post("/tracker/add_food", json={
|
||||
"person": "Sarah",
|
||||
"date": date.today().isoformat(),
|
||||
"food_id": sample_food.id,
|
||||
"quantity": 100.0,
|
||||
"meal_time": "Snack 1"
|
||||
})
|
||||
assert response.status_code == 200
|
||||
data = response.json()
|
||||
assert data["status"] == "success"
|
||||
|
||||
# Verify that a new tracked meal was created with the food
|
||||
tracked_meals = db_session.query(TrackedMeal).filter(
|
||||
TrackedMeal.tracked_day_id == tracked_day.id,
|
||||
TrackedMeal.meal_time == "Snack 1"
|
||||
).all()
|
||||
assert len(tracked_meals) == 1
|
||||
|
||||
tracked_meal = tracked_meals[0]
|
||||
assert tracked_meal.meal.name == sample_food.name # The meal name should be the food name
|
||||
assert tracked_meal.quantity == 1.0 # The meal quantity should be 1.0
|
||||
|
||||
# Verify the food is in the tracked meal's foods
|
||||
assert len(tracked_meal.meal.meal_foods) == 1
|
||||
assert tracked_meal.meal.meal_foods[0].food_id == sample_food.id
|
||||
assert tracked_meal.meal.meal_foods[0].quantity == 100.0
|
||||
|
||||
Reference in New Issue
Block a user