From 2f64656d4da193d68e51b5283b0a35caa18ddf68 Mon Sep 17 00:00:00 2001 From: Lars Date: Fri, 27 Mar 2026 19:44:18 +0100 Subject: [PATCH] feat: Migration 031 - Focus Area System v2.0 (dynamic, extensible) --- .../migrations/031_focus_area_system_v2.sql | 254 ++++++++++++++++++ 1 file changed, 254 insertions(+) create mode 100644 backend/migrations/031_focus_area_system_v2.sql diff --git a/backend/migrations/031_focus_area_system_v2.sql b/backend/migrations/031_focus_area_system_v2.sql new file mode 100644 index 0000000..d897459 --- /dev/null +++ b/backend/migrations/031_focus_area_system_v2.sql @@ -0,0 +1,254 @@ +-- Migration 031: Focus Area System v2.0 +-- Date: 2026-03-27 +-- Purpose: Dynamic, extensible focus areas with Many-to-Many goal contributions + +-- ============================================================================ +-- Part 1: New Tables +-- ============================================================================ + +-- Focus Area Definitions (dynamic, user-extensible) +CREATE TABLE IF NOT EXISTS focus_area_definitions ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + key VARCHAR(50) UNIQUE NOT NULL, -- e.g. 'strength', 'aerobic_endurance' + name_de VARCHAR(100) NOT NULL, + name_en VARCHAR(100), + icon VARCHAR(10), + description TEXT, + category VARCHAR(50), -- 'body_composition', 'training', 'endurance', 'coordination', 'mental', 'recovery', 'health' + is_active BOOLEAN DEFAULT true, + created_at TIMESTAMP DEFAULT NOW(), + updated_at TIMESTAMP DEFAULT NOW() +); + +CREATE INDEX idx_focus_area_key ON focus_area_definitions(key); +CREATE INDEX idx_focus_area_category ON focus_area_definitions(category); + +COMMENT ON TABLE focus_area_definitions IS 'Dynamic focus area registry - defines all available focus dimensions'; +COMMENT ON COLUMN focus_area_definitions.key IS 'Unique identifier for programmatic access'; +COMMENT ON COLUMN focus_area_definitions.category IS 'Grouping for UI display'; + +-- Many-to-Many: Goals contribute to Focus Areas +CREATE TABLE IF NOT EXISTS goal_focus_contributions ( + goal_id UUID NOT NULL REFERENCES goals(id) ON DELETE CASCADE, + focus_area_id UUID NOT NULL REFERENCES focus_area_definitions(id) ON DELETE CASCADE, + contribution_weight DECIMAL(5,2) DEFAULT 100.00 CHECK (contribution_weight >= 0 AND contribution_weight <= 100), + created_at TIMESTAMP DEFAULT NOW(), + PRIMARY KEY (goal_id, focus_area_id) +); + +CREATE INDEX idx_gfc_goal ON goal_focus_contributions(goal_id); +CREATE INDEX idx_gfc_focus_area ON goal_focus_contributions(focus_area_id); + +COMMENT ON TABLE goal_focus_contributions IS 'Maps goals to focus areas with contribution weights (0-100%)'; +COMMENT ON COLUMN goal_focus_contributions.contribution_weight IS 'How much this goal contributes to the focus area (0-100%)'; + +-- ============================================================================ +-- Part 2: Rename existing focus_areas table +-- ============================================================================ + +-- Old focus_areas table becomes user_focus_preferences +ALTER TABLE focus_areas RENAME TO user_focus_preferences; + +-- Add reference to new focus_area_definitions (for future use) +ALTER TABLE user_focus_preferences ADD COLUMN IF NOT EXISTS notes TEXT; + +COMMENT ON TABLE user_focus_preferences IS 'User-specific focus area weightings (legacy flat structure + new references)'; + +-- ============================================================================ +-- Part 3: Seed Data - Basis Focus Areas +-- ============================================================================ + +INSERT INTO focus_area_definitions (key, name_de, name_en, icon, category, description) VALUES +-- Body Composition +('weight_loss', 'Gewichtsverlust', 'Weight Loss', '📉', 'body_composition', 'Körpergewicht reduzieren'), +('muscle_gain', 'Muskelaufbau', 'Muscle Gain', '💪', 'body_composition', 'Muskelmasse aufbauen'), +('body_recomposition', 'Body Recomposition', 'Body Recomposition', '⚖️', 'body_composition', 'Gleichzeitig Fett abbauen und Muskeln aufbauen'), + +-- Training - Kraft +('strength', 'Maximalkraft', 'Strength', '🏋️', 'training', 'Maximale Kraftfähigkeit'), +('strength_endurance', 'Kraftausdauer', 'Strength Endurance', '💪🏃', 'training', 'Kraft über längere Zeit aufrechterhalten'), +('power', 'Schnellkraft', 'Power', '⚡', 'training', 'Kraft in kurzer Zeit entfalten'), + +-- Training - Beweglichkeit +('flexibility', 'Beweglichkeit', 'Flexibility', '🤸', 'training', 'Gelenkigkeit und Bewegungsumfang'), +('mobility', 'Mobilität', 'Mobility', '🦴', 'training', 'Aktive Beweglichkeit und Kontrolle'), + +-- Ausdauer +('aerobic_endurance', 'Aerobe Ausdauer', 'Aerobic Endurance', '🫁', 'endurance', 'VO2Max, lange moderate Belastung'), +('anaerobic_endurance', 'Anaerobe Ausdauer', 'Anaerobic Endurance', '⚡', 'endurance', 'Laktattoleranz, kurze intensive Belastung'), +('cardiovascular_health', 'Herz-Kreislauf', 'Cardiovascular Health', '❤️', 'endurance', 'Herzgesundheit und Ausdauer'), + +-- Koordination +('balance', 'Gleichgewicht', 'Balance', '⚖️', 'coordination', 'Statisches und dynamisches Gleichgewicht'), +('reaction', 'Reaktionsfähigkeit', 'Reaction', '⚡', 'coordination', 'Schnelligkeit der Reaktion auf Reize'), +('rhythm', 'Rhythmusgefühl', 'Rhythm', '🎵', 'coordination', 'Zeitliche Abstimmung von Bewegungen'), +('coordination', 'Koordination', 'Coordination', '🎯', 'coordination', 'Zusammenspiel verschiedener Bewegungen'), + +-- Mental +('stress_resistance', 'Stressresistenz', 'Stress Resistance', '🧘', 'mental', 'Umgang mit mentalem und physischem Stress'), +('concentration', 'Konzentration', 'Concentration', '🎯', 'mental', 'Fokussierung und Aufmerksamkeit'), +('willpower', 'Willenskraft', 'Willpower', '💎', 'mental', 'Durchhaltevermögen und Selbstdisziplin'), +('mental_health', 'Mentale Gesundheit', 'Mental Health', '🧠', 'mental', 'Psychisches Wohlbefinden'), + +-- Recovery +('sleep_quality', 'Schlafqualität', 'Sleep Quality', '😴', 'recovery', 'Erholsamer Schlaf'), +('regeneration', 'Regeneration', 'Regeneration', '♻️', 'recovery', 'Körperliche Erholung'), +('rest', 'Ruhe', 'Rest', '🛌', 'recovery', 'Aktive und passive Erholung'), + +-- Health +('metabolic_health', 'Stoffwechselgesundheit', 'Metabolic Health', '🔥', 'health', 'Blutzucker, Insulin, Stoffwechsel'), +('blood_pressure', 'Blutdruck', 'Blood Pressure', '❤️‍🩹', 'health', 'Gesunder Blutdruck'), +('hrv', 'Herzratenvariabilität', 'HRV', '💓', 'health', 'Autonomes Nervensystem'), +('general_health', 'Allgemeine Gesundheit', 'General Health', '🏥', 'health', 'Vitale Gesundheit und Wohlbefinden') +ON CONFLICT (key) DO NOTHING; + +-- ============================================================================ +-- Part 4: Auto-Mapping - Bestehende Goals zu Focus Areas +-- ============================================================================ + +-- Helper function to get focus_area_id by key +CREATE OR REPLACE FUNCTION get_focus_area_id(area_key VARCHAR) +RETURNS UUID AS $$ +BEGIN + RETURN (SELECT id FROM focus_area_definitions WHERE key = area_key LIMIT 1); +END; +$$ LANGUAGE plpgsql; + +-- Weight goals → weight_loss (100%) +INSERT INTO goal_focus_contributions (goal_id, focus_area_id, contribution_weight) +SELECT g.id, get_focus_area_id('weight_loss'), 100.00 +FROM goals g +WHERE g.goal_type = 'weight' +ON CONFLICT (goal_id, focus_area_id) DO NOTHING; + +-- Body Fat goals → weight_loss (60%) + body_recomposition (40%) +INSERT INTO goal_focus_contributions (goal_id, focus_area_id, contribution_weight) +SELECT g.id, fa.id, + CASE fa.key + WHEN 'weight_loss' THEN 60.00 + WHEN 'body_recomposition' THEN 40.00 + END +FROM goals g +CROSS JOIN focus_area_definitions fa +WHERE g.goal_type = 'body_fat' + AND fa.key IN ('weight_loss', 'body_recomposition') +ON CONFLICT (goal_id, focus_area_id) DO NOTHING; + +-- Lean Mass goals → muscle_gain (70%) + body_recomposition (30%) +INSERT INTO goal_focus_contributions (goal_id, focus_area_id, contribution_weight) +SELECT g.id, fa.id, + CASE fa.key + WHEN 'muscle_gain' THEN 70.00 + WHEN 'body_recomposition' THEN 30.00 + END +FROM goals g +CROSS JOIN focus_area_definitions fa +WHERE g.goal_type = 'lean_mass' + AND fa.key IN ('muscle_gain', 'body_recomposition') +ON CONFLICT (goal_id, focus_area_id) DO NOTHING; + +-- Strength goals → strength (70%) + muscle_gain (30%) +INSERT INTO goal_focus_contributions (goal_id, focus_area_id, contribution_weight) +SELECT g.id, fa.id, + CASE fa.key + WHEN 'strength' THEN 70.00 + WHEN 'muscle_gain' THEN 30.00 + END +FROM goals g +CROSS JOIN focus_area_definitions fa +WHERE g.goal_type = 'strength' + AND fa.key IN ('strength', 'muscle_gain') +ON CONFLICT (goal_id, focus_area_id) DO NOTHING; + +-- Flexibility goals → flexibility (100%) +INSERT INTO goal_focus_contributions (goal_id, focus_area_id, contribution_weight) +SELECT g.id, get_focus_area_id('flexibility'), 100.00 +FROM goals g +WHERE g.goal_type = 'flexibility' +ON CONFLICT (goal_id, focus_area_id) DO NOTHING; + +-- VO2Max goals → aerobic_endurance (80%) + cardiovascular_health (20%) +INSERT INTO goal_focus_contributions (goal_id, focus_area_id, contribution_weight) +SELECT g.id, fa.id, + CASE fa.key + WHEN 'aerobic_endurance' THEN 80.00 + WHEN 'cardiovascular_health' THEN 20.00 + END +FROM goals g +CROSS JOIN focus_area_definitions fa +WHERE g.goal_type = 'vo2max' + AND fa.key IN ('aerobic_endurance', 'cardiovascular_health') +ON CONFLICT (goal_id, focus_area_id) DO NOTHING; + +-- Resting Heart Rate goals → cardiovascular_health (100%) +INSERT INTO goal_focus_contributions (goal_id, focus_area_id, contribution_weight) +SELECT g.id, get_focus_area_id('cardiovascular_health'), 100.00 +FROM goals g +WHERE g.goal_type = 'rhr' +ON CONFLICT (goal_id, focus_area_id) DO NOTHING; + +-- Blood Pressure goals → blood_pressure (80%) + cardiovascular_health (20%) +INSERT INTO goal_focus_contributions (goal_id, focus_area_id, contribution_weight) +SELECT g.id, fa.id, + CASE fa.key + WHEN 'blood_pressure' THEN 80.00 + WHEN 'cardiovascular_health' THEN 20.00 + END +FROM goals g +CROSS JOIN focus_area_definitions fa +WHERE g.goal_type = 'bp' + AND fa.key IN ('blood_pressure', 'cardiovascular_health') +ON CONFLICT (goal_id, focus_area_id) DO NOTHING; + +-- HRV goals → hrv (70%) + stress_resistance (30%) +INSERT INTO goal_focus_contributions (goal_id, focus_area_id, contribution_weight) +SELECT g.id, fa.id, + CASE fa.key + WHEN 'hrv' THEN 70.00 + WHEN 'stress_resistance' THEN 30.00 + END +FROM goals g +CROSS JOIN focus_area_definitions fa +WHERE g.goal_type = 'hrv' + AND fa.key IN ('hrv', 'stress_resistance') +ON CONFLICT (goal_id, focus_area_id) DO NOTHING; + +-- Sleep Quality goals → sleep_quality (100%) +INSERT INTO goal_focus_contributions (goal_id, focus_area_id, contribution_weight) +SELECT g.id, get_focus_area_id('sleep_quality'), 100.00 +FROM goals g +WHERE g.goal_type = 'sleep_quality' +ON CONFLICT (goal_id, focus_area_id) DO NOTHING; + +-- Training Frequency goals → general catch-all (strength + endurance) +INSERT INTO goal_focus_contributions (goal_id, focus_area_id, contribution_weight) +SELECT g.id, fa.id, + CASE fa.key + WHEN 'strength' THEN 40.00 + WHEN 'aerobic_endurance' THEN 40.00 + WHEN 'general_health' THEN 20.00 + END +FROM goals g +CROSS JOIN focus_area_definitions fa +WHERE g.goal_type = 'training_frequency' + AND fa.key IN ('strength', 'aerobic_endurance', 'general_health') +ON CONFLICT (goal_id, focus_area_id) DO NOTHING; + +-- Cleanup helper function +DROP FUNCTION IF EXISTS get_focus_area_id(VARCHAR); + +-- ============================================================================ +-- Summary +-- ============================================================================ + +COMMENT ON TABLE focus_area_definitions IS +'v2.0: Dynamic focus areas - replaces hardcoded 6-dimension system. +26 base areas across 7 categories. User-extensible via admin UI.'; + +COMMENT ON TABLE goal_focus_contributions IS +'Many-to-Many mapping: Goals contribute to multiple focus areas with weights. +Auto-mapped from goal_type, editable by user.'; + +COMMENT ON TABLE user_focus_preferences IS +'Legacy flat structure (weight_loss_pct, muscle_gain_pct, etc.) remains for backward compatibility. +Future: Use focus_area_definitions + dynamic preferences.';