import { useCallback, useEffect, useState } from 'react' import { ChevronDown, ChevronUp, LayoutGrid } from 'lucide-react' import { Link } from 'react-router-dom' import { api, formatFastApiDetail } from '../utils/api' import { WidgetRenderer } from '../widgetSystem/dashboardWidgetRegistry' import { ensurePilotLabWidgetsRegistered } from '../widgetSystem/registerPilotLabWidgets' import { BODY_CHART_DAYS_DEFAULT, BODY_CHART_DAYS_MAX, BODY_CHART_DAYS_MIN, normalizeBodyChartDays, } from '../widgetSystem/bodyChartDays' import { moveWidget, normalizeLayoutForEditor, toggleWidget } from '../widgetSystem/layoutEditor' function catalogMetaById(catalog) { if (!catalog?.widgets?.length) return {} return Object.fromEntries(catalog.widgets.map((w) => [w.id, w])) } export default function DashboardLabPage() { ensurePilotLabWidgetsRegistered() const [refreshTick, setRefreshTick] = useState(0) const requestRefresh = () => setRefreshTick((t) => t + 1) const [catalog, setCatalog] = useState(null) const [bundle, setBundle] = useState(null) const [layout, setLayout] = useState(null) const [err, setErr] = useState(null) const [busy, setBusy] = useState(false) const [msg, setMsg] = useState(null) /** Während der Fokus im Körper-Chart-Feld: Rohstring, damit Tippen (z. B. „14“) nicht sofort geclamped wird */ const [bodyChartDaysDraft, setBodyChartDaysDraft] = useState(null) const metaById = catalogMetaById(catalog) const commitBodyChartDraftToLayout = useCallback((draftStr, baseLayout) => { const clamped = normalizeBodyChartDays(draftStr === '' || draftStr == null ? BODY_CHART_DAYS_DEFAULT : draftStr) return { ...baseLayout, widgets: baseLayout.widgets.map((x) => x.id !== 'body_overview' ? 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) setBodyChartDaysDraft(null) setLayout(normalizeLayoutForEditor(b.layout)) } catch (e) { setErr(formatFastApiDetail(null, e.message)) } }, []) useEffect(() => { load() }, [load]) const save = async () => { if (!layout) return let toSave = layout if (bodyChartDaysDraft !== null) { toSave = normalizeLayoutForEditor(commitBodyChartDraftToLayout(bodyChartDaysDraft, layout)) setLayout(toSave) setBodyChartDaysDraft(null) } setBusy(true) setMsg(null) setErr(null) try { await api.putAppDashboardLayout(toSave) setMsg('Layout gespeichert.') await load() } catch (e) { setErr(formatFastApiDetail(null, e.message)) } finally { setBusy(false) } } const reset = async () => { if (!confirm('Persönliches Layout löschen und Standard wiederherstellen?')) return setBusy(true) setMsg(null) setErr(null) try { const r = await api.resetAppDashboardLayout() setBodyChartDaysDraft(null) setLayout(normalizeLayoutForEditor(r.layout)) setMsg('Auf Standard zurückgesetzt.') await load() } catch (e) { setErr(formatFastApiDetail(null, e.message)) } finally { setBusy(false) } } const applyDefaultLocal = () => { if (bundle?.default_layout) { setBodyChartDaysDraft(null) setLayout(normalizeLayoutForEditor(structuredClone(bundle.default_layout))) setMsg('Standard geladen (noch nicht gespeichert).') } } if (err && !layout) { return (
{err}
Widget-System: Katalog, Registry, Renderer; optional pro Widget config (z. B.{' '}
Körper-Chart 7–90 Tage). Layout pro Profil in der DB — getrennt vom Produktiv-Dashboard.
Vergleich:{' '}
Pilot-Übersicht (festes Standard-Layout)
.
Status: {bundle.custom ? 'individuell gespeichert' : 'Standard (nicht in DB)'}
)} {err &&{err}
} {msg &&{msg}
}