mitai-jinkendo/backend/report_chart_fetch.py
Lars 62729d0648
All checks were successful
Deploy Development / deploy (push) Successful in 1m4s
Build Test / pytest-backend (push) Successful in 5s
Build Test / lint-backend (push) Successful in 1s
Build Test / build-frontend (push) Successful in 19s
feat: add report_export widget and enhance report generation capabilities
- Introduced the `report_export` widget to the dashboard, allowing users to generate structured PDF reports.
- Updated widget configuration to include `report_export` in the allowed widgets and added validation for its configuration.
- Enhanced the widget catalog with details for the new `report_export` entry.
- Implemented API endpoints for managing report profiles and generating PDFs.
- Added frontend components for configuring and displaying report settings.
- Updated tests to ensure proper validation and functionality of the new report generation features.
- Bumped application version to reflect the addition of the new widget and related functionalities.
2026-04-29 11:28:04 +02:00

140 lines
5.2 KiB
Python

"""
Chart-Daten für Berichts-PDF: dieselbe Logik wie /api/charts/* (Data Layer), ohne HTTP.
"""
from __future__ import annotations
from typing import Any, Callable
from data_layer.activity_metrics import (
build_training_type_distribution_chart_payload,
build_training_volume_chart_payload,
)
from data_layer.body_metrics import get_weight_trend_data
from data_layer.nutrition_chart_payloads import build_energy_balance_chart_payload
from data_layer.nutrition_metrics import get_nutrition_average_data
from data_layer.utils import serialize_dates
def _weight_trend_payload(profile_id: str, days: int) -> dict[str, Any]:
d = min(max(days, 7), 365)
trend_data = get_weight_trend_data(profile_id, d)
if trend_data["confidence"] == "insufficient":
return {
"chart_type": "line",
"data": {"labels": [], "datasets": []},
"metadata": {
"confidence": "insufficient",
"data_points": 0,
"message": "Nicht genug Daten für Trend-Analyse",
},
}
series = trend_data.get("series") or []
labels = [
pt["date"].isoformat() if hasattr(pt["date"], "isoformat") else str(pt["date"]) for pt in series
]
values = [pt["weight"] for pt in series]
return {
"chart_type": "line",
"data": {
"labels": labels,
"datasets": [
{
"label": "Gewicht",
"data": values,
"borderColor": "#1D9E75",
"backgroundColor": "rgba(29, 158, 117, 0.1)",
"borderWidth": 2,
"tension": 0.4,
"fill": True,
"pointRadius": 2,
}
],
},
"metadata": serialize_dates(
{
"confidence": trend_data["confidence"],
"data_points": trend_data["data_points"],
"first_value": trend_data["first_value"],
"last_value": trend_data["last_value"],
"delta": trend_data["delta"],
"direction": trend_data["direction"],
}
),
}
def _macro_distribution_payload(profile_id: str, days: int) -> dict[str, Any]:
d = min(max(days, 7), 90)
macro_data = get_nutrition_average_data(profile_id, d)
if macro_data["confidence"] == "insufficient":
return {
"chart_type": "pie",
"data": {"labels": [], "datasets": []},
"metadata": {"confidence": "insufficient", "message": "Keine Ernährungsdaten vorhanden"},
}
protein_kcal = macro_data["protein_avg"] * 4
carbs_kcal = macro_data["carbs_avg"] * 4
fat_kcal = macro_data["fat_avg"] * 9
total_kcal = protein_kcal + carbs_kcal + fat_kcal
if total_kcal == 0:
return {
"chart_type": "pie",
"data": {"labels": [], "datasets": []},
"metadata": {"confidence": "insufficient", "message": "Keine Makronährstoff-Daten"},
}
protein_pct = protein_kcal / total_kcal * 100
carbs_pct = carbs_kcal / total_kcal * 100
fat_pct = fat_kcal / total_kcal * 100
return {
"chart_type": "pie",
"data": {
"labels": ["Protein", "Kohlenhydrate", "Fett"],
"datasets": [
{
"data": [round(protein_pct, 1), round(carbs_pct, 1), round(fat_pct, 1)],
"backgroundColor": ["#1D9E75", "#F59E0B", "#EF4444"],
"borderWidth": 2,
"borderColor": "#fff",
}
],
},
"metadata": {"confidence": macro_data.get("confidence", "high")},
}
def _training_volume_payload(profile_id: str, window_days: int) -> dict[str, Any]:
w = max(4, min(52, window_days // 7))
return build_training_volume_chart_payload(profile_id, w)
_CHART_FETCHERS: dict[str, Callable[[str, int], dict[str, Any]]] = {
"weight_trend": _weight_trend_payload,
"energy_balance": lambda pid, d: build_energy_balance_chart_payload(pid, min(max(d, 7), 90)),
"macro_distribution": _macro_distribution_payload,
"training_volume": _training_volume_payload,
"training_type_distribution": lambda pid, d: build_training_type_distribution_chart_payload(
pid, min(max(d, 7), 90)
),
}
def fetch_chart_payload(chart_id: str, profile_id: str, window_days: int) -> dict[str, Any]:
fn = _CHART_FETCHERS.get(chart_id)
if not fn:
raise ValueError(f"Unbekanntes chart_id: {chart_id}")
return fn(profile_id, window_days)
CHART_CATALOG_FOR_API: list[dict[str, Any]] = [
{"id": "weight_trend", "title": "Gewichtstrend", "default_window_days": 90, "window_max": 365},
{"id": "energy_balance", "title": "Energiebilanz", "default_window_days": 28, "window_max": 90},
{"id": "macro_distribution", "title": "Makroverteilung (Ø)", "default_window_days": 28, "window_max": 90},
{"id": "training_volume", "title": "Trainingsvolumen (Wochen)", "default_window_days": 84, "window_max": 365},
{
"id": "training_type_distribution",
"title": "Trainingsart-Verteilung",
"default_window_days": 28,
"window_max": 90,
},
]