import { useState, useEffect } from 'react'
import {
LineChart, Line, BarChart, Bar,
XAxis, YAxis, Tooltip, ResponsiveContainer, CartesianGrid
} from 'recharts'
import { api } from '../utils/api'
import dayjs from 'dayjs'
const fmtDate = d => dayjs(d).format('DD.MM')
function ChartCard({ title, loading, error, children }) {
return (
{title}
{loading && (
)}
{error && (
{error}
)}
{!loading && !error && children}
)
}
/**
* Recovery Charts Component (R1-R5)
*
* Displays 5 recovery chart endpoints:
* - Recovery Score Timeline (R1)
* - HRV/RHR vs Baseline (R2)
* - Sleep Duration + Quality (R3)
* - Sleep Debt (R4)
* - Vital Signs Matrix (R5)
*/
export default function RecoveryCharts({ days = 28 }) {
const [recoveryData, setRecoveryData] = useState(null)
const [hrvRhrData, setHrvRhrData] = useState(null)
const [sleepData, setSleepData] = useState(null)
const [debtData, setDebtData] = useState(null)
const [vitalsData, setVitalsData] = useState(null)
const [loading, setLoading] = useState({})
const [errors, setErrors] = useState({})
useEffect(() => {
loadCharts()
}, [days])
const loadCharts = async () => {
// Load all 5 charts in parallel
await Promise.all([
loadRecoveryScore(),
loadHrvRhr(),
loadSleepQuality(),
loadSleepDebt(),
loadVitalSigns()
])
}
const loadRecoveryScore = async () => {
setLoading(l => ({...l, recovery: true}))
setErrors(e => ({...e, recovery: null}))
try {
const data = await api.getRecoveryScoreChart(days)
setRecoveryData(data)
} catch (err) {
setErrors(e => ({...e, recovery: err.message}))
} finally {
setLoading(l => ({...l, recovery: false}))
}
}
const loadHrvRhr = async () => {
setLoading(l => ({...l, hrvRhr: true}))
setErrors(e => ({...e, hrvRhr: null}))
try {
const data = await api.getHrvRhrBaselineChart(days)
setHrvRhrData(data)
} catch (err) {
setErrors(e => ({...e, hrvRhr: err.message}))
} finally {
setLoading(l => ({...l, hrvRhr: false}))
}
}
const loadSleepQuality = async () => {
setLoading(l => ({...l, sleep: true}))
setErrors(e => ({...e, sleep: null}))
try {
const data = await api.getSleepDurationQualityChart(days)
setSleepData(data)
} catch (err) {
setErrors(e => ({...e, sleep: err.message}))
} finally {
setLoading(l => ({...l, sleep: false}))
}
}
const loadSleepDebt = async () => {
setLoading(l => ({...l, debt: true}))
setErrors(e => ({...e, debt: null}))
try {
const data = await api.getSleepDebtChart(days)
setDebtData(data)
} catch (err) {
setErrors(e => ({...e, debt: err.message}))
} finally {
setLoading(l => ({...l, debt: false}))
}
}
const loadVitalSigns = async () => {
setLoading(l => ({...l, vitals: true}))
setErrors(e => ({...e, vitals: null}))
try {
const data = await api.getVitalSignsMatrixChart(7) // Last 7 days
setVitalsData(data)
} catch (err) {
setErrors(e => ({...e, vitals: err.message}))
} finally {
setLoading(l => ({...l, vitals: false}))
}
}
// R1: Recovery Score Timeline
const renderRecoveryScore = () => {
if (!recoveryData || recoveryData.metadata?.confidence === 'insufficient') {
return
Keine Recovery-Daten vorhanden
}
const chartData = recoveryData.data.labels.map((label, i) => ({
date: fmtDate(label),
score: recoveryData.data.datasets[0]?.data[i]
}))
return (
<>
Aktuell: {recoveryData.metadata.current_score}/100 · {recoveryData.metadata.data_points} Einträge
>
)
}
// R2: HRV/RHR vs Baseline
const renderHrvRhr = () => {
if (!hrvRhrData || hrvRhrData.metadata?.confidence === 'insufficient') {
return
Keine Vitalwerte vorhanden
}
const chartData = hrvRhrData.data.labels.map((label, i) => ({
date: fmtDate(label),
hrv: hrvRhrData.data.datasets[0]?.data[i],
rhr: hrvRhrData.data.datasets[1]?.data[i]
}))
return (
<>
HRV Ø {hrvRhrData.metadata.avg_hrv}ms · RHR Ø {hrvRhrData.metadata.avg_rhr}bpm
>
)
}
// R3: Sleep Duration + Quality
const renderSleepQuality = () => {
if (!sleepData || sleepData.metadata?.confidence === 'insufficient') {
return
Keine Schlafdaten vorhanden
}
const chartData = sleepData.data.labels.map((label, i) => ({
date: fmtDate(label),
duration: sleepData.data.datasets[0]?.data[i],
quality: sleepData.data.datasets[1]?.data[i]
}))
return (
<>
Ø {sleepData.metadata.avg_duration_hours}h Schlaf
>
)
}
// R4: Sleep Debt
const renderSleepDebt = () => {
if (!debtData || debtData.metadata?.confidence === 'insufficient') {
return
Keine Schlafdaten für Schulden-Berechnung
}
const chartData = debtData.data.labels.map((label, i) => ({
date: fmtDate(label),
debt: debtData.data.datasets[0]?.data[i]
}))
return (
<>
Aktuelle Schuld: {debtData.metadata.current_debt_hours.toFixed(1)}h
>
)
}
// R5: Vital Signs Matrix (Bar)
const renderVitalSigns = () => {
if (!vitalsData || vitalsData.metadata?.confidence === 'insufficient') {
return
Keine aktuellen Vitalwerte
}
const chartData = vitalsData.data.labels.map((label, i) => ({
name: label,
value: vitalsData.data.datasets[0]?.data[i]
}))
return (
<>
Letzte {vitalsData.metadata.data_points} Messwerte (7 Tage)
>
)
}
return (
{renderRecoveryScore()}
{renderHrvRhr()}
{renderSleepQuality()}
{renderSleepDebt()}
{renderVitalSigns()}
)
}