From 337667fc0740a0b59193c57d8d6005de97d9e7a5 Mon Sep 17 00:00:00 2001 From: Lars Date: Thu, 26 Mar 2026 16:20:35 +0100 Subject: [PATCH] feat: Phase 0a - Minimal Goal System (Strategic + Tactical) - Strategic Layer: Goal modes (weight_loss, strength, endurance, recomposition, health) - Tactical Layer: Concrete goal targets with progress tracking - Training phases (manual + auto-detection framework) - Fitness tests (standardized performance tracking) Backend: - Migration 022: goal_mode in profiles, goals, training_phases, fitness_tests tables - New router: routers/goals.py with full CRUD for goals, phases, tests - API endpoints: /api/goals/* (mode, list, create, update, delete) Frontend: - GoalsPage: Goal mode selector + goal management UI - Dashboard: Goals preview card with link - API integration: goal mode, CRUD operations, progress calculation Basis for 120+ placeholders and goal-aware analyses (Phase 0b) Co-Authored-By: Claude Opus 4.6 --- backend/main.py | 2 + backend/migrations/022_goal_system.sql | 147 ++++++ backend/routers/goals.py | 473 ++++++++++++++++++++ docs/GOALS_SYSTEM_UNIFIED_ANALYSIS.md | 595 +++++++++++++++++++++++++ frontend/src/App.jsx | 2 + frontend/src/pages/Dashboard.jsx | 15 + frontend/src/pages/GoalsPage.jsx | 562 +++++++++++++++++++++++ frontend/src/utils/api.js | 17 + 8 files changed, 1813 insertions(+) create mode 100644 backend/migrations/022_goal_system.sql create mode 100644 backend/routers/goals.py create mode 100644 docs/GOALS_SYSTEM_UNIFIED_ANALYSIS.md create mode 100644 frontend/src/pages/GoalsPage.jsx diff --git a/backend/main.py b/backend/main.py index 63faff3..738f07e 100644 --- a/backend/main.py +++ b/backend/main.py @@ -23,6 +23,7 @@ from routers import user_restrictions, access_grants, training_types, admin_trai from routers import admin_activity_mappings, sleep, rest_days from routers import vitals_baseline, blood_pressure # v9d Phase 2d Refactored from routers import evaluation # v9d/v9e Training Type Profiles (#15) +from routers import goals # v9e Goal System (Strategic + Tactical) # ── App Configuration ───────────────────────────────────────────────────────── DATA_DIR = Path(os.getenv("DATA_DIR", "./data")) @@ -97,6 +98,7 @@ app.include_router(rest_days.router) # /api/rest-days/* (v9d Phase 2a app.include_router(vitals_baseline.router) # /api/vitals/baseline/* (v9d Phase 2d Refactored) app.include_router(blood_pressure.router) # /api/blood-pressure/* (v9d Phase 2d Refactored) app.include_router(evaluation.router) # /api/evaluation/* (v9d/v9e Training Profiles #15) +app.include_router(goals.router) # /api/goals/* (v9e Goal System Strategic + Tactical) # ── Health Check ────────────────────────────────────────────────────────────── @app.get("/") diff --git a/backend/migrations/022_goal_system.sql b/backend/migrations/022_goal_system.sql new file mode 100644 index 0000000..5157148 --- /dev/null +++ b/backend/migrations/022_goal_system.sql @@ -0,0 +1,147 @@ +-- Migration 022: Goal System (Strategic + Tactical) +-- Date: 2026-03-26 +-- Purpose: Two-level goal architecture for AI-driven coaching + +-- ============================================================================ +-- STRATEGIC LAYER: Goal Modes +-- ============================================================================ + +-- Add goal_mode to profiles (strategic training direction) +ALTER TABLE profiles ADD COLUMN IF NOT EXISTS goal_mode VARCHAR(50) DEFAULT 'health'; + +COMMENT ON COLUMN profiles.goal_mode IS + 'Strategic goal mode: weight_loss, strength, endurance, recomposition, health. + Determines score weights and interpretation context for all analyses.'; + +-- ============================================================================ +-- TACTICAL LAYER: Concrete Goal Targets +-- ============================================================================ + +CREATE TABLE IF NOT EXISTS goals ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + profile_id UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE, + + -- Goal Classification + goal_type VARCHAR(50) NOT NULL, -- weight, body_fat, lean_mass, vo2max, strength, flexibility, bp, rhr + is_primary BOOLEAN DEFAULT false, + status VARCHAR(20) DEFAULT 'active', -- draft, active, reached, abandoned, expired + + -- Target Values + target_value DECIMAL(10,2), + current_value DECIMAL(10,2), + start_value DECIMAL(10,2), + unit VARCHAR(20), -- kg, %, ml/kg/min, bpm, mmHg, cm, reps + + -- Timeline + start_date DATE DEFAULT CURRENT_DATE, + target_date DATE, + reached_date DATE, + + -- Metadata + name VARCHAR(100), -- e.g., "Sommerfigur 2026" + description TEXT, + + -- Progress Tracking + progress_pct DECIMAL(5,2), -- Auto-calculated: (current - start) / (target - start) * 100 + projection_date DATE, -- Prognose wann Ziel erreicht wird + on_track BOOLEAN, -- true wenn Prognose <= target_date + + -- Timestamps + created_at TIMESTAMP DEFAULT NOW(), + updated_at TIMESTAMP DEFAULT NOW() +); + +CREATE INDEX IF NOT EXISTS idx_goals_profile ON goals(profile_id); +CREATE INDEX IF NOT EXISTS idx_goals_status ON goals(profile_id, status); +CREATE INDEX IF NOT EXISTS idx_goals_primary ON goals(profile_id, is_primary) WHERE is_primary = true; + +COMMENT ON TABLE goals IS 'Concrete user goals (tactical targets)'; +COMMENT ON COLUMN goals.goal_type IS 'Type of goal: weight, body_fat, lean_mass, vo2max, strength, flexibility, bp, rhr'; +COMMENT ON COLUMN goals.is_primary IS 'Primary goal gets highest priority in scoring and charts'; +COMMENT ON COLUMN goals.status IS 'draft = not yet started, active = in progress, reached = successfully completed, abandoned = given up, expired = deadline passed'; +COMMENT ON COLUMN goals.progress_pct IS 'Percentage progress: (current_value - start_value) / (target_value - start_value) * 100'; +COMMENT ON COLUMN goals.projection_date IS 'Projected date when goal will be reached based on current trend'; +COMMENT ON COLUMN goals.on_track IS 'true if projection_date <= target_date (goal reachable on time)'; + +-- ============================================================================ +-- TRAINING PHASES (Auto-Detection) +-- ============================================================================ + +CREATE TABLE IF NOT EXISTS training_phases ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + profile_id UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE, + + -- Phase Classification + phase_type VARCHAR(50) NOT NULL, -- calorie_deficit, calorie_surplus, deload, maintenance, periodization + detected_automatically BOOLEAN DEFAULT false, + confidence_score DECIMAL(3,2), -- 0.00 - 1.00 (Wie sicher ist die Erkennung?) + status VARCHAR(20) DEFAULT 'suggested', -- suggested, accepted, active, completed, rejected + + -- Timeframe + start_date DATE NOT NULL, + end_date DATE, + duration_days INT, + + -- Detection Criteria (JSONB für Flexibilität) + detection_params JSONB, -- { "avg_calories": 1800, "weight_trend": -0.3, ... } + + -- User Notes + notes TEXT, + + -- Timestamps + created_at TIMESTAMP DEFAULT NOW(), + updated_at TIMESTAMP DEFAULT NOW() +); + +CREATE INDEX IF NOT EXISTS idx_training_phases_profile ON training_phases(profile_id); +CREATE INDEX IF NOT EXISTS idx_training_phases_status ON training_phases(profile_id, status); +CREATE INDEX IF NOT EXISTS idx_training_phases_dates ON training_phases(profile_id, start_date, end_date); + +COMMENT ON TABLE training_phases IS 'Training phases detected from data patterns or manually defined'; +COMMENT ON COLUMN training_phases.phase_type IS 'calorie_deficit, calorie_surplus, deload, maintenance, periodization'; +COMMENT ON COLUMN training_phases.detected_automatically IS 'true if AI detected this phase from data patterns'; +COMMENT ON COLUMN training_phases.confidence_score IS 'AI confidence in detection (0.0 - 1.0)'; +COMMENT ON COLUMN training_phases.status IS 'suggested = AI proposed, accepted = user confirmed, active = currently running, completed = finished, rejected = user dismissed'; +COMMENT ON COLUMN training_phases.detection_params IS 'JSON with detection criteria: avg_calories, weight_trend, activity_volume, etc.'; + +-- ============================================================================ +-- FITNESS TESTS (Standardized Performance Tests) +-- ============================================================================ + +CREATE TABLE IF NOT EXISTS fitness_tests ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + profile_id UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE, + + -- Test Type + test_type VARCHAR(50) NOT NULL, -- cooper_12min, step_test, pushups_max, plank_max, flexibility_sit_reach, vo2max_est, strength_1rm_squat, strength_1rm_bench + result_value DECIMAL(10,2) NOT NULL, + result_unit VARCHAR(20) NOT NULL, -- meters, bpm, reps, seconds, cm, ml/kg/min, kg + + -- Test Metadata + test_date DATE NOT NULL, + test_conditions TEXT, -- Optional: Notizen zu Bedingungen + norm_category VARCHAR(30), -- sehr gut, gut, durchschnitt, unterdurchschnitt, schlecht + + -- Timestamps + created_at TIMESTAMP DEFAULT NOW() +); + +CREATE INDEX IF NOT EXISTS idx_fitness_tests_profile ON fitness_tests(profile_id); +CREATE INDEX IF NOT EXISTS idx_fitness_tests_type ON fitness_tests(profile_id, test_type); +CREATE INDEX IF NOT EXISTS idx_fitness_tests_date ON fitness_tests(profile_id, test_date); + +COMMENT ON TABLE fitness_tests IS 'Standardized fitness tests (Cooper, step test, strength tests, etc.)'; +COMMENT ON COLUMN fitness_tests.test_type IS 'cooper_12min, step_test, pushups_max, plank_max, flexibility_sit_reach, vo2max_est, strength_1rm_squat, strength_1rm_bench'; +COMMENT ON COLUMN fitness_tests.norm_category IS 'Performance category based on age/gender norms'; + +-- ============================================================================ +-- VERSION UPDATE +-- ============================================================================ + +-- Track migration +DO $$ +BEGIN + IF NOT EXISTS (SELECT 1 FROM schema_migrations WHERE version = '022') THEN + INSERT INTO schema_migrations (version, applied_at) VALUES ('022', NOW()); + END IF; +END $$; diff --git a/backend/routers/goals.py b/backend/routers/goals.py new file mode 100644 index 0000000..919bec3 --- /dev/null +++ b/backend/routers/goals.py @@ -0,0 +1,473 @@ +""" +Goals Router - Goal System (Strategic + Tactical) + +Endpoints for managing: +- Strategic goal modes (weight_loss, strength, etc.) +- Tactical goal targets (concrete values with deadlines) +- Training phase detection +- Fitness tests + +Part of v9e Goal System implementation. +""" +from fastapi import APIRouter, Depends, HTTPException +from pydantic import BaseModel +from typing import Optional, List +from datetime import date, datetime, timedelta +from decimal import Decimal +import json + +from db import get_db, get_cursor, r2d +from auth import require_auth + +router = APIRouter(prefix="/api/goals", tags=["goals"]) + +# ============================================================================ +# Pydantic Models +# ============================================================================ + +class GoalModeUpdate(BaseModel): + """Update strategic goal mode""" + goal_mode: str # weight_loss, strength, endurance, recomposition, health + +class GoalCreate(BaseModel): + """Create or update a concrete goal""" + goal_type: str # weight, body_fat, lean_mass, vo2max, strength, flexibility, bp, rhr + is_primary: bool = False + target_value: float + unit: str # kg, %, ml/kg/min, bpm, mmHg, cm, reps + target_date: Optional[date] = None + name: Optional[str] = None + description: Optional[str] = None + +class GoalUpdate(BaseModel): + """Update existing goal""" + target_value: Optional[float] = None + target_date: Optional[date] = None + status: Optional[str] = None # active, reached, abandoned, expired + name: Optional[str] = None + description: Optional[str] = None + +class TrainingPhaseCreate(BaseModel): + """Create training phase (manual or auto-detected)""" + phase_type: str # calorie_deficit, calorie_surplus, deload, maintenance, periodization + start_date: date + end_date: Optional[date] = None + notes: Optional[str] = None + +class FitnessTestCreate(BaseModel): + """Record fitness test result""" + test_type: str + result_value: float + result_unit: str + test_date: date + test_conditions: Optional[str] = None + +# ============================================================================ +# Strategic Layer: Goal Modes +# ============================================================================ + +@router.get("/mode") +def get_goal_mode(session: dict = Depends(require_auth)): + """Get user's current strategic goal mode""" + pid = session['profile_id'] + + with get_db() as conn: + cur = get_cursor(conn) + cur.execute( + "SELECT goal_mode FROM profiles WHERE id = %s", + (pid,) + ) + row = cur.fetchone() + if not row: + raise HTTPException(status_code=404, detail="Profil nicht gefunden") + + return { + "goal_mode": row['goal_mode'] or 'health', + "description": _get_goal_mode_description(row['goal_mode'] or 'health') + } + +@router.put("/mode") +def update_goal_mode(data: GoalModeUpdate, session: dict = Depends(require_auth)): + """Update user's strategic goal mode""" + pid = session['profile_id'] + + # Validate goal mode + valid_modes = ['weight_loss', 'strength', 'endurance', 'recomposition', 'health'] + if data.goal_mode not in valid_modes: + raise HTTPException( + status_code=400, + detail=f"Ungültiger Goal Mode. Erlaubt: {', '.join(valid_modes)}" + ) + + with get_db() as conn: + cur = get_cursor(conn) + cur.execute( + "UPDATE profiles SET goal_mode = %s WHERE id = %s", + (data.goal_mode, pid) + ) + + return { + "goal_mode": data.goal_mode, + "description": _get_goal_mode_description(data.goal_mode) + } + +def _get_goal_mode_description(mode: str) -> str: + """Get description for goal mode""" + descriptions = { + 'weight_loss': 'Gewichtsreduktion (Kaloriendefizit, Fettabbau)', + 'strength': 'Kraftaufbau (Muskelwachstum, progressive Belastung)', + 'endurance': 'Ausdauer (VO2Max, aerobe Kapazität)', + 'recomposition': 'Körperkomposition (gleichzeitig Fett ab- und Muskeln aufbauen)', + 'health': 'Allgemeine Gesundheit (ausgewogen, präventiv)' + } + return descriptions.get(mode, 'Unbekannt') + +# ============================================================================ +# Tactical Layer: Concrete Goals +# ============================================================================ + +@router.get("/list") +def list_goals(session: dict = Depends(require_auth)): + """List all goals for current user""" + pid = session['profile_id'] + + with get_db() as conn: + cur = get_cursor(conn) + cur.execute(""" + SELECT id, goal_type, is_primary, status, + target_value, current_value, start_value, unit, + start_date, target_date, reached_date, + name, description, + progress_pct, projection_date, on_track, + created_at, updated_at + FROM goals + WHERE profile_id = %s + ORDER BY is_primary DESC, created_at DESC + """, (pid,)) + + goals = [r2d(row) for row in cur.fetchall()] + + # Update current values for each goal + for goal in goals: + _update_goal_progress(conn, pid, goal) + + return goals + +@router.post("/create") +def create_goal(data: GoalCreate, session: dict = Depends(require_auth)): + """Create new goal""" + pid = session['profile_id'] + + with get_db() as conn: + cur = get_cursor(conn) + + # If this is set as primary, unset other primary goals + if data.is_primary: + cur.execute( + "UPDATE goals SET is_primary = false WHERE profile_id = %s", + (pid,) + ) + + # Get current value for this goal type + current_value = _get_current_value_for_goal_type(conn, pid, data.goal_type) + + # Insert goal + cur.execute(""" + INSERT INTO goals ( + profile_id, goal_type, is_primary, + target_value, current_value, start_value, unit, + target_date, name, description + ) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s) + RETURNING id + """, ( + pid, data.goal_type, data.is_primary, + data.target_value, current_value, current_value, data.unit, + data.target_date, data.name, data.description + )) + + goal_id = cur.fetchone()['id'] + + return {"id": goal_id, "message": "Ziel erstellt"} + +@router.put("/{goal_id}") +def update_goal(goal_id: str, data: GoalUpdate, session: dict = Depends(require_auth)): + """Update existing goal""" + pid = session['profile_id'] + + with get_db() as conn: + cur = get_cursor(conn) + + # Verify ownership + cur.execute( + "SELECT id FROM goals WHERE id = %s AND profile_id = %s", + (goal_id, pid) + ) + if not cur.fetchone(): + raise HTTPException(status_code=404, detail="Ziel nicht gefunden") + + # Build update query dynamically + updates = [] + params = [] + + if data.target_value is not None: + updates.append("target_value = %s") + params.append(data.target_value) + + if data.target_date is not None: + updates.append("target_date = %s") + params.append(data.target_date) + + if data.status is not None: + updates.append("status = %s") + params.append(data.status) + if data.status == 'reached': + updates.append("reached_date = CURRENT_DATE") + + if data.name is not None: + updates.append("name = %s") + params.append(data.name) + + if data.description is not None: + updates.append("description = %s") + params.append(data.description) + + if not updates: + raise HTTPException(status_code=400, detail="Keine Änderungen angegeben") + + updates.append("updated_at = NOW()") + params.extend([goal_id, pid]) + + cur.execute( + f"UPDATE goals SET {', '.join(updates)} WHERE id = %s AND profile_id = %s", + tuple(params) + ) + + return {"message": "Ziel aktualisiert"} + +@router.delete("/{goal_id}") +def delete_goal(goal_id: str, session: dict = Depends(require_auth)): + """Delete goal""" + pid = session['profile_id'] + + with get_db() as conn: + cur = get_cursor(conn) + cur.execute( + "DELETE FROM goals WHERE id = %s AND profile_id = %s", + (goal_id, pid) + ) + + if cur.rowcount == 0: + raise HTTPException(status_code=404, detail="Ziel nicht gefunden") + + return {"message": "Ziel gelöscht"} + +# ============================================================================ +# Training Phases +# ============================================================================ + +@router.get("/phases") +def list_training_phases(session: dict = Depends(require_auth)): + """List training phases""" + pid = session['profile_id'] + + with get_db() as conn: + cur = get_cursor(conn) + cur.execute(""" + SELECT id, phase_type, detected_automatically, confidence_score, + status, start_date, end_date, duration_days, + detection_params, notes, created_at + FROM training_phases + WHERE profile_id = %s + ORDER BY start_date DESC + """, (pid,)) + + return [r2d(row) for row in cur.fetchall()] + +@router.post("/phases") +def create_training_phase(data: TrainingPhaseCreate, session: dict = Depends(require_auth)): + """Create training phase (manual)""" + pid = session['profile_id'] + + with get_db() as conn: + cur = get_cursor(conn) + + duration = None + if data.end_date: + duration = (data.end_date - data.start_date).days + + cur.execute(""" + INSERT INTO training_phases ( + profile_id, phase_type, detected_automatically, + status, start_date, end_date, duration_days, notes + ) VALUES (%s, %s, %s, %s, %s, %s, %s, %s) + RETURNING id + """, ( + pid, data.phase_type, False, + 'active', data.start_date, data.end_date, duration, data.notes + )) + + phase_id = cur.fetchone()['id'] + + return {"id": phase_id, "message": "Trainingsphase erstellt"} + +@router.put("/phases/{phase_id}/status") +def update_phase_status( + phase_id: str, + status: str, + session: dict = Depends(require_auth) +): + """Update training phase status (accept/reject auto-detected phases)""" + pid = session['profile_id'] + + valid_statuses = ['suggested', 'accepted', 'active', 'completed', 'rejected'] + if status not in valid_statuses: + raise HTTPException( + status_code=400, + detail=f"Ungültiger Status. Erlaubt: {', '.join(valid_statuses)}" + ) + + with get_db() as conn: + cur = get_cursor(conn) + cur.execute( + "UPDATE training_phases SET status = %s WHERE id = %s AND profile_id = %s", + (status, phase_id, pid) + ) + + if cur.rowcount == 0: + raise HTTPException(status_code=404, detail="Trainingsphase nicht gefunden") + + return {"message": "Status aktualisiert"} + +# ============================================================================ +# Fitness Tests +# ============================================================================ + +@router.get("/tests") +def list_fitness_tests(session: dict = Depends(require_auth)): + """List all fitness tests""" + pid = session['profile_id'] + + with get_db() as conn: + cur = get_cursor(conn) + cur.execute(""" + SELECT id, test_type, result_value, result_unit, + test_date, test_conditions, norm_category, created_at + FROM fitness_tests + WHERE profile_id = %s + ORDER BY test_date DESC + """, (pid,)) + + return [r2d(row) for row in cur.fetchall()] + +@router.post("/tests") +def create_fitness_test(data: FitnessTestCreate, session: dict = Depends(require_auth)): + """Record fitness test result""" + pid = session['profile_id'] + + with get_db() as conn: + cur = get_cursor(conn) + + # Calculate norm category (simplified for now) + norm_category = _calculate_norm_category( + data.test_type, + data.result_value, + data.result_unit + ) + + cur.execute(""" + INSERT INTO fitness_tests ( + profile_id, test_type, result_value, result_unit, + test_date, test_conditions, norm_category + ) VALUES (%s, %s, %s, %s, %s, %s, %s) + RETURNING id + """, ( + pid, data.test_type, data.result_value, data.result_unit, + data.test_date, data.test_conditions, norm_category + )) + + test_id = cur.fetchone()['id'] + + return {"id": test_id, "norm_category": norm_category} + +# ============================================================================ +# Helper Functions +# ============================================================================ + +def _get_current_value_for_goal_type(conn, profile_id: str, goal_type: str) -> Optional[float]: + """Get current value for a goal type from latest data""" + cur = get_cursor(conn) + + if goal_type == 'weight': + cur.execute(""" + SELECT weight FROM weight_log + WHERE profile_id = %s + ORDER BY date DESC LIMIT 1 + """, (profile_id,)) + row = cur.fetchone() + return float(row['weight']) if row else None + + elif goal_type == 'body_fat': + cur.execute(""" + SELECT body_fat_pct FROM caliper_log + WHERE profile_id = %s + ORDER BY date DESC LIMIT 1 + """, (profile_id,)) + row = cur.fetchone() + return float(row['body_fat_pct']) if row else None + + elif goal_type == 'vo2max': + cur.execute(""" + SELECT vo2max FROM vitals_baseline + WHERE profile_id = %s AND vo2max IS NOT NULL + ORDER BY date DESC LIMIT 1 + """, (profile_id,)) + row = cur.fetchone() + return float(row['vo2max']) if row else None + + elif goal_type == 'rhr': + cur.execute(""" + SELECT resting_hr FROM vitals_baseline + WHERE profile_id = %s AND resting_hr IS NOT NULL + ORDER BY date DESC LIMIT 1 + """, (profile_id,)) + row = cur.fetchone() + return float(row['resting_hr']) if row else None + + return None + +def _update_goal_progress(conn, profile_id: str, goal: dict): + """Update goal progress (modifies goal dict in-place)""" + # Get current value + current = _get_current_value_for_goal_type(conn, profile_id, goal['goal_type']) + + if current is not None and goal['start_value'] is not None and goal['target_value'] is not None: + goal['current_value'] = current + + # Calculate progress percentage + total_delta = float(goal['target_value']) - float(goal['start_value']) + current_delta = current - float(goal['start_value']) + + if total_delta != 0: + progress_pct = (current_delta / total_delta) * 100 + goal['progress_pct'] = round(progress_pct, 2) + + # Simple linear projection + if goal['start_date'] and current_delta != 0: + days_elapsed = (date.today() - goal['start_date']).days + if days_elapsed > 0: + days_per_unit = days_elapsed / current_delta + remaining_units = float(goal['target_value']) - current + remaining_days = int(days_per_unit * remaining_units) + goal['projection_date'] = date.today() + timedelta(days=remaining_days) + + # Check if on track + if goal['target_date'] and goal['projection_date']: + goal['on_track'] = goal['projection_date'] <= goal['target_date'] + +def _calculate_norm_category(test_type: str, value: float, unit: str) -> Optional[str]: + """ + Calculate norm category for fitness test + (Simplified - would need age/gender-specific norms) + """ + # Placeholder - should use proper norm tables + return None diff --git a/docs/GOALS_SYSTEM_UNIFIED_ANALYSIS.md b/docs/GOALS_SYSTEM_UNIFIED_ANALYSIS.md new file mode 100644 index 0000000..f69134a --- /dev/null +++ b/docs/GOALS_SYSTEM_UNIFIED_ANALYSIS.md @@ -0,0 +1,595 @@ +# Zielesystem: Vereinheitlichte Analyse beider Fachkonzepte + +**Datum:** 26. März 2026 +**Basis:** +- `.claude/docs/functional/GOALS_VITALS.md` (v9e Spec) +- `.claude/docs/functional/mitai_jinkendo_konzept_diagramme_auswertungen_v2.md` + +--- + +## 1. Wichtige Erkenntnis: BEIDE Konzepte sind komplementär! + +### GOALS_VITALS.md definiert: +- **Konkrete Zielwerte** (z.B. "82kg bis 30.06.2026") +- 8 Zieltypen (Gewicht, KF%, VO2Max, etc.) +- Primär-/Nebenziel-Konzept +- Trainingsphasen (automatische Erkennung) +- Aktive Tests (Cooper, Liegestütze, etc.) +- 13 neue KI-Platzhalter + +### Konzept v2 definiert: +- **Goal Modes** (strategische Ausrichtung: weight_loss, strength, etc.) +- Score-Gewichtung je Goal Mode +- Chart-Priorisierung je Goal Mode +- Regelbasierte Interpretationen + +### Zusammenspiel: +``` +Goal MODE (v2) → "weight_loss" (strategische Ausrichtung) + ↓ +Primary GOAL (v9e) → "82kg bis 30.06.2026" (konkretes Ziel) +Secondary GOAL → "16% Körperfett" + ↓ +Training PHASE (v9e) → "Kaloriendefizit" (automatisch erkannt) + ↓ +Score Weights (v2) → body_progress: 0.30, nutrition: 0.25, ... + ↓ +Charts (v2) → Zeigen gewichtete Scores + Fortschritt zu Zielen +``` + +--- + +## 2. Zwei-Ebenen-Architektur + +### Ebene 1: STRATEGIC (Goal Modes aus v2) +**Was:** Grundsätzliche Trainingsausrichtung +**Werte:** weight_loss, strength, endurance, recomposition, health +**Zweck:** Bestimmt Score-Gewichtung und Interpretations-Kontext +**Beispiel:** "Ich will Kraft aufbauen" → mode: strength + +### Ebene 2: TACTICAL (Goal Targets aus v9e) +**Was:** Konkrete messbare Ziele +**Werte:** "82kg bis 30.06.2026", "VO2Max 55 ml/kg/min", "50 Liegestütze" +**Zweck:** Fortschritts-Tracking, Prognosen, Motivation +**Beispiel:** "Ich will 82kg wiegen" → target: Gewichtsziel + +### Beide zusammen = Vollständiges Zielesystem + +--- + +## 3. Überarbeitetes Datenmodell + +### Tabelle: `profiles` (erweitern) +```sql +-- Strategic Goal Mode (aus v2) +ALTER TABLE profiles ADD COLUMN goal_mode VARCHAR(50) DEFAULT 'health'; + +COMMENT ON COLUMN profiles.goal_mode IS + 'Strategic goal mode: weight_loss, strength, endurance, recomposition, health. + Determines score weights and interpretation context.'; +``` + +### Tabelle: `goals` (NEU, aus v9e) +```sql +CREATE TABLE goals ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + profile_id UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE, + + -- Goal Classification + goal_type VARCHAR(50) NOT NULL, -- weight, body_fat, lean_mass, vo2max, strength, flexibility, bp, rhr + is_primary BOOLEAN DEFAULT false, + status VARCHAR(20) DEFAULT 'active', -- draft, active, reached, abandoned, expired + + -- Target Values + target_value DECIMAL(10,2), + current_value DECIMAL(10,2), + start_value DECIMAL(10,2), + unit VARCHAR(20), -- kg, %, ml/kg/min, bpm, mmHg, cm, reps + + -- Timeline + start_date DATE DEFAULT CURRENT_DATE, + target_date DATE, + reached_date DATE, + + -- Metadata + name VARCHAR(100), -- z.B. "Sommerfigur 2026" + description TEXT, + + -- Progress Tracking + progress_pct DECIMAL(5,2), -- Auto-calculated: (current - start) / (target - start) * 100 + + -- Timestamps + created_at TIMESTAMP DEFAULT NOW(), + updated_at TIMESTAMP DEFAULT NOW(), + + -- Constraints + CHECK (progress_pct >= 0 AND progress_pct <= 100), + CHECK (status IN ('draft', 'active', 'reached', 'abandoned', 'expired')) +); + +-- Only one primary goal per profile +CREATE UNIQUE INDEX idx_goals_primary ON goals(profile_id, is_primary) WHERE is_primary = true; + +-- Index for active goals lookup +CREATE INDEX idx_goals_active ON goals(profile_id, status) WHERE status = 'active'; +``` + +### Tabelle: `training_phases` (NEU, aus v9e) +```sql +CREATE TABLE training_phases ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + profile_id UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE, + + -- Phase Type + phase_type VARCHAR(50) NOT NULL, + -- Werte: calorie_deficit, calorie_maintenance, calorie_surplus, + -- conditioning, hiit, max_strength, regeneration, competition_prep + + -- Detection + detected_automatically BOOLEAN DEFAULT false, + confidence_score DECIMAL(3,2), -- 0.00-1.00 + + -- Status + status VARCHAR(20) DEFAULT 'suggested', -- suggested, confirmed, active, ended + + -- Timeline + start_date DATE, + end_date DATE, + + -- Metadata + detection_reason TEXT, -- Why was this phase detected? + user_notes TEXT, + + -- Timestamps + created_at TIMESTAMP DEFAULT NOW(), + updated_at TIMESTAMP DEFAULT NOW() +); + +-- Only one active phase per profile +CREATE UNIQUE INDEX idx_phases_active ON training_phases(profile_id, status) WHERE status = 'active'; +``` + +### Tabelle: `fitness_tests` (NEU, aus v9e) +```sql +CREATE TABLE fitness_tests ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + profile_id UUID NOT NULL REFERENCES profiles(id) ON DELETE CASCADE, + + -- Test Type + test_type VARCHAR(50) NOT NULL, + -- Standard: cooper, step_test, pushups, squats, sit_reach, balance, grip_strength + -- Custom: user_defined + + -- Result + result_value DECIMAL(10,2) NOT NULL, + result_unit VARCHAR(20) NOT NULL, -- meters, bpm, reps, cm, seconds, kg + + -- Test Date + test_date DATE NOT NULL, + + -- Evaluation + norm_category VARCHAR(30), -- very_good, good, average, needs_improvement + percentile DECIMAL(5,2), -- Where user ranks vs. norm (0-100) + + -- Trend + improvement_vs_last DECIMAL(10,2), -- % change from previous test + + -- Metadata + notes TEXT, + conditions TEXT, -- e.g., "Nach 3h Schlaf, erkältet" + + -- Next Test Recommendation + recommended_retest_date DATE, + + created_at TIMESTAMP DEFAULT NOW() +); + +CREATE INDEX idx_fitness_tests_profile_type ON fitness_tests(profile_id, test_type, test_date DESC); +``` + +--- + +## 4. Vereinheitlichte API-Struktur + +### Goal Modes (Strategic) +```python +# routers/goals.py + +@router.get("/modes") +def get_goal_modes(): + """Get all strategic goal modes with score weights.""" + return GOAL_MODES # From v2 concept + +@router.post("/set-mode") +def set_goal_mode(goal_mode: str, session=Depends(require_auth)): + """Set user's strategic goal mode.""" + # Updates profiles.goal_mode +``` + +### Goal Targets (Tactical) +```python +@router.get("/targets") +def get_goal_targets(session=Depends(require_auth)): + """Get all active goal targets.""" + profile_id = session['profile_id'] + # Returns list from goals table + # Includes: primary + all secondary goals + +@router.post("/targets") +def create_goal_target(goal: GoalCreate, session=Depends(require_auth)): + """Create a new goal target.""" + # Inserts into goals table + # Auto-calculates progress_pct + +@router.get("/targets/{goal_id}") +def get_goal_detail(goal_id: str, session=Depends(require_auth)): + """Get detailed goal info with history.""" + # Returns goal + progress history + prognosis + +@router.put("/targets/{goal_id}/progress") +def update_goal_progress(goal_id: str, session=Depends(require_auth)): + """Recalculate goal progress.""" + # Auto-called after new measurements + # Updates current_value, progress_pct + +@router.post("/targets/{goal_id}/reach") +def mark_goal_reached(goal_id: str, session=Depends(require_auth)): + """Mark goal as reached.""" + # Sets status='reached', reached_date=today +``` + +### Training Phases +```python +@router.get("/phases/current") +def get_current_phase(session=Depends(require_auth)): + """Get active training phase.""" + +@router.get("/phases/detect") +def detect_phase(session=Depends(require_auth)): + """Run phase detection algorithm.""" + # Analyzes last 14 days + # Returns suggested phase + confidence + reasoning + +@router.post("/phases/confirm") +def confirm_phase(phase_id: str, session=Depends(require_auth)): + """Confirm detected phase.""" + # Sets status='active' +``` + +### Fitness Tests +```python +@router.get("/tests/types") +def get_test_types(): + """Get all available fitness tests.""" + +@router.post("/tests/{test_type}/execute") +def record_test_result( + test_type: str, + result_value: float, + result_unit: str, + session=Depends(require_auth) +): + """Record a fitness test result.""" + # Inserts into fitness_tests + # Auto-calculates norm_category, percentile, improvement + +@router.get("/tests/due") +def get_due_tests(session=Depends(require_auth)): + """Get tests that are due for retesting.""" +``` + +--- + +## 5. Neue KI-Platzhalter (kombiniert aus beiden Konzepten) + +### Strategic (aus v2) +```python +{{goal_mode}} # "weight_loss" +{{goal_mode_label}} # "Gewichtsreduktion" +{{goal_mode_description}} # "Fettabbau bei Erhalt der Magermasse" +``` + +### Tactical - Primary Goal (aus v9e) +```python +{{primary_goal_type}} # "weight" +{{primary_goal_name}} # "Sommerfigur 2026" +{{primary_goal_target}} # "82 kg bis 30.06.2026" +{{primary_goal_current}} # "85.2 kg" +{{primary_goal_start}} # "86.1 kg" +{{primary_goal_progress_pct}} # "72%" +{{primary_goal_progress_text}} # "72% erreicht (4 kg von 5,5 kg)" +{{primary_goal_days_remaining}} # "45 Tage" +{{primary_goal_prognosis}} # "Ziel voraussichtlich in 6 Wochen erreicht (3 Wochen früher!)" +{{primary_goal_on_track}} # "true" +``` + +### Tactical - Secondary Goals (aus v9e) +```python +{{secondary_goals_count}} # "2" +{{secondary_goals_list}} # "16% Körperfett, VO2Max 55 ml/kg/min" +{{secondary_goal_1_type}} # "body_fat" +{{secondary_goal_1_progress}} # "45%" +``` + +### Training Phase (aus v9e) +```python +{{current_phase}} # "calorie_deficit" +{{current_phase_label}} # "Kaloriendefizit" +{{phase_since}} # "seit 14 Tagen" +{{phase_confidence}} # "0.92" +{{phase_recommendation}} # "Krafttraining erhalten, Cardio moderat, Proteinzufuhr 2g/kg" +{{phase_detected_automatically}} # "true" +``` + +### Fitness Tests (aus v9e) +```python +{{test_last_cooper}} # "2.800m (VO2Max ~52) vor 3 Wochen" +{{test_last_cooper_date}} # "2026-03-05" +{{test_last_cooper_result}} # "2800" +{{test_last_cooper_vo2max}} # "52.3" +{{test_last_cooper_category}} # "good" +{{test_due_list}} # "Sit & Reach (seit 5 Wochen), Liegestütze (seit 4 Wochen)" +{{test_next_recommended}} # "Cooper-Test (in 2 Wochen fällig)" +{{fitness_score_overall}} # "72/100" +{{fitness_score_endurance}} # "good" +{{fitness_score_strength}} # "average" +{{fitness_score_flexibility}} # "needs_improvement" +``` + +### GESAMT: 35+ neue Platzhalter aus v9e +Plus die 84 aus v2 = **120+ neue Platzhalter total** + +--- + +## 6. Überarbeitete Implementierungs-Roadmap + +### Phase 0a: Minimal Goal System (3-4h) ⭐ **JETZT** + +**Strategic Layer:** +- DB: `goal_mode` in profiles +- Backend: GOAL_MODES aus v2 +- API: GET/SET goal mode +- UI: Goal Mode Selector (5 Modi) + +**Tactical Layer:** +- DB: `goals` table +- API: CRUD für goal targets +- UI: Goal Management Page (minimal) + - Liste aktiver Ziele + - Fortschrittsbalken + - "+ Neues Ziel" Button + +**Aufwand:** 3-4h (erweitert wegen Tactical Layer) + +--- + +### Phase 0b: Goal-Aware Placeholders (16-20h) + +**Strategic Placeholders:** +```python +{{goal_mode}} # Aus profiles.goal_mode +{{goal_mode_label}} # Aus GOAL_MODES mapping +``` + +**Tactical Placeholders:** +```python +{{primary_goal_type}} # Aus goals WHERE is_primary=true +{{primary_goal_target}} +{{primary_goal_progress_pct}} +{{primary_goal_prognosis}} # Berechnet aus Trend +``` + +**Score Calculations (goal-aware):** +```python +def get_body_progress_score(profile_id: str) -> str: + profile = get_profile_data(profile_id) + goal_mode = profile.get('goal_mode', 'health') + + # Get weights from v2 concept + weights = GOAL_MODES[goal_mode]['score_weights'] + + # Calculate sub-scores + fm_score = calculate_fm_progress(profile_id) + lbm_score = calculate_lbm_progress(profile_id) + + # Weight according to goal mode + if goal_mode == 'weight_loss': + total = 0.50 * fm_score + 0.30 * weight_score + 0.20 * lbm_score + elif goal_mode == 'strength': + total = 0.60 * lbm_score + 0.30 * fm_score + 0.10 * weight_score + # ... + + return f"{int(total)}/100" +``` + +--- + +### Phase 0c: Training Phases (4-6h) **PARALLEL** + +**DB:** +- `training_phases` table + +**Detection Algorithm:** +```python +def detect_current_phase(profile_id: str) -> dict: + """Detects training phase from last 14 days of data.""" + + # Analyze data + kcal_balance = get_kcal_balance_14d(profile_id) + training_dist = get_training_distribution_14d(profile_id) + weight_trend = get_weight_trend_14d(profile_id) + hrv_avg = get_hrv_avg_14d(profile_id) + volume_change = get_volume_change_14d(profile_id) + + # Phase Detection Rules + if kcal_balance < -300 and weight_trend < 0: + return { + 'phase': 'calorie_deficit', + 'confidence': 0.85, + 'reason': f'Avg kcal balance {kcal_balance}/day, weight -0.5kg/week' + } + + if training_dist['endurance'] > 60 and vo2max_trend > 0: + return { + 'phase': 'conditioning', + 'confidence': 0.78, + 'reason': f'{training_dist["endurance"]}% cardio, VO2max improving' + } + + if volume_change < -40 and hrv_avg < hrv_baseline * 0.85: + return { + 'phase': 'regeneration', + 'confidence': 0.92, + 'reason': f'Volume -40%, HRV below baseline, recovery needed' + } + + # Default + return { + 'phase': 'maintenance', + 'confidence': 0.50, + 'reason': 'No clear pattern detected' + } +``` + +**API:** +- GET /phases/current +- GET /phases/detect +- POST /phases/confirm + +**UI:** +- Dashboard Badge: "📊 Phase: Kaloriendefizit" +- Phase Detection Banner: "Wir haben erkannt: Kaloriendefizit-Phase. Stimmt das?" + +--- + +### Phase 0d: Fitness Tests (4-6h) **SPÄTER** + +**DB:** +- `fitness_tests` table + +**Test Definitions:** +```python +FITNESS_TESTS = { + 'cooper': { + 'name': 'Cooper-Test', + 'description': '12 Minuten laufen, maximale Distanz', + 'unit': 'meters', + 'interval_weeks': 6, + 'norm_tables': { # Simplified + 'male_30-39': {'very_good': 2800, 'good': 2500, 'average': 2200}, + 'female_30-39': {'very_good': 2500, 'good': 2200, 'average': 1900} + }, + 'calculate_vo2max': lambda distance: (distance - 504.9) / 44.73 + }, + 'pushups': { + 'name': 'Liegestütze-Test', + 'description': 'Maximale Anzahl ohne Pause', + 'unit': 'reps', + 'interval_weeks': 4, + 'norm_tables': { ... } + }, + # ... weitere Tests +} +``` + +**UI:** +- Tests Page mit Testliste +- Test Execution Flow (Anleitung → Eingabe → Auswertung) +- Test History mit Trend-Chart + +--- + +## 7. Priorisierte Reihenfolge + +### SOFORT (3-4h) +**Phase 0a:** Minimal Goal System (Strategic + Tactical) +- Basis für alles andere +- User kann Ziele setzen +- Score-Berechnungen können goal_mode nutzen + +### DIESE WOCHE (16-20h) +**Phase 0b:** Goal-Aware Placeholders +- 84 Platzhalter aus v2 +- 35+ Platzhalter aus v9e +- **TOTAL: 120+ Platzhalter** + +### PARALLEL (4-6h) +**Phase 0c:** Training Phases +- Automatische Erkennung +- Phase-aware Recommendations + +### SPÄTER (4-6h) +**Phase 0d:** Fitness Tests +- Enhancement, nicht kritisch für Charts + +--- + +## 8. Kritische Erkenntnisse + +### 1. GOALS_VITALS.md ist detaillierter +- Konkrete Implementierungs-Specs +- DB-Schema-Vorschläge +- 13 definierte KI-Platzhalter +- **ABER:** Fehlt Score-Gewichtung (das hat v2) + +### 2. Konzept v2 ist strategischer +- Goal Modes mit Score-Gewichtung +- Chart-Interpretationen +- Regelbasierte Logik +- **ABER:** Fehlt konkrete Ziel-Tracking (das hat v9e) + +### 3. Beide zusammen = Vollständig +- v2 (Goal Modes) + v9e (Goal Targets) = Komplettes Zielesystem +- v2 (Scores) + v9e (Tests) = Vollständiges Assessment +- v2 (Charts) + v9e (Phases) = Kontext-aware Visualisierung + +### 4. Meine ursprüngliche Analyse war incomplete +- Ich hatte nur v2 betrachtet +- v9e fügt kritische Details hinzu +- **Neue Gesamt-Schätzung:** 120+ Platzhalter (statt 84) + +--- + +## 9. Aktualisierte Empfehlung + +**JA zu Phase 0a (Minimal Goal System), ABER erweitert:** + +### Was Phase 0a umfassen muss (3-4h): + +1. **Strategic Layer (aus v2):** + - goal_mode in profiles + - GOAL_MODES Definition + - GET/SET endpoints + +2. **Tactical Layer (aus v9e):** + - goals Tabelle + - CRUD für Ziele + - Fortschritts-Berechnung + +3. **UI:** + - Goal Mode Selector (Settings) + - Goal Management Page (Basic) + - Dashboard Goal Widget + +### Was kann warten: +- Training Phases → Phase 0c (parallel) +- Fitness Tests → Phase 0d (später) +- Vollständige Test-Integration → v9f + +--- + +## 10. Nächste Schritte + +**JETZT:** +1. Phase 0a implementieren (3-4h) + - Strategic + Tactical Goal System +2. Dann Phase 0b (Goal-Aware Placeholders, 16-20h) +3. Parallel Phase 0c (Training Phases, 4-6h) + +**Soll ich mit Phase 0a (erweitert) starten?** +- Beide Goal-Konzepte integriert +- Ready für 120+ Platzhalter +- Basis für intelligentes Coach-System + +**Commit:** ae93b9d (muss aktualisiert werden) +**Neue Analyse:** GOALS_SYSTEM_UNIFIED_ANALYSIS.md diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index f1374a1..01d5139 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -35,6 +35,7 @@ import SubscriptionPage from './pages/SubscriptionPage' import SleepPage from './pages/SleepPage' import RestDaysPage from './pages/RestDaysPage' import VitalsPage from './pages/VitalsPage' +import GoalsPage from './pages/GoalsPage' import './app.css' function Nav() { @@ -172,6 +173,7 @@ function AppShell() { }/> }/> }/> + }/> }/> }/> }/> diff --git a/frontend/src/pages/Dashboard.jsx b/frontend/src/pages/Dashboard.jsx index 35462f5..bee4249 100644 --- a/frontend/src/pages/Dashboard.jsx +++ b/frontend/src/pages/Dashboard.jsx @@ -497,6 +497,21 @@ export default function Dashboard() { )} + {/* Goals Preview */} +
nav('/goals')}> +
+
🎯 Ziele
+ +
+
+ Definiere deine Trainingsmodus und konkrete Ziele für bessere KI-Analysen +
+
+ {/* Latest AI insight */}
diff --git a/frontend/src/pages/GoalsPage.jsx b/frontend/src/pages/GoalsPage.jsx new file mode 100644 index 0000000..d24d35f --- /dev/null +++ b/frontend/src/pages/GoalsPage.jsx @@ -0,0 +1,562 @@ +import { useState, useEffect } from 'react' +import { Target, Plus, Pencil, Trash2, TrendingUp, Calendar } from 'lucide-react' +import { api } from '../utils/api' +import dayjs from 'dayjs' +import 'dayjs/locale/de' +dayjs.locale('de') + +// Goal Mode Definitions +const GOAL_MODES = [ + { + id: 'weight_loss', + icon: '📉', + label: 'Gewichtsreduktion', + description: 'Kaloriendefizit, Fettabbau', + color: '#D85A30' + }, + { + id: 'strength', + icon: '💪', + label: 'Kraftaufbau', + description: 'Muskelwachstum, progressive Belastung', + color: '#378ADD' + }, + { + id: 'endurance', + icon: '🏃', + label: 'Ausdauer', + description: 'VO2Max, aerobe Kapazität', + color: '#1D9E75' + }, + { + id: 'recomposition', + icon: '⚖️', + label: 'Körperkomposition', + description: 'Gleichzeitig Fett ab- & Muskeln aufbauen', + color: '#7B68EE' + }, + { + id: 'health', + icon: '❤️', + label: 'Allgemeine Gesundheit', + description: 'Ausgewogen, präventiv', + color: '#E67E22' + } +] + +// Goal Type Definitions +const GOAL_TYPES = { + weight: { label: 'Gewicht', unit: 'kg', icon: '⚖️' }, + body_fat: { label: 'Körperfett', unit: '%', icon: '📊' }, + lean_mass: { label: 'Muskelmasse', unit: 'kg', icon: '💪' }, + vo2max: { label: 'VO2Max', unit: 'ml/kg/min', icon: '🫁' }, + strength: { label: 'Kraft', unit: 'kg', icon: '🏋️' }, + flexibility: { label: 'Beweglichkeit', unit: 'cm', icon: '🤸' }, + bp: { label: 'Blutdruck', unit: 'mmHg', icon: '❤️' }, + rhr: { label: 'Ruhepuls', unit: 'bpm', icon: '💓' } +} + +export default function GoalsPage() { + const [goalMode, setGoalMode] = useState(null) + const [goals, setGoals] = useState([]) + const [showGoalForm, setShowGoalForm] = useState(false) + const [editingGoal, setEditingGoal] = useState(null) + const [loading, setLoading] = useState(true) + const [error, setError] = useState(null) + const [toast, setToast] = useState(null) + + // Form state + const [formData, setFormData] = useState({ + goal_type: 'weight', + is_primary: false, + target_value: '', + unit: 'kg', + target_date: '', + name: '', + description: '' + }) + + useEffect(() => { + loadData() + }, []) + + const loadData = async () => { + setLoading(true) + setError(null) + try { + const [modeData, goalsData] = await Promise.all([ + api.getGoalMode(), + api.listGoals() + ]) + setGoalMode(modeData.goal_mode) + setGoals(goalsData) + } catch (err) { + console.error('Failed to load goals:', err) + setError('Fehler beim Laden der Ziele') + } finally { + setLoading(false) + } + } + + const showToast = (message, duration = 2000) => { + setToast(message) + setTimeout(() => setToast(null), duration) + } + + const handleGoalModeChange = async (newMode) => { + try { + await api.updateGoalMode(newMode) + setGoalMode(newMode) + showToast('✓ Trainingsmodus aktualisiert') + } catch (err) { + console.error('Failed to update goal mode:', err) + setError('Fehler beim Aktualisieren des Trainingsmodus') + } + } + + const handleCreateGoal = () => { + setEditingGoal(null) + setFormData({ + goal_type: 'weight', + is_primary: goals.length === 0, // First goal is primary by default + target_value: '', + unit: GOAL_TYPES['weight'].unit, + target_date: '', + name: '', + description: '' + }) + setShowGoalForm(true) + } + + const handleEditGoal = (goal) => { + setEditingGoal(goal.id) + setFormData({ + goal_type: goal.goal_type, + is_primary: goal.is_primary, + target_value: goal.target_value, + unit: goal.unit, + target_date: goal.target_date || '', + name: goal.name || '', + description: goal.description || '' + }) + setShowGoalForm(true) + } + + const handleGoalTypeChange = (type) => { + setFormData(f => ({ + ...f, + goal_type: type, + unit: GOAL_TYPES[type].unit + })) + } + + const handleSaveGoal = async () => { + if (!formData.target_value) { + setError('Bitte Zielwert eingeben') + return + } + + try { + const data = { + goal_type: formData.goal_type, + is_primary: formData.is_primary, + target_value: parseFloat(formData.target_value), + unit: formData.unit, + target_date: formData.target_date || null, + name: formData.name || null, + description: formData.description || null + } + + if (editingGoal) { + await api.updateGoal(editingGoal, data) + showToast('✓ Ziel aktualisiert') + } else { + await api.createGoal(data) + showToast('✓ Ziel erstellt') + } + + await loadData() + setShowGoalForm(false) + setEditingGoal(null) + } catch (err) { + console.error('Failed to save goal:', err) + setError(err.message || 'Fehler beim Speichern') + } + } + + const handleDeleteGoal = async (goalId) => { + if (!confirm('Ziel wirklich löschen?')) return + + try { + await api.deleteGoal(goalId) + showToast('✓ Ziel gelöscht') + await loadData() + } catch (err) { + console.error('Failed to delete goal:', err) + setError('Fehler beim Löschen') + } + } + + const getProgressColor = (progress) => { + if (progress >= 100) return 'var(--accent)' + if (progress >= 75) return '#1D9E75' + if (progress >= 50) return '#378ADD' + if (progress >= 25) return '#E67E22' + return '#D85A30' + } + + if (loading) { + return ( +
+
+
+
+
+ ) + } + + return ( +
+
+

Ziele

+
+ + {error && ( +
+

{error}

+
+ )} + + {toast && ( +
+ {toast} +
+ )} + + {/* Strategic Goal Mode Selection */} +
+

🎯 Trainingsmodus

+

+ Wähle deine grundlegende Trainingsausrichtung. Dies beeinflusst die Gewichtung + und Interpretation aller Analysen. +

+ +
+ {GOAL_MODES.map(mode => ( + + ))} +
+
+ + {/* Tactical Goals List */} +
+
+

🎯 Konkrete Ziele

+ +
+ + {goals.length === 0 ? ( +
+ +

Noch keine Ziele definiert

+ +
+ ) : ( +
+ {goals.map(goal => { + const typeInfo = GOAL_TYPES[goal.goal_type] || {} + return ( +
+
+
+
+ {typeInfo.icon} + + {goal.name || typeInfo.label} + + {goal.is_primary && ( + + PRIMÄR + + )} + + {goal.status === 'active' ? 'AKTIV' : goal.status?.toUpperCase()} + +
+ +
+
+ Start:{' '} + {goal.start_value} {goal.unit} +
+
+ Aktuell:{' '} + {goal.current_value || '—'} {goal.unit} +
+
+ Ziel:{' '} + {goal.target_value} {goal.unit} +
+ {goal.target_date && ( +
+ + {dayjs(goal.target_date).format('DD.MM.YYYY')} +
+ )} +
+ + {goal.progress_pct !== null && ( +
+
+ Fortschritt + {goal.progress_pct}% +
+
+
+
+ + {goal.on_track !== null && ( +
+ {goal.on_track ? ( + + ✓ Ziel voraussichtlich erreichbar bis {dayjs(goal.target_date).format('DD.MM.YYYY')} + + ) : ( + + ⚠ Prognose: {goal.projection_date ? dayjs(goal.projection_date).format('DD.MM.YYYY') : 'Offen'} + {goal.target_date && ' (später als geplant)'} + + )} +
+ )} +
+ )} +
+ +
+ + +
+
+
+ ) + })} +
+ )} +
+ + {/* Goal Form Modal */} + {showGoalForm && ( +
+
+

+ {editingGoal ? 'Ziel bearbeiten' : 'Neues Ziel'} +

+ +
+
+ + +
+ +
+ + setFormData(f => ({ ...f, name: e.target.value }))} + placeholder="z.B. Sommerfigur 2026" + /> +
+ +
+ +
+ setFormData(f => ({ ...f, target_value: e.target.value }))} + placeholder="Zielwert" + /> + +
+
+ +
+ + setFormData(f => ({ ...f, target_date: e.target.value }))} + /> +
+ +
+ +