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.
This commit is contained in:
parent
7f833b2cb1
commit
bc91396885
|
|
@ -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")
|
||||
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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 = [
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<div className="card section-gap" style={{ marginBottom: 16 }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 10 }}>
|
||||
<div>
|
||||
<div style={{ fontSize: 14, fontWeight: 700, color: 'var(--text1)' }}>Ernährung — Charts</div>
|
||||
<div style={{ fontSize: 12, color: 'var(--text3)' }}>API-Charts · {days} Tage</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-secondary"
|
||||
style={{ fontSize: 12, padding: '6px 12px' }}
|
||||
onClick={() => nav('/history', { state: { tab: 'nutrition' } })}
|
||||
>
|
||||
Verlauf →
|
||||
</button>
|
||||
</div>
|
||||
<NutritionCharts key={`${refreshTick}-${days}`} days={days} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -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 (
|
||||
<div className="card section-gap" style={{ marginBottom: 16, textAlign: 'center', padding: 24 }}>
|
||||
<div style={{ fontSize: 13, color: 'var(--text3)', marginBottom: 12 }}>Noch keine Fotos.</div>
|
||||
<button type="button" className="btn btn-primary" onClick={() => nav('/capture')}>
|
||||
Zur Erfassung
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="card section-gap" style={{ marginBottom: 16 }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 10 }}>
|
||||
<div style={{ fontSize: 14, fontWeight: 700, color: 'var(--text1)' }}>Fortschrittsfotos</div>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-secondary"
|
||||
style={{ fontSize: 12, padding: '6px 12px' }}
|
||||
onClick={() => nav('/history', { state: { tab: 'photos' } })}
|
||||
>
|
||||
Verlauf →
|
||||
</button>
|
||||
</div>
|
||||
{big && (
|
||||
<div
|
||||
role="presentation"
|
||||
style={{
|
||||
position: 'fixed',
|
||||
inset: 0,
|
||||
background: 'rgba(0,0,0,0.9)',
|
||||
zIndex: 100,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
}}
|
||||
onClick={() => setBig(null)}
|
||||
>
|
||||
<img src={api.photoUrl(big)} style={{ maxWidth: '100%', maxHeight: '100%', borderRadius: 8 }} alt="" />
|
||||
</div>
|
||||
)}
|
||||
<div className="photo-grid">
|
||||
{photos.map((p) => (
|
||||
<div key={p.id} style={{ position: 'relative' }}>
|
||||
<img
|
||||
src={api.photoUrl(p.id)}
|
||||
className="photo-thumb"
|
||||
alt=""
|
||||
onClick={() => setBig(p.id)}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
bottom: 4,
|
||||
left: 4,
|
||||
fontSize: 9,
|
||||
background: 'rgba(0,0,0,0.6)',
|
||||
color: 'white',
|
||||
padding: '1px 4px',
|
||||
borderRadius: 3,
|
||||
}}
|
||||
>
|
||||
{p.date?.slice(0, 10) || p.created?.slice(0, 10)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -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 (
|
||||
<div className="card section-gap" style={{ marginBottom: 16 }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 10 }}>
|
||||
<div>
|
||||
<div style={{ fontSize: 14, fontWeight: 700, color: 'var(--text1)' }}>Erholung — Charts</div>
|
||||
<div style={{ fontSize: 12, color: 'var(--text3)' }}>Schlaf, Recovery, Vitalwerte · {days} Tage</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-secondary"
|
||||
style={{ fontSize: 12, padding: '6px 12px' }}
|
||||
onClick={() => nav('/history', { state: { tab: 'recovery' } })}
|
||||
>
|
||||
Verlauf →
|
||||
</button>
|
||||
</div>
|
||||
<RecoveryCharts key={`${refreshTick}-${days}`} days={days} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -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() {
|
|||
<label style={{ fontSize: 12, color: 'var(--text2)', display: 'block', marginBottom: 4 }}>
|
||||
{w.id === 'body_overview'
|
||||
? 'Körper-Chart'
|
||||
: 'Aktivität (Verteilung & Konsistenz)'}{' '}
|
||||
: w.id === 'activity_overview'
|
||||
? 'Aktivität (Verteilung & Konsistenz)'
|
||||
: w.id === 'nutrition_detail_charts'
|
||||
? 'Ernährung — Charts'
|
||||
: 'Erholung — Charts'}{' '}
|
||||
— Zeitraum (Tage): {BODY_CHART_DAYS_MIN}–{BODY_CHART_DAYS_MAX}
|
||||
</label>
|
||||
<input
|
||||
|
|
@ -295,7 +304,11 @@ export default function DashboardLabPage() {
|
|||
aria-label={
|
||||
w.id === 'body_overview'
|
||||
? 'Körper-Chart Zeitraum in Tagen'
|
||||
: 'Aktivität Zeitraum in Tagen'
|
||||
: w.id === 'activity_overview'
|
||||
? 'Aktivität Zeitraum in Tagen'
|
||||
: w.id === 'nutrition_detail_charts'
|
||||
? 'Ernährungs-Charts Zeitraum in Tagen'
|
||||
: 'Erholungs-Charts Zeitraum in Tagen'
|
||||
}
|
||||
value={
|
||||
chartDaysDraftByWidgetId[w.id] !== undefined
|
||||
|
|
|
|||
|
|
@ -13,6 +13,9 @@ import StatusPillsWidget from '../components/dashboard-widgets/StatusPillsWidget
|
|||
import ProfileGoalsProgressWidget from '../components/dashboard-widgets/ProfileGoalsProgressWidget'
|
||||
import TrendKcalWeightWidget from '../components/dashboard-widgets/TrendKcalWeightWidget'
|
||||
import NutritionActivitySummaryWidget from '../components/dashboard-widgets/NutritionActivitySummaryWidget'
|
||||
import NutritionDetailChartsWidget from '../components/dashboard-widgets/NutritionDetailChartsWidget'
|
||||
import RecoveryChartsPanelWidget from '../components/dashboard-widgets/RecoveryChartsPanelWidget'
|
||||
import ProgressPhotosWidget from '../components/dashboard-widgets/ProgressPhotosWidget'
|
||||
import RecoverySleepRestWidget from '../components/dashboard-widgets/RecoverySleepRestWidget'
|
||||
import GoalsFocusTeaserWidget from '../components/dashboard-widgets/GoalsFocusTeaserWidget'
|
||||
import AiPipelineInsightWidget from '../components/dashboard-widgets/AiPipelineInsightWidget'
|
||||
|
|
@ -101,6 +104,27 @@ export function ensurePilotLabWidgetsRegistered() {
|
|||
Component: NutritionActivitySummaryWidget,
|
||||
mapProps: (ctx) => ({ 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,
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user