import { useState, useEffect, useMemo, useCallback } 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' 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 PilotKpiBoard({ 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 pushTileForId = useCallback( (id, out) => { if (id === 'body_fat') { if (!bf) return out.push(
Körperfett
{bf.pct}%
{bf.cat?.label || 'Caliper'}
, ) return } if (id === 'avg_kcal') { if (avgKcal == null) return out.push(
Ø Kalorien ({KPI_KCAL_WINDOW_DEFAULT}T)
{avgKcal} kcal
Ernährung
, ) return } const tk = parseRefTypeKey(id) if (!tk) return const tile = refByKey.get(tk) if (!tile?.latest) return const l = tile.latest out.push(
{tile.type_label}
{formatRefVal(l)} {l.unit ? ( {l.unit} ) : null}
Ref.wert
, ) }, [bf, avgKcal, refByKey], ) const visibleTiles = useMemo(() => { const out = [] for (const id of orderIds) { pushTileForId(id, out) } return out }, [orderIds, pushTileForId]) if (loading) { return (
) } if (err) { return (
{err}
) } if (visibleTiles.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).`}

{visibleTiles}
) }