mitai-jinkendo/frontend/src/pages/Dashboard.jsx
Lars c2b2c71ccd
All checks were successful
Deploy Development / deploy (push) Successful in 50s
Build Test / lint-backend (push) Successful in 0s
Build Test / build-frontend (push) Successful in 15s
feat: Enhance Dashboard layout with responsive greeting and metrics display
2026-04-05 08:03:53 +02:00

564 lines
27 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useState, useEffect, useRef } from 'react'
import { useNavigate } from 'react-router-dom'
import { Check, ChevronRight, Brain } from 'lucide-react'
import {
LineChart, Line, XAxis, YAxis, Tooltip,
ResponsiveContainer, CartesianGrid
} from 'recharts'
import { api } from '../utils/api'
import { useProfile } from '../context/ProfileContext'
import { getBfCategory } from '../utils/calc'
import TrialBanner from '../components/TrialBanner'
import EmailVerificationBanner from '../components/EmailVerificationBanner'
import TrainingTypeDistribution from '../components/TrainingTypeDistribution'
import SleepWidget from '../components/SleepWidget'
import RestDaysWidget from '../components/RestDaysWidget'
import { getInterpretation, getStatusColor, getStatusBg } from '../utils/interpret'
import Markdown from '../utils/Markdown'
import dayjs from 'dayjs'
import 'dayjs/locale/de'
dayjs.locale('de')
// ── Helpers ───────────────────────────────────────────────────────────────────
function rollingAvg(arr, key, w=7) {
return arr.map((d,i)=>{
const s=arr.slice(Math.max(0,i-w+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
})
}
// ── Quick Weight Entry ────────────────────────────────────────────────────────
function QuickWeight({ onSaved }) {
const [input, setInput] = useState('')
const [saving, setSaving] = useState(false)
const [saved, setSaved] = useState(false)
const [error, setError] = useState(null)
const [weightUsage, setWeightUsage] = useState(null)
const today = dayjs().format('YYYY-MM-DD')
const loadUsage = () => {
api.getFeatureUsage().then(features => {
const weightFeature = features.find(f => f.feature_id === 'weight_entries')
setWeightUsage(weightFeature)
}).catch(err => console.error('Failed to load usage:', err))
}
useEffect(()=>{
api.weightStats().then(s=>{
if(s?.latest?.date===today) setInput(String(s.latest.weight))
})
loadUsage()
},[])
const handleSave = async () => {
const w=parseFloat(input); if(!w||w<20||w>300) return
setSaving(true)
setError(null)
try{
await api.upsertWeight(today,w)
setSaved(true)
await loadUsage() // Reload usage after save
onSaved?.()
setTimeout(()=>setSaved(false),2000)
} catch(err) {
console.error('Save failed:', err)
setError(err.message || 'Fehler beim Speichern')
setTimeout(()=>setError(null), 5000)
} finally {
setSaving(false)
}
}
const isDisabled = saving || !input || (weightUsage && !weightUsage.allowed)
const tooltipText = weightUsage && !weightUsage.allowed
? `Limit erreicht (${weightUsage.used}/${weightUsage.limit}). Kontaktiere den Admin oder warte bis zum nächsten Reset.`
: ''
return (
<div>
{error && (
<div style={{padding:'8px 10px',background:'var(--danger-bg)',border:'1px solid var(--danger)',borderRadius:8,fontSize:12,color:'var(--danger)',marginBottom:8}}>
{error}
</div>
)}
<div style={{display:'flex',gap:8,alignItems:'center'}}>
<input type="number" min={20} max={300} step={0.1} className="form-input"
style={{flex:1,fontSize:17,fontWeight:600,textAlign:'center'}}
placeholder="kg eingeben" value={input} onChange={e=>setInput(e.target.value)}
onKeyDown={e=>e.key==='Enter'&&!isDisabled&&handleSave()}/>
<span style={{fontSize:13,color:'var(--text3)'}}>kg</span>
<div title={tooltipText} style={{display:'inline-block'}}>
<button
className="btn btn-primary"
style={{padding:'8px 14px', cursor: isDisabled ? 'not-allowed' : 'pointer'}}
onClick={handleSave}
disabled={isDisabled}
>
{saved ? <Check size={15}/>
: saving ? <div className="spinner" style={{width:14,height:14}}/>
: (weightUsage && !weightUsage.allowed) ? '🔒 Limit'
: 'Speichern'}
</button>
</div>
</div>
</div>
)
}
// ── Status Pill ───────────────────────────────────────────────────────────────
const PILL_TOOLTIPS = {
'WHR': 'Waist-Hip-Ratio: Taille ÷ Hüfte. Maß für Bauchfettverteilung. Ziel: <0,90 (M) / <0,85 (F)',
'WHtR': 'Waist-to-Height-Ratio: Taille ÷ Körpergröße. Gesündestest Maß: Ziel unter 0,50.',
'KF': 'Körperfettanteil in Prozent (aus Caliper-Messung).',
'Protein Ø7T': 'Durchschnittliche tägliche Proteinaufnahme der letzten 7 Tage vs. Zielbereich (1,62,2g/kg KG).',
}
function Pill({ label, value, status, sub }) {
const [tip, setTip] = useState(false)
const color = status==='good'?'var(--accent)':status==='warn'?'var(--warn)':'#D85A30'
const bg = status==='good'?'var(--accent-light)':status==='warn'?'var(--warn-bg)':'#FCEBEB'
const tipText = PILL_TOOLTIPS[label]
return (
<div style={{position:'relative'}}>
<div onClick={()=>tipText&&setTip(s=>!s)}
style={{display:'flex',alignItems:'center',gap:5,padding:'5px 10px',
borderRadius:20,background:bg,border:`1px solid ${color}44`,
cursor:tipText?'help':'default'}}>
<div style={{width:7,height:7,borderRadius:'50%',background:color,flexShrink:0}}/>
<span style={{fontSize:12,fontWeight:500,color:'var(--text2)'}}>{label}</span>
<span style={{fontSize:12,fontWeight:700,color}}>{value}</span>
{sub && <span style={{fontSize:10,color:'var(--text3)'}}>{sub}</span>}
{tipText && <span style={{fontSize:10,color:'var(--text3)',opacity:0.7}}></span>}
</div>
{tip && tipText && (
<div onClick={()=>setTip(false)} style={{
position:'absolute',bottom:'110%',left:0,zIndex:50,
background:'var(--surface)',border:'1px solid var(--border)',
borderRadius:8,padding:'8px 10px',fontSize:11,color:'var(--text2)',
minWidth:200,maxWidth:260,lineHeight:1.5,
boxShadow:'0 4px 16px rgba(0,0,0,0.15)'}}>
<strong>{label}</strong><br/>{tipText}
</div>
)}
</div>
)
}
// ── Stat Card ─────────────────────────────────────────────────────────────────
function StatCard({ icon, label, value, unit, delta, deltaGoodWhenNeg=false, sub, onClick, color }) {
const deltaColor = delta==null ? null
: (deltaGoodWhenNeg ? delta<0 : delta>0) ? 'var(--accent)' : 'var(--warn)'
return (
<div
className="dashboard-stat-card"
onClick={onClick}
style={{ cursor: onClick ? 'pointer' : 'default' }}
onMouseEnter={e=>onClick&&(e.currentTarget.style.borderColor='var(--accent)')}
onMouseLeave={e=>onClick&&(e.currentTarget.style.borderColor='var(--border)')}>
<div style={{fontSize:18,marginBottom:4}}>{icon}</div>
<div style={{fontSize:11,color:'var(--text3)',marginBottom:2}}>{label}</div>
<div style={{fontSize:19,fontWeight:700,color:color||'var(--text1)',lineHeight:1.1}}>
{value}<span style={{fontSize:12,fontWeight:400,color:'var(--text3)',marginLeft:2}}>{unit}</span>
</div>
{delta!=null && <div style={{fontSize:11,fontWeight:600,color:deltaColor,marginTop:2}}>
{delta>0?'+':''}{delta} {unit}
</div>}
{sub && <div style={{fontSize:10,color:'var(--text3)',marginTop:2}}>{sub}</div>}
</div>
)
}
// ── Combined Chart: Kcal + Weight ─────────────────────────────────────────────
function ComboChart({ weights, nutrition }) {
// Build unified date axis from last 30 days
const days = []
for (let i=29; i>=0; i--) days.push(dayjs().subtract(i,'day').format('YYYY-MM-DD'))
const wMap = {}; (weights||[]).forEach(w=>{ wMap[w.date]=w.weight })
const nMap = {}; (nutrition||[]).forEach(n=>{ nMap[n.date]=Math.round(n.kcal||0) })
// Forward-fill weight: carry last known weight to fill gaps
let lastW = null
const combined = days.map(date=>{
if (wMap[date]) lastW = wMap[date]
return {
date: dayjs(date).format('DD.MM'),
kcal: nMap[date]||null,
weight: wMap[date]||null, // actual measurement dots
weightLine:lastW, // interpolated line
}
}).filter(d=>d.kcal||d.weightLine)
const withAvg = rollingAvg(combined,'kcal')
const hasKcal = combined.some(d=>d.kcal)
const hasW = combined.some(d=>d.weightLine)
if (!hasKcal && !hasW) return (
<div style={{padding:20,textAlign:'center',fontSize:12,color:'var(--text3)'}}>
Mehr Ernährungs- und Gewichtsdaten für den Chart nötig
</div>
)
return (
<ResponsiveContainer width="100%" height={160}>
<LineChart data={withAvg} margin={{top:4,right:8,bottom:0,left:-20}}>
<CartesianGrid stroke="var(--border)" strokeDasharray="3 3"/>
<XAxis dataKey="date" tick={{fontSize:9,fill:'var(--text3)'}} tickLine={false}
interval={Math.max(0,Math.floor(withAvg.length/6)-1)}/>
{hasKcal && <YAxis yAxisId="kcal" tick={{fontSize:9,fill:'var(--text3)'}} tickLine={false} domain={['auto','auto']}/>}
{hasW && <YAxis yAxisId="weight" orientation="right" tick={{fontSize:9,fill:'var(--text3)'}} tickLine={false} domain={['auto','auto']}/>}
<Tooltip contentStyle={{background:'var(--surface)',border:'1px solid var(--border)',borderRadius:8,fontSize:11}}
formatter={(v,n)=>[v==null?'':`${Math.round(v)} ${n==='weightLine'||n==='weight'?'kg':'kcal'}`,
n==='kcal_avg'?'Ø Kalorien (7T)':n==='kcal'?'Kalorien':n==='weightLine'?'Gewicht (interpoliert)':'Gewicht Messung']}/>
{hasKcal && <Line yAxisId="kcal" type="monotone" dataKey="kcal" stroke="#EF9F2744" strokeWidth={1} dot={false} connectNulls={false}/>}
{hasKcal && <Line yAxisId="kcal" type="monotone" dataKey="kcal_avg" stroke="#EF9F27" strokeWidth={2} dot={false} connectNulls={true} name="kcal_avg"/>}
{hasW && <Line yAxisId="weight" type="monotone" dataKey="weightLine" stroke="#378ADD88" strokeWidth={1.5} dot={false} connectNulls={true} name="weightLine"/>}
{hasW && <Line yAxisId="weight" type="monotone" dataKey="weight" stroke="#378ADD" strokeWidth={0}
dot={(props)=>{ const {cx,cy,value}=props; return value!=null?<circle key={cx} cx={cx} cy={cy} r={4} fill="#378ADD" stroke="white" strokeWidth={1.5}/>:<g key={cx}/>}} connectNulls={false} name="weight"/>}
</LineChart>
</ResponsiveContainer>
)
}
// ── Main Dashboard ────────────────────────────────────────────────────────────
export default function Dashboard() {
const nav = useNavigate()
const { activeProfile } = useProfile()
const [stats, setStats] = useState(null)
const [weights, setWeights] = useState([])
const [calipers, setCalipers] = useState([])
const [circs, setCircs] = useState([])
const [nutrition, setNutrition] = useState([])
const [activities,setActivities]= useState([])
const [insights, setInsights] = useState([])
const [loading, setLoading] = useState(true)
const [showInsight, setShowInsight] = useState(false)
const [pipelineLoading, setPipelineLoading] = useState(false)
const [pipelineError, setPipelineError] = useState(null)
const load = () => Promise.all([
api.getStats(),
api.listWeight(60),
api.listCaliper(3),
api.listCirc(2),
api.listNutrition(30),
api.listActivity(30),
api.latestInsights(),
]).then(([s,w,ca,ci,n,a,ins])=>{
setStats(s); setWeights(w); setCalipers(ca); setCircs(ci)
setNutrition(n); setActivities(a)
setInsights(Array.isArray(ins)?ins:[])
setLoading(false)
}).catch(err => {
console.error('Dashboard load failed:', err)
// Set empty data on error so UI can still render
setStats(null); setWeights([]); setCalipers([]); setCircs([])
setNutrition([]); setActivities([]); setInsights([])
setLoading(false)
})
const runPipeline = async () => {
setPipelineLoading(true); setPipelineError(null)
try {
const pid = localStorage.getItem('mitai-jinkendo_active_profile')||''
await api.insightPipeline()
await load()
} catch(e) {
setPipelineError('Fehler: '+e.message)
} finally { setPipelineLoading(false) }
}
useEffect(()=>{
console.log('[Dashboard] Component mounted, loading data...')
load()
},[])
console.log('[Dashboard] Rendering, loading=', loading, 'activeProfile=', activeProfile?.name)
if (loading) return <div className="empty-state"><div className="spinner"/></div>
const latestCal = calipers[0]
const latestCir = circs[0]
const latestW = weights[0]
const prevW = weights[1]
const sex = activeProfile?.sex||'m'
const height = activeProfile?.height||178
// Deltas
const wDelta = latestW&&prevW ? Math.round((latestW.weight-prevW.weight)*10)/10 : null
const bfCat = latestCal?.body_fat_pct ? getBfCategory(latestCal.body_fat_pct,sex) : null
const bfPrev = calipers[1]?.body_fat_pct
const bfDelta = latestCal?.body_fat_pct&&bfPrev ? Math.round((latestCal.body_fat_pct-bfPrev)*10)/10 : null
// WHR / WHtR
const whr = latestCir?.c_waist&&latestCir?.c_hip ? Math.round(latestCir.c_waist/latestCir.c_hip*100)/100 : null
const whtr = latestCir?.c_waist&&height ? Math.round(latestCir.c_waist/height*100)/100 : null
// Nutrition averages (last 7 days)
const recentNutr = nutrition.filter(n=>n.date>=dayjs().subtract(7,'day').format('YYYY-MM-DD'))
const avgKcal = recentNutr.length ? Math.round(recentNutr.reduce((s,n)=>s+(n.kcal||0),0)/recentNutr.length) : null
const avgProtein = recentNutr.length ? Math.round(recentNutr.reduce((s,n)=>s+(n.protein_g||0),0)/recentNutr.length*10)/10 : null
const ptLow = Math.round((latestW?.weight||80)*1.6)
const proteinOk = avgProtein && avgProtein >= ptLow
// Activity (last 7 days)
const recentAct = activities.filter(a=>a.date>=dayjs().subtract(7,'day').format('YYYY-MM-DD'))
const actKcal = recentAct.length ? Math.round(recentAct.reduce((s,a)=>s+(a.kcal_active||0),0)) : null
// Status pills
const pills = []
if (whr) pills.push({label:'WHR', value:whr, status:whr<(sex==='m'?0.90:0.85)?'good':'warn', sub:`<${sex==='m'?'0,90':'0,85'}`})
if (whtr) pills.push({label:'WHtR', value:whtr, status:whtr<0.5?'good':'warn', sub:'<0,50'})
if (avgProtein) pills.push({label:'Protein Ø7T', value:avgProtein+'g', status:proteinOk?'good':'warn', sub:`Ziel ${ptLow}g`})
if (bfCat) pills.push({label:'KF', value:latestCal.body_fat_pct+'%', status:latestCal.body_fat_pct<(sex==='m'?18:25)?'good':'warn', sub:bfCat.label})
// Latest overall insight
const latestInsight = insights.find(i=>i.scope==='gesamt')||insights[0]
const hasAnyData = latestW||latestCal||nutrition.length>0
console.log('[Dashboard] hasAnyData=', hasAnyData, 'latestW=', !!latestW, 'latestCal=', !!latestCal, 'nutrition.length=', nutrition.length)
return (
<div className="dashboard-page">
{/* Header greeting */}
<div className="dashboard-greeting">
<h1 style={{fontSize:22,fontWeight:800,margin:0,color:'var(--text1)'}}>
Hallo, {activeProfile?.name||'Nutzer'} 👋
</h1>
<div className="dashboard-greeting__meta" style={{fontSize:12,color:'var(--text3)',marginTop:2}}>
{dayjs().format('dddd, DD. MMMM YYYY')}
{latestW && ` · Letztes Update ${dayjs(latestW.date).format('DD.MM.')}`}
</div>
</div>
{/* Email Verification Banner */}
{activeProfile && <EmailVerificationBanner profile={activeProfile}/>}
{/* Trial Banner */}
{activeProfile && <TrialBanner profile={activeProfile}/>}
{!hasAnyData && (
<div className="empty-state">
<h3>Willkommen bei Mitai Jinkendo!</h3>
<p>Starte mit deiner ersten Messung.</p>
<button className="btn btn-primary" onClick={()=>nav('/capture')}>
Erfassen starten
</button>
</div>
)}
{hasAnyData && <>
{/* Quick weight entry */}
<div className="card section-gap">
<div style={{display:'flex',alignItems:'center',justifyContent:'space-between',marginBottom:10}}>
<div style={{fontWeight:600,fontSize:14}}> Gewicht heute</div>
<button style={{background:'none',border:'none',fontSize:12,color:'var(--accent)',cursor:'pointer'}}
onClick={()=>nav('/weight')}>
Alle Einträge
</button>
</div>
<QuickWeight onSaved={load}/>
</div>
{/* Key metrics — Mobile: flex-wrap; Desktop: 4-spaltig (RESPONSIVE_UI P3) */}
<div className="dashboard-stat-grid">
<StatCard icon="⚖️" label="Gewicht" value={latestW?.weight??''} unit="kg"
delta={wDelta} deltaGoodWhenNeg={true}
sub={latestW ? dayjs(latestW.date).format('DD.MM.') : ''}
onClick={()=>nav('/history')} color="#378ADD"/>
{latestCal?.body_fat_pct && <StatCard icon="🫧" label="Körperfett" value={latestCal.body_fat_pct} unit="%"
delta={bfDelta} deltaGoodWhenNeg={true}
sub={bfCat?.label}
onClick={()=>nav('/history',{state:{tab:'body'}})} color={bfCat?.color}/>}
{latestCal?.lean_mass && <StatCard icon="💪" label="Magermasse" value={latestCal.lean_mass} unit="kg"
sub={latestCal.date ? dayjs(latestCal.date).format('DD.MM.') : ''}
onClick={()=>nav('/history',{state:{tab:'body'}})}/>}
{avgKcal && <StatCard icon="🍽️" label="Ø Kalorien" value={avgKcal} unit="kcal"
sub="letzte 7 Tage" onClick={()=>nav('/history',{state:{tab:'nutrition'}})} color="#EF9F27"/>}
</div>
{/* Status pills */}
{pills.length > 0 && (
<div style={{display:'flex',gap:6,flexWrap:'wrap',marginBottom:16}}>
{pills.map((p,i)=><Pill key={i} {...p}/>)}
</div>
)}
{/* Goals progress */}
{(activeProfile?.goal_weight||activeProfile?.goal_bf_pct) && latestW && (
<div className="card section-gap" style={{marginBottom:16}}>
<div style={{fontWeight:600,fontSize:13,marginBottom:10}}>🎯 Ziele</div>
{activeProfile?.goal_weight && latestW && (()=>{
const start = Math.max(...weights.map(w=>w.weight))
const curr = latestW.weight
const goal = activeProfile.goal_weight
const total = start - goal
const done = start - curr
const pct = total > 0 ? Math.min(100, Math.round(done/total*100)) : 100
const remain = Math.round((curr-goal)*10)/10
return (
<div style={{marginBottom:10}}>
<div style={{display:'flex',justifyContent:'space-between',fontSize:12,marginBottom:4}}>
<span>Gewicht: {curr} {goal} kg</span>
<span style={{color:'var(--accent)',fontWeight:600}}>{remain>0?`noch ${remain}kg`:'Ziel erreicht! 🎉'}</span>
</div>
<div style={{height:8,background:'var(--border)',borderRadius:4,overflow:'hidden'}}>
<div style={{height:'100%',width:`${pct}%`,background:'var(--accent)',borderRadius:4,transition:'width 0.5s'}}/>
</div>
<div style={{fontSize:10,color:'var(--text3)',marginTop:2}}>{pct}% des Weges</div>
</div>
)
})()}
{activeProfile?.goal_bf_pct && latestCal?.body_fat_pct && (()=>{
const curr = latestCal.body_fat_pct
const goal = activeProfile.goal_bf_pct
const remain= Math.round((curr-goal)*10)/10
const pct = curr<=goal ? 100 : Math.min(100,Math.round((1-(curr-goal)/Math.max(curr-goal,5))*100))
return (
<div>
<div style={{display:'flex',justifyContent:'space-between',fontSize:12,marginBottom:4}}>
<span>Körperfett: {curr}% {goal}%</span>
<span style={{color:'var(--accent)',fontWeight:600}}>{remain>0?`noch ${remain}%`:'Ziel erreicht! 🎉'}</span>
</div>
<div style={{height:8,background:'var(--border)',borderRadius:4,overflow:'hidden'}}>
<div style={{height:'100%',width:`${pct}%`,background:bfCat?.color||'var(--accent)',borderRadius:4}}/>
</div>
<div style={{fontSize:10,color:'var(--text3)',marginTop:2}}>Aktuell: {bfCat?.label}</div>
</div>
)
})()}
</div>
)}
{/* Combined chart */}
{(weights.length>2||nutrition.length>2) && (
<div className="card section-gap" style={{marginBottom:16}}>
<div style={{display:'flex',justifyContent:'space-between',alignItems:'center',marginBottom:8}}>
<div style={{fontWeight:600,fontSize:13}}>📊 Kalorien + Gewicht (30 Tage)</div>
<button style={{background:'none',border:'none',fontSize:12,color:'var(--accent)',cursor:'pointer'}}
onClick={()=>nav('/history',{state:{tab:'body'}})}>
Details
</button>
</div>
<ComboChart weights={weights} nutrition={nutrition}/>
<div style={{display:'flex',gap:16,justifyContent:'center',marginTop:6,fontSize:10,color:'var(--text3)'}}>
<span><span style={{display:'inline-block',width:12,height:2,background:'#EF9F27',verticalAlign:'middle',marginRight:3}}/>Ø Kalorien</span>
<span><span style={{display:'inline-block',width:12,height:2,background:'#378ADD',verticalAlign:'middle',marginRight:3}}/>Gewicht</span>
</div>
</div>
)}
{/* Activity + Nutrition summary row */}
<div className="dashboard-summary-row">
{(avgKcal||avgProtein) && (
<div className="card" style={{flex:1,cursor:'pointer'}} onClick={()=>nav('/history',{state:{tab:'nutrition'}})}>
<div style={{fontWeight:600,fontSize:12,marginBottom:8,color:'var(--text3)'}}>🍽 ERNÄHRUNG (Ø 7T)</div>
{avgKcal && <div style={{fontSize:16,fontWeight:700,color:'#EF9F27'}}>{avgKcal} kcal</div>}
{avgProtein && <div style={{fontSize:13,fontWeight:600,
color:proteinOk?'var(--accent)':'var(--warn)'}}>
{avgProtein}g Protein {proteinOk?'✓':'⚠️'}
</div>}
<div style={{fontSize:10,color:'var(--text3)',marginTop:4}}> Verlauf Ernährung</div>
</div>
)}
{actKcal!=null && (
<div className="card" style={{flex:1,cursor:'pointer'}} onClick={()=>nav('/history',{state:{tab:'activity'}})}>
<div style={{fontWeight:600,fontSize:12,marginBottom:8,color:'var(--text3)'}}>🏋 AKTIVITÄT (7T)</div>
<div style={{fontSize:16,fontWeight:700,color:'#EF9F27'}}>{actKcal} kcal</div>
<div style={{fontSize:13,color:'var(--text2)'}}>{recentAct.length} Trainings</div>
<div style={{fontSize:10,color:'var(--text3)',marginTop:4}}> Verlauf Aktivität</div>
</div>
)}
</div>
{/* Sleep Widget */}
<div style={{marginBottom:16}}>
<SleepWidget/>
</div>
{/* Rest Days Widget */}
<div style={{marginBottom:16}}>
<RestDaysWidget/>
</div>
{/* Training Type Distribution */}
{activities.length > 0 && (
<div className="card section-gap" style={{marginBottom:16}}>
<div style={{display:'flex',justifyContent:'space-between',alignItems:'center',marginBottom:12}}>
<div style={{fontWeight:600,fontSize:13}}>🏋 Trainingstyp-Verteilung</div>
<button style={{background:'none',border:'none',fontSize:12,color:'var(--accent)',cursor:'pointer'}}
onClick={()=>nav('/activity')}>
Details
</button>
</div>
<TrainingTypeDistribution days={28} />
</div>
)}
{/* Goals Preview */}
<div className="card section-gap" style={{marginBottom:16,cursor:'pointer'}}
onClick={()=>nav('/goals')}>
<div style={{display:'flex',justifyContent:'space-between',alignItems:'center',marginBottom:12}}>
<div style={{fontWeight:600,fontSize:13}}>🎯 Ziele</div>
<button style={{background:'none',border:'none',fontSize:12,color:'var(--accent)',cursor:'pointer'}}
onClick={(e)=>{e.stopPropagation();nav('/goals')}}>
Verwalten
</button>
</div>
<div style={{fontSize:12,color:'var(--text2)',padding:'8px 0'}}>
Definiere deine Trainingsmodus und konkrete Ziele für bessere KI-Analysen
</div>
</div>
{/* Latest AI insight */}
<div className="card section-gap">
<div style={{display:'flex',alignItems:'center',justifyContent:'space-between',marginBottom:8}}>
<div style={{fontWeight:600,fontSize:13}}>🤖 KI-Auswertung</div>
<button className="btn btn-secondary" style={{fontSize:11,padding:'4px 10px'}}
onClick={()=>nav('/analysis')}>
<Brain size={11}/> Analysen
</button>
</div>
{/* Pipeline trigger */}
<button className="btn btn-primary btn-full" style={{marginBottom:10}}
onClick={runPipeline} disabled={pipelineLoading}>
{pipelineLoading
? <><div className="spinner" style={{width:13,height:13}}/> Analyse läuft (3 Stufen)</>
: <><Brain size={13}/> 🔬 Mehrstufige Analyse starten</>}
</button>
{pipelineError && <div style={{fontSize:12,color:'#D85A30',marginBottom:8}}>{pipelineError}</div>}
{latestInsight ? (
<>
<div style={{fontSize:11,color:'var(--text3)',marginBottom:6}}>
Letzte Analyse: {dayjs(latestInsight.created).format('DD. MMMM YYYY, HH:mm')}
</div>
<div style={{maxHeight: showInsight?'none':120, overflow:'hidden', position:'relative'}}>
<Markdown text={latestInsight.content}/>
{!showInsight && (
<div style={{position:'absolute',bottom:0,left:0,right:0,height:40,
background:'linear-gradient(transparent,var(--surface))'}}/>
)}
</div>
<button style={{background:'none',border:'none',cursor:'pointer',
fontSize:12,color:'var(--accent)',marginTop:6,padding:0}}
onClick={()=>setShowInsight(s=>!s)}>
{showInsight?'▲ Weniger anzeigen':'▼ Vollständig anzeigen'}
</button>
</>
) : (
<div style={{fontSize:13,color:'var(--text3)',padding:'8px 0'}}>
Noch keine KI-Auswertung vorhanden.
<button className="btn btn-primary" style={{marginTop:8,display:'block',fontSize:12}}
onClick={()=>nav('/analysis')}>
Erste Analyse erstellen
</button>
</div>
)}
</div>
</>}
</div>
)
}