-- Migration 039: Vereins-Mitgliedschaft, Rollen pro Verein, aktiver Vereinskontext (Multi-Tenancy Phase 1) -- Erstellt: 2026-05-05 -- Mitgliedschaft Profil ↔ Verein CREATE TABLE club_members ( id SERIAL PRIMARY KEY, profile_id INT NOT NULL REFERENCES profiles(id) ON DELETE CASCADE, club_id INT NOT NULL REFERENCES clubs(id) ON DELETE CASCADE, status VARCHAR(20) NOT NULL DEFAULT 'active', created_at TIMESTAMP DEFAULT NOW(), updated_at TIMESTAMP DEFAULT NOW(), UNIQUE(profile_id, club_id) ); CREATE INDEX idx_club_members_profile ON club_members(profile_id); CREATE INDEX idx_club_members_club ON club_members(club_id); CREATE INDEX idx_club_members_status ON club_members(status); -- Rollen pro Mitgliedschaft (role_code: club_admin, trainer, division_lead, content_editor) CREATE TABLE club_member_roles ( id SERIAL PRIMARY KEY, club_member_id INT NOT NULL REFERENCES club_members(id) ON DELETE CASCADE, role_code VARCHAR(50) NOT NULL, division_id INT REFERENCES divisions(id) ON DELETE CASCADE, created_at TIMESTAMP DEFAULT NOW(), UNIQUE (club_member_id, role_code) ); CREATE INDEX idx_club_member_roles_member ON club_member_roles(club_member_id); CREATE INDEX idx_club_member_roles_code ON club_member_roles(role_code); -- Hauptverwalter:in (fachliche Referenz; Rechte über club_member_roles.club_admin) ALTER TABLE clubs ADD COLUMN IF NOT EXISTS primary_admin_profile_id INT REFERENCES profiles(id) ON DELETE SET NULL; CREATE INDEX idx_clubs_primary_admin ON clubs(primary_admin_profile_id); -- Persistenz gewählter aktiver Verein (UI); Request-Header kann überschreiben ALTER TABLE profiles ADD COLUMN IF NOT EXISTS active_club_id INT REFERENCES clubs(id) ON DELETE SET NULL; CREATE INDEX idx_profiles_active_club ON profiles(active_club_id); -- ── Backfill: Trainer/Co-Trainer aus Trainingsgruppen → Mitgliedschaft + Rolle trainer INSERT INTO club_members (profile_id, club_id, status) SELECT DISTINCT t.trainer_id, t.club_id, 'active' FROM training_groups t WHERE t.trainer_id IS NOT NULL ON CONFLICT (profile_id, club_id) DO NOTHING; INSERT INTO club_members (profile_id, club_id, status) SELECT DISTINCT elem::int, x.club_id, 'active' FROM ( SELECT club_id, co_trainer_ids FROM training_groups WHERE CASE WHEN jsonb_typeof(co_trainer_ids) = 'array' THEN jsonb_array_length(co_trainer_ids) ELSE 0 END > 0 ) x, LATERAL jsonb_array_elements_text(x.co_trainer_ids) AS elem ON CONFLICT (profile_id, club_id) DO NOTHING; INSERT INTO club_member_roles (club_member_id, role_code) SELECT cm.id, 'trainer' FROM club_members cm WHERE NOT EXISTS ( SELECT 1 FROM club_member_roles r WHERE r.club_member_id = cm.id AND r.role_code = 'trainer' ); -- Pro Verein: kleinste trainer_id einer Gruppe als primärer Admin (Pilot / CURR-008-Analog) UPDATE clubs c SET primary_admin_profile_id = sub.pid FROM ( SELECT DISTINCT ON (club_id) club_id, trainer_id AS pid FROM training_groups WHERE trainer_id IS NOT NULL ORDER BY club_id, id ASC ) sub WHERE c.id = sub.club_id AND c.primary_admin_profile_id IS NULL; INSERT INTO club_member_roles (club_member_id, role_code) SELECT cm.id, 'club_admin' FROM club_members cm INNER JOIN clubs cl ON cl.id = cm.club_id AND cl.primary_admin_profile_id = cm.profile_id WHERE NOT EXISTS ( SELECT 1 FROM club_member_roles r WHERE r.club_member_id = cm.id AND r.role_code = 'club_admin' ); -- Globale Admins: in jedem bestehenden Verein als club_admin spiegeln (volle Plattform-Sicht) INSERT INTO club_members (profile_id, club_id, status) SELECT p.id, c.id, 'active' FROM profiles p CROSS JOIN clubs c WHERE p.role IN ('admin', 'superadmin') ON CONFLICT (profile_id, club_id) DO NOTHING; INSERT INTO club_member_roles (club_member_id, role_code) SELECT cm.id, 'club_admin' FROM club_members cm INNER JOIN profiles p ON p.id = cm.profile_id AND p.role IN ('admin', 'superadmin') WHERE NOT EXISTS ( SELECT 1 FROM club_member_roles r WHERE r.club_member_id = cm.id AND r.role_code = 'club_admin' ); -- Default aktiver Verein: einziger Verein des Nutzers UPDATE profiles p SET active_club_id = sub.only_club FROM ( SELECT profile_id, MIN(club_id) AS only_club FROM club_members WHERE status = 'active' GROUP BY profile_id HAVING COUNT(DISTINCT club_id) = 1 ) sub WHERE p.id = sub.profile_id AND (p.active_club_id IS NULL);