import { useState, useEffect } from 'react'
import {
LineChart, Line, BarChart, Bar,
XAxis, YAxis, Tooltip, ResponsiveContainer, CartesianGrid, Legend
} from 'recharts'
import { api } from '../utils/api'
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 */}
{/* 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 (
)
})}
{/* 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
)
}
/**
* Nutrition Charts Component (E1-E5) - Konzept-konform v2.0
*
* E1: Energy Balance (mit 7d/14d Durchschnitten)
* E2: Protein Adequacy (mit 7d/28d Durchschnitten)
* E3: Weekly Macro Distribution (100% gestapelte Balken)
* E4: Nutrition Adherence Score (0-100, goal-aware)
* E5: Energy Availability Warning (Ampel-System)
*/
export default function NutritionCharts({ days = 28 }) {
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])
const loadCharts = async () => {
await Promise.all([
loadEnergyBalance(),
loadProteinAdequacy(),
loadMacroWeekly(),
loadAdherence(),
loadWarning()
])
}
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 Timeline (mit 7d/14d Durchschnitten)
const renderEnergyBalance = () => {
if (!energyData || energyData.metadata?.confidence === 'insufficient') {
return
Nicht genug Ernährungsdaten (min. 7 Tage)
}
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 (
<>
Ø {energyData.metadata.avg_kcal} kcal/Tag ·
Balance: {balance > 0 ? '+' : ''}{balance} kcal/Tag
· {energyData.metadata.data_points} Tage
>
)
}
// E2: Protein Adequacy Timeline (mit 7d/28d Durchschnitten)
const renderProteinAdequacy = () => {
if (!proteinData || proteinData.metadata?.confidence === 'insufficient') {
return
Nicht genug Protein-Daten (min. 7 Tage)
}
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],
targetLow: proteinData.data.datasets[3]?.data[i],
targetHigh: proteinData.data.datasets[4]?.data[i]
}))
return (
<>
{proteinData.metadata.days_in_target}/{proteinData.metadata.data_points} Tage im Zielbereich ({proteinData.metadata.target_compliance_pct}%)
>
)
}
// E3: Weekly Macro Distribution (100% gestapelte Balken)
const renderMacroWeekly = () => {
if (!macroWeeklyData || macroWeeklyData.metadata?.confidence === 'insufficient') {
return
Nicht genug Daten für Wochen-Analyse (min. 7 Tage)
}
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 (
<>
Ø Verteilung: P {meta.avg_protein_pct}% · C {meta.avg_carbs_pct}% · F {meta.avg_fat_pct}% ·
Konsistenz (CV): P {meta.protein_cv}% · C {meta.carbs_cv}% · F {meta.fat_cv}%
>
)
}
// 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()}
{renderMacroWeekly()}
{!loading.adherence && !errors.adherence && renderAdherence()}
{!loading.warning && !errors.warning && renderWarning()}
)
}