feat: update app version to 0.9t and enhance nutrition visualization
- Bumped application version to 0.9t and updated changelog with new features. - Integrated new chart payloads for energy balance, protein adequacy, and nutrition adherence to optimize data retrieval and reduce HTTP requests. - Updated NutritionCharts component to utilize prefetched chart payloads, improving loading efficiency and user experience. - Refactored History page to pass chart payloads, enhancing the visualization of nutrition trends without additional requests.
This commit is contained in:
parent
da1e0410cc
commit
3f6673b636
404
backend/data_layer/nutrition_chart_payloads.py
Normal file
404
backend/data_layer/nutrition_chart_payloads.py
Normal file
|
|
@ -0,0 +1,404 @@
|
||||||
|
"""
|
||||||
|
Chart.js-kompatible Payloads für Ernährungs-Charts (E1, E2, E4).
|
||||||
|
|
||||||
|
Gleiche Logik wie ``routers/charts.py`` — hier zentral, damit ``nutrition_viz``
|
||||||
|
und die API dieselbe Berechnung nutzen (Phase C, Issue 53).
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from typing import Any, Dict
|
||||||
|
|
||||||
|
from db import get_db, get_cursor
|
||||||
|
from data_layer.nutrition_metrics import (
|
||||||
|
get_energy_balance_data,
|
||||||
|
get_protein_adequacy_data,
|
||||||
|
get_protein_targets_data,
|
||||||
|
)
|
||||||
|
from data_layer.utils import calculate_confidence, safe_float, serialize_dates
|
||||||
|
|
||||||
|
|
||||||
|
def build_energy_balance_chart_payload(profile_id: str, days: int) -> Dict[str, Any]:
|
||||||
|
"""E1 Energiebilanz — identisch zu GET /api/charts/energy-balance."""
|
||||||
|
balance_meta = get_energy_balance_data(profile_id, days)
|
||||||
|
|
||||||
|
with get_db() as conn:
|
||||||
|
cur = get_cursor(conn)
|
||||||
|
cutoff = (datetime.now() - timedelta(days=days)).strftime("%Y-%m-%d")
|
||||||
|
|
||||||
|
cur.execute(
|
||||||
|
"""SELECT date, SUM(kcal)::float AS kcal
|
||||||
|
FROM nutrition_log
|
||||||
|
WHERE profile_id=%s AND date >= %s AND kcal IS NOT NULL
|
||||||
|
GROUP BY date
|
||||||
|
ORDER BY date""",
|
||||||
|
(profile_id, cutoff),
|
||||||
|
)
|
||||||
|
rows = cur.fetchall()
|
||||||
|
|
||||||
|
if not rows or len(rows) < 3:
|
||||||
|
return {
|
||||||
|
"chart_type": "line",
|
||||||
|
"data": {"labels": [], "datasets": []},
|
||||||
|
"metadata": {
|
||||||
|
"confidence": "insufficient",
|
||||||
|
"data_points": len(rows) if rows else 0,
|
||||||
|
"message": "Nicht genug Ernährungsdaten (min. 3 Tage)",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
estimated_tdee = balance_meta.get("estimated_tdee") or 0
|
||||||
|
if estimated_tdee <= 0:
|
||||||
|
return {
|
||||||
|
"chart_type": "line",
|
||||||
|
"data": {"labels": [], "datasets": []},
|
||||||
|
"metadata": {
|
||||||
|
"confidence": "insufficient",
|
||||||
|
"data_points": len(rows),
|
||||||
|
"message": "Kein Gewicht für TDEE-Schätzung (weight_log erforderlich)",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
labels = []
|
||||||
|
daily_values = []
|
||||||
|
avg_7d = []
|
||||||
|
avg_14d = []
|
||||||
|
|
||||||
|
for i, row in enumerate(rows):
|
||||||
|
labels.append(row["date"].isoformat())
|
||||||
|
daily_values.append(safe_float(row["kcal"]))
|
||||||
|
|
||||||
|
start_7d = max(0, i - 6)
|
||||||
|
window_7d = [safe_float(rows[j]["kcal"]) for j in range(start_7d, i + 1)]
|
||||||
|
avg_7d.append(round(sum(window_7d) / len(window_7d), 1) if window_7d else None)
|
||||||
|
|
||||||
|
start_14d = max(0, i - 13)
|
||||||
|
window_14d = [safe_float(rows[j]["kcal"]) for j in range(start_14d, i + 1)]
|
||||||
|
avg_14d.append(round(sum(window_14d) / len(window_14d), 1) if window_14d else None)
|
||||||
|
|
||||||
|
avg_intake = float(
|
||||||
|
balance_meta.get("avg_intake")
|
||||||
|
or (sum(daily_values) / len(daily_values) if daily_values else 0)
|
||||||
|
)
|
||||||
|
energy_balance = float(
|
||||||
|
balance_meta.get("energy_balance") or (avg_intake - estimated_tdee)
|
||||||
|
)
|
||||||
|
balance_status = balance_meta.get("status") or (
|
||||||
|
"deficit"
|
||||||
|
if energy_balance < -200
|
||||||
|
else "surplus"
|
||||||
|
if energy_balance > 200
|
||||||
|
else "maintenance"
|
||||||
|
)
|
||||||
|
|
||||||
|
datasets = [
|
||||||
|
{
|
||||||
|
"label": "Kalorien (täglich)",
|
||||||
|
"data": daily_values,
|
||||||
|
"borderColor": "#1D9E7599",
|
||||||
|
"backgroundColor": "rgba(29, 158, 117, 0.1)",
|
||||||
|
"borderWidth": 1.5,
|
||||||
|
"tension": 0.2,
|
||||||
|
"fill": False,
|
||||||
|
"pointRadius": 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Ø 7 Tage",
|
||||||
|
"data": avg_7d,
|
||||||
|
"borderColor": "#1D9E75",
|
||||||
|
"borderWidth": 2.5,
|
||||||
|
"tension": 0.3,
|
||||||
|
"fill": False,
|
||||||
|
"pointRadius": 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Ø 14 Tage",
|
||||||
|
"data": avg_14d,
|
||||||
|
"borderColor": "#085041",
|
||||||
|
"borderWidth": 2,
|
||||||
|
"tension": 0.3,
|
||||||
|
"fill": False,
|
||||||
|
"pointRadius": 0,
|
||||||
|
"borderDash": [6, 3],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "TDEE (geschätzt)",
|
||||||
|
"data": [estimated_tdee] * len(labels),
|
||||||
|
"borderColor": "#888",
|
||||||
|
"borderWidth": 1,
|
||||||
|
"borderDash": [5, 5],
|
||||||
|
"fill": False,
|
||||||
|
"pointRadius": 0,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
confidence = balance_meta.get("confidence") or "low"
|
||||||
|
|
||||||
|
return {
|
||||||
|
"chart_type": "line",
|
||||||
|
"data": {"labels": labels, "datasets": datasets},
|
||||||
|
"metadata": serialize_dates(
|
||||||
|
{
|
||||||
|
"confidence": confidence,
|
||||||
|
"data_points": len(rows),
|
||||||
|
"avg_kcal": round(avg_intake, 1),
|
||||||
|
"estimated_tdee": estimated_tdee,
|
||||||
|
"energy_balance": round(energy_balance, 1),
|
||||||
|
"balance_status": balance_status,
|
||||||
|
"first_date": rows[0]["date"],
|
||||||
|
"last_date": rows[-1]["date"],
|
||||||
|
}
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def build_protein_adequacy_chart_payload(profile_id: str, days: int) -> Dict[str, Any]:
|
||||||
|
"""E2 Protein Adequacy — identisch zu GET /api/charts/protein-adequacy."""
|
||||||
|
targets = get_protein_targets_data(profile_id)
|
||||||
|
|
||||||
|
with get_db() as conn:
|
||||||
|
cur = get_cursor(conn)
|
||||||
|
cutoff = (datetime.now() - timedelta(days=days)).strftime("%Y-%m-%d")
|
||||||
|
|
||||||
|
cur.execute(
|
||||||
|
"""SELECT date, SUM(protein_g)::float AS protein_g
|
||||||
|
FROM nutrition_log
|
||||||
|
WHERE profile_id=%s AND date >= %s AND protein_g IS NOT NULL
|
||||||
|
GROUP BY date
|
||||||
|
ORDER BY date""",
|
||||||
|
(profile_id, cutoff),
|
||||||
|
)
|
||||||
|
rows = cur.fetchall()
|
||||||
|
|
||||||
|
if not rows or len(rows) < 3:
|
||||||
|
return {
|
||||||
|
"chart_type": "line",
|
||||||
|
"data": {"labels": [], "datasets": []},
|
||||||
|
"metadata": {
|
||||||
|
"confidence": "insufficient",
|
||||||
|
"data_points": len(rows) if rows else 0,
|
||||||
|
"message": "Nicht genug Protein-Daten (min. 3 Tage)",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
labels = []
|
||||||
|
daily_values = []
|
||||||
|
avg_7d = []
|
||||||
|
avg_28d = []
|
||||||
|
|
||||||
|
for i, row in enumerate(rows):
|
||||||
|
labels.append(row["date"].isoformat())
|
||||||
|
daily_values.append(safe_float(row["protein_g"]))
|
||||||
|
|
||||||
|
start_7d = max(0, i - 6)
|
||||||
|
window_7d = [safe_float(rows[j]["protein_g"]) for j in range(start_7d, i + 1)]
|
||||||
|
avg_7d.append(round(sum(window_7d) / len(window_7d), 1) if window_7d else None)
|
||||||
|
|
||||||
|
start_28d = max(0, i - 27)
|
||||||
|
window_28d = [safe_float(rows[j]["protein_g"]) for j in range(start_28d, i + 1)]
|
||||||
|
avg_28d.append(round(sum(window_28d) / len(window_28d), 1) if window_28d else None)
|
||||||
|
|
||||||
|
target_low = targets["protein_target_low"]
|
||||||
|
target_high = targets["protein_target_high"]
|
||||||
|
|
||||||
|
datasets = [
|
||||||
|
{
|
||||||
|
"label": "Protein (täglich)",
|
||||||
|
"data": daily_values,
|
||||||
|
"borderColor": "#1D9E7599",
|
||||||
|
"backgroundColor": "rgba(29, 158, 117, 0.1)",
|
||||||
|
"borderWidth": 1.5,
|
||||||
|
"tension": 0.2,
|
||||||
|
"fill": False,
|
||||||
|
"pointRadius": 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Ø 7 Tage",
|
||||||
|
"data": avg_7d,
|
||||||
|
"borderColor": "#1D9E75",
|
||||||
|
"borderWidth": 2.5,
|
||||||
|
"tension": 0.3,
|
||||||
|
"fill": False,
|
||||||
|
"pointRadius": 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Ø 28 Tage",
|
||||||
|
"data": avg_28d,
|
||||||
|
"borderColor": "#085041",
|
||||||
|
"borderWidth": 2,
|
||||||
|
"tension": 0.3,
|
||||||
|
"fill": False,
|
||||||
|
"pointRadius": 0,
|
||||||
|
"borderDash": [6, 3],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"label": "Ziel Min",
|
||||||
|
"data": [target_low] * len(labels),
|
||||||
|
"borderColor": "#888",
|
||||||
|
"borderWidth": 1,
|
||||||
|
"borderDash": [5, 5],
|
||||||
|
"fill": False,
|
||||||
|
"pointRadius": 0,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
datasets.append(
|
||||||
|
{
|
||||||
|
"label": "Ziel Max",
|
||||||
|
"data": [target_high] * len(labels),
|
||||||
|
"borderColor": "#888",
|
||||||
|
"borderWidth": 1,
|
||||||
|
"borderDash": [5, 5],
|
||||||
|
"fill": False,
|
||||||
|
"pointRadius": 0,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
confidence = calculate_confidence(len(rows), days, "general")
|
||||||
|
|
||||||
|
days_in_target = sum(1 for v in daily_values if target_low <= v <= target_high)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"chart_type": "line",
|
||||||
|
"data": {"labels": labels, "datasets": datasets},
|
||||||
|
"metadata": serialize_dates(
|
||||||
|
{
|
||||||
|
"confidence": confidence,
|
||||||
|
"data_points": len(rows),
|
||||||
|
"target_low": round(target_low, 1),
|
||||||
|
"target_high": round(target_high, 1),
|
||||||
|
"days_in_target": days_in_target,
|
||||||
|
"target_compliance_pct": round(
|
||||||
|
days_in_target / len(daily_values) * 100, 1
|
||||||
|
)
|
||||||
|
if daily_values
|
||||||
|
else 0,
|
||||||
|
"first_date": rows[0]["date"],
|
||||||
|
"last_date": rows[-1]["date"],
|
||||||
|
}
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def build_nutrition_adherence_score_payload(profile_id: str, days: int) -> Dict[str, Any]:
|
||||||
|
"""E4 Adhärenz — identisch zu GET /api/charts/nutrition-adherence-score."""
|
||||||
|
with get_db() as conn:
|
||||||
|
cur = get_cursor(conn)
|
||||||
|
cur.execute("SELECT goal_mode FROM profiles WHERE id = %s", (profile_id,))
|
||||||
|
profile_row = cur.fetchone()
|
||||||
|
goal_mode = (
|
||||||
|
profile_row["goal_mode"]
|
||||||
|
if profile_row and profile_row["goal_mode"]
|
||||||
|
else "health"
|
||||||
|
)
|
||||||
|
|
||||||
|
cutoff = (datetime.now() - timedelta(days=days)).strftime("%Y-%m-%d")
|
||||||
|
|
||||||
|
cur.execute(
|
||||||
|
"""WITH daily AS (
|
||||||
|
SELECT date,
|
||||||
|
COALESCE(SUM(kcal), 0)::float AS dk,
|
||||||
|
COALESCE(SUM(protein_g), 0)::float AS dp,
|
||||||
|
COALESCE(SUM(carbs_g), 0)::float AS dc,
|
||||||
|
COALESCE(SUM(fat_g), 0)::float AS df FROM nutrition_log
|
||||||
|
WHERE profile_id=%s AND date >= %s AND kcal IS NOT NULL
|
||||||
|
GROUP BY date
|
||||||
|
)
|
||||||
|
SELECT COUNT(*)::int AS cnt,
|
||||||
|
AVG(dk) AS avg_kcal,
|
||||||
|
STDDEV(dk) AS std_kcal,
|
||||||
|
AVG(dp) AS avg_protein,
|
||||||
|
AVG(dc) AS avg_carbs,
|
||||||
|
AVG(df) AS avg_fat
|
||||||
|
FROM daily""",
|
||||||
|
(profile_id, cutoff),
|
||||||
|
)
|
||||||
|
stats = cur.fetchone()
|
||||||
|
|
||||||
|
if not stats or stats["cnt"] < 7:
|
||||||
|
return {
|
||||||
|
"score": 0,
|
||||||
|
"components": {},
|
||||||
|
"metadata": {
|
||||||
|
"confidence": "insufficient",
|
||||||
|
"message": "Nicht genug Daten (min. 7 Tage)",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
protein_data = get_protein_adequacy_data(profile_id, days)
|
||||||
|
|
||||||
|
calorie_adherence = 70.0
|
||||||
|
protein_adequacy_pct = protein_data.get("adequacy_score", 0)
|
||||||
|
protein_adherence = min(100, protein_adequacy_pct)
|
||||||
|
|
||||||
|
kcal_cv = (
|
||||||
|
(safe_float(stats["std_kcal"]) / safe_float(stats["avg_kcal"]) * 100)
|
||||||
|
if safe_float(stats["avg_kcal"]) > 0
|
||||||
|
else 100
|
||||||
|
)
|
||||||
|
intake_consistency = max(0, 100 - kcal_cv)
|
||||||
|
|
||||||
|
food_quality = 60.0
|
||||||
|
|
||||||
|
if goal_mode == "weight_loss":
|
||||||
|
weights = {
|
||||||
|
"calorie": 0.35,
|
||||||
|
"protein": 0.25,
|
||||||
|
"consistency": 0.20,
|
||||||
|
"quality": 0.20,
|
||||||
|
}
|
||||||
|
elif goal_mode == "strength":
|
||||||
|
weights = {
|
||||||
|
"calorie": 0.25,
|
||||||
|
"protein": 0.35,
|
||||||
|
"consistency": 0.20,
|
||||||
|
"quality": 0.20,
|
||||||
|
}
|
||||||
|
elif goal_mode == "endurance":
|
||||||
|
weights = {
|
||||||
|
"calorie": 0.30,
|
||||||
|
"protein": 0.20,
|
||||||
|
"consistency": 0.20,
|
||||||
|
"quality": 0.30,
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
weights = {
|
||||||
|
"calorie": 0.25,
|
||||||
|
"protein": 0.25,
|
||||||
|
"consistency": 0.25,
|
||||||
|
"quality": 0.25,
|
||||||
|
}
|
||||||
|
|
||||||
|
final_score = (
|
||||||
|
calorie_adherence * weights["calorie"]
|
||||||
|
+ protein_adherence * weights["protein"]
|
||||||
|
+ intake_consistency * weights["consistency"]
|
||||||
|
+ food_quality * weights["quality"]
|
||||||
|
)
|
||||||
|
|
||||||
|
components = {
|
||||||
|
"calorie_adherence": round(calorie_adherence, 1),
|
||||||
|
"protein_adherence": round(protein_adherence, 1),
|
||||||
|
"intake_consistency": round(intake_consistency, 1),
|
||||||
|
"food_quality": round(food_quality, 1),
|
||||||
|
}
|
||||||
|
|
||||||
|
weak_areas = [k for k, v in components.items() if v < 60]
|
||||||
|
if weak_areas:
|
||||||
|
recommendation = f"Verbesserungspotenzial: {', '.join(weak_areas)}"
|
||||||
|
else:
|
||||||
|
recommendation = "Gute Adhärenz, weiter so!"
|
||||||
|
|
||||||
|
return {
|
||||||
|
"score": round(final_score, 1),
|
||||||
|
"components": components,
|
||||||
|
"goal_mode": goal_mode,
|
||||||
|
"weights": weights,
|
||||||
|
"recommendation": recommendation,
|
||||||
|
"metadata": {
|
||||||
|
"confidence": calculate_confidence(stats["cnt"], days, "general"),
|
||||||
|
"data_points": stats["cnt"],
|
||||||
|
"days_analyzed": days,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
@ -17,6 +17,11 @@ from data_layer.nutrition_interpretation import (
|
||||||
build_nutrition_correlation_heuristic_items,
|
build_nutrition_correlation_heuristic_items,
|
||||||
build_nutrition_history_kpi_tiles,
|
build_nutrition_history_kpi_tiles,
|
||||||
)
|
)
|
||||||
|
from data_layer.nutrition_chart_payloads import (
|
||||||
|
build_energy_balance_chart_payload,
|
||||||
|
build_nutrition_adherence_score_payload,
|
||||||
|
build_protein_adequacy_chart_payload,
|
||||||
|
)
|
||||||
from data_layer.nutrition_metrics import (
|
from data_layer.nutrition_metrics import (
|
||||||
estimate_tdee_kcal_from_latest_weight,
|
estimate_tdee_kcal_from_latest_weight,
|
||||||
get_energy_availability_warning_payload,
|
get_energy_availability_warning_payload,
|
||||||
|
|
@ -244,6 +249,8 @@ def get_nutrition_history_viz_bundle(profile_id: str, days: int) -> Dict[str, An
|
||||||
"calorie_balance_daily": [],
|
"calorie_balance_daily": [],
|
||||||
"protein_vs_lean_mass": {"points": [], "protein_target_low_g": None},
|
"protein_vs_lean_mass": {"points": [], "protein_target_low_g": None},
|
||||||
"nutrition_correlation_heuristics": [],
|
"nutrition_correlation_heuristics": [],
|
||||||
|
"chart_payloads": {},
|
||||||
|
"chart_payloads_days": None,
|
||||||
"meta": {"layer_1": "nutrition_metrics", "layer_2b": "nutrition_viz"},
|
"meta": {"layer_1": "nutrition_metrics", "layer_2b": "nutrition_viz"},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -312,6 +319,20 @@ def get_nutrition_history_viz_bundle(profile_id: str, days: int) -> Dict[str, An
|
||||||
weeks_for_weekly = max(4, min(52, (chart_days_for_pipeline + 6) // 7))
|
weeks_for_weekly = max(4, min(52, (chart_days_for_pipeline + 6) // 7))
|
||||||
weekly_chart = get_weekly_macro_distribution_chart_data(profile_id, weeks_for_weekly)
|
weekly_chart = get_weekly_macro_distribution_chart_data(profile_id, weeks_for_weekly)
|
||||||
|
|
||||||
|
# E1/E2/E4 Chart.js-Payloads — gleiche Funktionen wie /api/charts/* (kein zweiter HTTP-Roundtrip im Verlauf)
|
||||||
|
days_for_embedded_charts = max(7, min(int(chart_days_for_pipeline), 90))
|
||||||
|
chart_payloads = {
|
||||||
|
"energy_balance": build_energy_balance_chart_payload(
|
||||||
|
profile_id, days_for_embedded_charts
|
||||||
|
),
|
||||||
|
"protein_adequacy": build_protein_adequacy_chart_payload(
|
||||||
|
profile_id, days_for_embedded_charts
|
||||||
|
),
|
||||||
|
"nutrition_adherence": build_nutrition_adherence_score_payload(
|
||||||
|
profile_id, days_for_embedded_charts
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
conf = navg.get("confidence") or "medium"
|
conf = navg.get("confidence") or "medium"
|
||||||
if targets.get("confidence") == "insufficient":
|
if targets.get("confidence") == "insufficient":
|
||||||
conf = "insufficient"
|
conf = "insufficient"
|
||||||
|
|
@ -362,6 +383,8 @@ def get_nutrition_history_viz_bundle(profile_id: str, days: int) -> Dict[str, An
|
||||||
"protein_target_low_g": pt_low if pt_low > 0 else None,
|
"protein_target_low_g": pt_low if pt_low > 0 else None,
|
||||||
},
|
},
|
||||||
"nutrition_correlation_heuristics": nutrition_correlation_heuristics,
|
"nutrition_correlation_heuristics": nutrition_correlation_heuristics,
|
||||||
|
"chart_payloads": chart_payloads,
|
||||||
|
"chart_payloads_days": days_for_embedded_charts,
|
||||||
"meta": {
|
"meta": {
|
||||||
"layer_1": "nutrition_metrics",
|
"layer_1": "nutrition_metrics",
|
||||||
"layer_2b": "nutrition_viz",
|
"layer_2b": "nutrition_viz",
|
||||||
|
|
|
||||||
|
|
@ -46,9 +46,7 @@ from data_layer.recovery_chart_payloads import (
|
||||||
from data_layer.nutrition_metrics import (
|
from data_layer.nutrition_metrics import (
|
||||||
get_nutrition_average_data,
|
get_nutrition_average_data,
|
||||||
get_protein_targets_data,
|
get_protein_targets_data,
|
||||||
get_protein_adequacy_data,
|
|
||||||
get_macro_consistency_data,
|
get_macro_consistency_data,
|
||||||
get_energy_balance_data,
|
|
||||||
get_weekly_macro_distribution_chart_data,
|
get_weekly_macro_distribution_chart_data,
|
||||||
get_energy_availability_warning_payload,
|
get_energy_availability_warning_payload,
|
||||||
)
|
)
|
||||||
|
|
@ -77,6 +75,11 @@ from data_layer.correlations import (
|
||||||
calculate_top_drivers
|
calculate_top_drivers
|
||||||
)
|
)
|
||||||
from data_layer.utils import serialize_dates, safe_float, calculate_confidence
|
from data_layer.utils import serialize_dates, safe_float, calculate_confidence
|
||||||
|
from data_layer.nutrition_chart_payloads import (
|
||||||
|
build_energy_balance_chart_payload,
|
||||||
|
build_protein_adequacy_chart_payload,
|
||||||
|
build_nutrition_adherence_score_payload,
|
||||||
|
)
|
||||||
|
|
||||||
router = APIRouter(prefix="/api/charts", tags=["charts"])
|
router = APIRouter(prefix="/api/charts", tags=["charts"])
|
||||||
|
|
||||||
|
|
@ -462,136 +465,7 @@ def get_energy_balance_chart(
|
||||||
Chart.js line chart with multiple datasets
|
Chart.js line chart with multiple datasets
|
||||||
"""
|
"""
|
||||||
profile_id = session['profile_id']
|
profile_id = session['profile_id']
|
||||||
|
return build_energy_balance_chart_payload(profile_id, days)
|
||||||
balance_meta = get_energy_balance_data(profile_id, days)
|
|
||||||
|
|
||||||
from db import get_db, get_cursor
|
|
||||||
with get_db() as conn:
|
|
||||||
cur = get_cursor(conn)
|
|
||||||
cutoff = (datetime.now() - timedelta(days=days)).strftime('%Y-%m-%d')
|
|
||||||
|
|
||||||
cur.execute(
|
|
||||||
"""SELECT date, SUM(kcal)::float AS kcal
|
|
||||||
FROM nutrition_log
|
|
||||||
WHERE profile_id=%s AND date >= %s AND kcal IS NOT NULL
|
|
||||||
GROUP BY date
|
|
||||||
ORDER BY date""",
|
|
||||||
(profile_id, cutoff),
|
|
||||||
)
|
|
||||||
rows = cur.fetchall()
|
|
||||||
|
|
||||||
if not rows or len(rows) < 3:
|
|
||||||
return {
|
|
||||||
"chart_type": "line",
|
|
||||||
"data": {
|
|
||||||
"labels": [],
|
|
||||||
"datasets": []
|
|
||||||
},
|
|
||||||
"metadata": {
|
|
||||||
"confidence": "insufficient",
|
|
||||||
"data_points": len(rows) if rows else 0,
|
|
||||||
"message": "Nicht genug Ernährungsdaten (min. 3 Tage)"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
estimated_tdee = balance_meta.get("estimated_tdee") or 0
|
|
||||||
if estimated_tdee <= 0:
|
|
||||||
return {
|
|
||||||
"chart_type": "line",
|
|
||||||
"data": {
|
|
||||||
"labels": [],
|
|
||||||
"datasets": []
|
|
||||||
},
|
|
||||||
"metadata": {
|
|
||||||
"confidence": "insufficient",
|
|
||||||
"data_points": len(rows),
|
|
||||||
"message": "Kein Gewicht für TDEE-Schätzung (weight_log erforderlich)"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
labels = []
|
|
||||||
daily_values = []
|
|
||||||
avg_7d = []
|
|
||||||
avg_14d = []
|
|
||||||
|
|
||||||
for i, row in enumerate(rows):
|
|
||||||
labels.append(row['date'].isoformat())
|
|
||||||
daily_values.append(safe_float(row['kcal']))
|
|
||||||
|
|
||||||
start_7d = max(0, i - 6)
|
|
||||||
window_7d = [safe_float(rows[j]['kcal']) for j in range(start_7d, i + 1)]
|
|
||||||
avg_7d.append(round(sum(window_7d) / len(window_7d), 1) if window_7d else None)
|
|
||||||
|
|
||||||
start_14d = max(0, i - 13)
|
|
||||||
window_14d = [safe_float(rows[j]['kcal']) for j in range(start_14d, i + 1)]
|
|
||||||
avg_14d.append(round(sum(window_14d) / len(window_14d), 1) if window_14d else None)
|
|
||||||
|
|
||||||
avg_intake = float(balance_meta.get("avg_intake") or (sum(daily_values) / len(daily_values) if daily_values else 0))
|
|
||||||
energy_balance = float(balance_meta.get("energy_balance") or (avg_intake - estimated_tdee))
|
|
||||||
balance_status = balance_meta.get("status") or (
|
|
||||||
"deficit" if energy_balance < -200 else "surplus" if energy_balance > 200 else "maintenance"
|
|
||||||
)
|
|
||||||
|
|
||||||
datasets = [
|
|
||||||
{
|
|
||||||
"label": "Kalorien (täglich)",
|
|
||||||
"data": daily_values,
|
|
||||||
"borderColor": "#1D9E7599",
|
|
||||||
"backgroundColor": "rgba(29, 158, 117, 0.1)",
|
|
||||||
"borderWidth": 1.5,
|
|
||||||
"tension": 0.2,
|
|
||||||
"fill": False,
|
|
||||||
"pointRadius": 2
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"label": "Ø 7 Tage",
|
|
||||||
"data": avg_7d,
|
|
||||||
"borderColor": "#1D9E75",
|
|
||||||
"borderWidth": 2.5,
|
|
||||||
"tension": 0.3,
|
|
||||||
"fill": False,
|
|
||||||
"pointRadius": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"label": "Ø 14 Tage",
|
|
||||||
"data": avg_14d,
|
|
||||||
"borderColor": "#085041",
|
|
||||||
"borderWidth": 2,
|
|
||||||
"tension": 0.3,
|
|
||||||
"fill": False,
|
|
||||||
"pointRadius": 0,
|
|
||||||
"borderDash": [6, 3]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"label": "TDEE (geschätzt)",
|
|
||||||
"data": [estimated_tdee] * len(labels),
|
|
||||||
"borderColor": "#888",
|
|
||||||
"borderWidth": 1,
|
|
||||||
"borderDash": [5, 5],
|
|
||||||
"fill": False,
|
|
||||||
"pointRadius": 0
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
confidence = balance_meta.get("confidence") or "low"
|
|
||||||
|
|
||||||
return {
|
|
||||||
"chart_type": "line",
|
|
||||||
"data": {
|
|
||||||
"labels": labels,
|
|
||||||
"datasets": datasets
|
|
||||||
},
|
|
||||||
"metadata": serialize_dates({
|
|
||||||
"confidence": confidence,
|
|
||||||
"data_points": len(rows),
|
|
||||||
"avg_kcal": round(avg_intake, 1),
|
|
||||||
"estimated_tdee": estimated_tdee,
|
|
||||||
"energy_balance": round(energy_balance, 1),
|
|
||||||
"balance_status": balance_status,
|
|
||||||
"first_date": rows[0]['date'],
|
|
||||||
"last_date": rows[-1]['date']
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/macro-distribution")
|
@router.get("/macro-distribution")
|
||||||
|
|
@ -706,136 +580,7 @@ def get_protein_adequacy_chart(
|
||||||
Chart.js line chart with protein intake + averages + target bands
|
Chart.js line chart with protein intake + averages + target bands
|
||||||
"""
|
"""
|
||||||
profile_id = session['profile_id']
|
profile_id = session['profile_id']
|
||||||
|
return build_protein_adequacy_chart_payload(profile_id, days)
|
||||||
# Get protein targets
|
|
||||||
targets = get_protein_targets_data(profile_id)
|
|
||||||
|
|
||||||
from db import get_db, get_cursor
|
|
||||||
with get_db() as conn:
|
|
||||||
cur = get_cursor(conn)
|
|
||||||
cutoff = (datetime.now() - timedelta(days=days)).strftime('%Y-%m-%d')
|
|
||||||
|
|
||||||
cur.execute(
|
|
||||||
"""SELECT date, SUM(protein_g)::float AS protein_g
|
|
||||||
FROM nutrition_log
|
|
||||||
WHERE profile_id=%s AND date >= %s AND protein_g IS NOT NULL
|
|
||||||
GROUP BY date
|
|
||||||
ORDER BY date""",
|
|
||||||
(profile_id, cutoff),
|
|
||||||
)
|
|
||||||
rows = cur.fetchall()
|
|
||||||
|
|
||||||
if not rows or len(rows) < 3:
|
|
||||||
return {
|
|
||||||
"chart_type": "line",
|
|
||||||
"data": {
|
|
||||||
"labels": [],
|
|
||||||
"datasets": []
|
|
||||||
},
|
|
||||||
"metadata": {
|
|
||||||
"confidence": "insufficient",
|
|
||||||
"data_points": len(rows) if rows else 0,
|
|
||||||
"message": "Nicht genug Protein-Daten (min. 3 Tage)"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Prepare data
|
|
||||||
labels = []
|
|
||||||
daily_values = []
|
|
||||||
avg_7d = []
|
|
||||||
avg_28d = []
|
|
||||||
|
|
||||||
for i, row in enumerate(rows):
|
|
||||||
labels.append(row['date'].isoformat())
|
|
||||||
daily_values.append(safe_float(row['protein_g']))
|
|
||||||
|
|
||||||
# 7d rolling average
|
|
||||||
start_7d = max(0, i - 6)
|
|
||||||
window_7d = [safe_float(rows[j]['protein_g']) for j in range(start_7d, i + 1)]
|
|
||||||
avg_7d.append(round(sum(window_7d) / len(window_7d), 1) if window_7d else None)
|
|
||||||
|
|
||||||
# 28d rolling average
|
|
||||||
start_28d = max(0, i - 27)
|
|
||||||
window_28d = [safe_float(rows[j]['protein_g']) for j in range(start_28d, i + 1)]
|
|
||||||
avg_28d.append(round(sum(window_28d) / len(window_28d), 1) if window_28d else None)
|
|
||||||
|
|
||||||
# Add target range bands
|
|
||||||
target_low = targets['protein_target_low']
|
|
||||||
target_high = targets['protein_target_high']
|
|
||||||
|
|
||||||
datasets = [
|
|
||||||
{
|
|
||||||
"label": "Protein (täglich)",
|
|
||||||
"data": daily_values,
|
|
||||||
"borderColor": "#1D9E7599",
|
|
||||||
"backgroundColor": "rgba(29, 158, 117, 0.1)",
|
|
||||||
"borderWidth": 1.5,
|
|
||||||
"tension": 0.2,
|
|
||||||
"fill": False,
|
|
||||||
"pointRadius": 2
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"label": "Ø 7 Tage",
|
|
||||||
"data": avg_7d,
|
|
||||||
"borderColor": "#1D9E75",
|
|
||||||
"borderWidth": 2.5,
|
|
||||||
"tension": 0.3,
|
|
||||||
"fill": False,
|
|
||||||
"pointRadius": 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"label": "Ø 28 Tage",
|
|
||||||
"data": avg_28d,
|
|
||||||
"borderColor": "#085041",
|
|
||||||
"borderWidth": 2,
|
|
||||||
"tension": 0.3,
|
|
||||||
"fill": False,
|
|
||||||
"pointRadius": 0,
|
|
||||||
"borderDash": [6, 3]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"label": "Ziel Min",
|
|
||||||
"data": [target_low] * len(labels),
|
|
||||||
"borderColor": "#888",
|
|
||||||
"borderWidth": 1,
|
|
||||||
"borderDash": [5, 5],
|
|
||||||
"fill": False,
|
|
||||||
"pointRadius": 0
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
datasets.append({
|
|
||||||
"label": "Ziel Max",
|
|
||||||
"data": [target_high] * len(labels),
|
|
||||||
"borderColor": "#888",
|
|
||||||
"borderWidth": 1,
|
|
||||||
"borderDash": [5, 5],
|
|
||||||
"fill": False,
|
|
||||||
"pointRadius": 0
|
|
||||||
})
|
|
||||||
|
|
||||||
from data_layer.utils import calculate_confidence
|
|
||||||
confidence = calculate_confidence(len(rows), days, "general")
|
|
||||||
|
|
||||||
days_in_target = sum(1 for v in daily_values if target_low <= v <= target_high)
|
|
||||||
|
|
||||||
return {
|
|
||||||
"chart_type": "line",
|
|
||||||
"data": {
|
|
||||||
"labels": labels,
|
|
||||||
"datasets": datasets
|
|
||||||
},
|
|
||||||
"metadata": serialize_dates({
|
|
||||||
"confidence": confidence,
|
|
||||||
"data_points": len(rows),
|
|
||||||
"target_low": round(target_low, 1),
|
|
||||||
"target_high": round(target_high, 1),
|
|
||||||
"days_in_target": days_in_target,
|
|
||||||
"target_compliance_pct": round(days_in_target / len(daily_values) * 100, 1) if daily_values else 0,
|
|
||||||
"first_date": rows[0]['date'],
|
|
||||||
"last_date": rows[-1]['date']
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/nutrition-consistency")
|
@router.get("/nutrition-consistency")
|
||||||
|
|
@ -963,138 +708,7 @@ def get_nutrition_adherence_score(
|
||||||
}
|
}
|
||||||
"""
|
"""
|
||||||
profile_id = session['profile_id']
|
profile_id = session['profile_id']
|
||||||
|
return build_nutrition_adherence_score_payload(profile_id, days)
|
||||||
from db import get_db, get_cursor
|
|
||||||
from data_layer.nutrition_metrics import (
|
|
||||||
get_protein_adequacy_data,
|
|
||||||
calculate_macro_consistency_score
|
|
||||||
)
|
|
||||||
|
|
||||||
# Get user's goal mode (weight_loss, strength, endurance, etc.)
|
|
||||||
with get_db() as conn:
|
|
||||||
cur = get_cursor(conn)
|
|
||||||
cur.execute("SELECT goal_mode FROM profiles WHERE id = %s", (profile_id,))
|
|
||||||
profile_row = cur.fetchone()
|
|
||||||
goal_mode = profile_row['goal_mode'] if profile_row and profile_row['goal_mode'] else 'health'
|
|
||||||
|
|
||||||
cutoff = (datetime.now() - timedelta(days=days)).strftime('%Y-%m-%d')
|
|
||||||
|
|
||||||
# Get nutrition data
|
|
||||||
cur.execute(
|
|
||||||
"""WITH daily AS (
|
|
||||||
SELECT date,
|
|
||||||
COALESCE(SUM(kcal), 0)::float AS dk,
|
|
||||||
COALESCE(SUM(protein_g), 0)::float AS dp,
|
|
||||||
COALESCE(SUM(carbs_g), 0)::float AS dc,
|
|
||||||
COALESCE(SUM(fat_g), 0)::float AS df FROM nutrition_log
|
|
||||||
WHERE profile_id=%s AND date >= %s AND kcal IS NOT NULL
|
|
||||||
GROUP BY date
|
|
||||||
)
|
|
||||||
SELECT COUNT(*)::int AS cnt,
|
|
||||||
AVG(dk) AS avg_kcal,
|
|
||||||
STDDEV(dk) AS std_kcal,
|
|
||||||
AVG(dp) AS avg_protein,
|
|
||||||
AVG(dc) AS avg_carbs,
|
|
||||||
AVG(df) AS avg_fat
|
|
||||||
FROM daily""",
|
|
||||||
(profile_id, cutoff),
|
|
||||||
)
|
|
||||||
stats = cur.fetchone()
|
|
||||||
|
|
||||||
if not stats or stats['cnt'] < 7:
|
|
||||||
return {
|
|
||||||
"score": 0,
|
|
||||||
"components": {},
|
|
||||||
"metadata": {
|
|
||||||
"confidence": "insufficient",
|
|
||||||
"message": "Nicht genug Daten (min. 7 Tage)"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# Get protein adequacy
|
|
||||||
protein_data = get_protein_adequacy_data(profile_id, days)
|
|
||||||
|
|
||||||
# Calculate components based on goal mode
|
|
||||||
components = {}
|
|
||||||
|
|
||||||
# 1. Calorie adherence (placeholder, needs goal-specific logic)
|
|
||||||
calorie_adherence = 70.0 # TODO: Calculate based on TDEE target
|
|
||||||
|
|
||||||
# 2. Protein adherence
|
|
||||||
protein_adequacy_pct = protein_data.get('adequacy_score', 0)
|
|
||||||
protein_adherence = min(100, protein_adequacy_pct)
|
|
||||||
|
|
||||||
# 3. Intake consistency (low volatility = good)
|
|
||||||
kcal_cv = (safe_float(stats['std_kcal']) / safe_float(stats['avg_kcal']) * 100) if safe_float(stats['avg_kcal']) > 0 else 100
|
|
||||||
intake_consistency = max(0, 100 - kcal_cv) # Invert: low CV = high score
|
|
||||||
|
|
||||||
# 4. Food quality (placeholder for fiber/sugar analysis)
|
|
||||||
food_quality = 60.0 # TODO: Calculate from fiber/sugar data
|
|
||||||
|
|
||||||
# Goal-specific weighting (from concept E4)
|
|
||||||
if goal_mode == 'weight_loss':
|
|
||||||
weights = {
|
|
||||||
'calorie': 0.35,
|
|
||||||
'protein': 0.25,
|
|
||||||
'consistency': 0.20,
|
|
||||||
'quality': 0.20
|
|
||||||
}
|
|
||||||
elif goal_mode == 'strength':
|
|
||||||
weights = {
|
|
||||||
'calorie': 0.25,
|
|
||||||
'protein': 0.35,
|
|
||||||
'consistency': 0.20,
|
|
||||||
'quality': 0.20
|
|
||||||
}
|
|
||||||
elif goal_mode == 'endurance':
|
|
||||||
weights = {
|
|
||||||
'calorie': 0.30,
|
|
||||||
'protein': 0.20,
|
|
||||||
'consistency': 0.20,
|
|
||||||
'quality': 0.30
|
|
||||||
}
|
|
||||||
else: # health, recomposition
|
|
||||||
weights = {
|
|
||||||
'calorie': 0.25,
|
|
||||||
'protein': 0.25,
|
|
||||||
'consistency': 0.25,
|
|
||||||
'quality': 0.25
|
|
||||||
}
|
|
||||||
|
|
||||||
# Calculate weighted score
|
|
||||||
final_score = (
|
|
||||||
calorie_adherence * weights['calorie'] +
|
|
||||||
protein_adherence * weights['protein'] +
|
|
||||||
intake_consistency * weights['consistency'] +
|
|
||||||
food_quality * weights['quality']
|
|
||||||
)
|
|
||||||
|
|
||||||
components = {
|
|
||||||
'calorie_adherence': round(calorie_adherence, 1),
|
|
||||||
'protein_adherence': round(protein_adherence, 1),
|
|
||||||
'intake_consistency': round(intake_consistency, 1),
|
|
||||||
'food_quality': round(food_quality, 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
# Generate recommendation
|
|
||||||
weak_areas = [k for k, v in components.items() if v < 60]
|
|
||||||
if weak_areas:
|
|
||||||
recommendation = f"Verbesserungspotenzial: {', '.join(weak_areas)}"
|
|
||||||
else:
|
|
||||||
recommendation = "Gute Adhärenz, weiter so!"
|
|
||||||
|
|
||||||
return {
|
|
||||||
"score": round(final_score, 1),
|
|
||||||
"components": components,
|
|
||||||
"goal_mode": goal_mode,
|
|
||||||
"weights": weights,
|
|
||||||
"recommendation": recommendation,
|
|
||||||
"metadata": {
|
|
||||||
"confidence": calculate_confidence(stats['cnt'], days, "general"),
|
|
||||||
"data_points": stats['cnt'],
|
|
||||||
"days_analyzed": days
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/energy-availability-warning")
|
@router.get("/energy-availability-warning")
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ Semantic Versioning: MAJOR.MINOR.PATCH
|
||||||
- PATCH: Bugfix, kleine Änderung, Refactor
|
- PATCH: Bugfix, kleine Änderung, Refactor
|
||||||
"""
|
"""
|
||||||
|
|
||||||
APP_VERSION = "0.9s"
|
APP_VERSION = "0.9t"
|
||||||
BUILD_DATE = "2026-04-20"
|
BUILD_DATE = "2026-04-20"
|
||||||
DB_SCHEMA_VERSION = "20260409c" # 048/049 vitals_baseline.source csv + SAVEPOINT Import
|
DB_SCHEMA_VERSION = "20260409c" # 048/049 vitals_baseline.source csv + SAVEPOINT Import
|
||||||
|
|
||||||
|
|
@ -36,6 +36,14 @@ MODULE_VERSIONS = {
|
||||||
}
|
}
|
||||||
|
|
||||||
CHANGELOG = [
|
CHANGELOG = [
|
||||||
|
{
|
||||||
|
"version": "0.9t",
|
||||||
|
"date": "2026-04-20",
|
||||||
|
"changes": [
|
||||||
|
"Phase C: data_layer/nutrition_chart_payloads (E1/E2/E4) — gemeinsam mit /api/charts/*",
|
||||||
|
"nutrition-history-viz: chart_payloads + chart_payloads_days; Verlauf NutritionCharts ohne 3 Extra-HTTP-Calls",
|
||||||
|
],
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"version": "0.9s",
|
"version": "0.9s",
|
||||||
"date": "2026-04-20",
|
"date": "2026-04-20",
|
||||||
|
|
|
||||||
|
|
@ -205,12 +205,14 @@ export function WeeklyMacroDistributionPanel({ macroWeeklyData, loading, error }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Nutrition Charts (E1–E5). Verlauf: `showWeeklyMacroDistribution={false}` wenn E3 separat (z. B. neben Donut) gerendert wird.
|
* Nutrition Charts (E1–E5). Verlauf: `showWeeklyMacroDistribution={false}` wenn E3 separat (z. B. neben Donut) gerendert wird.
|
||||||
|
* @param {object} [prefetchedChartPayloads] — aus GET /charts/nutrition-history-viz (`chart_payloads`): E1/E2/E4 ohne Extra-Requests (Phase C).
|
||||||
*/
|
*/
|
||||||
export default function NutritionCharts({
|
export default function NutritionCharts({
|
||||||
days = 28,
|
days = 28,
|
||||||
showWeeklyMacroDistribution = true,
|
showWeeklyMacroDistribution = true,
|
||||||
/** Verlauf: E5-Kachel liegt in nutrition-history-viz KPIs — doppelte Karte ausblenden */
|
/** Verlauf: E5-Kachel liegt in nutrition-history-viz KPIs — doppelte Karte ausblenden */
|
||||||
hideEnergyAvailabilityCard = false,
|
hideEnergyAvailabilityCard = false,
|
||||||
|
prefetchedChartPayloads = null,
|
||||||
}) {
|
}) {
|
||||||
const [energyData, setEnergyData] = useState(null)
|
const [energyData, setEnergyData] = useState(null)
|
||||||
const [proteinData, setProteinData] = useState(null)
|
const [proteinData, setProteinData] = useState(null)
|
||||||
|
|
@ -226,20 +228,44 @@ export default function NutritionCharts({
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
loadCharts()
|
loadCharts()
|
||||||
}, [days, showWeeklyMacroDistribution, hideEnergyAvailabilityCard])
|
}, [days, showWeeklyMacroDistribution, hideEnergyAvailabilityCard, prefetchedChartPayloads])
|
||||||
|
|
||||||
const loadCharts = async () => {
|
const loadCharts = async () => {
|
||||||
const tasks = [
|
const p = prefetchedChartPayloads
|
||||||
loadEnergyBalance(),
|
const tasks = []
|
||||||
loadProteinAdequacy(),
|
|
||||||
loadAdherence(),
|
if (p?.energy_balance) {
|
||||||
]
|
setEnergyData(p.energy_balance)
|
||||||
|
setLoading((l) => ({ ...l, energy: false }))
|
||||||
|
setErrors((e) => ({ ...e, energy: null }))
|
||||||
|
} else {
|
||||||
|
tasks.push(loadEnergyBalance())
|
||||||
|
}
|
||||||
|
|
||||||
|
if (p?.protein_adequacy) {
|
||||||
|
setProteinData(p.protein_adequacy)
|
||||||
|
setLoading((l) => ({ ...l, protein: false }))
|
||||||
|
setErrors((e) => ({ ...e, protein: null }))
|
||||||
|
} else {
|
||||||
|
tasks.push(loadProteinAdequacy())
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showWeeklyMacroDistribution) {
|
||||||
|
tasks.push(loadMacroWeekly())
|
||||||
|
}
|
||||||
|
|
||||||
|
if (p?.nutrition_adherence) {
|
||||||
|
setAdherenceData(p.nutrition_adherence)
|
||||||
|
setLoading((l) => ({ ...l, adherence: false }))
|
||||||
|
setErrors((e) => ({ ...e, adherence: null }))
|
||||||
|
} else {
|
||||||
|
tasks.push(loadAdherence())
|
||||||
|
}
|
||||||
|
|
||||||
if (!hideEnergyAvailabilityCard) {
|
if (!hideEnergyAvailabilityCard) {
|
||||||
tasks.push(loadWarning())
|
tasks.push(loadWarning())
|
||||||
}
|
}
|
||||||
if (showWeeklyMacroDistribution) {
|
|
||||||
tasks.splice(2, 0, loadMacroWeekly())
|
|
||||||
}
|
|
||||||
await Promise.all(tasks)
|
await Promise.all(tasks)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1120,7 +1120,12 @@ function NutritionSection({ insights, onRequest, loadingSlug, filterActiveSlugs
|
||||||
<div style={{ fontSize: 12, fontWeight: 600, color: 'var(--text3)', marginBottom: 8, marginTop: 4 }}>
|
<div style={{ fontSize: 12, fontWeight: 600, color: 'var(--text3)', marginBottom: 8, marginTop: 4 }}>
|
||||||
Zeitverläufe (Energie & Protein)
|
Zeitverläufe (Energie & Protein)
|
||||||
</div>
|
</div>
|
||||||
<NutritionCharts days={chartDays} showWeeklyMacroDistribution={false} hideEnergyAvailabilityCard />
|
<NutritionCharts
|
||||||
|
days={chartDays}
|
||||||
|
showWeeklyMacroDistribution={false}
|
||||||
|
hideEnergyAvailabilityCard
|
||||||
|
prefetchedChartPayloads={viz.chart_payloads}
|
||||||
|
/>
|
||||||
|
|
||||||
<InsightBox insights={insights} slugs={filterActiveSlugs(['ernaehrung'])} onRequest={onRequest} loading={loadingSlug}/>
|
<InsightBox insights={insights} slugs={filterActiveSlugs(['ernaehrung'])} onRequest={onRequest} loading={loadingSlug}/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user