diff --git a/backend/main.py b/backend/main.py index f4bf4f8..192033b 100644 --- a/backend/main.py +++ b/backend/main.py @@ -19,7 +19,7 @@ from routers import auth, profiles, weight, circumference, caliper from routers import activity, nutrition, photos, insights, prompts from routers import admin, stats, exportdata, importdata from routers import subscription, coupons, features, tiers_mgmt, tier_limits -from routers import user_restrictions, access_grants +from routers import user_restrictions, access_grants, training_types # ── App Configuration ───────────────────────────────────────────────────────── DATA_DIR = Path(os.getenv("DATA_DIR", "./data")) @@ -85,6 +85,9 @@ app.include_router(tier_limits.router) # /api/tier-limits (admin) app.include_router(user_restrictions.router) # /api/user-restrictions (admin) app.include_router(access_grants.router) # /api/access-grants (admin) +# v9d Training Types +app.include_router(training_types.router) # /api/training-types/* + # ── Health Check ────────────────────────────────────────────────────────────── @app.get("/") def root(): diff --git a/backend/migrations/004_training_types.sql b/backend/migrations/004_training_types.sql new file mode 100644 index 0000000..59b254c --- /dev/null +++ b/backend/migrations/004_training_types.sql @@ -0,0 +1,86 @@ +-- Migration 004: Training Types & Categories +-- Part of v9d: Schlaf + Sport-Vertiefung +-- Created: 2026-03-21 + +-- ======================================== +-- 1. Create training_types table +-- ======================================== +CREATE TABLE IF NOT EXISTS training_types ( + id SERIAL PRIMARY KEY, + category VARCHAR(50) NOT NULL, -- Main category: 'cardio', 'strength', 'hiit', etc. + subcategory VARCHAR(50), -- Optional: 'running', 'hypertrophy', etc. + name_de VARCHAR(100) NOT NULL, -- German display name + name_en VARCHAR(100) NOT NULL, -- English display name + icon VARCHAR(10), -- Emoji icon + sort_order INTEGER DEFAULT 0, -- For UI ordering + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- ======================================== +-- 2. Add training type columns to activity_log +-- ======================================== +ALTER TABLE activity_log + ADD COLUMN IF NOT EXISTS training_type_id INTEGER REFERENCES training_types(id), + ADD COLUMN IF NOT EXISTS training_category VARCHAR(50), -- Denormalized for fast queries + ADD COLUMN IF NOT EXISTS training_subcategory VARCHAR(50); -- Denormalized + +-- ======================================== +-- 3. Create indexes +-- ======================================== +CREATE INDEX IF NOT EXISTS idx_activity_training_type ON activity_log(training_type_id); +CREATE INDEX IF NOT EXISTS idx_activity_training_category ON activity_log(training_category); +CREATE INDEX IF NOT EXISTS idx_training_types_category ON training_types(category); + +-- ======================================== +-- 4. Seed training types data +-- ======================================== + +-- Cardio (Ausdauer) +INSERT INTO training_types (category, subcategory, name_de, name_en, icon, sort_order) VALUES + ('cardio', 'running', 'Laufen', 'Running', '🏃', 100), + ('cardio', 'cycling', 'Radfahren', 'Cycling', '🚴', 101), + ('cardio', 'swimming', 'Schwimmen', 'Swimming', '🏊', 102), + ('cardio', 'rowing', 'Rudern', 'Rowing', '🚣', 103), + ('cardio', 'other', 'Sonstiges Cardio', 'Other Cardio', '❤️', 104); + +-- Kraft +INSERT INTO training_types (category, subcategory, name_de, name_en, icon, sort_order) VALUES + ('strength', 'hypertrophy', 'Hypertrophie', 'Hypertrophy', '💪', 200), + ('strength', 'maxstrength', 'Maximalkraft', 'Max Strength', '🏋️', 201), + ('strength', 'endurance', 'Kraftausdauer', 'Strength Endurance', '🔁', 202), + ('strength', 'functional', 'Funktionell', 'Functional', '⚡', 203); + +-- Schnellkraft / HIIT +INSERT INTO training_types (category, subcategory, name_de, name_en, icon, sort_order) VALUES + ('hiit', 'hiit', 'HIIT', 'HIIT', '🔥', 300), + ('hiit', 'explosive', 'Explosiv', 'Explosive', '💥', 301), + ('hiit', 'circuit', 'Circuit Training', 'Circuit Training', '🔄', 302); + +-- Kampfsport / Technikkraft +INSERT INTO training_types (category, subcategory, name_de, name_en, icon, sort_order) VALUES + ('martial_arts', 'technique', 'Techniktraining', 'Technique Training', '🥋', 400), + ('martial_arts', 'sparring', 'Sparring / Wettkampf', 'Sparring / Competition', '🥊', 401), + ('martial_arts', 'strength', 'Kraft für Kampfsport', 'Martial Arts Strength', '⚔️', 402); + +-- Mobility & Dehnung +INSERT INTO training_types (category, subcategory, name_de, name_en, icon, sort_order) VALUES + ('mobility', 'static', 'Statisches Dehnen', 'Static Stretching', '🧘', 500), + ('mobility', 'dynamic', 'Dynamisches Dehnen', 'Dynamic Stretching', '🤸', 501), + ('mobility', 'yoga', 'Yoga', 'Yoga', '🕉️', 502), + ('mobility', 'fascia', 'Faszienarbeit', 'Fascia Work', '🎯', 503); + +-- Erholung (aktiv) +INSERT INTO training_types (category, subcategory, name_de, name_en, icon, sort_order) VALUES + ('recovery', 'walk', 'Spaziergang', 'Walk', '🚶', 600), + ('recovery', 'swim_light', 'Leichtes Schwimmen', 'Light Swimming', '🏊', 601), + ('recovery', 'regeneration', 'Regenerationseinheit', 'Regeneration', '💆', 602); + +-- General / Uncategorized +INSERT INTO training_types (category, subcategory, name_de, name_en, icon, sort_order) VALUES + ('other', NULL, 'Sonstiges', 'Other', '📝', 900); + +-- ======================================== +-- 5. Add comment +-- ======================================== +COMMENT ON TABLE training_types IS 'v9d: Training type categories and subcategories'; +COMMENT ON TABLE activity_log IS 'Extended in v9d with training_type_id for categorization'; diff --git a/backend/models.py b/backend/models.py index 380e300..86bdb8c 100644 --- a/backend/models.py +++ b/backend/models.py @@ -84,6 +84,9 @@ class ActivityEntry(BaseModel): rpe: Optional[int] = None source: Optional[str] = 'manual' notes: Optional[str] = None + training_type_id: Optional[int] = None # v9d: Training type categorization + training_category: Optional[str] = None # v9d: Denormalized category + training_subcategory: Optional[str] = None # v9d: Denormalized subcategory class NutritionDay(BaseModel): diff --git a/backend/routers/training_types.py b/backend/routers/training_types.py new file mode 100644 index 0000000..ab24785 --- /dev/null +++ b/backend/routers/training_types.py @@ -0,0 +1,123 @@ +""" +Training Types API - v9d + +Provides hierarchical list of training categories and subcategories +for activity classification. +""" +from fastapi import APIRouter, Depends +from db import get_db, get_cursor +from auth import require_auth + +router = APIRouter(prefix="/api/training-types", tags=["training-types"]) + + +@router.get("") +def list_training_types(session: dict = Depends(require_auth)): + """ + Get all training types, grouped by category. + + Returns hierarchical structure: + { + "cardio": [ + {"id": 1, "subcategory": "running", "name_de": "Laufen", ...}, + ... + ], + "strength": [...], + ... + } + """ + with get_db() as conn: + cur = get_cursor(conn) + cur.execute(""" + SELECT id, category, subcategory, name_de, name_en, icon, sort_order + FROM training_types + ORDER BY sort_order, category, subcategory + """) + rows = cur.fetchall() + + # Group by category + grouped = {} + for row in rows: + cat = row['category'] + if cat not in grouped: + grouped[cat] = [] + grouped[cat].append({ + 'id': row['id'], + 'category': row['category'], + 'subcategory': row['subcategory'], + 'name_de': row['name_de'], + 'name_en': row['name_en'], + 'icon': row['icon'], + 'sort_order': row['sort_order'] + }) + + return grouped + + +@router.get("/flat") +def list_training_types_flat(session: dict = Depends(require_auth)): + """ + Get all training types as flat list (for simple dropdown). + """ + with get_db() as conn: + cur = get_cursor(conn) + cur.execute(""" + SELECT id, category, subcategory, name_de, name_en, icon + FROM training_types + ORDER BY sort_order + """) + rows = cur.fetchall() + + return [dict(row) for row in rows] + + +@router.get("/categories") +def list_categories(session: dict = Depends(require_auth)): + """ + Get list of unique categories with metadata. + """ + categories = { + 'cardio': { + 'name_de': 'Cardio (Ausdauer)', + 'name_en': 'Cardio (Endurance)', + 'icon': '❤️', + 'color': '#EF4444' + }, + 'strength': { + 'name_de': 'Kraft', + 'name_en': 'Strength', + 'icon': '💪', + 'color': '#3B82F6' + }, + 'hiit': { + 'name_de': 'Schnellkraft / HIIT', + 'name_en': 'Power / HIIT', + 'icon': '🔥', + 'color': '#F59E0B' + }, + 'martial_arts': { + 'name_de': 'Kampfsport', + 'name_en': 'Martial Arts', + 'icon': '🥋', + 'color': '#8B5CF6' + }, + 'mobility': { + 'name_de': 'Mobility & Dehnung', + 'name_en': 'Mobility & Stretching', + 'icon': '🧘', + 'color': '#10B981' + }, + 'recovery': { + 'name_de': 'Erholung (aktiv)', + 'name_en': 'Recovery (active)', + 'icon': '💆', + 'color': '#6B7280' + }, + 'other': { + 'name_de': 'Sonstiges', + 'name_en': 'Other', + 'icon': '📝', + 'color': '#9CA3AF' + } + } + return categories diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 75e3a4b..defb903 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -1,6 +1,6 @@ import { useEffect } from 'react' import { BrowserRouter, Routes, Route, NavLink, useNavigate } from 'react-router-dom' -import { LayoutDashboard, PlusSquare, TrendingUp, BarChart2, Settings } from 'lucide-react' +import { LayoutDashboard, PlusSquare, TrendingUp, BarChart2, Settings, LogOut } from 'lucide-react' import { ProfileProvider, useProfile } from './context/ProfileContext' import { AuthProvider, useAuth } from './context/AuthContext' import { setProfileId } from './utils/api' @@ -50,10 +50,17 @@ function Nav() { } function AppShell() { - const { session, loading: authLoading, needsSetup } = useAuth() + const { session, loading: authLoading, needsSetup, logout } = useAuth() const { activeProfile, loading: profileLoading } = useProfile() const nav = useNavigate() + const handleLogout = () => { + if (confirm('Wirklich abmelden?')) { + logout() + window.location.href = '/' + } + } + useEffect(()=>{ if (session?.profile_id) { setProfileId(session.profile_id) @@ -119,12 +126,32 @@ function AppShell() {