refactor: improve vital signs data handling and frontend display
- Introduced a new function `_merge_vitals_baseline_rows` to streamline the retrieval and merging of vital signs data, ensuring the latest non-empty values are prioritized. - Updated SQL queries in `build_vital_signs_matrix_chart_payload` to enhance data retrieval efficiency and accuracy. - Refactored the `renderVitalSigns` function in the `RecoveryDashboardOverview` component to improve handling of vital signs data, including better fallback messaging and chart rendering logic. - Enhanced user feedback by providing clearer messages when no vital data is available, improving overall user experience.
This commit is contained in:
parent
d4868b3797
commit
819914b7cc
|
|
@ -7,7 +7,7 @@ Ausgelagert aus routers/charts.py (Issue 53 / Layer 1).
|
|||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Any, Dict
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from db import get_db, get_cursor
|
||||
from data_layer.recovery_metrics import (
|
||||
|
|
@ -356,15 +356,38 @@ def build_sleep_debt_chart_payload(profile_id: str, days: int) -> Dict[str, Any]
|
|||
}
|
||||
|
||||
|
||||
VITAL_BASELINE_KEYS = ("resting_hr", "hrv", "vo2_max", "spo2", "respiratory_rate")
|
||||
|
||||
|
||||
def _vitals_row_has_any_value(row: Any) -> bool:
|
||||
if not row:
|
||||
return False
|
||||
for k in ("resting_hr", "hrv", "vo2_max", "spo2", "respiratory_rate"):
|
||||
for k in VITAL_BASELINE_KEYS:
|
||||
if row.get(k) is not None:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _merge_vitals_baseline_rows(rows: Any) -> tuple[Optional[Dict[str, Any]], Optional[Any]]:
|
||||
"""
|
||||
Pro Kennzahl den jeweils neuesten nicht-leeren Wert (Zeilen sortiert: date DESC).
|
||||
So können KPIs (Aggregation über Zeilen) Daten haben, obwohl die jüngste Zeile leer ist.
|
||||
"""
|
||||
if not rows:
|
||||
return None, None
|
||||
merged: Dict[str, Any] = {k: None for k in VITAL_BASELINE_KEYS}
|
||||
for row in rows:
|
||||
for k in VITAL_BASELINE_KEYS:
|
||||
if merged[k] is None and row.get(k) is not None:
|
||||
merged[k] = row[k]
|
||||
if all(merged[k] is not None for k in VITAL_BASELINE_KEYS):
|
||||
break
|
||||
if not _vitals_row_has_any_value(merged):
|
||||
return None, None
|
||||
newest_date = rows[0].get("date") if rows else None
|
||||
return merged, newest_date
|
||||
|
||||
|
||||
def _bp_row_complete(row: Any) -> bool:
|
||||
return bool(row and row.get("systolic") is not None and row.get("diastolic") is not None)
|
||||
|
||||
|
|
@ -381,10 +404,10 @@ def build_vital_signs_matrix_chart_payload(profile_id: str, days: int) -> Dict[s
|
|||
days = 365
|
||||
cutoff = (datetime.now() - timedelta(days=days)).strftime("%Y-%m-%d")
|
||||
|
||||
vitals_row = None
|
||||
bp_row = None
|
||||
vitals_measured_at = None
|
||||
bp_measured_at = None
|
||||
vitals_for_items: Optional[Dict[str, Any]] = None
|
||||
|
||||
with get_db() as conn:
|
||||
cur = get_cursor(conn)
|
||||
|
|
@ -393,27 +416,24 @@ def build_vital_signs_matrix_chart_payload(profile_id: str, days: int) -> Dict[s
|
|||
FROM vitals_baseline
|
||||
WHERE profile_id=%s AND date >= %s
|
||||
ORDER BY date DESC
|
||||
LIMIT 1""",
|
||||
LIMIT 200""",
|
||||
(profile_id, cutoff),
|
||||
)
|
||||
vitals_row = cur.fetchone()
|
||||
if vitals_row and vitals_row.get("date") is not None:
|
||||
d = vitals_row["date"]
|
||||
vitals_measured_at = d.isoformat() if hasattr(d, "isoformat") else str(d)
|
||||
|
||||
if not _vitals_row_has_any_value(vitals_row):
|
||||
vitals_merged, vitals_date = _merge_vitals_baseline_rows(cur.fetchall())
|
||||
if vitals_merged is None:
|
||||
cur.execute(
|
||||
"""SELECT date, resting_hr, hrv, vo2_max, spo2, respiratory_rate
|
||||
FROM vitals_baseline
|
||||
WHERE profile_id=%s
|
||||
ORDER BY date DESC
|
||||
LIMIT 1""",
|
||||
LIMIT 400""",
|
||||
(profile_id,),
|
||||
)
|
||||
vitals_row = cur.fetchone()
|
||||
if vitals_row and vitals_row.get("date") is not None:
|
||||
d = vitals_row["date"]
|
||||
vitals_measured_at = d.isoformat() if hasattr(d, "isoformat") else str(d)
|
||||
vitals_merged, vitals_date = _merge_vitals_baseline_rows(cur.fetchall())
|
||||
if vitals_merged is not None:
|
||||
vitals_for_items = dict(vitals_merged)
|
||||
if vitals_date is not None:
|
||||
vitals_measured_at = vitals_date.isoformat() if hasattr(vitals_date, "isoformat") else str(vitals_date)
|
||||
|
||||
cur.execute(
|
||||
"""SELECT measured_at, systolic, diastolic
|
||||
|
|
@ -440,10 +460,6 @@ def build_vital_signs_matrix_chart_payload(profile_id: str, days: int) -> Dict[s
|
|||
if bp_row and bp_row.get("measured_at") is not None:
|
||||
bp_measured_at = bp_row["measured_at"]
|
||||
|
||||
# Dict-like rows for assessment (exclude date/measured_at from value checks)
|
||||
vitals_for_items = dict(vitals_row) if vitals_row else None
|
||||
if vitals_for_items and "date" in vitals_for_items:
|
||||
vitals_for_items = {k: v for k, v in vitals_for_items.items() if k != "date"}
|
||||
bp_for_items = None
|
||||
if bp_row:
|
||||
bp_for_items = {"systolic": bp_row.get("systolic"), "diastolic": bp_row.get("diastolic")}
|
||||
|
|
|
|||
|
|
@ -316,24 +316,57 @@ export default function RecoveryDashboardOverview({
|
|||
}
|
||||
|
||||
const renderVitalSigns = () => {
|
||||
if (!vitalsData || vitalsData.metadata?.confidence === 'insufficient') {
|
||||
if (!vitalsData) {
|
||||
return (
|
||||
<div style={{ padding: 20, textAlign: 'center', color: 'var(--text3)', fontSize: 12 }}>
|
||||
{vitalsData?.metadata?.message || 'Keine aktuellen Vitalwerte'}
|
||||
Keine Vital-Matrix-Daten
|
||||
</div>
|
||||
)
|
||||
}
|
||||
const items = vitalsData.metadata?.vital_items || []
|
||||
const chartRows = items.map((it) => ({
|
||||
const meta = vitalsData.metadata || {}
|
||||
const items = meta.vital_items || []
|
||||
const ds0 = vitalsData.data?.datasets?.[0]
|
||||
const hasRawChart =
|
||||
Array.isArray(vitalsData.data?.labels) &&
|
||||
vitalsData.data.labels.length > 0 &&
|
||||
Array.isArray(ds0?.data) &&
|
||||
ds0.data.length > 0
|
||||
const ins = meta.confidence === 'insufficient'
|
||||
if (ins && items.length === 0 && !hasRawChart) {
|
||||
return (
|
||||
<div style={{ padding: 20, textAlign: 'center', color: 'var(--text3)', fontSize: 12 }}>
|
||||
{meta.message || 'Keine aktuellen Vitalwerte'}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
let chartRows = items.map((it) => ({
|
||||
name: it.label_de,
|
||||
value: it.bar_value ?? 0,
|
||||
value: Number(it.bar_value ?? 0),
|
||||
fill: barFillForTone(it.tone),
|
||||
tone: it.tone,
|
||||
}))
|
||||
if (chartRows.length === 0 && hasRawChart) {
|
||||
const bg = ds0.backgroundColor
|
||||
chartRows = vitalsData.data.labels.map((name, i) => ({
|
||||
name,
|
||||
value: Number(ds0.data[i] ?? 0),
|
||||
fill: Array.isArray(bg) ? bg[i] || '#1D9E75' : bg || '#1D9E75',
|
||||
tone: 'neutral',
|
||||
}))
|
||||
}
|
||||
|
||||
const vitDate = vitalsData.metadata?.vitals_measured_at
|
||||
const bpDate = vitalsData.metadata?.blood_pressure_measured_at
|
||||
const disclaimer = vitalsData.metadata?.disclaimer_de
|
||||
if (items.length === 0 && chartRows.length === 0) {
|
||||
return (
|
||||
<div style={{ padding: 20, textAlign: 'center', color: 'var(--text3)', fontSize: 12 }}>
|
||||
Keine Vitalwerte zur Anzeige (Server lieferte weder Kennzeilen noch Diagrammdaten).
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const vitDate = meta.vitals_measured_at
|
||||
const bpDate = meta.blood_pressure_measured_at
|
||||
const disclaimer = meta.disclaimer_de
|
||||
|
||||
return (
|
||||
<>
|
||||
|
|
@ -391,6 +424,12 @@ export default function RecoveryDashboardOverview({
|
|||
</div>
|
||||
) : null}
|
||||
|
||||
{items.length === 0 && chartRows.length > 0 ? (
|
||||
<div style={{ fontSize: 11, color: 'var(--text3)', marginBottom: 8 }}>
|
||||
Diagramm aus Server-Daten (ohne Zonen-Detail — bitte App aktualisieren oder Cache leeren).
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{chartRows.length > 0 ? (
|
||||
<>
|
||||
<div style={{ fontSize: 11, color: 'var(--text3)', marginBottom: 6 }}>
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user