import { useState, useEffect } from 'react'
import { useNavigate, useLocation } from 'react-router-dom'
import { useProfile } from '../context/ProfileContext'
import {
LineChart, Line, BarChart, Bar,
XAxis, YAxis, Tooltip, ResponsiveContainer, CartesianGrid,
ReferenceLine, PieChart, Pie, Cell, ComposedChart
} from 'recharts'
import { ChevronRight, Brain, ChevronDown, ChevronUp, Trash2, Info } from 'lucide-react'
import { api } from '../utils/api'
import { photoMonthKey, photoSortKey, formatPhotoCaption } from '../utils/photoDisplay'
import { getBfCategory } from '../utils/calc'
import { getStatusColor, getStatusBg } from '../utils/interpret'
import Markdown from '../utils/Markdown'
import TrainingTypeDistribution from '../components/TrainingTypeDistribution'
import NutritionCharts from '../components/NutritionCharts'
import RecoveryCharts from '../components/RecoveryCharts'
import dayjs from 'dayjs'
import 'dayjs/locale/de'
dayjs.locale('de')
function rollingAvg(arr, key, window=7) {
return arr.map((d,i) => {
const s = arr.slice(Math.max(0,i-window+1),i+1).map(x=>x[key]).filter(v=>v!=null)
return s.length ? {...d,[`${key}_avg`]:Math.round(s.reduce((a,b)=>a+b)/s.length*10)/10} : d
})
}
const fmtDate = d => dayjs(d).format('DD.MM')
function NavToCaliper() {
const nav = useNavigate()
return
}
function NavToCircum() {
const nav = useNavigate()
return
}
function EmptySection({ text, to, toLabel }) {
const nav = useNavigate()
return (
🤖 KI-AUSWERTUNGEN
{slugs.map(slug=>(
{relevant.length===0 && (
Noch keine Auswertung. Klicke oben um eine zu erstellen.
)}
{relevant.map(ins=>(
setExpanded(expanded===ins.id?null:ins.id)}>
{dayjs(ins.created).format('DD. MMM YYYY, HH:mm')} · {LABELS[ins.scope]||ins.scope}
{expanded===ins.id?
:}
{expanded===ins.id &&
}
))}
)
}
// ── Period selector ───────────────────────────────────────────────────────────
function PeriodSelector({ value, onChange }) {
const opts = [{v:30,l:'30 Tage'},{v:90,l:'90 Tage'},{v:180,l:'6 Monate'},{v:365,l:'1 Jahr'},{v:9999,l:'Alles'}]
return (
{opts.map(o=>(
))}
)
}
// ── Body Section — Layer 2b: Daten nur aus GET /api/charts/body-history-viz ──
function BodySection({ profile, insights, onRequest, loadingSlug, filterActiveSlugs }) {
const [period, setPeriod] = useState(90)
const [groupedGoals, setGroupedGoals] = useState(null)
const [viz, setViz] = useState(null)
const [vizLoading, setVizLoading] = useState(true)
const [vizError, setVizError] = useState(null)
const sex = profile?.sex || 'm'
useEffect(() => {
let cancelled = false
api.listGoalsGrouped()
.then(g => { if (!cancelled) setGroupedGoals(g) })
.catch(() => { if (!cancelled) setGroupedGoals({}) })
return () => { cancelled = true }
}, [])
useEffect(() => {
let cancelled = false
setVizLoading(true)
setVizError(null)
api.getBodyHistoryViz(period)
.then(data => {
if (!cancelled) {
setViz(data)
setVizLoading(false)
}
})
.catch(e => {
if (!cancelled) {
setVizError(e.message || 'Laden fehlgeschlagen')
setVizLoading(false)
}
})
return () => { cancelled = true }
}, [period])
const w = viz?.weight
const cal = viz?.caliper
const circ = viz?.circumference
const summary = viz?.summary || {}
const wCd = (w?.series || []).map(row => ({
date: fmtDate(row.date),
weight: row.weight,
avg7: row.avg7,
avg14: row.avg14,
}))
const hasWeight = (w?.data_points || 0) >= 2
const avgAll = w?.overall_avg_kg
const minW = w?.min_kg
const maxW = w?.max_kg
const trendPeriods = w?.trend_periods || []
const bfCd = (cal?.series || []).map(s => ({
date: fmtDate(s.date),
bf: s.body_fat_pct,
}))
const propChartData = (circ?.proportion_series || []).map(p => ({
date: fmtDate(p.date),
vTaper: p.v_taper_cm,
vTaper_avg: p.v_taper_cm_avg,
belly: p.belly_cm,
}))
const showBellyOnProp = propChartData.some(d => d.belly != null && d.belly !== undefined)
const idxSeriesRaw = circ?.index_series || []
const idxSeries = idxSeriesRaw.map(row => ({ ...row, date: fmtDate(row.date) }))
const idxOk = circ?.index_usable
const cirCd = (circ?.fallback_multiline || []).map(r => ({
date: fmtDate(r.date),
waist: r.waist,
hip: r.hip,
belly: r.belly,
}))
const bfCat = summary.body_fat_pct != null ? getBfCategory(summary.body_fat_pct, sex) : null
const goalW = viz?.profile?.goal_weight_kg ?? profile?.goal_weight
const goalBf = viz?.profile?.goal_bf_pct ?? profile?.goal_bf_pct
const rules = (viz?.interpretation_tiles || []).map(t => ({
category: t.category,
icon: t.icon,
status: t.status,
title: t.title,
detail: t.detail,
value: t.value,
related_placeholder_keys: t.related_placeholder_keys,
}))
const kpiTiles = buildBodyKpiTiles({
summary,
rules,
trendPeriods,
minW,
maxW,
avgAll,
dataPoints: w?.data_points,
sex,
bfCat,
goalW,
})
const hasAnyData =
(w?.data_points > 0) ||
(cal?.data_points > 0) ||
(cirCd.length > 0)
if (vizLoading && !viz) {
return (
)
}
if (vizError) {
return (
)
}
if (!hasAnyData) {
return (
)
}
return (
Daten und Kennzahlen aus dem Backend-Bundle (gleiche Quelle wie Platzhalter). Training: Verlauf → Aktivität.
{viz?.meta?.layer_2a_alignment && (
{viz.meta.layer_2a_alignment}
)}
{vizLoading && (
Aktualisiere…
)}
{hasWeight && (
Gewicht · {w?.data_points || 0} Einträge
{avgAll != null && (
)}
{goalW != null && (
)}
[`${v} kg`, n === 'weight' ? 'Täglich' : n === 'avg7' ? 'Ø 7 Tage' : 'Ø 14 Tage']} />
● Täglich
Ø 7T
Ø 14T
Ø Gesamt
)}
{bfCd.length >= 2 && (
[`${v}%`, 'KF%']} />
{goalBf != null && }
Magermasse aus Gewicht und KF% — zweite Kurve entfällt.
)}
{propChartData.length >= 2 && (
Silhouette & Proportion
V-Taper (Brust − Taille) in cm.
{showBellyOnProp && <> Bauch (rechte Achse).>}
{showBellyOnProp && }
{
if (name === 'vTaper' || name === 'vTaper_avg') return [`${v} cm`, name === 'vTaper_avg' ? 'Ø V-Taper (3 Messungen)' : 'Brust − Taille']
if (name === 'belly') return [`${v} cm`, 'Bauch']
return [v, name]
}}
/>
{showBellyOnProp && }
Brust − Taille
gleitender Mittelwert
{showBellyOnProp && Bauch (cm)}
)}
{idxOk && (
Relative Entwicklung der Umfänge
Index 100 = erste Messung im Zeitraum.
[`${v} Index`, n === 'chest_idx' ? 'Brust' : n === 'waist_idx' ? 'Taille' : 'Bauch']} />
{idxSeries.some(d => d.chest_idx != null) && }
{idxSeries.some(d => d.waist_idx != null) && }
{idxSeries.some(d => d.belly_idx != null) && }
Brust
Taille
Bauch
)}
{propChartData.length < 2 && cirCd.length >= 2 && (
Umfänge (Taille / Hüfte / Bauch)
Mit Brust- und Taillenumfang erscheint die Proportionen-Ansicht oben.
[`${v} cm`, n]} />
{cirCd.some(d => d.belly) && }
)}
)
}
// ── Nutrition Section ─────────────────────────────────────────────────────────
function NutritionSection({ nutrition, weights, profile, insights, onRequest, loadingSlug, filterActiveSlugs }) {
const [period, setPeriod] = useState(30)
if (!nutrition?.length) return (
)
const cutoff = dayjs().subtract(period,'day').format('YYYY-MM-DD')
const filtN = nutrition.filter(d=>period===9999||d.date>=cutoff)
const sorted = [...filtN].sort((a,b)=>a.date.localeCompare(b.date))
if (!filtN.length) return (
)
const n = filtN.length
const avgKcal = Math.round(filtN.reduce((s,d)=>s+(d.kcal||0),0)/n)
const avgProtein = Math.round(filtN.reduce((s,d)=>s+(d.protein_g||0),0)/n*10)/10
const avgFat = Math.round(filtN.reduce((s,d)=>s+(d.fat_g||0),0)/n*10)/10
const avgCarbs = Math.round(filtN.reduce((s,d)=>s+(d.carbs_g||0),0)/n*10)/10
const latestW = weights?.[0]?.weight||80
const ptLow = Math.round(latestW*1.6)
const ptHigh = Math.round(latestW*2.2)
const proteinOk = avgProtein>=ptLow
// Stacked macro bar (daily)
const cdMacro = sorted.map(d=>({
date: fmtDate(d.date),
Protein: Math.round(d.protein_g||0),
KH: Math.round(d.carbs_g||0),
Fett: Math.round(d.fat_g||0),
kcal: Math.round(d.kcal||0),
}))
// Pie
const totalMacroKcal = avgProtein*4+avgCarbs*4+avgFat*9
const pieData = [
{name:'Protein',value:Math.round(avgProtein*4/totalMacroKcal*100),color:'#1D9E75'},
{name:'KH', value:Math.round(avgCarbs*4/totalMacroKcal*100), color:'#D4537E'},
{name:'Fett', value:Math.round(avgFat*9/totalMacroKcal*100), color:'#378ADD'},
]
// Weekly macro bars
const weeklyMap={}
filtN.forEach(d=>{
const wk=dayjs(d.date).format('YYYY-WW')
const weekNum = (() => { const dt=new Date(d.date); dt.setHours(0,0,0,0); dt.setDate(dt.getDate()+4-(dt.getDay()||7)); const y=new Date(dt.getFullYear(),0,1); return Math.ceil(((dt-y)/86400000+1)/7) })()
if(!weeklyMap[wk]) weeklyMap[wk]={label:'KW'+weekNum,n:0,protein:0,carbs:0,fat:0,kcal:0}
weeklyMap[wk].protein+=d.protein_g||0; weeklyMap[wk].carbs+=d.carbs_g||0
weeklyMap[wk].fat+=d.fat_g||0; weeklyMap[wk].kcal+=d.kcal||0; weeklyMap[wk].n++
})
const weeklyData=Object.values(weeklyMap).slice(-12).map(w=>({
label:w.label,
Protein:Math.round(w.protein/w.n),
KH:Math.round(w.carbs/w.n),
Fett:Math.round(w.fat/w.n),
kcal:Math.round(w.kcal/w.n),
}))
// Rules
const macroRules=[]
if(!proteinOk) macroRules.push({status:'bad',icon:'🥩',category:'Protein',
title:`Unterversorgung: ${avgProtein}g/Tag (Ziel ${ptLow}–${ptHigh}g)`,
detail:`1,6–2,2g/kg KG. Fehlend: ~${ptLow-Math.round(avgProtein)}g täglich. Konsequenz: Muskelverlust bei Defizit.`,
value:avgProtein+'g'})
else macroRules.push({status:'good',icon:'🥩',category:'Protein',
title:`Gut: ${avgProtein}g/Tag (Ziel ${ptLow}–${ptHigh}g)`,
detail:`Ausreichend für Muskelerhalt und -aufbau.`,value:avgProtein+'g'})
const protPct=Math.round(avgProtein*4/totalMacroKcal*100)
if(protPct<20) macroRules.push({status:'warn',icon:'📊',category:'Makro-Anteil',
title:`Protein-Anteil niedrig: ${protPct}% der Kalorien`,
detail:`Empfehlung: 25–35%. Aktuell: ${protPct}% P / ${Math.round(avgCarbs*4/totalMacroKcal*100)}% KH / ${Math.round(avgFat*9/totalMacroKcal*100)}% F`,
value:protPct+'%'})
return (
{[['Ø Kalorien',avgKcal+' kcal','#EF9F27'],['Ø Protein',avgProtein+'g',proteinOk?'#1D9E75':'#D85A30'],
['Ø Fett',avgFat+'g','#378ADD'],['Ø KH',avgCarbs+'g','#D4537E'],
['Einträge',n+' T','var(--text3)']].map(([l,v,c])=>(
))}
{/* Stacked macro bars (daily) */}
Makroverteilung täglich (g) · {sorted[0]?.date?.slice(0,7)} – {sorted[sorted.length-1]?.date?.slice(0,7)}
[`${v}g`,n]}/>
Protein
KH
Fett
Protein-Ziel
{/* Pie + macro breakdown */}
Ø Makroverteilung · {n} Tage ({sorted[0]?.date?.slice(0,10)} – {sorted[sorted.length-1]?.date?.slice(0,10)})
{pieData.map((e,i)=>| )}
|
[`${v}%`,n]}/>
{pieData.map(p=>(
{p.name}
{p.value}%
{Math.round(p.name==='Protein'?avgProtein:p.name==='KH'?avgCarbs:avgFat)}g
{p.name==='Protein' &&
{proteinOk?'✓':'⚠️'} Ziel {ptLow}g
}
))}
Gesamt: {avgKcal} kcal/Tag
{/* Weekly stacked bars */}
{weeklyData.length>=2 && (
Makros pro Woche (Ø g/Tag)
[`${v}g`,n]}/>
)}
BEWERTUNG
{macroRules.map((item,i)=>
)}
{/* New Nutrition Charts (Phase 0c) */}
)
}
// ── Activity Section ──────────────────────────────────────────────────────────
function ActivitySection({ activities, insights, onRequest, loadingSlug, filterActiveSlugs, globalQualityLevel }) {
const [period, setPeriod] = useState(30)
if (!activities?.length) return (
)
const cutoff = dayjs().subtract(period,'day').format('YYYY-MM-DD')
// Issue #31: Backend already filters by global quality level - only filter by period here
const filtA = activities.filter(d => period === 9999 || d.date >= cutoff)
const byDate={}
filtA.forEach(a=>{ byDate[a.date]=(byDate[a.date]||0)+(a.kcal_active||0) })
const cd=Object.entries(byDate).sort((a,b)=>a[0].localeCompare(b[0])).map(([date,kcal])=>({date:fmtDate(date),kcal:Math.round(kcal)}))
const totalKcal=Math.round(filtA.reduce((s,a)=>s+(a.kcal_active||0),0))
const totalMin =Math.round(filtA.reduce((s,a)=>s+(a.duration_min||0),0))
const hrData =filtA.filter(a=>a.hr_avg)
const avgHr =hrData.length?Math.round(hrData.reduce((s,a)=>s+a.hr_avg,0)/hrData.length):null
const types={}; filtA.forEach(a=>{ types[a.activity_type]=(types[a.activity_type]||0)+1 })
const topTypes=Object.entries(types).sort((a,b)=>b[1]-a[1])
const daysWithAct=new Set(filtA.map(a=>a.date)).size
const totalDays=Math.min(period,dayjs().diff(dayjs(filtA[filtA.length-1]?.date),'day')+1)
const consistency=totalDays>0?Math.round(daysWithAct/totalDays*100):0
const actRules=[{
status:consistency>=70?'good':consistency>=40?'warn':'bad',
icon:'📅', category:'Konsistenz',
title:`${consistency}% aktive Tage (${daysWithAct}/${Math.min(period,30)} Tage)`,
detail:consistency>=70?'Ausgezeichnete Regelmäßigkeit.':consistency>=40?'Ziel: 4–5 Einheiten/Woche.':'Mehr Regelmäßigkeit empfohlen.',
value:consistency+'%'
}]
return (
{/* Issue #31: Show active global quality filter */}
{globalQualityLevel && globalQualityLevel !== 'all' && (
{globalQualityLevel === 'quality' && '✓ Filter: Hochwertig (excellent, good, acceptable)'}
{globalQualityLevel === 'very_good' && '✓✓ Filter: Sehr gut (excellent, good)'}
{globalQualityLevel === 'excellent' && '⭐ Filter: Exzellent (nur excellent)'}
Hier ändern →
)}
{[['Trainings',filtA.length,'var(--text1)'],['Kcal',totalKcal,'#EF9F27'],
['Stunden',Math.round(totalMin/60*10)/10,'#378ADD'],
avgHr?['Ø HF',avgHr+' bpm','#D85A30']:null].filter(Boolean).map(([l,v,c])=>(
))}
Aktive Kalorien / Tag
[`${v} kcal`]}/>
Trainingsarten
{topTypes.map(([type,count])=>(
BEWERTUNG
{actRules.map((item,i)=>
)}
)
}
// ── Correlation Section ───────────────────────────────────────────────────────
function CorrelationSection({ corrData, insights, profile, onRequest, loadingSlug, filterActiveSlugs }) {
const filtered = (corrData||[]).filter(d=>d.kcal&&d.weight)
if (filtered.length < 5) return (
)
const sex = profile?.sex||'m'
const height = profile?.height||178
const latestW = filtered[filtered.length-1]?.weight||80
const age = profile?.dob ? Math.floor((Date.now()-new Date(profile.dob))/(365.25*24*3600*1000)) : 35
const bmr = sex==='m' ? 10*latestW+6.25*height-5*age+5 : 10*latestW+6.25*height-5*age-161
const tdee = Math.round(bmr*1.4) // light activity baseline
// Chart 1: Kcal vs Weight
const kcalVsW = rollingAvg(filtered.map(d=>({...d,date:fmtDate(d.date)})),'kcal')
// Chart 2: Protein vs Lean Mass (only days with both)
const protVsLean = filtered.filter(d=>d.protein_g&&d.lean_mass)
.map(d=>({date:fmtDate(d.date),protein:d.protein_g,lean:d.lean_mass}))
// Chart 3: Activity kcal vs Weight change
const actVsW = filtered.filter(d=>d.weight)
.map((d,i,arr)=>{
const prev = arr[i-1]
return {
date: fmtDate(d.date),
weight: d.weight,
weightDelta: prev ? Math.round((d.weight-prev.weight)*10)/10 : null,
kcal: d.kcal||0,
}
}).filter(d=>d.weightDelta!==null)
// Chart 4: Calorie balance (intake - estimated TDEE)
const balance = filtered.map(d=>({
date: fmtDate(d.date),
balance: Math.round((d.kcal||0) - tdee),
}))
const balWithAvg = rollingAvg(balance,'balance')
const avgBalance = Math.round(balance.reduce((s,d)=>s+d.balance,0)/balance.length)
// ── Correlation insights ──
const corrInsights = []
// 1. Kcal → Weight correlation
if (filtered.length >= 14) {
const highKcal = filtered.filter(d=>d.kcal>tdee+200)
const lowKcal = filtered.filter(d=>d.kcal=3 && lowKcal.length>=3) {
const avgWHigh = Math.round(highKcal.reduce((s,d)=>s+d.weight,0)/highKcal.length*10)/10
const avgWLow = Math.round(lowKcal.reduce((s,d)=>s+d.weight,0)/lowKcal.length*10)/10
corrInsights.push({
icon:'📊', status: avgWLow < avgWHigh ? 'good' : 'warn',
title: avgWLow < avgWHigh
? `Kalorienreduktion wirkt: Ø ${avgWLow}kg bei Defizit vs. ${avgWHigh}kg bei Überschuss`
: `Kein klarer Kalorieneffekt auf Gewicht erkennbar`,
detail: `Tage mit Überschuss (>${tdee+200} kcal): Ø ${avgWHigh}kg · Tage mit Defizit (<${tdee-200} kcal): Ø ${avgWLow}kg`,
})
}
}
// 2. Protein → Lean mass
if (protVsLean.length >= 3) {
const ptLow = Math.round(latestW*1.6)
const highProt = protVsLean.filter(d=>d.protein>=ptLow)
const lowProt = protVsLean.filter(d=>d.protein=2 && lowProt.length>=2) {
const avgLH = Math.round(highProt.reduce((s,d)=>s+d.lean,0)/highProt.length*10)/10
const avgLL = Math.round(lowProt.reduce((s,d)=>s+d.lean,0)/lowProt.length*10)/10
corrInsights.push({
icon:'🥩', status: avgLH >= avgLL ? 'good' : 'warn',
title: `Hohe Proteinzufuhr (≥${ptLow}g): Ø ${avgLH}kg Mager · Niedrig: Ø ${avgLL}kg`,
detail: `${highProt.length} Messpunkte mit hoher vs. ${lowProt.length} mit niedriger Proteinzufuhr verglichen.`,
})
}
}
// 3. Avg balance
corrInsights.push({
icon: avgBalance < -100 ? '✅' : avgBalance > 200 ? '⬆️' : '➡️',
status: avgBalance < -100 ? 'good' : avgBalance > 300 ? 'warn' : 'good',
title: `Ø Kalorienbilanz: ${avgBalance>0?'+':''}${avgBalance} kcal/Tag`,
detail: `Geschätzter TDEE: ${tdee} kcal (Mifflin-St Jeor ×1,4). ${
avgBalance<-500?'Starkes Defizit – Muskelerhalt durch ausreichend Protein sicherstellen.':
avgBalance<-100?'Moderates Defizit – ideal für Fettabbau bei Muskelerhalt.':
avgBalance>300?'Kalorienüberschuss – günstig für Muskelaufbau, Fettzunahme möglich.':
'Nahezu ausgeglichen – Gewicht sollte stabil bleiben.'}`,
})
return (
{/* Chart 1: Kcal vs Weight */}
📉 Kalorien (Ø 7T) vs. Gewicht
[`${Math.round(v)} ${n==='weight'?'kg':'kcal'}`,n==='kcal_avg'?'Ø Kalorien':'Gewicht']}/>
Gestrichelt: geschätzter TDEE {tdee} kcal · — Kalorien · — Gewicht
{/* Chart 2: Calorie balance */}
⚖️ Kalorienbilanz (Aufnahme − TDEE {tdee} kcal)
[`${v>0?'+':''}${v} kcal`,n==='balance_avg'?'Ø 7T Bilanz':'Tagesbilanz']}/>
Über 0 = Überschuss · Unter 0 = Defizit · Ø {avgBalance>0?'+':''}{avgBalance} kcal/Tag
{/* Chart 3: Protein vs Lean Mass */}
{protVsLean.length >= 3 && (
🥩 Protein vs. Magermasse
[`${v}${n==='protein'?'g':' kg'}`,n==='protein'?'Protein':'Mager']}/>
— Protein g/Tag · ● Magermasse kg
)}
{/* Correlation insights */}
{corrInsights.length > 0 && (
KORRELATIONSAUSSAGEN
{corrInsights.map((item,i) => (
{item.icon}
{item.title}
{item.detail}
))}
ℹ️ TDEE-Schätzung basiert auf Mifflin-St Jeor ×1,4 (leicht aktiv). Für genauere Werte Aktivitätsdaten erfassen.
)}
)
}
// ── Photo Grid ────────────────────────────────────────────────────────────────
function PhotoGrid() {
const [photos, setPhotos] = useState([])
const [big, setBig] = useState(null)
const load = () => api.listPhotos().then(setPhotos)
useEffect(() => {
load()
}, [])
const handleDelete = async (id) => {
if (!confirm('Dieses Foto löschen?')) return
try {
await api.deletePhoto(id)
if (big === id) setBig(null)
await load()
} catch (e) {
alert(e.message || 'Löschen fehlgeschlagen')
}
}
if (!photos.length) {
return
}
const sorted = [...photos].sort((a, b) => photoSortKey(b).localeCompare(photoSortKey(a)))
const byMonth = new Map()
for (const p of sorted) {
const mk = photoMonthKey(p)
if (!byMonth.has(mk)) byMonth.set(mk, [])
byMonth.get(mk).push(p)
}
const monthKeys = [...byMonth.keys()].sort((a, b) => b.localeCompare(a))
return (
<>
{big && (
setBig(null)}
role="presentation"
>
)}
{monthKeys.map((mk) => (
{dayjs(`${mk}-01`).format('MMMM YYYY')}
{byMonth.get(mk).map((p) => (
})
setBig(p.id)}
alt=""
/>
{formatPhotoCaption(p)}
))}
))}
>
)
}
// ── Main ──────────────────────────────────────────────────────────────────────
// ── Recovery Section ──────────────────────────────────────────────────────────
function RecoverySection({ insights, onRequest, loadingSlug, filterActiveSlugs }) {
const [period, setPeriod] = useState(28)
return (
Erholung, Schlaf, HRV, Ruhepuls und weitere Vitalwerte im Überblick.
{/* Recovery Charts (Phase 0c) */}
)
}
const TABS = [
{ id:'body', label:'⚖️ Körper' },
{ id:'nutrition', label:'🍽️ Ernährung' },
{ id:'activity', label:'🏋️ Aktivität' },
{ id:'recovery', label:'😴 Erholung' },
{ id:'correlation', label:'🔗 Korrelation' },
{ id:'photos', label:'📷 Fotos' },
]
export default function History() {
const { activeProfile } = useProfile() // Issue #31: Get global quality filter
const location = useLocation?.() || {}
const [tab, setTab] = useState((location.state?.tab)||'body')
const [weights, setWeights] = useState([])
const [calipers, setCalipers] = useState([])
const [circs, setCircs] = useState([])
const [nutrition, setNutrition] = useState([])
const [activities, setActivities] = useState([])
const [corrData, setCorrData] = useState([])
const [insights, setInsights] = useState([])
const [prompts, setPrompts] = useState([])
const [profile, setProfile] = useState(null)
const [loading, setLoading] = useState(true)
const [loadingSlug,setLoadingSlug]= useState(null)
const loadAll = () => Promise.all([
api.listWeight(365), api.listCaliper(), api.listCirc(),
api.listNutrition(90), api.listActivity(25_000),
api.nutritionCorrelations(), api.latestInsights(), api.getProfile(),
api.listPrompts(),
]).then(([w,ca,ci,n,a,corr,ins,p,pr])=>{
setWeights(w); setCalipers(ca); setCircs(ci)
setNutrition(n); setActivities(a); setCorrData(corr)
setInsights(Array.isArray(ins)?ins:[]); setProfile(p)
setPrompts(Array.isArray(pr)?pr:[])
setLoading(false)
})
useEffect(() => {
loadAll()
}, [activeProfile?.quality_filter_level])
useEffect(() => {
const t = location.state?.tab
if (t && TABS.some(x => x.id === t)) setTab(t)
}, [location.state?.tab])
const requestInsight = async (slug) => {
setLoadingSlug(slug)
try {
const result = await api.runInsight(slug)
// result is already JSON, not a Response object
const ins = await api.latestInsights()
setInsights(Array.isArray(ins)?ins:[])
} catch(e){
alert('KI-Fehler: '+e.message)
}
finally{ setLoadingSlug(null) }
}
if(loading) return
// Filter active prompts
const activeSlugs = prompts.filter(p=>p.active).map(p=>p.slug)
const filterActiveSlugs = (slugs) => slugs.filter(s=>activeSlugs.includes(s))
const sp={insights,onRequest:requestInsight,loadingSlug,filterActiveSlugs}
return (
Verlauf & Auswertung
{tab==='body' &&
}
{tab==='nutrition' &&
}
{tab==='activity' &&
}
{tab==='recovery' &&
}
{tab==='correlation' &&
}
{tab==='photos' &&
}
)
}