import { useState, useEffect } from 'react' import { LineChart, Line, BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer, CartesianGrid, Legend, ComposedChart, ReferenceArea, } from 'recharts' import { api } from '../utils/api' import { MACRO_CHART, NUTRITION_MACRO_CHART_BLOCK_PX } from '../utils/macroChartTheme' import dayjs from 'dayjs' const fmtDate = d => dayjs(d).format('DD.MM') function ChartCard({ title, loading, error, children }) { return (
{title}
{loading && (
)} {error && (
{error}
)} {!loading && !error && children}
) } function ScoreCard({ title, score, components, goal_mode, recommendation }) { const scoreColor = score >= 80 ? '#1D9E75' : score >= 60 ? '#F59E0B' : '#EF4444' return (
{title}
{/* Score Circle */}
{score}
/ 100
{/* Components Breakdown */}
{Object.entries(components).map(([key, value]) => { const barColor = value >= 80 ? '#1D9E75' : value >= 60 ? '#F59E0B' : '#EF4444' const label = { 'calorie_adherence': 'Kalorien-Adhärenz', 'protein_adherence': 'Protein-Adhärenz', 'intake_consistency': 'Konsistenz', 'food_quality': 'Lebensmittelqualität' }[key] || key return (
{label} {value}
) })}
{/* Recommendation */}
💡 {recommendation}
{/* Goal Mode */}
Optimiert für: {goal_mode || 'health'}
) } function WarningCard({ title, warning_level, triggers, message }) { const levelConfig = { 'warning': { icon: '⚠️', color: '#EF4444', bg: 'rgba(239, 68, 68, 0.1)' }, 'caution': { icon: '⚡', color: '#F59E0B', bg: 'rgba(245, 158, 11, 0.1)' }, 'none': { icon: '✅', color: '#1D9E75', bg: 'rgba(29, 158, 117, 0.1)' } }[warning_level] || levelConfig['none'] return (
{title}
{/* Status Badge */}
{levelConfig.icon} {message}
{/* Triggers List */} {triggers && triggers.length > 0 && (
Auffällige Indikatoren:
    {triggers.map((t, i) => (
  • {t}
  • ))}
)}
Heuristische Einschätzung, keine medizinische Diagnose
) } /** Wöchentliche Makro-Verteilung (E3) — für Verlauf neben Donut nutzbar. */ export function WeeklyMacroDistributionPanel({ macroWeeklyData, loading, error }) { if (loading) { return (
) } if (error) { return (
{error}
) } if (!macroWeeklyData || macroWeeklyData.metadata?.confidence === 'insufficient') { const msg = macroWeeklyData?.metadata?.message || 'Nicht genug Daten für Wochen-Analyse (min. 7 Tage)' return (
{msg}
) } const chartData = macroWeeklyData.data.labels.map((label, i) => ({ week: label, protein: macroWeeklyData.data.datasets[0]?.data[i], carbs: macroWeeklyData.data.datasets[1]?.data[i], fat: macroWeeklyData.data.datasets[2]?.data[i], })) const meta = macroWeeklyData.metadata return ( <>
Anteil der Kalorien aus jedem Makronährstoff pro Kalenderwoche (100 % gestapelt). Gut vergleichbar mit der Donut-Übersicht links.
[`${v}%`, name]} />
Ø Verteilung: P {meta.avg_protein_pct}% · KH {meta.avg_carbs_pct}% · F {meta.avg_fat_pct}% · Variabilität (CV): P{' '} {meta.protein_cv}% · KH {meta.carbs_cv}% · F {meta.fat_cv}%
) } /** * Nutrition Charts (E1–E5). Verlauf: `showWeeklyMacroDistribution={false}` wenn E3 separat (z. B. neben Donut) gerendert wird. */ export default function NutritionCharts({ days = 28, showWeeklyMacroDistribution = true, /** Verlauf: E5-Kachel liegt in nutrition-history-viz KPIs — doppelte Karte ausblenden */ hideEnergyAvailabilityCard = false, }) { const [energyData, setEnergyData] = useState(null) const [proteinData, setProteinData] = useState(null) const [macroWeeklyData, setMacroWeeklyData] = useState(null) const [adherenceData, setAdherenceData] = useState(null) const [warningData, setWarningData] = useState(null) const [loading, setLoading] = useState({}) const [errors, setErrors] = useState({}) // Weeks for macro distribution (proportional to days selected) const weeks = Math.max(4, Math.min(52, Math.ceil(days / 7))) useEffect(() => { loadCharts() }, [days, showWeeklyMacroDistribution, hideEnergyAvailabilityCard]) const loadCharts = async () => { const tasks = [ loadEnergyBalance(), loadProteinAdequacy(), loadAdherence(), ] if (!hideEnergyAvailabilityCard) { tasks.push(loadWarning()) } if (showWeeklyMacroDistribution) { tasks.splice(2, 0, loadMacroWeekly()) } await Promise.all(tasks) } const loadEnergyBalance = async () => { setLoading(l => ({...l, energy: true})) setErrors(e => ({...e, energy: null})) try { const data = await api.getEnergyBalanceChart(days) setEnergyData(data) } catch (err) { setErrors(e => ({...e, energy: err.message})) } finally { setLoading(l => ({...l, energy: false})) } } const loadProteinAdequacy = async () => { setLoading(l => ({...l, protein: true})) setErrors(e => ({...e, protein: null})) try { const data = await api.getProteinAdequacyChart(days) setProteinData(data) } catch (err) { setErrors(e => ({...e, protein: err.message})) } finally { setLoading(l => ({...l, protein: false})) } } const loadMacroWeekly = async () => { setLoading(l => ({...l, macro: true})) setErrors(e => ({...e, macro: null})) try { const data = await api.getWeeklyMacroDistributionChart(weeks) setMacroWeeklyData(data) } catch (err) { setErrors(e => ({...e, macro: err.message})) } finally { setLoading(l => ({...l, macro: false})) } } const loadAdherence = async () => { setLoading(l => ({...l, adherence: true})) setErrors(e => ({...e, adherence: null})) try { const data = await api.getNutritionAdherenceScore(days) setAdherenceData(data) } catch (err) { setErrors(e => ({...e, adherence: err.message})) } finally { setLoading(l => ({...l, adherence: false})) } } const loadWarning = async () => { setLoading(l => ({...l, warning: true})) setErrors(e => ({...e, warning: null})) try { const data = await api.getEnergyAvailabilityWarning(Math.min(days, 28)) setWarningData(data) } catch (err) { setErrors(e => ({...e, warning: err.message})) } finally { setLoading(l => ({...l, warning: false})) } } // E1: Energy Balance — klare Farben (kein hellgraues Gewirr) const renderEnergyBalance = () => { if (!energyData || energyData.metadata?.confidence === 'insufficient') { const msg = energyData?.metadata?.message || 'Nicht genug Ernährungsdaten für die Energiebilanz.' return (
{msg}
) } const chartData = energyData.data.labels.map((label, i) => ({ date: fmtDate(label), täglich: energyData.data.datasets[0]?.data[i], avg7d: energyData.data.datasets[1]?.data[i], avg14d: energyData.data.datasets[2]?.data[i], tdee: energyData.data.datasets[3]?.data[i], })) const balance = energyData.metadata?.energy_balance || 0 const balanceColor = balance < -200 ? '#EF4444' : balance > 200 ? '#F59E0B' : '#1D9E75' return ( <>
Tägliche Aufnahme, gleitende Mittel und geschätzter TDEE — Linien sind farblich getrennt (Legende unten).
Ø {energyData.metadata.avg_kcal} kcal/Tag · Balance: {balance > 0 ? '+' : ''} {balance} kcal/Tag · {energyData.metadata.data_points} Tage
) } // E2: Protein — Zielzone als Fläche, Linien klar von E1 abgrenzbar const renderProteinAdequacy = () => { if (!proteinData || proteinData.metadata?.confidence === 'insufficient') { const msg = proteinData?.metadata?.message || 'Nicht genug Protein-Daten für dieses Diagramm.' return (
{msg}
) } const tl = proteinData.metadata.target_low const th = proteinData.metadata.target_high const chartData = proteinData.data.labels.map((label, i) => ({ date: fmtDate(label), täglich: proteinData.data.datasets[0]?.data[i], avg7d: proteinData.data.datasets[1]?.data[i], avg28d: proteinData.data.datasets[2]?.data[i], })) return ( <>
Grüne Zone = empfohlenes Protein-Ziel (g/Tag). Tägliche Werte und Mittel — andere Farben als Energiebilanz oben.
{tl != null && th != null && ( )}
Ziel {tl}–{th} g/Tag · {proteinData.metadata.days_in_target}/{proteinData.metadata.data_points} Tage im Zielbereich ( {proteinData.metadata.target_compliance_pct}%)
) } // E4: Nutrition Adherence Score const renderAdherence = () => { if (!adherenceData || adherenceData.metadata?.confidence === 'insufficient') { return (
Nicht genug Daten (min. 7 Tage)
) } return ( ) } // E5: Energy Availability Warning const renderWarning = () => { if (!warningData) { return (
Keine Daten verfügbar
) } return ( ) } return (
{renderEnergyBalance()} {renderProteinAdequacy()} {showWeeklyMacroDistribution && ( )} {!loading.adherence && !errors.adherence && renderAdherence()} {!hideEnergyAvailabilityCard && !loading.warning && !errors.warning && renderWarning()}
) }