diff --git a/frontend/src/app.css b/frontend/src/app.css index fa1b72a..af03b56 100644 --- a/frontend/src/app.css +++ b/frontend/src/app.css @@ -199,28 +199,25 @@ body { font-family: var(--font); background: var(--bg); color: var(--text1); -we .page-title { font-size: 20px; font-weight: 700; margin-bottom: 16px; } /* Verlauf: Mobile Tabs horizontale Leiste, Desktop vertikal links (P4 / RESPONSIVE_UI §5.2) */ -/* Körper-Verlauf: kompakte Bewertungs-Kacheln */ -.body-eval-grid { +/* Körper-Verlauf: KPI-Übersicht (Hover = Details, kein Klick) */ +.body-kpi-overview { display: grid; - grid-template-columns: repeat(auto-fill, minmax(148px, 1fr)); + grid-template-columns: repeat(auto-fill, minmax(158px, 1fr)); gap: 8px; + margin-bottom: 12px; } -.body-eval-tile { - display: block; - width: 100%; +.body-kpi-card { background: var(--surface2); border-radius: 10px; - padding: 8px 10px; - cursor: pointer; + padding: 10px 10px 10px 12px; border: 1px solid var(--border); - font: inherit; - color: inherit; + cursor: help; text-align: left; transition: border-color 0.15s ease, box-shadow 0.15s ease; } -.body-eval-tile:hover { +.body-kpi-card:hover { border-color: var(--border2); - box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06); + box-shadow: 0 1px 5px rgba(0, 0, 0, 0.07); } .history-page__title { diff --git a/frontend/src/pages/History.jsx b/frontend/src/pages/History.jsx index bdf4820..799a5d3 100644 --- a/frontend/src/pages/History.jsx +++ b/frontend/src/pages/History.jsx @@ -85,48 +85,184 @@ function RuleCard({ item }) { ) } -/** Kompakte Bewertungs-Kacheln (z. B. Körper-Verlauf) */ -function EvaluationTileGrid({ items }) { - const [open, setOpen] = useState(null) - if (!items?.length) return null +function verdictShort(status) { + if (status === 'good') return 'Gut' + if (status === 'warn') return 'Hinweis' + return 'Achtung' +} + +/** Eine KPI-Kachelzeile aus Summary + Interpretationsregeln (ohne Duplikate zur reinen Bewertungsliste). */ +function buildBodyKpiTiles({ + summary, rules, trendPeriods, minW, maxW, avgAll, dataPoints, sex, bfCat, goalW, +}) { + const tiles = [] + + if (summary.weight_kg != null) { + const t90 = trendPeriods.find(t => t.label === '90T') + const t30 = trendPeriods.find(t => t.label === '30T') + const d = t90?.diff_kg ?? t30?.diff_kg ?? trendPeriods[0]?.diff_kg + let st = 'good' + let vs = 'Stabil' + if (d != null) { + if (d < -0.25) { st = 'good'; vs = 'Trend ↓' } + else if (d > 0.25) { st = 'warn'; vs = 'Trend ↑' } + else { st = 'good'; vs = 'Stabil' } + } + const trendBits = trendPeriods.length + ? trendPeriods.map(t => `${t.label} ${t.diff_kg > 0 ? '+' : ''}${t.diff_kg} kg`).join(' · ') + : '' + const hoverBody = [ + 'Gewicht im gewählten Zeitraum (letzter Messwert).', + avgAll != null ? `Durchschnitt: ${avgAll} kg` : null, + minW != null && maxW != null ? `Min. / Max.: ${minW} – ${maxW} kg` : null, + trendBits ? `Änderung: ${trendBits}` : null, + goalW != null ? `Profil-Zielgewicht: ${goalW} kg` : null, + ].filter(Boolean).join('\n') + + tiles.push({ + key: 'weight', + category: 'Gewicht', + icon: '⚖️', + value: `${summary.weight_kg} kg`, + sublabel: dataPoints ? `${dataPoints} Messwerte` : '', + verdict: vs, + status: st, + hoverTop: 'Gewicht', + hoverBody, + keys: ['weight_aktuell', 'weight_trend'], + }) + } + + const kfRule = rules.find(r => r.category === 'Körperfett') + if (summary.body_fat_pct != null) { + tiles.push({ + key: 'bf', + category: 'Körperfett', + icon: '🫧', + value: `${summary.body_fat_pct}%`, + valueColor: bfCat?.color, + sublabel: bfCat?.label || summary.bf_category_label || '', + verdict: verdictShort(kfRule?.status || 'good'), + status: kfRule?.status || 'good', + hoverTop: kfRule?.title || 'Körperfettanteil', + hoverBody: [kfRule?.detail, kfRule?.related_placeholder_keys?.length ? `Registry: ${kfRule.related_placeholder_keys.join(', ')}` : ''].filter(Boolean).join('\n\n'), + }) + } + + const mmRule = rules.find(r => r.category === 'Muskelmasse') + if (summary.lean_mass_kg != null || summary.ffmi != null) { + const valParts = [] + if (summary.lean_mass_kg != null) valParts.push(`${summary.lean_mass_kg} kg`) + if (summary.ffmi != null) valParts.push(`FFMI ${summary.ffmi}`) + tiles.push({ + key: 'lean_ffmi', + category: 'Magermasse', + icon: '💪', + value: valParts.join(' · ') || '—', + sublabel: 'Lean / FFMI', + verdict: mmRule ? verdictShort(mmRule.status) : '—', + status: mmRule?.status || 'good', + hoverTop: mmRule?.title || 'Muskelmasse', + hoverBody: [mmRule?.detail, mmRule?.related_placeholder_keys?.length ? `Registry: ${mmRule.related_placeholder_keys.join(', ')}` : ''].filter(Boolean).join('\n\n'), + }) + } + + const bmiRule = rules.find(r => r.category === 'BMI') + if (bmiRule) { + tiles.push({ + key: 'bmi', + category: 'BMI', + icon: '📋', + value: bmiRule.value || '—', + sublabel: 'Body-Mass-Index', + verdict: verdictShort(bmiRule.status), + status: bmiRule.status, + hoverTop: bmiRule.title, + hoverBody: [bmiRule.detail, bmiRule.related_placeholder_keys?.length ? `Registry: ${bmiRule.related_placeholder_keys.join(', ')}` : ''].filter(Boolean).join('\n\n'), + }) + } + + const whrRule = rules.find(r => r.category === 'Fettverteilung') + if (summary.whr != null) { + const ok = summary.whr < (sex === 'm' ? 0.9 : 0.85) + tiles.push({ + key: 'whr', + category: 'WHR', + icon: '📐', + value: String(summary.whr), + sublabel: 'Taille ÷ Hüfte', + verdict: whrRule ? verdictShort(whrRule.status) : (ok ? 'Gut' : 'Hinweis'), + status: whrRule?.status || (ok ? 'good' : 'warn'), + hoverTop: whrRule?.title || 'Waist-Hip-Ratio', + hoverBody: [whrRule?.detail, !whrRule && `Ziel unter ${sex === 'm' ? '0,90' : '0,85'}.`, whrRule?.related_placeholder_keys?.length ? `Registry: ${whrRule.related_placeholder_keys.join(', ')}` : ''].filter(Boolean).join('\n\n'), + }) + } + + const whtrRule = rules.find(r => r.category === 'Taille/Größe') + if (summary.whtr != null) { + const ok = summary.whtr < 0.5 + tiles.push({ + key: 'whtr', + category: 'WHtR', + icon: '📏', + value: String(summary.whtr), + sublabel: 'Taille ÷ Größe', + verdict: whtrRule ? verdictShort(whtrRule.status) : (ok ? 'Gut' : 'Hinweis'), + status: whtrRule?.status || (ok ? 'good' : 'warn'), + hoverTop: whtrRule?.title || 'Waist-to-Height-Ratio', + hoverBody: [whtrRule?.detail, !whtrRule && 'Ziel unter 0,50 (WHO).', whtrRule?.related_placeholder_keys?.length ? `Registry: ${whtrRule.related_placeholder_keys.join(', ')}` : ''].filter(Boolean).join('\n\n'), + }) + } + + const lastRule = rules.find(r => r.category.startsWith('Seit letzter')) + if (lastRule) { + tiles.push({ + key: 'delta', + category: 'Messvergleich', + icon: '📊', + value: lastRule.value || '—', + sublabel: 'seit Vorperiode', + verdict: verdictShort(lastRule.status), + status: lastRule.status, + hoverTop: lastRule.title, + hoverBody: [lastRule.detail, lastRule.related_placeholder_keys?.length ? `Registry: ${lastRule.related_placeholder_keys.join(', ')}` : ''].filter(Boolean).join('\n\n'), + }) + } + + return tiles +} + +/** KPI-Kacheln: Kurzvergleich sichtbar, ausführlicher Text per nativem Hover (`title`). */ +function BodyKpiOverview({ tiles }) { + if (!tiles?.length) return null return (