diff --git a/backend/migrations/081_club_creation_request_superseded.sql b/backend/migrations/081_club_creation_request_superseded.sql new file mode 100644 index 0000000..f78606c --- /dev/null +++ b/backend/migrations/081_club_creation_request_superseded.sql @@ -0,0 +1,13 @@ +-- Migration 081: Status superseded wenn freigegebener Verein gelöscht wurde + +ALTER TABLE club_creation_requests + DROP CONSTRAINT IF EXISTS club_creation_requests_status_check; + +ALTER TABLE club_creation_requests + ADD CONSTRAINT club_creation_requests_status_check + CHECK (status IN ('pending', 'approved', 'rejected', 'withdrawn', 'superseded')); + +-- Bestehende Drift: approved ohne Verein (ON DELETE SET NULL auf created_club_id) +UPDATE club_creation_requests +SET status = 'superseded', updated_at = NOW() +WHERE status = 'approved' AND created_club_id IS NULL; diff --git a/backend/routers/club_creation_requests.py b/backend/routers/club_creation_requests.py index fb09e03..3aeea18 100644 --- a/backend/routers/club_creation_requests.py +++ b/backend/routers/club_creation_requests.py @@ -112,6 +112,14 @@ class CreationRequestCreate(BaseModel): message: Optional[str] = Field(None, max_length=2000) +def _normalize_creation_request_row(row: Dict[str, Any]) -> Dict[str, Any]: + """Approved ohne Verein → superseded (z. B. nach Vereinslöschung, FK SET NULL).""" + d = dict(row) + if d.get("status") == "approved" and not d.get("created_club_id"): + d["status"] = "superseded" + return d + + def _response_one(cur, req_id: int, viewer_profile_id: int) -> Dict[str, Any]: cur.execute( """ @@ -125,7 +133,7 @@ def _response_one(cur, req_id: int, viewer_profile_id: int) -> Dict[str, Any]: row = cur.fetchone() if not row: raise HTTPException(status_code=404, detail="Antrag nicht gefunden") - return r2d(row) + return _normalize_creation_request_row(r2d(row)) def _assert_platform_admin(tenant: TenantContext) -> None: @@ -157,7 +165,7 @@ def get_my_creation_requests(tenant: TenantContext = Depends(get_tenant_context) """, (pid,), ) - return [r2d(r) for r in cur.fetchall()] + return [_normalize_creation_request_row(r2d(r)) for r in cur.fetchall()] @router.post("/me/club-creation-requests", status_code=201) diff --git a/backend/routers/clubs.py b/backend/routers/clubs.py index bca67e9..e57b0d1 100644 --- a/backend/routers/clubs.py +++ b/backend/routers/clubs.py @@ -336,6 +336,16 @@ def delete_club(club_id: int, tenant: TenantContext = Depends(get_tenant_context if not cur.fetchone(): raise HTTPException(404, "Verein nicht gefunden") + # Gründungsanträge: Freigabe verliert Gültigkeit wenn Verein entfernt wird + cur.execute( + """ + UPDATE club_creation_requests + SET status = 'superseded', updated_at = NOW() + WHERE created_club_id = %s AND status = 'approved' + """, + (club_id,), + ) + # Delete (CASCADE handles divisions and groups) cur.execute("DELETE FROM clubs WHERE id = %s", (club_id,)) conn.commit() diff --git a/backend/version.py b/backend/version.py index d74872c..1a2f84c 100644 --- a/backend/version.py +++ b/backend/version.py @@ -1,8 +1,8 @@ # Shinkan Jinkendo Version Information -APP_VERSION = "0.8.191" +APP_VERSION = "0.8.192" BUILD_DATE = "2026-06-06" -DB_SCHEMA_VERSION = "20260606080" +DB_SCHEMA_VERSION = "20260606081" MODULE_VERSIONS = { "legal_documents": "1.4.0", # Admin: Live-Vorschau pro Abschnitt + modale Vollvorschau (Editor + Dokumentenliste) @@ -11,10 +11,10 @@ MODULE_VERSIONS = { "tenant_context": "1.1.0", # M3: account_state + email_verified im TenantContext "capabilities": "1.0.1", # resolve_capabilities_map für /me/entitlements "account_lifecycle": "1.1.0", # Phase A: account_onboarding_gate API-Middleware - "clubs": "0.4.1", # Alle geschützten Endpoints Depends(get_tenant_context); profile_id/role aus TenantContext + "clubs": "0.4.2", # delete_club: Gründungsanträge → superseded "club_memberships": "1.0.1", # Depends(get_tenant_context) "club_join_requests": "1.0.1", # Depends(get_tenant_context) - "club_creation_requests": "1.0.0", # M7: Gründungsanträge + Admin-Freigabe + "club_creation_requests": "1.0.1", # superseded wenn freigegebener Verein gelöscht "admin_users": "1.0.0", # GET /api/admin/users "club_features": "1.2.0", # M4: club_features_map für /me/entitlements "entitlements": "1.0.0", # GET /api/me/entitlements — capabilities + features diff --git a/frontend/src/context/AuthContext.jsx b/frontend/src/context/AuthContext.jsx index d770977..b9fd27a 100644 --- a/frontend/src/context/AuthContext.jsx +++ b/frontend/src/context/AuthContext.jsx @@ -9,6 +9,7 @@ import { } from 'react' import api, { ACTIVE_CLUB_STORAGE_KEY } from '../utils/api' import { activeClubMemberships } from '../utils/activeClub' +import { clearCoachSessionStorage } from '../utils/trainingPlanUtils' const AuthContext = createContext(null) @@ -131,11 +132,7 @@ export function AuthProvider({ children }) { setUser(null) localStorage.removeItem('authToken') localStorage.removeItem(ACTIVE_CLUB_STORAGE_KEY) - for (const key of Object.keys(sessionStorage)) { - if (key.startsWith('sj_coach_')) { - sessionStorage.removeItem(key) - } - } + clearCoachSessionStorage() }, []) const value = useMemo( diff --git a/frontend/src/pages/LoginPage.jsx b/frontend/src/pages/LoginPage.jsx index 60a97e8..00d22b4 100644 --- a/frontend/src/pages/LoginPage.jsx +++ b/frontend/src/pages/LoginPage.jsx @@ -2,6 +2,7 @@ import React, { useState, useEffect } from 'react' import { useNavigate, Link } from 'react-router-dom' import { useAuth } from '../context/AuthContext' import api from '../utils/api' +import { clearCoachSessionStorage } from '../utils/trainingPlanUtils' function LoginPage() { const [mode, setMode] = useState('login') // 'login' or 'register' @@ -18,6 +19,12 @@ function LoginPage() { const navigate = useNavigate() const { checkAuth } = useAuth() + useEffect(() => { + if (!localStorage.getItem('authToken')) { + clearCoachSessionStorage() + } + }, []) + useEffect(() => { if (mode !== 'register') return api.listPublicClubsDirectory().then(setPublicClubs).catch(() => setPublicClubs([])) diff --git a/frontend/src/pages/OnboardingPage.jsx b/frontend/src/pages/OnboardingPage.jsx index dcb8092..cc16999 100644 --- a/frontend/src/pages/OnboardingPage.jsx +++ b/frontend/src/pages/OnboardingPage.jsx @@ -19,8 +19,14 @@ const creationStatusLabel = (s) => approved: 'freigegeben', rejected: 'abgelehnt', withdrawn: 'zurückgezogen', + superseded: 'Verein entfernt', })[s] || s +/** Freigabe noch gültig (Verein existiert). */ +function isActiveApprovedCreation(req) { + return req.status === 'approved' && req.created_club_id +} + /** * Onboarding für Nutzer ohne aktive Vereinsmitgliedschaft (Phase A). */ @@ -264,7 +270,7 @@ export default function OnboardingPage() { {myCreationRequests.map((r) => (
  • {r.proposed_name} — {creationStatusLabel(r.status)} - {r.status === 'approved' && r.created_club_name + {isActiveApprovedCreation(r) && r.created_club_name ? ` (${r.created_club_name})` : null} {r.status === 'pending' ? ( @@ -288,7 +294,7 @@ export default function OnboardingPage() { ) : null} - {r.status === 'approved' ? ( + {isActiveApprovedCreation(r) ? ( <> {' '}