""" Interpretation + KPI-Kacheln für Layer 2b Ernährungs-Verlauf. Gleiche Schwellen wie zuvor im Frontend (History.jsx); Ausgabe strukturiert für KpiTilesOverview (keys = related_placeholder_keys). """ from __future__ import annotations from typing import Any, Dict, List, Optional def _verdict(status: str) -> str: if status == "good": return "Gut" if status == "warn": return "Hinweis" return "Achtung" def build_nutrition_history_kpi_tiles( navg: Dict[str, Any], targets: Dict[str, Any], date_span_label: str, n_days_with_entries: int, ) -> List[Dict[str, Any]]: """ KPI-Kacheln wie buildNutritionKpiTiles im Frontend (Kalorien/KH/Fett + Regeln). """ kcal_avg = round(float(navg.get("kcal_avg") or 0)) avg_carbs = round(float(navg.get("carbs_avg") or 0) * 10) / 10 avg_fat = round(float(navg.get("fat_avg") or 0) * 10) / 10 avg_protein = round(float(navg.get("protein_avg") or 0) * 10) / 10 pt_low = round(float(targets.get("protein_target_low") or 0)) pt_high = round(float(targets.get("protein_target_high") or 0)) targets_ok = targets.get("confidence") != "insufficient" and pt_low > 0 protein_ok = targets_ok and avg_protein >= pt_low total_macro_kcal = avg_protein * 4 + avg_carbs * 4 + avg_fat * 9 prot_pct = ( round(avg_protein * 4 / total_macro_kcal * 100) if total_macro_kcal > 0 else 0 ) kh_pct = ( round(avg_carbs * 4 / total_macro_kcal * 100) if total_macro_kcal > 0 else 0 ) fat_pct = ( round(avg_fat * 9 / total_macro_kcal * 100) if total_macro_kcal > 0 else 0 ) tiles: List[Dict[str, Any]] = [ { "key": "kcal", "category": "Kalorien (Ø)", "icon": "🔥", "value": f"{kcal_avg} kcal", "sublabel": date_span_label, "status": "good", "verdict": "Gut", "hoverTop": "Durchschnittliche tägliche Energie", "hoverBody": f"Mittel über {n_days_with_entries} Tage mit Ernährungseinträgen im gewählten Zeitraum.", "keys": ["nutrition_score"], }, { "key": "carbs", "category": "KH (Ø)", "icon": "🌾", "value": f"{avg_carbs} g", "sublabel": "Kohlenhydrate / Tag", "status": "good", "verdict": "Gut", "hoverTop": "Durchschnittliche Kohlenhydrate", "hoverBody": "Summe der täglichen Werte im Zeitraum, gemittelt.", "keys": ["nutrition_summary"], }, { "key": "fat", "category": "Fett (Ø)", "icon": "🧈", "value": f"{avg_fat} g", "sublabel": "Fett / Tag", "status": "good", "verdict": "Gut", "hoverTop": "Durchschnittliches Fett", "hoverBody": "Summe der täglichen Werte im Zeitraum, gemittelt.", "keys": ["nutrition_summary"], }, ] if not targets_ok: tiles.append( { "key": "eval-protein", "category": "Protein", "icon": "🥩", "value": f"{avg_protein}g", "sublabel": "Referenzgewicht fehlt", "status": "warn", "verdict": _verdict("warn"), "hint": "Ohne aktuelles Körpergewicht lässt sich das Protein-Ziel (g/kg) nicht bewerten.", "hoverTop": "Protein-Ziel nicht berechenbar", "hoverBody": "Für 1,6–2,2 g/kg wird ein aktuelles Körpergewicht benötigt.", "keys": ["protein_adequacy"], } ) elif not protein_ok: miss = max(0, pt_low - round(avg_protein)) tiles.append( { "key": "eval-protein", "category": "Protein", "icon": "🥩", "value": f"{avg_protein}g", "sublabel": f"Unterversorgung: {avg_protein}g/Tag (Ziel {pt_low}–{pt_high}g)", "status": "bad", "verdict": _verdict("bad"), "hint": ( f"~{miss} g Protein/Tag fehlen – bei Defizit Muskelerhalt gefährdet." ), "hoverTop": f"Unterversorgung: {avg_protein}g/Tag (Ziel {pt_low}–{pt_high}g)", "hoverBody": ( f"1,6–2,2g/kg KG. Fehlend: ~{miss}g täglich. " "Konsequenz: Muskelverlust bei Defizit." ), "keys": ["protein_adequacy", "nutrition_score"], } ) else: tiles.append( { "key": "eval-protein", "category": "Protein", "icon": "🥩", "value": f"{avg_protein}g", "sublabel": f"Gut: {avg_protein}g/Tag (Ziel {pt_low}–{pt_high}g)", "status": "good", "verdict": _verdict("good"), "hoverTop": f"Gut: {avg_protein}g/Tag (Ziel {pt_low}–{pt_high}g)", "hoverBody": "Ausreichend für Muskelerhalt und -aufbau.", "keys": ["protein_adequacy", "nutrition_score"], } ) if prot_pct < 20 and total_macro_kcal > 0: tiles.append( { "key": "eval-macro-pct", "category": "Makro-Anteil", "icon": "📊", "value": f"{prot_pct}%", "sublabel": f"Protein-Anteil niedrig: {prot_pct}% der Kalorien", "status": "warn", "verdict": _verdict("warn"), "hint": ( f"Protein-Kalorienanteil niedrig (P {prot_pct} % / KH {kh_pct} % / F {fat_pct} %); " "Ziel oft 25–35 %." ), "hoverTop": f"Protein-Anteil niedrig: {prot_pct}% der Kalorien", "hoverBody": ( f"Empfehlung oft 25–35%. Aktuell: {prot_pct}% P / {kh_pct}% KH / {fat_pct}% F" ), "keys": ["nutrition_summary"], } ) return tiles def build_energy_availability_kpi_tile(ea: Dict[str, Any]) -> Optional[Dict[str, Any]]: """E5: nur bei caution/warning — gleiche Daten wie /charts/energy-availability-warning.""" level = str(ea.get("warning_level") or "none").strip().lower() if level == "none": return None triggers: List[str] = list(ea.get("triggers") or []) msg = str(ea.get("message") or "").strip() st = "bad" if level == "warning" else "warn" first = triggers[0] if triggers else msg if len(first) > 90: first = first[:87] + "…" meta = ea.get("metadata") if isinstance(ea.get("metadata"), dict) else {} note = str(meta.get("note") or "") hover_lines = [msg] + [f"• {t}" for t in triggers] if note: hover_lines.append(note) return { "key": "energy-availability-e5", "category": "Energieverfügbarkeit", "icon": "⚡", "value": "Achtung" if level == "warning" else "Hinweis", "sublabel": first or "Signale prüfen", "status": st, "verdict": _verdict(st), "hint": msg, "hoverTop": "Energieverfügbarkeit (Heuristik)", "hoverBody": "\n".join(hover_lines), "keys": ["nutrition_score"], } def build_macro_donut_from_averages(navg: Dict[str, Any]) -> Optional[List[Dict[str, Any]]]: """Anteile in % der Makro-kcal + Gramm für Legende.""" p = float(navg.get("protein_avg") or 0) c = float(navg.get("carbs_avg") or 0) f = float(navg.get("fat_avg") or 0) pkcal, ckcal, fkcal = p * 4, c * 4, f * 9 tot = pkcal + ckcal + fkcal if tot <= 0: return None return [ {"name": "Protein", "value": round(pkcal / tot * 100), "color": "#4a8f72", "grams": round(p, 1)}, {"name": "KH", "value": round(ckcal / tot * 100), "color": "#c17d45", "grams": round(c, 1)}, {"name": "Fett", "value": round(fkcal / tot * 100), "color": "#6e8eb8", "grams": round(f, 1)}, ]