From bc9139688527c62d6fe64634d3bd9e2f8c9482a6 Mon Sep 17 00:00:00 2001 From: Lars Date: Tue, 7 Apr 2026 20:58:44 +0200 Subject: [PATCH] feat: Add new widgets and enhance configuration validation - Introduced "nutrition_detail_charts", "recovery_charts_panel", and "progress_photos" widgets to the dashboard. - Updated widget configuration validation to support new widgets, including chart days for nutrition and recovery charts. - Enhanced the widget catalog and dashboard layout to include the new features. - Bumped app_dashboard version to 1.7.0 to reflect these additions and improvements. --- backend/dashboard_widget_config.py | 6 ++ backend/tests/test_dashboard_widget_config.py | 14 +++ backend/version.py | 2 +- backend/widget_catalog.py | 15 ++++ .../NutritionDetailChartsWidget.jsx | 32 +++++++ .../ProgressPhotosWidget.jsx | 86 +++++++++++++++++++ .../RecoveryChartsPanelWidget.jsx | 32 +++++++ frontend/src/pages/DashboardLabPage.jsx | 19 +++- .../widgetSystem/registerPilotLabWidgets.js | 24 ++++++ 9 files changed, 226 insertions(+), 4 deletions(-) create mode 100644 frontend/src/components/dashboard-widgets/NutritionDetailChartsWidget.jsx create mode 100644 frontend/src/components/dashboard-widgets/ProgressPhotosWidget.jsx create mode 100644 frontend/src/components/dashboard-widgets/RecoveryChartsPanelWidget.jsx diff --git a/backend/dashboard_widget_config.py b/backend/dashboard_widget_config.py index 57c6a58..729c1ea 100644 --- a/backend/dashboard_widget_config.py +++ b/backend/dashboard_widget_config.py @@ -18,6 +18,8 @@ WIDGETS_ALLOWING_CONFIG: frozenset[str] = frozenset({ "kpi_board", "quick_capture", "trend_kcal_weight", + "nutrition_detail_charts", + "recovery_charts_panel", }) _QUICK_CAPTURE_KEYS: frozenset[str] = frozenset({ @@ -58,6 +60,10 @@ def validate_widget_entry_config(widget_id: str, raw: Any) -> dict[str, Any]: return _validate_quick_capture_config(raw) if widget_id == "trend_kcal_weight": return _validate_chart_days_only(raw, label="trend_kcal_weight") + if widget_id == "nutrition_detail_charts": + return _validate_chart_days_only(raw, label="nutrition_detail_charts") + if widget_id == "recovery_charts_panel": + return _validate_chart_days_only(raw, label="recovery_charts_panel") raise ValueError(f"Widget {widget_id}: keine Konfiguration unterstützt") diff --git a/backend/tests/test_dashboard_widget_config.py b/backend/tests/test_dashboard_widget_config.py index f2287fc..9f30f19 100644 --- a/backend/tests/test_dashboard_widget_config.py +++ b/backend/tests/test_dashboard_widget_config.py @@ -71,6 +71,20 @@ def test_quick_capture_visibility(): validate_widget_entry_config("quick_capture", {"extra": 1}) +def test_nutrition_detail_charts_days(): + assert validate_widget_entry_config("nutrition_detail_charts", {}) == {} + assert validate_widget_entry_config("nutrition_detail_charts", {"chart_days": 60}) == {"chart_days": 60} + with pytest.raises(ValueError): + validate_widget_entry_config("nutrition_detail_charts", {"chart_days": 3}) + + +def test_recovery_charts_panel_days(): + assert validate_widget_entry_config("recovery_charts_panel", {}) == {} + assert validate_widget_entry_config("recovery_charts_panel", {"chart_days": 28}) == {"chart_days": 28} + with pytest.raises(ValueError): + validate_widget_entry_config("recovery_charts_panel", {"chart_days": 99}) + + def test_trend_kcal_weight_chart_days(): assert validate_widget_entry_config("trend_kcal_weight", {}) == {} assert validate_widget_entry_config("trend_kcal_weight", {"chart_days": 30}) == {"chart_days": 30} diff --git a/backend/version.py b/backend/version.py index e2cb67f..6dca031 100644 --- a/backend/version.py +++ b/backend/version.py @@ -30,7 +30,7 @@ MODULE_VERSIONS = { "importdata": "1.0.0", "membership": "2.1.0", "workflow": "0.6.0", # Phase 4: End Node Template Engine - "app_dashboard": "1.6.2", # quick_capture: Sichtbarkeit show_* konfigurierbar + "app_dashboard": "1.7.0", # nutrition_detail_charts, recovery_charts_panel, progress_photos } CHANGELOG = [ diff --git a/backend/widget_catalog.py b/backend/widget_catalog.py index b0c8f9e..bf7cd33 100644 --- a/backend/widget_catalog.py +++ b/backend/widget_catalog.py @@ -77,6 +77,21 @@ WIDGET_CATALOG: list[WidgetCatalogEntry] = [ "title": "Ernährung & Aktivität Kurz", "description": "Ø 7T Kacheln", }, + { + "id": "nutrition_detail_charts", + "title": "Ernährung — Detaillierte Charts", + "description": "Phase-0c NutritionCharts (optional chart_days 7–90, Default 30)", + }, + { + "id": "recovery_charts_panel", + "title": "Erholung — Charts R1–R5", + "description": "RecoveryCharts wie Verlauf (optional chart_days 7–90, Default 28)", + }, + { + "id": "progress_photos", + "title": "Fortschrittsfotos", + "description": "Galerie der hochgeladenen Fotos", + }, { "id": "recovery_sleep_rest", "title": "Erholung", diff --git a/frontend/src/components/dashboard-widgets/NutritionDetailChartsWidget.jsx b/frontend/src/components/dashboard-widgets/NutritionDetailChartsWidget.jsx new file mode 100644 index 0000000..a3958cd --- /dev/null +++ b/frontend/src/components/dashboard-widgets/NutritionDetailChartsWidget.jsx @@ -0,0 +1,32 @@ +import { useNavigate } from 'react-router-dom' +import NutritionCharts from '../NutritionCharts' +import { normalizeBodyChartDays } from '../../widgetSystem/bodyChartDays' + +/** + * Phase-0c-Ernährungscharts (wie „Detaillierte Charts“ im Verlauf). + * @param {{ refreshTick?: number, chartDays?: number }} props + */ +export default function NutritionDetailChartsWidget({ refreshTick = 0, chartDays }) { + const nav = useNavigate() + const days = chartDays != null ? normalizeBodyChartDays(chartDays) : 30 + + return ( +
+
+
+
Ernährung — Charts
+
API-Charts · {days} Tage
+
+ +
+ +
+ ) +} diff --git a/frontend/src/components/dashboard-widgets/ProgressPhotosWidget.jsx b/frontend/src/components/dashboard-widgets/ProgressPhotosWidget.jsx new file mode 100644 index 0000000..2ce4009 --- /dev/null +++ b/frontend/src/components/dashboard-widgets/ProgressPhotosWidget.jsx @@ -0,0 +1,86 @@ +import { useState, useEffect } from 'react' +import { useNavigate } from 'react-router-dom' +import { api } from '../../utils/api' + +/** + * Fortschrittsfotos (Galerie wie Verlauf-Tab Fotos). + */ +export default function ProgressPhotosWidget({ refreshTick = 0 }) { + const nav = useNavigate() + const [photos, setPhotos] = useState([]) + const [big, setBig] = useState(null) + + useEffect(() => { + api.listPhotos().then(setPhotos).catch(() => setPhotos([])) + }, [refreshTick]) + + if (!photos.length) { + return ( +
+
Noch keine Fotos.
+ +
+ ) + } + + return ( +
+
+
Fortschrittsfotos
+ +
+ {big && ( +
setBig(null)} + > + +
+ )} +
+ {photos.map((p) => ( +
+ setBig(p.id)} + /> +
+ {p.date?.slice(0, 10) || p.created?.slice(0, 10)} +
+
+ ))} +
+
+ ) +} diff --git a/frontend/src/components/dashboard-widgets/RecoveryChartsPanelWidget.jsx b/frontend/src/components/dashboard-widgets/RecoveryChartsPanelWidget.jsx new file mode 100644 index 0000000..4047427 --- /dev/null +++ b/frontend/src/components/dashboard-widgets/RecoveryChartsPanelWidget.jsx @@ -0,0 +1,32 @@ +import { useNavigate } from 'react-router-dom' +import RecoveryCharts from '../RecoveryCharts' +import { normalizeBodyChartDays } from '../../widgetSystem/bodyChartDays' + +/** + * Erholung R1–R5 (wie Verlauf Erholung). + * @param {{ refreshTick?: number, chartDays?: number }} props + */ +export default function RecoveryChartsPanelWidget({ refreshTick = 0, chartDays }) { + const nav = useNavigate() + const days = chartDays != null ? normalizeBodyChartDays(chartDays) : 28 + + return ( +
+
+
+
Erholung — Charts
+
Schlaf, Recovery, Vitalwerte · {days} Tage
+
+ +
+ +
+ ) +} diff --git a/frontend/src/pages/DashboardLabPage.jsx b/frontend/src/pages/DashboardLabPage.jsx index fc38925..555f6f4 100644 --- a/frontend/src/pages/DashboardLabPage.jsx +++ b/frontend/src/pages/DashboardLabPage.jsx @@ -15,7 +15,12 @@ import QuickCaptureConfigEditor from '../widgetSystem/QuickCaptureConfigEditor' import { moveWidget, normalizeLayoutForEditor, toggleWidget } from '../widgetSystem/layoutEditor' /** Widgets mit optionalem config.chart_days (7–90), gleiche UX im Editor */ -const CHART_DAYS_WIDGET_IDS = new Set(['body_overview', 'activity_overview']) +const CHART_DAYS_WIDGET_IDS = new Set([ + 'body_overview', + 'activity_overview', + 'nutrition_detail_charts', + 'recovery_charts_panel', +]) function catalogMetaById(catalog) { if (!catalog?.widgets?.length) return {} @@ -283,7 +288,11 @@ export default function DashboardLabPage() { ({ refreshTick: ctx.refreshTick }), }) + registerDashboardWidget({ + id: 'nutrition_detail_charts', + Component: NutritionDetailChartsWidget, + mapProps: (ctx) => ({ + refreshTick: ctx.refreshTick, + chartDays: ctx.layoutEntry?.config?.chart_days, + }), + }) + registerDashboardWidget({ + id: 'recovery_charts_panel', + Component: RecoveryChartsPanelWidget, + mapProps: (ctx) => ({ + refreshTick: ctx.refreshTick, + chartDays: ctx.layoutEntry?.config?.chart_days, + }), + }) + registerDashboardWidget({ + id: 'progress_photos', + Component: ProgressPhotosWidget, + mapProps: (ctx) => ({ refreshTick: ctx.refreshTick }), + }) registerDashboardWidget({ id: 'recovery_sleep_rest', Component: RecoverySleepRestWidget,