feat: Introduce quick capture widget configuration and validation
- Added support for the "quick_capture" widget, allowing users to configure visibility for weight and baseline vitals (resting HR, HRV, VO₂max). - Implemented validation logic to ensure correct configuration input and prevent errors. - Updated the widget catalog and dashboard layout to reflect the new quick capture features. - Removed the "training_type_distribution" widget from the catalog as part of the refactor. - Bumped app_dashboard version to 1.6.2 to incorporate these enhancements.
This commit is contained in:
parent
3d498d03c1
commit
7f833b2cb1
|
|
@ -16,8 +16,15 @@ WIDGETS_ALLOWING_CONFIG: frozenset[str] = frozenset({
|
|||
"body_overview",
|
||||
"activity_overview",
|
||||
"kpi_board",
|
||||
"quick_capture",
|
||||
"trend_kcal_weight",
|
||||
"training_type_distribution",
|
||||
})
|
||||
|
||||
_QUICK_CAPTURE_KEYS: frozenset[str] = frozenset({
|
||||
"show_weight",
|
||||
"show_resting_hr",
|
||||
"show_hrv",
|
||||
"show_vo2_max",
|
||||
})
|
||||
|
||||
_KPI_TILE_FIXED: frozenset[str] = frozenset({"body_fat", "avg_kcal"})
|
||||
|
|
@ -47,14 +54,34 @@ def validate_widget_entry_config(widget_id: str, raw: Any) -> dict[str, Any]:
|
|||
return _validate_chart_days_only(raw, label="activity_overview")
|
||||
if widget_id == "kpi_board":
|
||||
return _validate_kpi_board_config(raw)
|
||||
if widget_id == "quick_capture":
|
||||
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 == "training_type_distribution":
|
||||
return _validate_distribution_days_only(raw)
|
||||
|
||||
raise ValueError(f"Widget {widget_id}: keine Konfiguration unterstützt")
|
||||
|
||||
|
||||
def _validate_quick_capture_config(raw: dict[str, Any]) -> dict[str, Any]:
|
||||
label = "quick_capture"
|
||||
unknown = set(raw) - _QUICK_CAPTURE_KEYS
|
||||
if unknown:
|
||||
raise ValueError(f"{label}: unbekannte config-Felder: {sorted(unknown)}")
|
||||
out: dict[str, bool] = {}
|
||||
for k in _QUICK_CAPTURE_KEYS:
|
||||
if k not in raw:
|
||||
continue
|
||||
v = raw[k]
|
||||
if not isinstance(v, bool):
|
||||
raise ValueError(f"{label}: {k} muss boolean sein")
|
||||
out[k] = v
|
||||
merged = {k: True for k in _QUICK_CAPTURE_KEYS}
|
||||
merged.update(out)
|
||||
if not any(merged.values()):
|
||||
raise ValueError(f"{label}: mindestens ein Bereich muss sichtbar sein (show_*)")
|
||||
return out
|
||||
|
||||
|
||||
def _kpi_tile_id_valid(tid: str) -> bool:
|
||||
if tid in _KPI_TILE_FIXED:
|
||||
return True
|
||||
|
|
@ -130,15 +157,3 @@ def _validate_chart_days_only(raw: dict[str, Any], *, label: str) -> dict[str, A
|
|||
return {"chart_days": v}
|
||||
|
||||
|
||||
def _validate_distribution_days_only(raw: dict[str, Any]) -> dict[str, Any]:
|
||||
label = "training_type_distribution"
|
||||
allowed = frozenset({"distribution_days"})
|
||||
unknown = set(raw) - allowed
|
||||
if unknown:
|
||||
raise ValueError(f"{label}: unbekannte config-Felder: {sorted(unknown)}")
|
||||
if "distribution_days" not in raw:
|
||||
return {}
|
||||
v = _parse_chart_days(raw["distribution_days"], label)
|
||||
if v < 7 or v > 120:
|
||||
raise ValueError(f"{label}: distribution_days muss zwischen 7 und 120 liegen")
|
||||
return {"distribution_days": v}
|
||||
|
|
|
|||
|
|
@ -45,6 +45,32 @@ def test_kpi_board_tiles():
|
|||
validate_widget_entry_config("kpi_board", {"extra": 1})
|
||||
|
||||
|
||||
def test_quick_capture_visibility():
|
||||
assert validate_widget_entry_config("quick_capture", {}) == {}
|
||||
assert validate_widget_entry_config("quick_capture", {"show_weight": False}) == {"show_weight": False}
|
||||
full = {
|
||||
"show_weight": True,
|
||||
"show_resting_hr": False,
|
||||
"show_hrv": True,
|
||||
"show_vo2_max": False,
|
||||
}
|
||||
assert validate_widget_entry_config("quick_capture", full) == full
|
||||
with pytest.raises(ValueError):
|
||||
validate_widget_entry_config("quick_capture", {"show_weight": "yes"})
|
||||
with pytest.raises(ValueError):
|
||||
validate_widget_entry_config(
|
||||
"quick_capture",
|
||||
{
|
||||
"show_weight": False,
|
||||
"show_resting_hr": False,
|
||||
"show_hrv": False,
|
||||
"show_vo2_max": False,
|
||||
},
|
||||
)
|
||||
with pytest.raises(ValueError):
|
||||
validate_widget_entry_config("quick_capture", {"extra": 1})
|
||||
|
||||
|
||||
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}
|
||||
|
|
@ -52,17 +78,6 @@ def test_trend_kcal_weight_chart_days():
|
|||
validate_widget_entry_config("trend_kcal_weight", {"chart_days": 6})
|
||||
|
||||
|
||||
def test_training_type_distribution_days():
|
||||
assert validate_widget_entry_config("training_type_distribution", {}) == {}
|
||||
assert validate_widget_entry_config(
|
||||
"training_type_distribution", {"distribution_days": 28}
|
||||
) == {"distribution_days": 28}
|
||||
with pytest.raises(ValueError):
|
||||
validate_widget_entry_config("training_type_distribution", {"distribution_days": 5})
|
||||
with pytest.raises(ValueError):
|
||||
validate_widget_entry_config("training_type_distribution", {"distribution_days": 200})
|
||||
|
||||
|
||||
def test_kpi_board_legacy_chart_days_dropped():
|
||||
"""Nur chart_days (Alt-Layouts) → automatische Kachelwahl, kein Ø-Kal-Fenster mehr."""
|
||||
assert validate_widget_entry_config("kpi_board", {"chart_days": 14}) == {}
|
||||
|
|
|
|||
|
|
@ -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.0", # P1 Produkt-Widgets im Katalog + Default nur Kern-5 aktiv
|
||||
"app_dashboard": "1.6.2", # quick_capture: Sichtbarkeit show_* konfigurierbar
|
||||
}
|
||||
|
||||
CHANGELOG = [
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ WIDGET_CATALOG: list[WidgetCatalogEntry] = [
|
|||
{
|
||||
"id": "quick_capture",
|
||||
"title": "Schnelleingabe",
|
||||
"description": "Gewicht und Vitalwerte erfassen",
|
||||
"description": "Gewicht + Baseline-Vitals; optional show_weight / show_resting_hr / show_hrv / show_vo2_max (false = aus)",
|
||||
},
|
||||
{
|
||||
"id": "kpi_board",
|
||||
|
|
@ -40,7 +40,7 @@ WIDGET_CATALOG: list[WidgetCatalogEntry] = [
|
|||
{
|
||||
"id": "activity_overview",
|
||||
"title": "Aktivität",
|
||||
"description": "Training & Konsistenz (optional: config chart_days 7–90)",
|
||||
"description": "Trainingstyp-Verteilung (Kuchen) + Konsistenz — Zeitraum über config chart_days 7–90",
|
||||
},
|
||||
{
|
||||
"id": "dashboard_greeting",
|
||||
|
|
@ -82,11 +82,6 @@ WIDGET_CATALOG: list[WidgetCatalogEntry] = [
|
|||
"title": "Erholung",
|
||||
"description": "Schlaf-Widget & Ruhetage",
|
||||
},
|
||||
{
|
||||
"id": "training_type_distribution",
|
||||
"title": "Training Verteilung",
|
||||
"description": "Kuchen Trainingstypen (optional config distribution_days 7–120, Default 28)",
|
||||
},
|
||||
{
|
||||
"id": "goals_focus_teaser",
|
||||
"title": "Ziele Teaser",
|
||||
|
|
|
|||
|
|
@ -1,25 +0,0 @@
|
|||
import { useNavigate } from 'react-router-dom'
|
||||
import TrainingTypeDistribution from '../TrainingTypeDistribution'
|
||||
|
||||
/**
|
||||
* @param {{ refreshTick?: number, distributionDays?: number }} props
|
||||
*/
|
||||
export default function TrainingTypeDistributionWidget({ refreshTick = 0, distributionDays = 28 }) {
|
||||
const nav = useNavigate()
|
||||
const days = Math.max(7, Math.min(120, Number(distributionDays) || 28))
|
||||
|
||||
return (
|
||||
<div className="card section-gap" style={{ marginBottom: 16 }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 8 }}>
|
||||
<div>
|
||||
<div style={{ fontSize: 14, fontWeight: 700, color: 'var(--text1)' }}>Training</div>
|
||||
<div style={{ fontSize: 12, color: 'var(--text3)' }}>Verteilung der Trainingstypen ({days} Tage)</div>
|
||||
</div>
|
||||
<button type="button" className="btn btn-secondary" style={{ fontSize: 12, padding: '6px 12px' }} onClick={() => nav('/activity')}>
|
||||
Details →
|
||||
</button>
|
||||
</div>
|
||||
<TrainingTypeDistribution key={`${refreshTick}-${days}`} days={days} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -6,8 +6,16 @@ import { api } from '../../utils/api'
|
|||
|
||||
/**
|
||||
* Schnelleingabe: Gewicht + Baseline Vitals (Ruhepuls, HRV, VO₂max) für heute.
|
||||
* @param {{ onSaved?: () => void, captureConfig?: Record<string, unknown> }} props
|
||||
* captureConfig: show_weight, show_resting_hr, show_hrv, show_vo2_max (false = ausblenden; fehlend = true)
|
||||
*/
|
||||
export default function PilotQuickCapture({ onSaved }) {
|
||||
export default function PilotQuickCapture({ onSaved, captureConfig }) {
|
||||
const cfgRaw = captureConfig && typeof captureConfig === 'object' ? captureConfig : {}
|
||||
const showWeight = cfgRaw.show_weight !== false
|
||||
const showRestingHr = cfgRaw.show_resting_hr !== false
|
||||
const showHrv = cfgRaw.show_hrv !== false
|
||||
const showVo2 = cfgRaw.show_vo2_max !== false
|
||||
const showVitalsBlock = showRestingHr || showHrv || showVo2
|
||||
const today = dayjs().format('YYYY-MM-DD')
|
||||
const [weightInput, setWeightInput] = useState('')
|
||||
const [weightSaving, setWeightSaving] = useState(false)
|
||||
|
|
@ -77,12 +85,17 @@ export default function PilotQuickCapture({ onSaved }) {
|
|||
setVOk(false)
|
||||
try {
|
||||
const payload = { date: today }
|
||||
if (vForm.resting_hr) payload.resting_hr = parseInt(vForm.resting_hr, 10)
|
||||
if (vForm.hrv) payload.hrv = parseInt(vForm.hrv, 10)
|
||||
if (vForm.vo2_max) payload.vo2_max = parseFloat(vForm.vo2_max)
|
||||
if (showRestingHr && vForm.resting_hr) payload.resting_hr = parseInt(vForm.resting_hr, 10)
|
||||
if (showHrv && vForm.hrv) payload.hrv = parseInt(vForm.hrv, 10)
|
||||
if (showVo2 && vForm.vo2_max) payload.vo2_max = parseFloat(vForm.vo2_max)
|
||||
|
||||
if (!payload.resting_hr && !payload.hrv && !payload.vo2_max) {
|
||||
setVErr('Mindestens Ruhepuls, HRV oder VO₂max angeben.')
|
||||
const hint = [showRestingHr && 'Ruhepuls', showHrv && 'HRV', showVo2 && 'VO₂max'].filter(Boolean).join(', ')
|
||||
setVErr(
|
||||
hint
|
||||
? `Mindestens einen sichtbaren Wert angeben (${hint}).`
|
||||
: 'Keine Vitalfelder sichtbar.'
|
||||
)
|
||||
setVSaving(false)
|
||||
return
|
||||
}
|
||||
|
|
@ -112,17 +125,34 @@ export default function PilotQuickCapture({ onSaved }) {
|
|||
background: 'var(--surface2)',
|
||||
}
|
||||
|
||||
if (!showWeight && !showVitalsBlock) {
|
||||
return (
|
||||
<div className="card section-gap">
|
||||
<div className="card-title">Schnelleingabe (heute)</div>
|
||||
<p style={{ fontSize: 13, color: 'var(--text3)', margin: 0 }}>
|
||||
Für dieses Widget sind keine Eingabebereiche aktiviert. Im Dashboard-Lab die Sichtbarkeit prüfen
|
||||
oder <Link to="/vitals">Vitalwerte-Seite</Link> nutzen.
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="card section-gap">
|
||||
<div className="card-title">Schnelleingabe (heute)</div>
|
||||
<p style={{ fontSize: 12, color: 'var(--text2)', marginBottom: 12, lineHeight: 1.5 }}>
|
||||
Gewicht separat; Vitalwerte typischerweise gemeinsam.{' '}
|
||||
<Link to="/vitals" style={{ color: 'var(--accent)', fontSize: 12 }}>
|
||||
Volle Vitalwerte-Seite →
|
||||
</Link>
|
||||
</p>
|
||||
{(showWeight || showVitalsBlock) && (
|
||||
<p style={{ fontSize: 12, color: 'var(--text2)', marginBottom: 12, lineHeight: 1.5 }}>
|
||||
{showWeight && showVitalsBlock && 'Gewicht separat; Vitalwerte typischerweise gemeinsam. '}
|
||||
{showWeight && !showVitalsBlock && 'Gewicht für heute. '}
|
||||
{!showWeight && showVitalsBlock && 'Baseline-Vitalwerte für heute. '}
|
||||
<Link to="/vitals" style={{ color: 'var(--accent)', fontSize: 12 }}>
|
||||
Volle Vitalwerte-Seite →
|
||||
</Link>
|
||||
</p>
|
||||
)}
|
||||
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 12 }}>
|
||||
{showWeight && (
|
||||
<div style={cellStyle}>
|
||||
<div style={{ fontSize: 11, fontWeight: 600, color: 'var(--text3)', marginBottom: 6 }}>Gewicht</div>
|
||||
{weightErr && (
|
||||
|
|
@ -152,35 +182,84 @@ export default function PilotQuickCapture({ onSaved }) {
|
|||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div style={{ ...cellStyle, flex: '2 1 280px' }}>
|
||||
{showVitalsBlock && (
|
||||
<div style={{ ...cellStyle, flex: showWeight ? '2 1 280px' : '1 1 280px' }}>
|
||||
<div style={{ fontSize: 11, fontWeight: 600, color: 'var(--text3)', marginBottom: 6 }}>
|
||||
Vitalwerte (Baseline)
|
||||
</div>
|
||||
{vErr && <div style={{ fontSize: 11, color: 'var(--danger)', marginBottom: 6 }}>{vErr}</div>}
|
||||
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(88px, 1fr))', gap: 8 }}>
|
||||
<input
|
||||
type="number"
|
||||
className="form-input"
|
||||
placeholder="Ruhepuls"
|
||||
value={vForm.resting_hr}
|
||||
onChange={(e) => setVForm((f) => ({ ...f, resting_hr: e.target.value }))}
|
||||
/>
|
||||
<input
|
||||
type="number"
|
||||
className="form-input"
|
||||
placeholder="HRV"
|
||||
value={vForm.hrv}
|
||||
onChange={(e) => setVForm((f) => ({ ...f, hrv: e.target.value }))}
|
||||
/>
|
||||
<input
|
||||
type="number"
|
||||
className="form-input"
|
||||
step={0.1}
|
||||
placeholder="VO₂max"
|
||||
value={vForm.vo2_max}
|
||||
onChange={(e) => setVForm((f) => ({ ...f, vo2_max: e.target.value }))}
|
||||
/>
|
||||
<div
|
||||
style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: 'repeat(auto-fit, minmax(120px, 1fr))',
|
||||
gap: '12px 10px',
|
||||
}}
|
||||
>
|
||||
{showRestingHr && (
|
||||
<div>
|
||||
<label
|
||||
htmlFor="pqc-resting-hr"
|
||||
className="form-label"
|
||||
style={{ display: 'block', marginBottom: 4, fontSize: 11, fontWeight: 600, color: 'var(--text2)' }}
|
||||
>
|
||||
Ruhepuls
|
||||
<span style={{ fontWeight: 400, color: 'var(--text3)' }}> (bpm)</span>
|
||||
</label>
|
||||
<input
|
||||
id="pqc-resting-hr"
|
||||
type="number"
|
||||
className="form-input"
|
||||
style={{ width: '100%' }}
|
||||
inputMode="numeric"
|
||||
value={vForm.resting_hr}
|
||||
onChange={(e) => setVForm((f) => ({ ...f, resting_hr: e.target.value }))}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{showHrv && (
|
||||
<div>
|
||||
<label
|
||||
htmlFor="pqc-hrv"
|
||||
className="form-label"
|
||||
style={{ display: 'block', marginBottom: 4, fontSize: 11, fontWeight: 600, color: 'var(--text2)' }}
|
||||
>
|
||||
HRV
|
||||
<span style={{ fontWeight: 400, color: 'var(--text3)' }}> (ms)</span>
|
||||
</label>
|
||||
<input
|
||||
id="pqc-hrv"
|
||||
type="number"
|
||||
className="form-input"
|
||||
style={{ width: '100%' }}
|
||||
inputMode="numeric"
|
||||
value={vForm.hrv}
|
||||
onChange={(e) => setVForm((f) => ({ ...f, hrv: e.target.value }))}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{showVo2 && (
|
||||
<div>
|
||||
<label
|
||||
htmlFor="pqc-vo2"
|
||||
className="form-label"
|
||||
style={{ display: 'block', marginBottom: 4, fontSize: 11, fontWeight: 600, color: 'var(--text2)' }}
|
||||
>
|
||||
VO₂max
|
||||
</label>
|
||||
<input
|
||||
id="pqc-vo2"
|
||||
type="number"
|
||||
className="form-input"
|
||||
style={{ width: '100%' }}
|
||||
step={0.1}
|
||||
inputMode="decimal"
|
||||
value={vForm.vo2_max}
|
||||
onChange={(e) => setVForm((f) => ({ ...f, vo2_max: e.target.value }))}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
|
|
@ -192,6 +271,7 @@ export default function PilotQuickCapture({ onSaved }) {
|
|||
{vOk ? '✓ Gespeichert' : vSaving ? '…' : 'Vitalwerte speichern'}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import {
|
|||
normalizeBodyChartDays,
|
||||
} from '../widgetSystem/bodyChartDays'
|
||||
import KpiBoardConfigEditor from '../widgetSystem/KpiBoardConfigEditor'
|
||||
import QuickCaptureConfigEditor from '../widgetSystem/QuickCaptureConfigEditor'
|
||||
import { moveWidget, normalizeLayoutForEditor, toggleWidget } from '../widgetSystem/layoutEditor'
|
||||
|
||||
/** Widgets mit optionalem config.chart_days (7–90), gleiche UX im Editor */
|
||||
|
|
@ -234,6 +235,27 @@ export default function DashboardLabPage() {
|
|||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{w.id === 'quick_capture' && (
|
||||
<QuickCaptureConfigEditor
|
||||
config={w.config || {}}
|
||||
onChange={(next) =>
|
||||
setLayout((L) =>
|
||||
normalizeLayoutForEditor({
|
||||
...L,
|
||||
widgets: L.widgets.map((x, j) => {
|
||||
if (j !== i) return x
|
||||
const cfg = { ...(x.config || {}) }
|
||||
for (const k of ['show_weight', 'show_resting_hr', 'show_hrv', 'show_vo2_max']) {
|
||||
delete cfg[k]
|
||||
}
|
||||
Object.assign(cfg, next)
|
||||
return { ...x, config: cfg }
|
||||
}),
|
||||
})
|
||||
)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{w.id === 'kpi_board' && (
|
||||
<KpiBoardConfigEditor
|
||||
tiles={Object.prototype.hasOwnProperty.call(w.config || {}, 'tiles') ? w.config.tiles : undefined}
|
||||
|
|
|
|||
67
frontend/src/widgetSystem/QuickCaptureConfigEditor.jsx
Normal file
67
frontend/src/widgetSystem/QuickCaptureConfigEditor.jsx
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
/**
|
||||
* Sichtbarkeit der Teile im Schnelleingabe-Widget (Dashboard-Lab).
|
||||
* Default: alle sichtbar (leeres config).
|
||||
*/
|
||||
const KEYS = [
|
||||
{ key: 'show_weight', label: 'Gewicht' },
|
||||
{ key: 'show_resting_hr', label: 'Ruhepuls' },
|
||||
{ key: 'show_hrv', label: 'HRV' },
|
||||
{ key: 'show_vo2_max', label: 'VO₂max' },
|
||||
]
|
||||
|
||||
function mergeFromConfig(config) {
|
||||
const c = config || {}
|
||||
return {
|
||||
show_weight: c.show_weight !== false,
|
||||
show_resting_hr: c.show_resting_hr !== false,
|
||||
show_hrv: c.show_hrv !== false,
|
||||
show_vo2_max: c.show_vo2_max !== false,
|
||||
}
|
||||
}
|
||||
|
||||
/** @param {{ config: Record<string, unknown>, onChange: (next: Record<string, boolean>) => void }} props */
|
||||
export default function QuickCaptureConfigEditor({ config, onChange }) {
|
||||
const vis = mergeFromConfig(config)
|
||||
|
||||
const setKey = (k, checked) => {
|
||||
const next = { ...vis, [k]: checked }
|
||||
if (!next.show_weight && !next.show_resting_hr && !next.show_hrv && !next.show_vo2_max) {
|
||||
return
|
||||
}
|
||||
const stored = {}
|
||||
for (const { key } of KEYS) {
|
||||
if (!next[key]) stored[key] = false
|
||||
}
|
||||
onChange(stored)
|
||||
}
|
||||
|
||||
const resetAllVisible = () => onChange({})
|
||||
|
||||
return (
|
||||
<div style={{ marginTop: 10, marginLeft: 28 }}>
|
||||
<div style={{ fontSize: 12, color: 'var(--text2)', marginBottom: 8, lineHeight: 1.5 }}>
|
||||
<strong>Schnelleingabe:</strong> welche Bereiche angezeigt werden. Ohne Eintrag = alles sichtbar.
|
||||
</div>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
|
||||
{KEYS.map(({ key, label }) => (
|
||||
<label key={key} style={{ display: 'flex', alignItems: 'center', gap: 8, fontSize: 13, cursor: 'pointer' }}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={vis[key]}
|
||||
onChange={(e) => setKey(key, e.target.checked)}
|
||||
/>
|
||||
<span>{label}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-secondary"
|
||||
style={{ marginTop: 10, fontSize: 12, padding: '6px 12px' }}
|
||||
onClick={resetAllVisible}
|
||||
>
|
||||
Alle einblenden (Standard)
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -14,7 +14,6 @@ import ProfileGoalsProgressWidget from '../components/dashboard-widgets/ProfileG
|
|||
import TrendKcalWeightWidget from '../components/dashboard-widgets/TrendKcalWeightWidget'
|
||||
import NutritionActivitySummaryWidget from '../components/dashboard-widgets/NutritionActivitySummaryWidget'
|
||||
import RecoverySleepRestWidget from '../components/dashboard-widgets/RecoverySleepRestWidget'
|
||||
import TrainingTypeDistributionWidget from '../components/dashboard-widgets/TrainingTypeDistributionWidget'
|
||||
import GoalsFocusTeaserWidget from '../components/dashboard-widgets/GoalsFocusTeaserWidget'
|
||||
import AiPipelineInsightWidget from '../components/dashboard-widgets/AiPipelineInsightWidget'
|
||||
import { normalizeBodyChartDays } from './bodyChartDays'
|
||||
|
|
@ -34,7 +33,10 @@ export function ensurePilotLabWidgetsRegistered() {
|
|||
registerDashboardWidget({
|
||||
id: 'quick_capture',
|
||||
Component: PilotQuickCapture,
|
||||
mapProps: (ctx) => ({ onSaved: ctx.requestRefresh }),
|
||||
mapProps: (ctx) => ({
|
||||
onSaved: ctx.requestRefresh,
|
||||
captureConfig: ctx.layoutEntry?.config || {},
|
||||
}),
|
||||
})
|
||||
registerDashboardWidget({
|
||||
id: 'kpi_board',
|
||||
|
|
@ -104,17 +106,6 @@ export function ensurePilotLabWidgetsRegistered() {
|
|||
Component: RecoverySleepRestWidget,
|
||||
mapProps: () => ({}),
|
||||
})
|
||||
registerDashboardWidget({
|
||||
id: 'training_type_distribution',
|
||||
Component: TrainingTypeDistributionWidget,
|
||||
mapProps: (ctx) => ({
|
||||
refreshTick: ctx.refreshTick,
|
||||
distributionDays:
|
||||
ctx.layoutEntry?.config?.distribution_days != null
|
||||
? Number(ctx.layoutEntry.config.distribution_days)
|
||||
: 28,
|
||||
}),
|
||||
})
|
||||
registerDashboardWidget({
|
||||
id: 'goals_focus_teaser',
|
||||
Component: GoalsFocusTeaserWidget,
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user