diff --git a/backend/placeholder_resolver.py b/backend/placeholder_resolver.py index 633f4eb..928fd0e 100644 --- a/backend/placeholder_resolver.py +++ b/backend/placeholder_resolver.py @@ -290,6 +290,138 @@ def get_trainingstyp_verteilung(profile_id: str, days: int = 14) -> str: return ", ".join(parts) +def get_sleep_avg_duration(profile_id: str, days: int = 7) -> str: + """Calculate average sleep duration in hours.""" + with get_db() as conn: + cur = get_cursor(conn) + cutoff = (datetime.now() - timedelta(days=days)).strftime('%Y-%m-%d') + cur.execute( + """SELECT sleep_segments FROM sleep_log + WHERE profile_id=%s AND date >= %s + ORDER BY date DESC""", + (profile_id, cutoff) + ) + rows = cur.fetchall() + + if not rows: + return "nicht verfügbar" + + total_minutes = 0 + for row in rows: + segments = row['sleep_segments'] + if segments: + # Sum duration_min from all segments + for seg in segments: + total_minutes += seg.get('duration_min', 0) + + if total_minutes == 0: + return "nicht verfügbar" + + avg_hours = total_minutes / len(rows) / 60 + return f"{avg_hours:.1f}h" + + +def get_sleep_avg_quality(profile_id: str, days: int = 7) -> str: + """Calculate average sleep quality (Deep+REM %).""" + with get_db() as conn: + cur = get_cursor(conn) + cutoff = (datetime.now() - timedelta(days=days)).strftime('%Y-%m-%d') + cur.execute( + """SELECT sleep_segments FROM sleep_log + WHERE profile_id=%s AND date >= %s + ORDER BY date DESC""", + (profile_id, cutoff) + ) + rows = cur.fetchall() + + if not rows: + return "nicht verfügbar" + + total_quality = 0 + count = 0 + for row in rows: + segments = row['sleep_segments'] + if segments: + deep_rem_min = sum(s.get('duration_min', 0) for s in segments if s.get('stage') in ['Deep', 'REM']) + total_min = sum(s.get('duration_min', 0) for s in segments) + if total_min > 0: + quality_pct = (deep_rem_min / total_min) * 100 + total_quality += quality_pct + count += 1 + + if count == 0: + return "nicht verfügbar" + + avg_quality = total_quality / count + return f"{avg_quality:.0f}% (Deep+REM)" + + +def get_rest_days_count(profile_id: str, days: int = 30) -> str: + """Count rest days in the given period.""" + with get_db() as conn: + cur = get_cursor(conn) + cutoff = (datetime.now() - timedelta(days=days)).strftime('%Y-%m-%d') + cur.execute( + """SELECT COUNT(DISTINCT date) as count FROM rest_days + WHERE profile_id=%s AND date >= %s""", + (profile_id, cutoff) + ) + row = cur.fetchone() + count = row['count'] if row else 0 + return f"{count} Ruhetage" + + +def get_vitals_avg_hr(profile_id: str, days: int = 7) -> str: + """Calculate average resting heart rate.""" + with get_db() as conn: + cur = get_cursor(conn) + cutoff = (datetime.now() - timedelta(days=days)).strftime('%Y-%m-%d') + cur.execute( + """SELECT AVG(resting_hr) as avg FROM vitals_baseline + WHERE profile_id=%s AND date >= %s AND resting_hr IS NOT NULL""", + (profile_id, cutoff) + ) + row = cur.fetchone() + + if row and row['avg']: + return f"{int(row['avg'])} bpm" + return "nicht verfügbar" + + +def get_vitals_avg_hrv(profile_id: str, days: int = 7) -> str: + """Calculate average heart rate variability.""" + with get_db() as conn: + cur = get_cursor(conn) + cutoff = (datetime.now() - timedelta(days=days)).strftime('%Y-%m-%d') + cur.execute( + """SELECT AVG(hrv) as avg FROM vitals_baseline + WHERE profile_id=%s AND date >= %s AND hrv IS NOT NULL""", + (profile_id, cutoff) + ) + row = cur.fetchone() + + if row and row['avg']: + return f"{int(row['avg'])} ms" + return "nicht verfügbar" + + +def get_vitals_vo2_max(profile_id: str) -> str: + """Get latest VO2 Max value.""" + with get_db() as conn: + cur = get_cursor(conn) + cur.execute( + """SELECT vo2_max FROM vitals_baseline + WHERE profile_id=%s AND vo2_max IS NOT NULL + ORDER BY date DESC LIMIT 1""", + (profile_id,) + ) + row = cur.fetchone() + + if row and row['vo2_max']: + return f"{row['vo2_max']:.1f} ml/kg/min" + return "nicht verfügbar" + + # ── Placeholder Registry ────────────────────────────────────────────────────── PLACEHOLDER_MAP: Dict[str, Callable[[str], str]] = { @@ -323,6 +455,16 @@ PLACEHOLDER_MAP: Dict[str, Callable[[str], str]] = { '{{activity_detail}}': get_activity_detail, '{{trainingstyp_verteilung}}': get_trainingstyp_verteilung, + # Schlaf & Erholung + '{{sleep_avg_duration}}': lambda pid: get_sleep_avg_duration(pid, 7), + '{{sleep_avg_quality}}': lambda pid: get_sleep_avg_quality(pid, 7), + '{{rest_days_count}}': lambda pid: get_rest_days_count(pid, 30), + + # Vitalwerte + '{{vitals_avg_hr}}': lambda pid: get_vitals_avg_hr(pid, 7), + '{{vitals_avg_hrv}}': lambda pid: get_vitals_avg_hrv(pid, 7), + '{{vitals_vo2_max}}': get_vitals_vo2_max, + # Zeitraum '{{datum_heute}}': lambda pid: datetime.now().strftime('%d.%m.%Y'), '{{zeitraum_7d}}': lambda pid: 'letzte 7 Tage',