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 __future__ import annotations
|
||||||
|
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from typing import Any, Dict
|
from typing import Any, Dict, Optional
|
||||||
|
|
||||||
from db import get_db, get_cursor
|
from db import get_db, get_cursor
|
||||||
from data_layer.recovery_metrics import (
|
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:
|
def _vitals_row_has_any_value(row: Any) -> bool:
|
||||||
if not row:
|
if not row:
|
||||||
return False
|
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:
|
if row.get(k) is not None:
|
||||||
return True
|
return True
|
||||||
return False
|
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:
|
def _bp_row_complete(row: Any) -> bool:
|
||||||
return bool(row and row.get("systolic") is not None and row.get("diastolic") is not None)
|
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
|
days = 365
|
||||||
cutoff = (datetime.now() - timedelta(days=days)).strftime("%Y-%m-%d")
|
cutoff = (datetime.now() - timedelta(days=days)).strftime("%Y-%m-%d")
|
||||||
|
|
||||||
vitals_row = None
|
|
||||||
bp_row = None
|
bp_row = None
|
||||||
vitals_measured_at = None
|
vitals_measured_at = None
|
||||||
bp_measured_at = None
|
bp_measured_at = None
|
||||||
|
vitals_for_items: Optional[Dict[str, Any]] = None
|
||||||
|
|
||||||
with get_db() as conn:
|
with get_db() as conn:
|
||||||
cur = get_cursor(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
|
FROM vitals_baseline
|
||||||
WHERE profile_id=%s AND date >= %s
|
WHERE profile_id=%s AND date >= %s
|
||||||
ORDER BY date DESC
|
ORDER BY date DESC
|
||||||
LIMIT 1""",
|
LIMIT 200""",
|
||||||
(profile_id, cutoff),
|
(profile_id, cutoff),
|
||||||
)
|
)
|
||||||
vitals_row = cur.fetchone()
|
vitals_merged, vitals_date = _merge_vitals_baseline_rows(cur.fetchall())
|
||||||
if vitals_row and vitals_row.get("date") is not None:
|
if vitals_merged is 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):
|
|
||||||
cur.execute(
|
cur.execute(
|
||||||
"""SELECT date, resting_hr, hrv, vo2_max, spo2, respiratory_rate
|
"""SELECT date, resting_hr, hrv, vo2_max, spo2, respiratory_rate
|
||||||
FROM vitals_baseline
|
FROM vitals_baseline
|
||||||
WHERE profile_id=%s
|
WHERE profile_id=%s
|
||||||
ORDER BY date DESC
|
ORDER BY date DESC
|
||||||
LIMIT 1""",
|
LIMIT 400""",
|
||||||
(profile_id,),
|
(profile_id,),
|
||||||
)
|
)
|
||||||
vitals_row = cur.fetchone()
|
vitals_merged, vitals_date = _merge_vitals_baseline_rows(cur.fetchall())
|
||||||
if vitals_row and vitals_row.get("date") is not None:
|
if vitals_merged is not None:
|
||||||
d = vitals_row["date"]
|
vitals_for_items = dict(vitals_merged)
|
||||||
vitals_measured_at = d.isoformat() if hasattr(d, "isoformat") else str(d)
|
if vitals_date is not None:
|
||||||
|
vitals_measured_at = vitals_date.isoformat() if hasattr(vitals_date, "isoformat") else str(vitals_date)
|
||||||
|
|
||||||
cur.execute(
|
cur.execute(
|
||||||
"""SELECT measured_at, systolic, diastolic
|
"""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:
|
if bp_row and bp_row.get("measured_at") is not None:
|
||||||
bp_measured_at = bp_row["measured_at"]
|
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
|
bp_for_items = None
|
||||||
if bp_row:
|
if bp_row:
|
||||||
bp_for_items = {"systolic": bp_row.get("systolic"), "diastolic": bp_row.get("diastolic")}
|
bp_for_items = {"systolic": bp_row.get("systolic"), "diastolic": bp_row.get("diastolic")}
|
||||||
|
|
|
||||||
|
|
@ -316,24 +316,57 @@ export default function RecoveryDashboardOverview({
|
||||||
}
|
}
|
||||||
|
|
||||||
const renderVitalSigns = () => {
|
const renderVitalSigns = () => {
|
||||||
if (!vitalsData || vitalsData.metadata?.confidence === 'insufficient') {
|
if (!vitalsData) {
|
||||||
return (
|
return (
|
||||||
<div style={{ padding: 20, textAlign: 'center', color: 'var(--text3)', fontSize: 12 }}>
|
<div style={{ padding: 20, textAlign: 'center', color: 'var(--text3)', fontSize: 12 }}>
|
||||||
{vitalsData?.metadata?.message || 'Keine aktuellen Vitalwerte'}
|
Keine Vital-Matrix-Daten
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
const items = vitalsData.metadata?.vital_items || []
|
const meta = vitalsData.metadata || {}
|
||||||
const chartRows = items.map((it) => ({
|
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,
|
name: it.label_de,
|
||||||
value: it.bar_value ?? 0,
|
value: Number(it.bar_value ?? 0),
|
||||||
fill: barFillForTone(it.tone),
|
fill: barFillForTone(it.tone),
|
||||||
tone: 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
|
if (items.length === 0 && chartRows.length === 0) {
|
||||||
const bpDate = vitalsData.metadata?.blood_pressure_measured_at
|
return (
|
||||||
const disclaimer = vitalsData.metadata?.disclaimer_de
|
<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 (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
@ -391,6 +424,12 @@ export default function RecoveryDashboardOverview({
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : 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 ? (
|
{chartRows.length > 0 ? (
|
||||||
<>
|
<>
|
||||||
<div style={{ fontSize: 11, color: 'var(--text3)', marginBottom: 6 }}>
|
<div style={{ fontSize: 11, color: 'var(--text3)', marginBottom: 6 }}>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user