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)
|
||||
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(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(admin.router, tags=["admin"])
|
||||
app.include_router(export.router, tags=["export"])
|
||||
app.include_router(charts.router, tags=["charts"])
|
||||
|
||||
# Add a logging middleware to see incoming requests
|
||||
@app.middleware("http")
|
||||
|
||||
@@ -88,6 +88,11 @@
|
||||
<i class="bi bi-calendar-check"></i> Tracker
|
||||
</button>
|
||||
</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">
|
||||
<a class="nav-link" href="/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