""" Orientierende Zonen-Einschätzungen für Vitalwerte (Layer 1, Issue 53). Keine Diagnose — typische Referenzbereiche für UI/Coaching. """ from __future__ import annotations from typing import Any, Dict, List, Optional, Set from data_layer.utils import safe_float Tone = str # good | warn | bad | neutral def _item( key: str, label_de: str, value_display: str, tone: Tone, zone_label_de: str, hint_de: str, sort_order: int, ) -> Dict[str, Any]: return { "key": key, "label_de": label_de, "value_display": value_display, "tone": tone, "zone_label_de": zone_label_de, "hint_de": hint_de, "sort_order": sort_order, } def assess_resting_hr(bpm: float) -> tuple: if bpm < 50: return ( "warn", "Niedrig", "Unter 50 bpm kann bei Sportlern normal sein — sonst ärztlich klären, wenn neu oder mit Beschwerden.", ) if bpm < 60: return ("good", "Günstig / athletisch", "Häufig bei gut trainierten Personen im unteren Normbereich.") if bpm <= 100: return ("good", "Im üblichen Normbereich", "Typischer Ruhepuls bei Erwachsenen oft ca. 60–100 bpm.") if bpm <= 110: return ("warn", "Leicht erhöht", "Kann durch Stress, Krankheit, Koffein oder Untrainiertheit erhöht sein — Verlauf beobachten.") return ("bad", "Deutlich erhöht", "Bei anhaltend hohem Ruhepuls medizinische Abklärung sinnvoll.") def assess_hrv_ms(ms: float) -> tuple: _ = ms return ( "neutral", "Individuell", "HRV (ms) ist sehr personenabhängig; Aussagekraft vor allem im Vergleich zu deiner eigenen Basis/Trend.", ) def assess_blood_pressure(systolic: float, diastolic: float) -> tuple: sys_, dia = systolic, diastolic if sys_ >= 180 or dia >= 110: return ("bad", "Sehr hoch", "Sehr hohe Werte — bei Beschwerden oder neu aufgetreten ärztlich zeitnah abklären.") if sys_ >= 140 or dia >= 90: return ( "bad", "Erhöht", "Liegt in einem Bereich, der oft als Hypertonie eingestuft wird — Bestätigung und Beratung durch ärztliche Messung.", ) if sys_ >= 130 or dia >= 85: return ("warn", "Hochnormal", "Oberer Normal-/hochnormaler Bereich — Lebensstil und Verlauf beachten.") if sys_ < 120 and dia < 80: return ("good", "Optimal", "Liegt in einem oft als günstig beschriebenen Bereich (<120/80 mmHg).") return ("good", "Normal", "Im gängigen Zielbereich für viele Erwachsene.") def assess_spo2(pct: float) -> tuple: if pct >= 97: return ("good", "Günstig", "Sauerstoffsättigung im üblichen Zielbereich.") if pct >= 95: return ("good", "Unauffällig", "Häufig noch als normal eingestuft; Verlauf bei Atembeschwerden beobachten.") if pct >= 90: return ("warn", "Leicht vermindert", "Unter 95 % kann je nach Kontext relevant sein — bei Symptomen abklären.") return ("bad", "Niedrig", "Niedrige SpO2 — bei anhaltend unter 90 % oder Beschwerden ärztlich vorstellen.") def assess_respiratory_rate(rpm: float) -> tuple: if 12 <= rpm <= 20: return ("good", "Im üblichen Bereich", "Ruheatmung oft ca. 12–20/min.") if 10 <= rpm < 12 or 20 < rpm <= 24: return ("warn", "Grenzbereich", "Leicht außerhalb des häufig zitierten Ruhebereichs — Kontext (Belastung, Stress) beachten.") return ("bad", "Auffällig", "Deutlich außerhalb typischer Ruhewerte — bei Beschwerden medizinisch abklären.") def assess_vo2_max(value: float) -> tuple: _ = value return ( "neutral", "Orientativ", "VO2max hängt stark von Alter, Geschlecht und Messmethode ab; Trends in der App sind aussagekräftiger als Einzelwerte.", ) def build_vital_items_from_rows( vitals_row: Optional[Dict[str, Any]], bp_row: Optional[Dict[str, Any]], omit_keys: Optional[Set[str]] = None, ) -> List[Dict[str, Any]]: """omit_keys: z. B. {'resting_hr','hrv'} wenn Einordnung zentral im Herz-/Autonomie-Block steht.""" skip = omit_keys or set() items: List[Dict[str, Any]] = [] order = 0 if vitals_row: rhr = vitals_row.get("resting_hr") if rhr is not None and "resting_hr" not in skip: v = safe_float(rhr) t, z, h = assess_resting_hr(v) items.append(_item("resting_hr", "Ruhepuls", f"{v:.0f} bpm", t, z, h, order)) order += 1 hrv = vitals_row.get("hrv") if hrv is not None and "hrv" not in skip: v = safe_float(hrv) t, z, h = assess_hrv_ms(v) items.append(_item("hrv", "HRV", f"{v:.0f} ms", t, z, h, order)) order += 1 vo2 = vitals_row.get("vo2_max") if vo2 is not None: v = safe_float(vo2) t, z, h = assess_vo2_max(v) items.append(_item("vo2_max", "VO2max", f"{v:.1f} ml/kg/min", t, z, h, order)) order += 1 spo2 = vitals_row.get("spo2") if spo2 is not None: v = safe_float(spo2) t, z, h = assess_spo2(v) items.append(_item("spo2", "SpO2", f"{v:.0f} %", t, z, h, order)) order += 1 rr = vitals_row.get("respiratory_rate") if rr is not None: v = safe_float(rr) t, z, h = assess_respiratory_rate(v) items.append(_item("respiratory_rate", "Atemfrequenz", f"{v:.0f} /min", t, z, h, order)) order += 1 if bp_row and bp_row.get("systolic") is not None and bp_row.get("diastolic") is not None: sys_v = safe_float(bp_row["systolic"]) dia_v = safe_float(bp_row["diastolic"]) t, z, h = assess_blood_pressure(sys_v, dia_v) items.append(_item("blood_pressure", "Blutdruck", f"{sys_v:.0f}/{dia_v:.0f} mmHg", t, z, h, order)) return items