Update Version and Enhance Club Creation Request Management
All checks were successful
Deploy Development / deploy (push) Successful in 42s
Test Suite / pytest-backend (push) Successful in 39s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m20s
All checks were successful
Deploy Development / deploy (push) Successful in 42s
Test Suite / pytest-backend (push) Successful in 39s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m20s
- Incremented application version to 0.8.192 and database schema version to 20260606081. - Updated club module versions for 'clubs' and 'club_creation_requests' to reflect recent changes. - Implemented logic to mark approved club creation requests as 'superseded' when the associated club is deleted. - Refactored frontend components to clear session storage for coach-related keys upon logout and during login checks. - Enhanced onboarding page to accurately display the status of club creation requests based on their validity.
This commit is contained in:
parent
37785135b1
commit
fa10450315
13
backend/migrations/081_club_creation_request_superseded.sql
Normal file
13
backend/migrations/081_club_creation_request_superseded.sql
Normal file
|
|
@ -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;
|
||||||
|
|
@ -112,6 +112,14 @@ class CreationRequestCreate(BaseModel):
|
||||||
message: Optional[str] = Field(None, max_length=2000)
|
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]:
|
def _response_one(cur, req_id: int, viewer_profile_id: int) -> Dict[str, Any]:
|
||||||
cur.execute(
|
cur.execute(
|
||||||
"""
|
"""
|
||||||
|
|
@ -125,7 +133,7 @@ def _response_one(cur, req_id: int, viewer_profile_id: int) -> Dict[str, Any]:
|
||||||
row = cur.fetchone()
|
row = cur.fetchone()
|
||||||
if not row:
|
if not row:
|
||||||
raise HTTPException(status_code=404, detail="Antrag nicht gefunden")
|
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:
|
def _assert_platform_admin(tenant: TenantContext) -> None:
|
||||||
|
|
@ -157,7 +165,7 @@ def get_my_creation_requests(tenant: TenantContext = Depends(get_tenant_context)
|
||||||
""",
|
""",
|
||||||
(pid,),
|
(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)
|
@router.post("/me/club-creation-requests", status_code=201)
|
||||||
|
|
|
||||||
|
|
@ -336,6 +336,16 @@ def delete_club(club_id: int, tenant: TenantContext = Depends(get_tenant_context
|
||||||
if not cur.fetchone():
|
if not cur.fetchone():
|
||||||
raise HTTPException(404, "Verein nicht gefunden")
|
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)
|
# Delete (CASCADE handles divisions and groups)
|
||||||
cur.execute("DELETE FROM clubs WHERE id = %s", (club_id,))
|
cur.execute("DELETE FROM clubs WHERE id = %s", (club_id,))
|
||||||
conn.commit()
|
conn.commit()
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
# Shinkan Jinkendo Version Information
|
# Shinkan Jinkendo Version Information
|
||||||
|
|
||||||
APP_VERSION = "0.8.191"
|
APP_VERSION = "0.8.192"
|
||||||
BUILD_DATE = "2026-06-06"
|
BUILD_DATE = "2026-06-06"
|
||||||
DB_SCHEMA_VERSION = "20260606080"
|
DB_SCHEMA_VERSION = "20260606081"
|
||||||
|
|
||||||
MODULE_VERSIONS = {
|
MODULE_VERSIONS = {
|
||||||
"legal_documents": "1.4.0", # Admin: Live-Vorschau pro Abschnitt + modale Vollvorschau (Editor + Dokumentenliste)
|
"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
|
"tenant_context": "1.1.0", # M3: account_state + email_verified im TenantContext
|
||||||
"capabilities": "1.0.1", # resolve_capabilities_map für /me/entitlements
|
"capabilities": "1.0.1", # resolve_capabilities_map für /me/entitlements
|
||||||
"account_lifecycle": "1.1.0", # Phase A: account_onboarding_gate API-Middleware
|
"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_memberships": "1.0.1", # Depends(get_tenant_context)
|
||||||
"club_join_requests": "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
|
"admin_users": "1.0.0", # GET /api/admin/users
|
||||||
"club_features": "1.2.0", # M4: club_features_map für /me/entitlements
|
"club_features": "1.2.0", # M4: club_features_map für /me/entitlements
|
||||||
"entitlements": "1.0.0", # GET /api/me/entitlements — capabilities + features
|
"entitlements": "1.0.0", # GET /api/me/entitlements — capabilities + features
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import {
|
||||||
} from 'react'
|
} from 'react'
|
||||||
import api, { ACTIVE_CLUB_STORAGE_KEY } from '../utils/api'
|
import api, { ACTIVE_CLUB_STORAGE_KEY } from '../utils/api'
|
||||||
import { activeClubMemberships } from '../utils/activeClub'
|
import { activeClubMemberships } from '../utils/activeClub'
|
||||||
|
import { clearCoachSessionStorage } from '../utils/trainingPlanUtils'
|
||||||
|
|
||||||
const AuthContext = createContext(null)
|
const AuthContext = createContext(null)
|
||||||
|
|
||||||
|
|
@ -131,11 +132,7 @@ export function AuthProvider({ children }) {
|
||||||
setUser(null)
|
setUser(null)
|
||||||
localStorage.removeItem('authToken')
|
localStorage.removeItem('authToken')
|
||||||
localStorage.removeItem(ACTIVE_CLUB_STORAGE_KEY)
|
localStorage.removeItem(ACTIVE_CLUB_STORAGE_KEY)
|
||||||
for (const key of Object.keys(sessionStorage)) {
|
clearCoachSessionStorage()
|
||||||
if (key.startsWith('sj_coach_')) {
|
|
||||||
sessionStorage.removeItem(key)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const value = useMemo(
|
const value = useMemo(
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@ import React, { useState, useEffect } from 'react'
|
||||||
import { useNavigate, Link } from 'react-router-dom'
|
import { useNavigate, Link } from 'react-router-dom'
|
||||||
import { useAuth } from '../context/AuthContext'
|
import { useAuth } from '../context/AuthContext'
|
||||||
import api from '../utils/api'
|
import api from '../utils/api'
|
||||||
|
import { clearCoachSessionStorage } from '../utils/trainingPlanUtils'
|
||||||
|
|
||||||
function LoginPage() {
|
function LoginPage() {
|
||||||
const [mode, setMode] = useState('login') // 'login' or 'register'
|
const [mode, setMode] = useState('login') // 'login' or 'register'
|
||||||
|
|
@ -18,6 +19,12 @@ function LoginPage() {
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const { checkAuth } = useAuth()
|
const { checkAuth } = useAuth()
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!localStorage.getItem('authToken')) {
|
||||||
|
clearCoachSessionStorage()
|
||||||
|
}
|
||||||
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (mode !== 'register') return
|
if (mode !== 'register') return
|
||||||
api.listPublicClubsDirectory().then(setPublicClubs).catch(() => setPublicClubs([]))
|
api.listPublicClubsDirectory().then(setPublicClubs).catch(() => setPublicClubs([]))
|
||||||
|
|
|
||||||
|
|
@ -19,8 +19,14 @@ const creationStatusLabel = (s) =>
|
||||||
approved: 'freigegeben',
|
approved: 'freigegeben',
|
||||||
rejected: 'abgelehnt',
|
rejected: 'abgelehnt',
|
||||||
withdrawn: 'zurückgezogen',
|
withdrawn: 'zurückgezogen',
|
||||||
|
superseded: 'Verein entfernt',
|
||||||
})[s] || s
|
})[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).
|
* Onboarding für Nutzer ohne aktive Vereinsmitgliedschaft (Phase A).
|
||||||
*/
|
*/
|
||||||
|
|
@ -264,7 +270,7 @@ export default function OnboardingPage() {
|
||||||
{myCreationRequests.map((r) => (
|
{myCreationRequests.map((r) => (
|
||||||
<li key={r.id} style={{ marginBottom: '0.35rem' }}>
|
<li key={r.id} style={{ marginBottom: '0.35rem' }}>
|
||||||
{r.proposed_name} — {creationStatusLabel(r.status)}
|
{r.proposed_name} — {creationStatusLabel(r.status)}
|
||||||
{r.status === 'approved' && r.created_club_name
|
{isActiveApprovedCreation(r) && r.created_club_name
|
||||||
? ` (${r.created_club_name})`
|
? ` (${r.created_club_name})`
|
||||||
: null}
|
: null}
|
||||||
{r.status === 'pending' ? (
|
{r.status === 'pending' ? (
|
||||||
|
|
@ -288,7 +294,7 @@ export default function OnboardingPage() {
|
||||||
</button>
|
</button>
|
||||||
</>
|
</>
|
||||||
) : null}
|
) : null}
|
||||||
{r.status === 'approved' ? (
|
{isActiveApprovedCreation(r) ? (
|
||||||
<>
|
<>
|
||||||
{' '}
|
{' '}
|
||||||
<button
|
<button
|
||||||
|
|
|
||||||
|
|
@ -283,6 +283,17 @@ export function coachBranchPicksStorageKey(unitId) {
|
||||||
return `sj_coach_branches_${unitId}`
|
return `sj_coach_branches_${unitId}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Coach-Sitzung (P-12): alle sj_coach_*-Keys aus sessionStorage entfernen. */
|
||||||
|
export function clearCoachSessionStorage() {
|
||||||
|
if (typeof sessionStorage === 'undefined') return
|
||||||
|
for (let i = sessionStorage.length - 1; i >= 0; i -= 1) {
|
||||||
|
const k = sessionStorage.key(i)
|
||||||
|
if (k && k.startsWith('sj_coach_')) {
|
||||||
|
sessionStorage.removeItem(k)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Index zum Springen: Co-Trainer-Link atBranch+preferSo — Gate falls noch offen, sonst erste Kachel im Stream.
|
* Index zum Springen: Co-Trainer-Link atBranch+preferSo — Gate falls noch offen, sonst erste Kachel im Stream.
|
||||||
*/
|
*/
|
||||||
|
|
|
||||||
|
|
@ -368,7 +368,13 @@ test('P-12: sessionStorage wird bei Logout bereinigt (sj_coach_* Schlüssel)', a
|
||||||
// App.jsx DesktopSidebar-Logout zeigt confirm() — in Playwright headless akzeptieren
|
// App.jsx DesktopSidebar-Logout zeigt confirm() — in Playwright headless akzeptieren
|
||||||
page.once('dialog', dialog => dialog.accept());
|
page.once('dialog', dialog => dialog.accept());
|
||||||
await page.getByRole('button', { name: 'Abmelden' }).click();
|
await page.getByRole('button', { name: 'Abmelden' }).click();
|
||||||
|
await page.waitForURL((url) => url.pathname.includes('/login'), { timeout: 15000 });
|
||||||
await page.waitForLoadState('networkidle');
|
await page.waitForLoadState('networkidle');
|
||||||
|
// Nach Hard-Redirect: LoginPage bereinigt sj_coach_* wenn kein Token
|
||||||
|
await page.waitForFunction(
|
||||||
|
() => sessionStorage.getItem('sj_coach_step_42') === null,
|
||||||
|
{ timeout: 10000 },
|
||||||
|
);
|
||||||
|
|
||||||
const nachLogout = await page.evaluate(() => ({
|
const nachLogout = await page.evaluate(() => ({
|
||||||
step: sessionStorage.getItem('sj_coach_step_42'),
|
step: sessionStorage.getItem('sj_coach_step_42'),
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user