import { useCallback, useEffect, useState } from 'react' import { Link } from 'react-router-dom' import { Download, FileText, Plus, Save, Trash2 } from 'lucide-react' import { api, formatFastApiDetail } from '../utils/api' import { useAuth } from '../context/AuthContext' import { useProfile } from '../context/ProfileContext' import UsageBadge from '../components/UsageBadge' import BodyHistoryVizConfigEditor from '../widgetSystem/BodyHistoryVizConfigEditor' import NutritionHistoryVizConfigEditor from '../widgetSystem/NutritionHistoryVizConfigEditor' import FitnessHistoryVizConfigEditor from '../widgetSystem/FitnessHistoryVizConfigEditor' import RecoveryHistoryVizConfigEditor from '../widgetSystem/RecoveryHistoryVizConfigEditor' import HistoryOverviewVizConfigEditor from '../widgetSystem/HistoryOverviewVizConfigEditor' import { BODY_CHART_DAYS_DEFAULT, BODY_CHART_DAYS_MAX, BODY_CHART_DAYS_MIN, normalizeBodyChartDays, } from '../widgetSystem/bodyChartDays' const VIZ_BUNDLES_FALLBACK = [ { id: 'body_history_viz', title: 'Körper (Verlauf-Bundle)' }, { id: 'nutrition_history_viz', title: 'Ernährung (Verlauf-Bundle)' }, { id: 'fitness_history_viz', title: 'Fitness (Verlauf-Bundle)' }, { id: 'recovery_history_viz', title: 'Erholung (Verlauf-Bundle)' }, { id: 'history_overview_viz', title: 'Gesamtübersicht (Korrelationen)' }, ] export default function ReportConfigurePage() { const { canExport } = useAuth() const { activeProfile } = useProfile() const [exportUsage, setExportUsage] = useState(null) const [catalog, setCatalog] = useState(null) const [definitions, setDefinitions] = useState([]) const [selectedId, setSelectedId] = useState(null) const [busy, setBusy] = useState(false) const [msg, setMsg] = useState(null) const [err, setErr] = useState(null) const chartDaysMin = catalog?.chart_days?.min ?? BODY_CHART_DAYS_MIN const chartDaysMax = catalog?.chart_days?.max ?? BODY_CHART_DAYS_MAX const vizBundles = catalog?.viz_bundles?.length ? catalog.viz_bundles : VIZ_BUNDLES_FALLBACK const selected = definitions.find((d) => d.id === selectedId) || null const load = useCallback(async () => { setErr(null) try { const [cat, bundle] = await Promise.all([api.getReportsCatalog(), api.listReportDefinitions()]) setCatalog(cat) setDefinitions(bundle.definitions || []) } catch (e) { setErr(formatFastApiDetail(null, e.message)) } }, []) useEffect(() => { api.getFeatureUsage().then((features) => { const exportFeature = features.find((f) => f.feature_id === 'data_export') setExportUsage(exportFeature) }).catch(() => {}) }, []) useEffect(() => { if (!activeProfile?.id) return load() }, [activeProfile?.id, load]) useEffect(() => { if (!definitions.length) { setSelectedId(null) return } if (!selectedId || !definitions.some((d) => d.id === selectedId)) { setSelectedId(definitions[0].id) } }, [definitions, selectedId]) const patchSelectedPayload = useCallback((fn) => { setDefinitions((defs) => defs.map((d) => { if (d.id !== selectedId) return d const nextPayload = fn(d.payload || { version: 1, document_title: '', blocks: [] }) return { ...d, payload: nextPayload } }), ) }, [selectedId]) const reportNewBlock = (kind) => { const charts = catalog?.charts || [] const first = charts[0] const firstBundleId = vizBundles[0]?.id || 'body_history_viz' if (kind === 'section') return { type: 'section', title: 'Neue Überschrift' } if (kind === 'viz_bundle') return { type: 'viz_bundle', bundle_id: firstBundleId, config: {} } if (kind === 'chart') return { type: 'chart', chart_id: first?.id || 'weight_trend', window_days: first?.default_window_days || 28, } return { type: 'ai_insight', title: '', insight_id: null } } const handleCreateDefinition = async () => { setBusy(true) setErr(null) setMsg(null) try { const r = await api.createReportDefinition({ name: `Bericht ${definitions.length + 1}`, }) const d = r.definition setDefinitions((x) => [...x, d]) setSelectedId(d.id) setMsg('Neuer Bericht angelegt. Inhalt speichern nicht vergessen, wenn du Änderungen machst.') } catch (e) { setErr(formatFastApiDetail(null, e.message)) } finally { setBusy(false) } } const handleSave = async () => { if (!selected) return if (!selected.payload?.blocks?.length) { setErr('Mindestens ein Block erforderlich.') return } setBusy(true) setErr(null) setMsg(null) try { await api.updateReportDefinition(selected.id, { name: selected.name?.trim() || 'Bericht', payload: selected.payload, }) setMsg('Gespeichert.') await load() } catch (e) { setErr(formatFastApiDetail(null, e.message)) } finally { setBusy(false) } } const handleDeleteDefinition = async () => { if (!selected) return if (!confirm(`Bericht „${selected.name}“ wirklich löschen?`)) return setBusy(true) setErr(null) setMsg(null) try { await api.deleteReportDefinition(selected.id) setDefinitions((defs) => defs.filter((d) => d.id !== selected.id)) setSelectedId(null) setMsg('Bericht gelöscht.') } catch (e) { setErr(formatFastApiDetail(null, e.message)) } finally { setBusy(false) } } const handleGeneratePdf = async () => { if (!selected) return setBusy(true) setErr(null) setMsg(null) try { await api.generateStructuredReportPdf(selected.id) setMsg('PDF wurde heruntergeladen.') } catch (e) { setErr(formatFastApiDetail(null, e.message)) } finally { setBusy(false) } } const setSelectedName = (name) => { setDefinitions((defs) => defs.map((d) => (d.id === selectedId ? { ...d, name } : d))) } const vizBundleChartDays = (config) => normalizeBodyChartDays(config?.chart_days ?? BODY_CHART_DAYS_DEFAULT) if (!catalog) { return (
Lade Katalog…
Hier legst du einen oder mehrere strukturierte PDF-Berichte an. Pro Block vom Typ „Verlauf-Bundle“ gilt ein Zeitraum in Tagen (wie bei der Übersicht).{' '} Technisch: Es sind dieselben{' '} Daten-Bundles und dieselbe Konfiguration wie bei den Verlauf-Widgets — im PDF werden sie serverseitig gerendert (nicht die React-Komponenten der Startseite).
{!canExport && (Noch kein Bericht vorhanden. Lege mit „Neuer Bericht“ einen Standard an.
)} {canExport && selected && ( <> setSelectedName(e.target.value)} style={{ marginBottom: 14, maxWidth: 420 }} /> patchSelectedPayload((p) => ({ ...p, document_title: e.target.value, })) } style={{ marginBottom: 14 }} />