import { useCallback, useEffect, useMemo, useState } from 'react' import { Link } from 'react-router-dom' import { ChevronDown, ChevronUp, LayoutDashboard, Search } from 'lucide-react' import { api, formatFastApiDetail } from '../utils/api' import { ensurePilotLabWidgetsRegistered } from '../widgetSystem/registerPilotLabWidgets' import { BODY_CHART_DAYS_DEFAULT, BODY_CHART_DAYS_MAX, BODY_CHART_DAYS_MIN, normalizeBodyChartDays, } from '../widgetSystem/bodyChartDays' import KpiBoardConfigEditor from '../widgetSystem/KpiBoardConfigEditor' import QuickCaptureConfigEditor from '../widgetSystem/QuickCaptureConfigEditor' import { moveWidget, normalizeLayoutForEditor, toggleWidget } from '../widgetSystem/layoutEditor' 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 {} return Object.fromEntries(catalog.widgets.map((w) => [w.id, w])) } export default function DashboardConfigurePage() { ensurePilotLabWidgetsRegistered() const [bundle, setBundle] = useState(null) const [catalog, setCatalog] = useState(null) const [layout, setLayout] = useState(null) const [search, setSearch] = useState('') const [busy, setBusy] = useState(false) const [msg, setMsg] = useState(null) const [err, setErr] = useState(null) const [chartDaysDraftByWidgetId, setChartDaysDraftByWidgetId] = useState({}) const metaById = useMemo(() => catalogMetaById(catalog), [catalog]) const isWidgetCatalogAllowed = useCallback( (widgetId) => { const m = metaById[widgetId] if (m == null) return true return m.allowed !== false }, [metaById], ) const commitChartDaysDraftToLayout = useCallback((draftStr, baseLayout, widgetId) => { const clamped = normalizeBodyChartDays( draftStr === '' || draftStr == null ? BODY_CHART_DAYS_DEFAULT : draftStr ) return { ...baseLayout, widgets: baseLayout.widgets.map((x) => x.id !== widgetId ? x : { ...x, config: { ...x.config, chart_days: clamped } } ), } }, []) const load = useCallback(async () => { setErr(null) try { const [cat, b] = await Promise.all([api.getAppWidgetsCatalog(), api.getAppDashboardLayout()]) setCatalog(cat) setBundle(b) setChartDaysDraftByWidgetId({}) const base = b.custom ? b.layout : structuredClone(b.product_default_layout) setLayout(normalizeLayoutForEditor(base)) } catch (e) { setErr(formatFastApiDetail(null, e.message)) } }, []) useEffect(() => { load() }, [load]) const save = async () => { if (!layout) return let toSave = layout const draftEntries = Object.entries(chartDaysDraftByWidgetId) if (draftEntries.length) { for (const [wid, val] of draftEntries) { toSave = normalizeLayoutForEditor(commitChartDaysDraftToLayout(val, toSave, wid)) } setLayout(toSave) setChartDaysDraftByWidgetId({}) } setBusy(true) setMsg(null) setErr(null) try { await api.putAppDashboardLayout(toSave) setMsg('Dein Dashboard wurde gespeichert.') await load() } catch (e) { setErr(formatFastApiDetail(null, e.message)) } finally { setBusy(false) } } const resetToSystem = async () => { if (!window.confirm('Dein individuelles Layout löschen und System-Standard wiederherstellen?')) return setBusy(true) setMsg(null) setErr(null) try { const r = await api.resetAppDashboardLayout() setChartDaysDraftByWidgetId({}) setLayout(normalizeLayoutForEditor(r.layout)) setMsg('Auf System-Standard zurückgesetzt.') await load() } catch (e) { setErr(formatFastApiDetail(null, e.message)) } finally { setBusy(false) } } const searchLower = search.trim().toLowerCase() const libraryIndices = useMemo(() => { if (!layout?.widgets) return [] return layout.widgets .map((w, i) => i) .filter((i) => { const w = layout.widgets[i] if (w.enabled || !isWidgetCatalogAllowed(w.id)) return false if (!searchLower) return true const m = metaById[w.id] const hay = `${m?.title || ''} ${m?.description || ''} ${w.id}`.toLowerCase() return hay.includes(searchLower) }) }, [layout, searchLower, metaById, isWidgetCatalogAllowed]) const activeIndices = useMemo(() => { if (!layout?.widgets) return [] return layout.widgets .map((w, i) => i) .filter((i) => layout.widgets[i].enabled && isWidgetCatalogAllowed(layout.widgets[i].id)) }, [layout, isWidgetCatalogAllowed]) if (err && !layout) { return (

{err}

) } if (!layout) { return (
) } return (
← Einstellungen

Übersicht anpassen

Wähle Kacheln für deine Startseite. Änderungen gelten nur für dein Profil – der System-Standard bleibt erhalten, bis du speicherst. Gesperrte Kacheln (Abonnement) erscheinen nicht in der Auswahl.

{!bundle?.custom && (

Du bearbeitest gerade das System-Standardlayout. Mit „Speichern“ legst du deine persönliche Version ab.

)}
setSearch(e.target.value)} aria-label="Widgets durchsuchen" style={{ flex: 1 }} />
Auf der Übersicht aktiv · {activeIndices.length}
{err &&

{err}

} {msg &&

{msg}

}
    {activeIndices.map((i) => { const w = layout.widgets[i] const label = metaById[w.id]?.title || w.id const chartDaysVal = w.config?.chart_days != null ? normalizeBodyChartDays(w.config.chart_days) : BODY_CHART_DAYS_DEFAULT return (
  • {w.id === 'quick_capture' && ( 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' && ( setLayout((L) => normalizeLayoutForEditor({ ...L, widgets: L.widgets.map((x, j) => { if (j !== i) return x const cfg = { ...(x.config || {}) } if (next === undefined) { delete cfg.tiles } else { cfg.tiles = next } return { ...x, config: cfg } }), }) ) } /> )} {CHART_DAYS_WIDGET_IDS.has(w.id) && (
    setChartDaysDraftByWidgetId((prev) => ({ ...prev, [w.id]: String(chartDaysVal), })) } onChange={(e) => setChartDaysDraftByWidgetId((prev) => ({ ...prev, [w.id]: e.target.value, })) } onBlur={(e) => { const raw = e.target.value setLayout((L) => normalizeLayoutForEditor(commitChartDaysDraftToLayout(raw, L, w.id)) ) setChartDaysDraftByWidgetId((prev) => { const next = { ...prev } delete next[w.id] return next }) }} onKeyDown={(e) => { if (e.key === 'Enter') e.currentTarget.blur() }} />
    )}
  • ) })}
Bibliothek · hinzufügen
{libraryIndices.length === 0 ? (

{searchLower ? 'Keine passenden inaktiven Widgets.' : 'Alle verfügbaren Kacheln sind schon aktiv.'}

) : (
    {libraryIndices.map((i) => { const w = layout.widgets[i] const m = metaById[w.id] return (
  • {m?.title || w.id}
    {m?.description || ''}
  • ) })}
)}
Zur Übersicht
) }