adding macro details to tracker, changing charts to stacked bar chart of macros

This commit is contained in:
2026-01-06 06:49:43 -08:00
parent e91611d441
commit 70b45ede71
5 changed files with 339 additions and 106 deletions

View File

@@ -44,7 +44,10 @@ async def get_charts_data(
day_totals = calculate_day_nutrition_tracked(tracked_meals, db) day_totals = calculate_day_nutrition_tracked(tracked_meals, db)
chart_data.append({ chart_data.append({
"date": tracked_day.date.isoformat(), "date": tracked_day.date.isoformat(),
"calories": round(day_totals.get("calories", 0), 2) "calories": round(day_totals.get("calories", 0), 2),
"protein": round(day_totals.get("protein", 0), 2),
"fat": round(day_totals.get("fat", 0), 2),
"net_carbs": round(day_totals.get("net_carbs", 0), 2)
}) })
return chart_data return chart_data

View File

@@ -3,9 +3,10 @@ from fastapi.responses import HTMLResponse, RedirectResponse
from sqlalchemy.orm import Session, joinedload from sqlalchemy.orm import Session, joinedload
from datetime import date, datetime, timedelta from datetime import date, datetime, timedelta
from typing import List, Optional, Union from typing import List, Optional, Union
import logging
# Import from the database module # Import from the database module
from app.database import get_db, Meal, Template, TemplateMeal, TrackedDay, TrackedMeal, calculate_meal_nutrition, MealFood, TrackedMealFood, Food, calculate_day_nutrition_tracked from app.database import get_db, Meal, Template, TemplateMeal, TrackedDay, TrackedMeal, calculate_meal_nutrition, MealFood, TrackedMealFood, Food, calculate_day_nutrition_tracked, Plan
from main import templates from main import templates
router = APIRouter() router = APIRouter()
@@ -38,6 +39,29 @@ async def tracker_page(request: Request, person: str = "Sarah", date: str = None
db.add(tracked_day) db.add(tracked_day)
db.commit() db.commit()
db.refresh(tracked_day) db.refresh(tracked_day)
# Check if we need to sync from Plan (if no tracked meals exist)
existing_meals_count = db.query(TrackedMeal).filter(
TrackedMeal.tracked_day_id == tracked_day.id
).count()
if existing_meals_count == 0:
# Look for planned meals
planned_meals = db.query(Plan).filter(
Plan.person == person,
Plan.date == current_date
).all()
if planned_meals:
logging.info(f"Syncing {len(planned_meals)} planned meals to tracker for {person} on {current_date}")
for plan in planned_meals:
tracked_meal = TrackedMeal(
tracked_day_id=tracked_day.id,
meal_id=plan.meal_id,
meal_time=plan.meal_time
)
db.add(tracked_meal)
db.commit()
# Get tracked meals for this day with eager loading of meal foods # Get tracked meals for this day with eager loading of meal foods
tracked_meals = db.query(TrackedMeal).options( tracked_meals = db.query(TrackedMeal).options(

View File

@@ -5,7 +5,7 @@
{% block content %} {% block content %}
<div class="container mt-4 d-flex flex-column min-vh-100"> <div class="container mt-4 d-flex flex-column min-vh-100">
<h2><i class="bi bi-graph-up"></i> Daily Calories Chart</h2> <h2><i class="bi bi-graph-up"></i> Daily Calories Chart</h2>
<div class="row mb-4"> <div class="row mb-4">
<div class="col-md-6"> <div class="col-md-6">
<div class="card"> <div class="card">
@@ -22,7 +22,7 @@
</div> </div>
</div> </div>
</div> </div>
<div class="row flex-grow-1"> <div class="row flex-grow-1">
<div class="col-md-12 h-100"> <div class="col-md-12 h-100">
<div class="card h-100"> <div class="card h-100">
@@ -40,8 +40,10 @@
</div> </div>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script> <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chartjs-plugin-datalabels@2.0.0"></script>
<script> <script>
let chart; let chart;
const MACRO_KEYS = ['net_carbs', 'fat', 'protein'];
function resizeChart() { function resizeChart() {
const container = document.getElementById('chartContainer'); const container = document.getElementById('chartContainer');
@@ -51,7 +53,12 @@
} }
} }
document.addEventListener('DOMContentLoaded', function() { document.addEventListener('DOMContentLoaded', function () {
// Register the plugin
if (typeof ChartDataLabels !== 'undefined') {
Chart.register(ChartDataLabels);
}
const daysSelect = document.getElementById('daysSelect'); const daysSelect = document.getElementById('daysSelect');
const loadBtn = document.getElementById('loadChartBtn'); const loadBtn = document.getElementById('loadChartBtn');
const ctx = document.getElementById('caloriesChart').getContext('2d'); const ctx = document.getElementById('caloriesChart').getContext('2d');
@@ -63,7 +70,7 @@
// Default load for 7 days // Default load for 7 days
loadChart(7); loadChart(7);
loadBtn.addEventListener('click', function() { loadBtn.addEventListener('click', function () {
const selectedDays = parseInt(daysSelect.value); const selectedDays = parseInt(daysSelect.value);
loadChart(selectedDays); loadChart(selectedDays);
}); });
@@ -76,7 +83,14 @@
data.sort((a, b) => new Date(a.date) - new Date(b.date)); data.sort((a, b) => new Date(a.date) - new Date(b.date));
const labels = data.map(item => item.date); const labels = data.map(item => item.date);
const calories = data.map(item => item.calories);
// Calculate calories from macros
// Protein: 4 cals/g
// Fat: 9 cals/g
// Net Carbs: 4 cals/g
const proteinCals = data.map(item => item.protein * 4);
const fatCals = data.map(item => item.fat * 9);
const netCarbsCals = data.map(item => item.net_carbs * 4);
if (chart) { if (chart) {
chart.destroy(); chart.destroy();
@@ -86,17 +100,32 @@
resizeChart(); resizeChart();
chart = new Chart(ctx, { chart = new Chart(ctx, {
type: 'line', type: 'bar', // Switch to bar chart
data: { data: {
labels: labels, labels: labels,
datasets: [{ datasets: [
label: 'Calories', {
data: calories, label: 'Net Carbs',
borderColor: 'rgb(75, 192, 192)', data: netCarbsCals,
backgroundColor: 'rgba(75, 192, 192, 0.2)', backgroundColor: 'rgba(255, 193, 7, 0.8)', // Bootstrap warning (Yellow)
tension: 0.1, borderColor: '#ffc107',
fill: true borderWidth: 1
}] },
{
label: 'Fat',
data: fatCals,
backgroundColor: 'rgba(220, 53, 69, 0.8)', // Bootstrap danger (Red)
borderColor: '#dc3545',
borderWidth: 1
},
{
label: 'Protein',
data: proteinCals,
backgroundColor: 'rgba(25, 135, 84, 0.8)', // Bootstrap success (Green)
borderColor: '#198754',
borderWidth: 1
}
]
}, },
options: { options: {
responsive: true, responsive: true,
@@ -104,17 +133,69 @@
scales: { scales: {
y: { y: {
beginAtZero: true, beginAtZero: true,
stacked: true, // Enable stacking for Y axis
title: { title: {
display: true, display: true,
text: 'Calories' text: 'Calories'
} }
}, },
x: { x: {
stacked: true, // Enable stacking for X axis
title: { title: {
display: true, display: true,
text: 'Date' text: 'Date'
} }
} }
},
plugins: {
tooltip: {
callbacks: {
label: function (context) {
let label = context.dataset.label || '';
if (label) {
label += ': ';
}
if (context.parsed.y !== null) {
const dayData = data[context.dataIndex];
const macroKey = MACRO_KEYS[context.datasetIndex];
const grams = dayData[macroKey];
label += Math.round(context.parsed.y) + ' cals (' + Math.round(grams) + 'g)';
}
return label;
}
}
},
datalabels: {
color: 'white',
font: {
weight: 'bold',
size: 11
},
display: function (context) {
const dayData = data[context.dataIndex];
const pC = dayData.protein * 4;
const fC = dayData.fat * 9;
const ncC = dayData.net_carbs * 4;
const calcTotal = pC + fC + ncC;
const value = context.dataset.data[context.dataIndex];
return calcTotal > 0 && (value / calcTotal) > 0.05;
},
formatter: function (value, context) {
const dayData = data[context.dataIndex];
const pC = dayData.protein * 4;
const fC = dayData.fat * 9;
const ncC = dayData.net_carbs * 4;
const calcTotal = pC + fC + ncC;
const totalCals = calcTotal || 1;
const percent = Math.round((value / totalCals) * 100);
const macroKey = MACRO_KEYS[context.datasetIndex];
const grams = Math.round(dayData[macroKey]);
return grams + 'g\n' + percent + '%';
}
}
} }
} }
}); });

View File

@@ -4,7 +4,8 @@
<div class="col-md-8"> <div class="col-md-8">
<!-- Date Navigation --> <!-- Date Navigation -->
<div class="d-flex justify-content-between align-items-center mb-4"> <div class="d-flex justify-content-between align-items-center mb-4">
<button class="btn btn-outline-secondary" onclick="navigateDate('{{ prev_date }}')" data-testid="navigate-yesterday"> <button class="btn btn-outline-secondary" onclick="navigateDate('{{ prev_date }}')"
data-testid="navigate-yesterday">
<i class="bi bi-chevron-left"></i> Yesterday <i class="bi bi-chevron-left"></i> Yesterday
</button> </button>
<div class="text-center"> <div class="text-center">
@@ -15,7 +16,8 @@
<span class="badge bg-success">As Planned</span> <span class="badge bg-success">As Planned</span>
{% endif %} {% endif %}
</div> </div>
<button class="btn btn-outline-secondary" onclick="navigateDate('{{ next_date }}')" data-testid="navigate-tomorrow"> <button class="btn btn-outline-secondary" onclick="navigateDate('{{ next_date }}')"
data-testid="navigate-tomorrow">
Tomorrow <i class="bi bi-chevron-right"></i> Tomorrow <i class="bi bi-chevron-right"></i>
</button> </button>
</div> </div>
@@ -45,10 +47,12 @@
<div class="card-header d-flex justify-content-between align-items-center"> <div class="card-header d-flex justify-content-between align-items-center">
<h5 class="mb-0">{{ meal_time }}</h5> <h5 class="mb-0">{{ meal_time }}</h5>
<div class="d-flex gap-2"> <div class="d-flex gap-2">
<button class="btn btn-sm btn-outline-success" onclick="addMealToTime('{{ meal_time }}')" data-testid="add-meal-{{ meal_time|slugify }}"> <button class="btn btn-sm btn-outline-success" onclick="addMealToTime('{{ meal_time }}')"
data-testid="add-meal-{{ meal_time|slugify }}">
<i class="bi bi-plus"></i> Add Meal <i class="bi bi-plus"></i> Add Meal
</button> </button>
<button class="btn btn-sm btn-info text-white" onclick="addSingleFoodToTime('{{ meal_time }}')" data-testid="add-food-{{ meal_time|slugify }}"> <button class="btn btn-sm btn-info text-white" onclick="addSingleFoodToTime('{{ meal_time }}')"
data-testid="add-food-{{ meal_time|slugify }}">
<i class="bi bi-plus-circle"></i> Add Food <i class="bi bi-plus-circle"></i> Add Food
</button> </button>
</div> </div>
@@ -57,89 +61,166 @@
<div id="meals-{{ meal_time|slugify }}"> <div id="meals-{{ meal_time|slugify }}">
{% set meals_for_time = [] %} {% set meals_for_time = [] %}
{% for tracked_meal in tracked_meals %} {% for tracked_meal in tracked_meals %}
{% if tracked_meal.meal_time == meal_time %} {% if tracked_meal.meal_time == meal_time %}
{% set _ = meals_for_time.append(tracked_meal) %} {% set _ = meals_for_time.append(tracked_meal) %}
{% endif %} {% endif %}
{% endfor %} {% endfor %}
{% if meals_for_time %} {% if meals_for_time %}
{% for tracked_meal in meals_for_time %} {% for tracked_meal in meals_for_time %}
{# 1. Create stable slugs #} {# 1. Create stable slugs #}
{% set meal_time_slug = meal_time|slugify %} {% set meal_time_slug = meal_time|slugify %}
{% set meal_name_safe = tracked_meal.meal.name|slugify %} {% set meal_name_safe = tracked_meal.meal.name|slugify %}
{# 2. Construct the core Unique Meal ID for non-ambiguous locating #}
{% set unique_meal_id = meal_time_slug + '-' + meal_name_safe + '-' + loop.index|string %}
<div class="mb-3 p-3 bg-light rounded" data-testid="meal-card-{{ unique_meal_id }}">
<div class="d-flex justify-content-between align-items-center mb-2">
<div>
<strong data-testid="meal-name-{{ unique_meal_id }}">{{ tracked_meal.meal.name }}</strong>
</div>
<div>
<button class="btn btn-sm btn-outline-secondary me-1" onclick="editTrackedMeal('{{ tracked_meal.id }}')" title="Edit Meal" data-testid="edit-meal-{{ unique_meal_id }}">
<i class="bi bi-pencil"></i>
</button>
<button class="btn btn-sm btn-outline-danger" onclick="removeMeal('{{ tracked_meal.id }}')" data-testid="delete-meal-{{ unique_meal_id }}">
<i class="bi bi-trash"></i>
</button>
</div>
</div>
<!-- Food Breakdown -->
<div class="ms-3">
<div class="row row-cols-1 row-cols-sm-2">
{% set overrides = {} %}
{% set all_override_ids = [] %}
{% set deleted_food_ids = [] %}
{% for tmf in tracked_meal.tracked_foods %}
{% if not tmf.is_deleted %}
{% set _ = overrides.update({tmf.food_id: tmf}) %}
{% else %}
{% set _ = deleted_food_ids.append(tmf.food_id) %}
{% endif %}
{% set _ = all_override_ids.append(tmf.food_id) %}
{% endfor %}
{% set displayed_food_ids = [] %} {# 2. Construct the core Unique Meal ID for non-ambiguous locating #}
{% set unique_meal_id = meal_time_slug + '-' + meal_name_safe + '-' + loop.index|string %}
{# Display base meal foods, applying overrides #} <div class="mb-3 p-3 bg-light rounded" data-testid="meal-card-{{ unique_meal_id }}">
{% for meal_food in tracked_meal.meal.meal_foods %} <div class="d-flex justify-content-between align-items-center mb-2">
{# Only show base meal food if it's not deleted and there's no active override for it #} <div>
{% if meal_food.food_id not in deleted_food_ids and meal_food.food_id not in overrides.keys() %} <strong data-testid="meal-name-{{ unique_meal_id }}">{{ tracked_meal.meal.name
<div class="col"> }}</strong>
{% set food_name_safe = meal_food.food.name|slugify %} </div>
<div class="d-flex justify-content-between small text-muted" data-testid="food-display-{{ unique_meal_id }}-{{ food_name_safe }}-{{ loop.index }}"> <div>
<span>• {{ meal_food.food.name }}</span> <button class="btn btn-sm btn-outline-secondary me-1"
<span class="text-end">{{ meal_food.quantity }} {{ meal_food.food.serving_unit }}</span> onclick="editTrackedMeal('{{ tracked_meal.id }}')" title="Edit Meal"
</div> data-testid="edit-meal-{{ unique_meal_id }}">
</div> <i class="bi bi-pencil"></i>
{% else %} </button>
<!-- 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() }} --> <button class="btn btn-sm btn-outline-danger"
{% endif %} onclick="removeMeal('{{ tracked_meal.id }}')"
{% set _ = displayed_food_ids.append(meal_food.food_id) %} data-testid="delete-meal-{{ unique_meal_id }}">
{% endfor %} <i class="bi bi-trash"></i>
</button>
{# Display overridden and new foods #}
{% for food_id, tmf in overrides.items() %}
{% set food_name_safe = tmf.food.name|slugify %}
<div class="col">
<div class="d-flex justify-content-between small text-muted" data-testid="food-display-{{ unique_meal_id }}-{{ food_name_safe }}-{{ loop.index }}">
<span>• {{ tmf.food.name }}</span>
<span class="text-end">{{ tmf.quantity }} g</span>
</div>
</div>
{% endfor %}
</div>
{% if not tracked_meal.meal.meal_foods %}
<div class="col-12">
<div class="small text-muted">No foods in this meal</div>
</div>
{% endif %}
</div> </div>
</div> </div>
{% endfor %}
<!-- Food Breakdown -->
<div class="ms-3">
{% set overrides = {} %}
{% set all_override_ids = [] %}
{% set deleted_food_ids = [] %}
{% for tmf in tracked_meal.tracked_foods %}
{% if not tmf.is_deleted %}
{% set _ = overrides.update({tmf.food_id: tmf}) %}
{% else %}
{% set _ = deleted_food_ids.append(tmf.food_id) %}
{% endif %}
{% set _ = all_override_ids.append(tmf.food_id) %}
{% endfor %}
{% set meal_totals = {'carbs': 0, 'fiber': 0, 'fat': 0, 'protein': 0, 'calories': 0} %}
{% set state = {'has_foods': False} %}
<div class="table-responsive">
<table class="table table-sm table-striped small mb-0">
<thead>
<tr>
<th style="width: 40%">Food</th>
<th class="text-end">Carbs</th>
<th class="text-end">Net Carbs</th>
<th class="text-end">Fat</th>
<th class="text-end">Protein</th>
<th class="text-end">Cals</th>
</tr>
</thead>
<tbody>
{# Display base meal foods, applying overrides #}
{% for meal_food in tracked_meal.meal.meal_foods %}
{% if meal_food.food_id not in deleted_food_ids and meal_food.food_id not in
overrides.keys() %}
{% set _ = state.update({'has_foods': True}) %}
{% set food = meal_food.food %}
{% set qty = meal_food.quantity %}
{% set mult = qty / food.serving_size if food.serving_size > 0 else 0 %}
{% set row_carbs = (food.carbs or 0) * mult %}
{% set row_fiber = (food.fiber or 0) * mult %}
{% set row_fat = (food.fat or 0) * mult %}
{% set row_protein = (food.protein or 0) * mult %}
{% set row_cals = (food.calories or 0) * mult %}
{# Accumulate Totals #}
{% set _ = meal_totals.update({'carbs': meal_totals.carbs + row_carbs}) %}
{% set _ = meal_totals.update({'fiber': meal_totals.fiber + row_fiber}) %}
{% set _ = meal_totals.update({'fat': meal_totals.fat + row_fat}) %}
{% set _ = meal_totals.update({'protein': meal_totals.protein + row_protein}) %}
{% set _ = meal_totals.update({'calories': meal_totals.calories + row_cals}) %}
{% set food_name_safe = food.name|slugify %}
<tr data-testid="food-row-{{ unique_meal_id }}-{{ food_name_safe }}">
<td>
{{ food.name }}
<span class="text-muted ms-1">({{ qty|round(1) }} {{ food.serving_unit
}})</span>
</td>
<td class="text-end">{{ "%.1f"|format(row_carbs) }}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_protein) }}g</td>
<td class="text-end fw-bold">{{ "%.0f"|format(row_cals) }}</td>
</tr>
{% endif %}
{% endfor %}
{# Display overridden/new foods #}
{% for food_id, tmf in overrides.items() %}
{% set _ = state.update({'has_foods': True}) %}
{% set food = tmf.food %}
{% set qty = tmf.quantity %}
{# Overrides are always in grams #}
{% set mult = qty / food.serving_size if food.serving_size > 0 else 0 %}
{% set row_carbs = (food.carbs or 0) * mult %}
{% set row_fiber = (food.fiber or 0) * mult %}
{% set row_fat = (food.fat or 0) * mult %}
{% set row_protein = (food.protein or 0) * mult %}
{% set row_cals = (food.calories or 0) * mult %}
{# Accumulate Totals #}
{% set _ = meal_totals.update({'carbs': meal_totals.carbs + row_carbs}) %}
{% set _ = meal_totals.update({'fiber': meal_totals.fiber + row_fiber}) %}
{% set _ = meal_totals.update({'fat': meal_totals.fat + row_fat}) %}
{% set _ = meal_totals.update({'protein': meal_totals.protein + row_protein}) %}
{% set _ = meal_totals.update({'calories': meal_totals.calories + row_cals}) %}
{% set food_name_safe = food.name|slugify %}
<tr class="table-info"
data-testid="food-row-{{ unique_meal_id }}-{{ food_name_safe }}">
<td>
{{ food.name }} <i class="bi bi-pencil-fill x-small text-muted"
title="Custom Quantity"></i>
<span class="text-muted ms-1">({{ qty|round(1) }} g)</span>
</td>
<td class="text-end">{{ "%.1f"|format(row_carbs) }}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_protein) }}g</td>
<td class="text-end fw-bold">{{ "%.0f"|format(row_cals) }}</td>
</tr>
{% endfor %}
{% if not state.has_foods %}
<tr>
<td colspan="6" class="text-center text-muted">No foods in this meal</td>
</tr>
{% else %}
{# Summary Row #}
<tr class="table-secondary fw-bold">
<td>Total</td>
<td class="text-end">{{ "%.1f"|format(meal_totals.carbs) }}g</td>
<td class="text-end">{{ "%.1f"|format(meal_totals.carbs - meal_totals.fiber)
}}g</td>
<td class="text-end">{{ "%.1f"|format(meal_totals.fat) }}g</td>
<td class="text-end">{{ "%.1f"|format(meal_totals.protein) }}g</td>
<td class="text-end">{{ "%.0f"|format(meal_totals.calories) }}</td>
</tr>
{% endif %}
</tbody>
</table>
</div>
</div>
</div>
{% endfor %}
{% else %} {% else %}
<p class="text-muted mb-0">No meals tracked</p> <p class="text-muted mb-0">No meals tracked</p>
{% endif %} {% endif %}
</div> </div>
@@ -380,10 +461,10 @@
const response = await fetch(`/tracker/get_tracked_meal_foods/${trackedMealId}`); const response = await fetch(`/tracker/get_tracked_meal_foods/${trackedMealId}`);
const data = await response.json(); const data = await response.json();
console.log('Response from get_tracked_meal_foods:', data); console.log('Response from get_tracked_meal_foods:', data);
const container = document.getElementById('editMealFoodsList'); const container = document.getElementById('editMealFoodsList');
container.innerHTML = ''; container.innerHTML = '';
if (data.status === 'success') { if (data.status === 'success') {
if (data.meal_foods.length === 0) { if (data.meal_foods.length === 0) {
container.innerHTML = '<em>No foods added yet</em>'; container.innerHTML = '<em>No foods added yet</em>';
@@ -434,7 +515,7 @@
async function saveTrackedMeal() { async function saveTrackedMeal() {
const trackedMealId = document.getElementById('editTrackedMealId').value; const trackedMealId = document.getElementById('editTrackedMealId').value;
const inputs = document.querySelectorAll('#editMealFoodsList input[type="number"]'); const inputs = document.querySelectorAll('#editMealFoodsList input[type="number"]');
const foods = []; const foods = [];
inputs.forEach(input => { inputs.forEach(input => {
const foodData = { const foodData = {
@@ -453,7 +534,7 @@
}; };
console.log('Payload being sent to /tracker/update_tracked_meal_foods:', JSON.stringify(payload, null, 2)); console.log('Payload being sent to /tracker/update_tracked_meal_foods:', JSON.stringify(payload, null, 2));
try { try {
const response = await fetch('/tracker/update_tracked_meal_foods', { const response = await fetch('/tracker/update_tracked_meal_foods', {
method: 'POST', method: 'POST',
@@ -477,10 +558,10 @@
async function saveAsNewMeal() { async function saveAsNewMeal() {
const mealName = prompt('Enter name for new meal:'); const mealName = prompt('Enter name for new meal:');
if (!mealName) return; if (!mealName) return;
const trackedMealId = document.getElementById('editTrackedMealId').value; const trackedMealId = document.getElementById('editTrackedMealId').value;
const inputs = document.querySelectorAll('#editMealFoodsList input[type="number"]'); const inputs = document.querySelectorAll('#editMealFoodsList input[type="number"]');
const foods = []; const foods = [];
inputs.forEach(input => { inputs.forEach(input => {
foods.push({ foods.push({
@@ -488,7 +569,7 @@
quantity: parseFloat(input.value) // Quantity is now grams quantity: parseFloat(input.value) // Quantity is now grams
}); });
}); });
try { try {
const response = await fetch('/tracker/save_as_new_meal', { const response = await fetch('/tracker/save_as_new_meal', {
method: 'POST', method: 'POST',
@@ -499,9 +580,9 @@
foods: foods foods: foods
}) })
}); });
const result = await response.json(); const result = await response.json();
if (result.status === 'success') { if (result.status === 'success') {
bootstrap.Modal.getInstance(document.getElementById('editTrackedMealModal')).hide(); bootstrap.Modal.getInstance(document.getElementById('editTrackedMealModal')).hide();
alert('New meal saved successfully!'); alert('New meal saved successfully!');
@@ -545,7 +626,7 @@
alert('Error: ' + error.message); alert('Error: ' + error.message);
} }
} }
// Reset page (clear all meals and foods) // Reset page (clear all meals and foods)
async function resetPage() { async function resetPage() {
if (confirm('Are you sure you want to clear all meals and foods for this day? This action cannot be undone.')) { if (confirm('Are you sure you want to clear all meals and foods for this day? This action cannot be undone.')) {

44
tests/test_charts_api.py Normal file
View File

@@ -0,0 +1,44 @@
import pytest
from datetime import date, timedelta
from app.database import TrackedDay, TrackedMeal, TrackedMealFood, Meal, MealFood, Food
class TestChartsData:
"""Test charts data API"""
def test_charts_api_returns_macros(self, client, sample_meal, db_session):
"""Test that /api/charts returns protein, fat, and net_carbs"""
# Create a tracked day with data
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 meal
tracked_meal = TrackedMeal(
tracked_day_id=tracked_day.id,
meal_id=sample_meal.id,
meal_time="Breakfast"
)
db_session.add(tracked_meal)
db_session.commit()
# Fetch chart data
response = client.get("/api/charts?person=Sarah&days=7")
assert response.status_code == 200
data = response.json()
assert len(data) > 0
# Check fields
item = data[0]
assert "date" in item
assert "calories" in item
assert "protein" in item
assert "fat" in item
assert "net_carbs" in item
# Check values (protein > 0 based on sample_meal)
assert item["protein"] >= 0
assert item["fat"] >= 0
assert item["net_carbs"] >= 0