mirror of
https://github.com/sstent/foodplanner.git
synced 2025-12-06 08:01:47 +00:00
added chart
This commit is contained in:
49
app/api/routes/charts.py
Normal file
49
app/api/routes/charts.py
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
from fastapi import APIRouter, Depends, Query
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
from datetime import date, timedelta
|
||||||
|
from typing import List
|
||||||
|
from app.database import get_db, TrackedDay, TrackedMeal, calculate_day_nutrition_tracked
|
||||||
|
|
||||||
|
router = APIRouter(tags=["charts"])
|
||||||
|
|
||||||
|
@router.get("/charts", response_class=HTMLResponse)
|
||||||
|
async def charts_page(request: Request, person: str = "Sarah", db: Session = Depends(get_db)):
|
||||||
|
"""Render the charts page"""
|
||||||
|
from main import templates
|
||||||
|
return templates.TemplateResponse("charts.html", {
|
||||||
|
"request": request,
|
||||||
|
"person": person
|
||||||
|
})
|
||||||
|
|
||||||
|
@router.get("/api/charts", response_model=List[dict])
|
||||||
|
async def get_charts_data(
|
||||||
|
person: str = Query(..., description="Person name (e.g., Sarah)"),
|
||||||
|
days: int = Query(7, description="Number of past days to fetch data for", ge=1, le=30),
|
||||||
|
db: Session = Depends(get_db)
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Get daily calorie data for the last N days for a person.
|
||||||
|
Returns list of {"date": "YYYY-MM-DD", "calories": float} sorted by date descending.
|
||||||
|
"""
|
||||||
|
|
||||||
|
end_date = date.today()
|
||||||
|
start_date = end_date - timedelta(days=days - 1)
|
||||||
|
|
||||||
|
tracked_days = db.query(TrackedDay).filter(
|
||||||
|
TrackedDay.person == person,
|
||||||
|
TrackedDay.date >= start_date,
|
||||||
|
TrackedDay.date <= end_date
|
||||||
|
).order_by(TrackedDay.date.desc()).all()
|
||||||
|
|
||||||
|
chart_data = []
|
||||||
|
for tracked_day in tracked_days:
|
||||||
|
tracked_meals = db.query(TrackedMeal).filter(
|
||||||
|
TrackedMeal.tracked_day_id == tracked_day.id
|
||||||
|
).all()
|
||||||
|
day_totals = calculate_day_nutrition_tracked(tracked_meals, db)
|
||||||
|
chart_data.append({
|
||||||
|
"date": tracked_day.date.isoformat(),
|
||||||
|
"calories": round(day_totals.get("calories", 0), 2)
|
||||||
|
})
|
||||||
|
|
||||||
|
return chart_data
|
||||||
3
main.py
3
main.py
@@ -50,7 +50,7 @@ async def lifespan(app: FastAPI):
|
|||||||
app = FastAPI(title="Meal Planner", lifespan=lifespan)
|
app = FastAPI(title="Meal Planner", lifespan=lifespan)
|
||||||
templates = Jinja2Templates(directory="templates")
|
templates = Jinja2Templates(directory="templates")
|
||||||
|
|
||||||
from app.api.routes import foods, meals, plans, templates as templates_router, weekly_menu, tracker, admin, export
|
from app.api.routes import foods, meals, plans, templates as templates_router, weekly_menu, tracker, admin, export, charts
|
||||||
|
|
||||||
app.include_router(foods.router, tags=["foods"])
|
app.include_router(foods.router, tags=["foods"])
|
||||||
app.include_router(meals.router, tags=["meals"])
|
app.include_router(meals.router, tags=["meals"])
|
||||||
@@ -60,6 +60,7 @@ app.include_router(weekly_menu.router, tags=["weekly_menu"])
|
|||||||
app.include_router(tracker.router, tags=["tracker"])
|
app.include_router(tracker.router, tags=["tracker"])
|
||||||
app.include_router(admin.router, tags=["admin"])
|
app.include_router(admin.router, tags=["admin"])
|
||||||
app.include_router(export.router, tags=["export"])
|
app.include_router(export.router, tags=["export"])
|
||||||
|
app.include_router(charts.router, tags=["charts"])
|
||||||
|
|
||||||
# Add a logging middleware to see incoming requests
|
# Add a logging middleware to see incoming requests
|
||||||
@app.middleware("http")
|
@app.middleware("http")
|
||||||
|
|||||||
@@ -88,6 +88,11 @@
|
|||||||
<i class="bi bi-calendar-check"></i> Tracker
|
<i class="bi bi-calendar-check"></i> Tracker
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
|
<li class="nav-item" role="presentation">
|
||||||
|
<button class="nav-link" onclick="location.href='/charts'">
|
||||||
|
<i class="bi bi-graph-up"></i> Charts
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
<li class="nav-item" role="presentation">
|
<li class="nav-item" role="presentation">
|
||||||
<a class="nav-link" href="/admin">
|
<a class="nav-link" href="/admin">
|
||||||
<i class="bi bi-gear"></i> Admin
|
<i class="bi bi-gear"></i> Admin
|
||||||
|
|||||||
109
templates/charts.html
Normal file
109
templates/charts.html
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block title %}Daily Calories Chart{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container mt-4">
|
||||||
|
<h2><i class="bi bi-graph-up"></i> Daily Calories Chart</h2>
|
||||||
|
|
||||||
|
<div class="row mb-4">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<label for="daysSelect" class="form-label">Select Time Period:</label>
|
||||||
|
<select class="form-select" id="daysSelect">
|
||||||
|
<option value="7">Past 7 Days</option>
|
||||||
|
<option value="30">Past 30 Days</option>
|
||||||
|
</select>
|
||||||
|
<button class="btn btn-primary mt-2" id="loadChartBtn">Load Chart</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-12">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="mb-0">Calories Consumed</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<canvas id="caloriesChart" width="400" height="200"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||||
|
<script>
|
||||||
|
let chart;
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const daysSelect = document.getElementById('daysSelect');
|
||||||
|
const loadBtn = document.getElementById('loadChartBtn');
|
||||||
|
const ctx = document.getElementById('caloriesChart').getContext('2d');
|
||||||
|
|
||||||
|
// Default load for 7 days
|
||||||
|
loadChart(7);
|
||||||
|
|
||||||
|
loadBtn.addEventListener('click', function() {
|
||||||
|
const selectedDays = parseInt(daysSelect.value);
|
||||||
|
loadChart(selectedDays);
|
||||||
|
});
|
||||||
|
|
||||||
|
function loadChart(days) {
|
||||||
|
fetch(`/api/charts?person=Sarah&days=${days}`)
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
// Sort data by date ascending for chart
|
||||||
|
data.sort((a, b) => new Date(a.date) - new Date(b.date));
|
||||||
|
|
||||||
|
const labels = data.map(item => item.date);
|
||||||
|
const calories = data.map(item => item.calories);
|
||||||
|
|
||||||
|
if (chart) {
|
||||||
|
chart.destroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
chart = new Chart(ctx, {
|
||||||
|
type: 'line',
|
||||||
|
data: {
|
||||||
|
labels: labels,
|
||||||
|
datasets: [{
|
||||||
|
label: 'Calories',
|
||||||
|
data: calories,
|
||||||
|
borderColor: 'rgb(75, 192, 192)',
|
||||||
|
backgroundColor: 'rgba(75, 192, 192, 0.2)',
|
||||||
|
tension: 0.1,
|
||||||
|
fill: true
|
||||||
|
}]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
scales: {
|
||||||
|
y: {
|
||||||
|
beginAtZero: true,
|
||||||
|
title: {
|
||||||
|
display: true,
|
||||||
|
text: 'Calories'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
x: {
|
||||||
|
title: {
|
||||||
|
display: true,
|
||||||
|
text: 'Date'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error loading chart data:', error);
|
||||||
|
alert('Failed to load chart data');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
113
tests/test_charts.py
Normal file
113
tests/test_charts.py
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
import pytest
|
||||||
|
from datetime import date, timedelta
|
||||||
|
from app.database import TrackedDay, TrackedMeal, Meal, MealFood, Food, calculate_day_nutrition_tracked
|
||||||
|
from sqlalchemy import create_engine
|
||||||
|
from sqlalchemy.orm import sessionmaker
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def sample_chart_data(db_session):
|
||||||
|
"""Create sample tracked data for chart testing"""
|
||||||
|
# Create sample food
|
||||||
|
food = Food(
|
||||||
|
name="Sample Food",
|
||||||
|
serving_size="100g",
|
||||||
|
serving_unit="g",
|
||||||
|
calories=100.0,
|
||||||
|
protein=10.0,
|
||||||
|
carbs=20.0,
|
||||||
|
fat=5.0
|
||||||
|
)
|
||||||
|
db_session.add(food)
|
||||||
|
db_session.commit()
|
||||||
|
db_session.refresh(food)
|
||||||
|
|
||||||
|
# Create sample meal
|
||||||
|
meal = Meal(
|
||||||
|
name="Sample Meal",
|
||||||
|
meal_type="breakfast",
|
||||||
|
meal_time="Breakfast"
|
||||||
|
)
|
||||||
|
db_session.add(meal)
|
||||||
|
db_session.commit()
|
||||||
|
db_session.refresh(meal)
|
||||||
|
|
||||||
|
# Link meal to food
|
||||||
|
meal_food = MealFood(
|
||||||
|
meal_id=meal.id,
|
||||||
|
food_id=food.id,
|
||||||
|
quantity=1.0
|
||||||
|
)
|
||||||
|
db_session.add(meal_food)
|
||||||
|
db_session.commit()
|
||||||
|
|
||||||
|
# Create tracked days
|
||||||
|
person = "Sarah"
|
||||||
|
today = date.today()
|
||||||
|
tracked_days = []
|
||||||
|
|
||||||
|
for i in range(3): # Last 3 days
|
||||||
|
tracked_date = today - timedelta(days=i)
|
||||||
|
tracked_day = TrackedDay(
|
||||||
|
person=person,
|
||||||
|
date=tracked_date,
|
||||||
|
is_modified=False
|
||||||
|
)
|
||||||
|
db_session.add(tracked_day)
|
||||||
|
db_session.commit()
|
||||||
|
db_session.refresh(tracked_day)
|
||||||
|
|
||||||
|
# Add a tracked meal
|
||||||
|
tracked_meal = TrackedMeal(
|
||||||
|
tracked_day_id=tracked_day.id,
|
||||||
|
meal_id=meal.id,
|
||||||
|
meal_time="Breakfast",
|
||||||
|
quantity=1.0
|
||||||
|
)
|
||||||
|
db_session.add(tracked_meal)
|
||||||
|
db_session.commit()
|
||||||
|
|
||||||
|
tracked_days.append(tracked_day)
|
||||||
|
|
||||||
|
return tracked_days, meal
|
||||||
|
|
||||||
|
def test_get_charts_data(client, db_session, sample_chart_data):
|
||||||
|
"""Test the charts data endpoint returns correct calorie data"""
|
||||||
|
tracked_days, meal = sample_chart_data
|
||||||
|
|
||||||
|
# Expected calories: 100 per day
|
||||||
|
expected_data = [
|
||||||
|
{
|
||||||
|
"date": tracked_days[0].date.isoformat(),
|
||||||
|
"calories": 100.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": tracked_days[1].date.isoformat(),
|
||||||
|
"calories": 100.0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": tracked_days[2].date.isoformat(),
|
||||||
|
"calories": 100.0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
response = client.get("/api/charts?person=Sarah&days=3")
|
||||||
|
assert response.status_code == 200
|
||||||
|
data = response.json()
|
||||||
|
|
||||||
|
# Sort by date descending
|
||||||
|
data_sorted = sorted(data, key=lambda x: x["date"], reverse=True)
|
||||||
|
assert data_sorted == expected_data
|
||||||
|
|
||||||
|
def test_get_charts_data_default_days(client, db_session, sample_chart_data):
|
||||||
|
"""Test default days parameter"""
|
||||||
|
response = client.get("/api/charts?person=Sarah")
|
||||||
|
assert response.status_code == 200
|
||||||
|
data = response.json()
|
||||||
|
assert len(data) == 3 # Should return last 3 days
|
||||||
|
|
||||||
|
def test_get_charts_data_no_data(client, db_session):
|
||||||
|
"""Test endpoint when no tracked data exists"""
|
||||||
|
response = client.get("/api/charts?person=Sarah&days=7")
|
||||||
|
assert response.status_code == 200
|
||||||
|
data = response.json()
|
||||||
|
assert data == [] # Empty list if no data
|
||||||
Reference in New Issue
Block a user