import { useState, useEffect, useMemo } from 'react' import { Link } from 'react-router-dom' import dayjs from 'dayjs' import { api } from '../../utils/api' import { getBfCategory } from '../../utils/calc' import { useProfile } from '../../context/ProfileContext' import { KPI_KCAL_WINDOW_DEFAULT } from '../../widgetSystem/bodyChartDays' import { kpiTileOrderFromConfig } from '../../widgetSystem/kpiBoardTiles' import KpiTilesOverview from '../KpiTilesOverview' const MAX_KPI = 9 function formatRefVal(row) { if (row.value_numeric != null && row.value_numeric !== '') { const n = Number(row.value_numeric) return Number.isFinite(n) ? String(n) : String(row.value_numeric) } return row.value_text != null ? String(row.value_text) : '–' } function parseRefTypeKey(tileId) { if (!tileId.startsWith('ref:')) return null return tileId.slice(4) || null } function buildAutoTileIds(refTiles, hasBf, hasKcal) { const ids = [] for (const t of refTiles) { if (t?.type_key) ids.push(`ref:${t.type_key}`) } if (hasBf) ids.push('body_fat') if (hasKcal) ids.push('avg_kcal') return ids.slice(0, MAX_KPI) } /** * KPIs: Referenzwerte, Körperfett, Ø Kalorien — max. 9 Kacheln. * @param {{ refreshTick?: number, kpiConfig?: Record }} props * kpiConfig.tiles: geordnete Kachel-ids; fehlend = automatische Belegung (wie bisher). */ export default function KpiBoardWidget({ refreshTick = 0, kpiConfig }) { const manualOrder = useMemo(() => kpiTileOrderFromConfig(kpiConfig), [kpiConfig]) const { activeProfile } = useProfile() const sex = activeProfile?.sex || 'm' const [refTiles, setRefTiles] = useState([]) const [refByKey, setRefByKey] = useState(() => new Map()) const [bf, setBf] = useState(null) const [avgKcal, setAvgKcal] = useState(null) const [loading, setLoading] = useState(true) const [err, setErr] = useState(null) useEffect(() => { let cancelled = false ;(async () => { try { setLoading(true) const kcalDays = KPI_KCAL_WINDOW_DEFAULT const nutrLimit = Math.min(2000, Math.max(60, kcalDays * 5)) const [summary, calipers, nutrition] = await Promise.all([ api.listProfileReferenceValuesSummary().catch(() => ({ tiles: [] })), api.listCaliper(3).catch(() => []), api.listNutrition(nutrLimit).catch(() => []), ]) if (cancelled) return const tiles = Array.isArray(summary?.tiles) ? summary.tiles.filter((t) => t?.latest) : [] const map = new Map(tiles.map((t) => [t.type_key, t])) const latestCal = Array.isArray(calipers) && calipers[0]?.body_fat_pct != null ? calipers[0] : null const recentNutr = (nutrition || []).filter( (n) => n.date >= dayjs().subtract(kcalDays, 'day').format('YYYY-MM-DD'), ) const kcal = recentNutr.length > 0 ? Math.round(recentNutr.reduce((s, n) => s + (n.kcal || 0), 0) / recentNutr.length) : null const wantBf = !!latestCal?.body_fat_pct const wantKcal = kcal != null && kcal > 0 setRefTiles(tiles) setRefByKey(map) setBf( wantBf ? { pct: latestCal.body_fat_pct, cat: getBfCategory(latestCal.body_fat_pct, sex), date: latestCal.date, } : null, ) setAvgKcal(wantKcal ? kcal : null) setErr(null) } catch (e) { if (!cancelled) { setErr(e.message || 'KPIs konnten nicht geladen werden') setRefTiles([]) setRefByKey(new Map()) } } finally { if (!cancelled) setLoading(false) } })() return () => { cancelled = true } }, [refreshTick, sex]) const orderIds = useMemo(() => { if (manualOrder !== undefined) { return manualOrder } const hasBf = !!bf const hasKcal = avgKcal != null && avgKcal > 0 return buildAutoTileIds(refTiles, hasBf, hasKcal) }, [manualOrder, refTiles, bf, avgKcal]) const kpiTiles = useMemo(() => { const out = [] for (const id of orderIds) { if (id === 'body_fat') { if (!bf) continue out.push({ key: 'kpi-bf', status: 'good', category: 'Körperfett', icon: '🫧', value: `${bf.pct}%`, sublabel: bf.cat?.label || 'Caliper', valueColor: bf.cat?.color, hoverTop: 'Körperfett (Caliper)', hoverBody: `Letzte Messung: ${bf.date ? dayjs(bf.date).format('DD.MM.YYYY') : '—'}.\n` + 'Wert aus dem Caliper-Log; die Farbe/Kategorie richtet sich nach Geschlecht und üblicher Spanne.', }) continue } if (id === 'avg_kcal') { if (avgKcal == null) continue out.push({ key: 'kpi-kcal', status: 'good', category: `Ø Kalorien (${KPI_KCAL_WINDOW_DEFAULT}T)`, icon: '🍽️', value: `${avgKcal} kcal`, sublabel: 'Ernährung', valueColor: '#EF9F27', hoverTop: `Ø Kalorien (${KPI_KCAL_WINDOW_DEFAULT} Tage)`, hoverBody: `Durchschnitt der täglichen Kalorien aus dem Ernährungs-Log über die letzten ${KPI_KCAL_WINDOW_DEFAULT} Tage (Mittel über alle geladenen Tageseinträge im Fenster).`, }) continue } const tk = parseRefTypeKey(id) if (!tk) continue const tile = refByKey.get(tk) if (!tile?.latest) continue const l = tile.latest const valStr = formatRefVal(l) const withUnit = l.unit ? `${valStr} ${l.unit}`.trim() : valStr out.push({ key: `ref-${tk}`, status: 'good', category: tile.type_label, icon: '📌', value: withUnit, sublabel: 'Ref.wert', hoverTop: tile.type_label, hoverBody: 'Persönlicher Referenzwert aus dem Profil. Verwaltung unter Einstellungen → Referenzwerte.', }) } return out }, [orderIds, bf, avgKcal, refByKey]) if (loading) { return (
) } if (err) { return (
{err}
) } if (kpiTiles.length === 0) { return (
Kennzahlen

Noch keine Daten oder keine passenden Kacheln.{' '} Referenzwerte ,{' '} Caliper ,{' '} Ernährung .

) } return (
Kennzahlen

{manualOrder !== undefined ? 'Ausgewählte Kacheln in festgelegter Reihenfolge (ohne Daten werden Kacheln ausgelassen).' : `Bis ${MAX_KPI} Kacheln: Referenzwerte, Körperfett, Ø Kalorien (${KPI_KCAL_WINDOW_DEFAULT}T).`}

) }