feat(memberships, profiles, clubs): enhance active club membership handling
All checks were successful
Deploy Development / deploy (push) Successful in 40s
Test Suite / pytest-backend (push) Successful in 25s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 7s
Test Suite / playwright-tests (push) Successful in 23s
All checks were successful
Deploy Development / deploy (push) Successful in 40s
Test Suite / pytest-backend (push) Successful in 25s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 7s
Test Suite / playwright-tests (push) Successful in 23s
- Introduced a new utility function to filter and return only active club memberships, improving role management and access control. - Updated various components and pages to utilize the new active club memberships function, ensuring only relevant memberships are considered. - Enhanced user interface elements to reflect the status of club memberships, including visual indicators for inactive memberships. - Improved backend logic for resolving tenant contexts and managing club roles based on active memberships.
This commit is contained in:
parent
624c19dcba
commit
24c70c5ea0
|
|
@ -91,7 +91,12 @@ def can_manage_club_org(cur, profile_id: int, club_id: int, global_role: Optiona
|
|||
|
||||
|
||||
def can_plan_in_club(cur, profile_id: int, club_id: int, global_role: Optional[str]) -> bool:
|
||||
"""Trainingsgruppen anlegen / planen: Admin-Rollen im Verein oder Plattform."""
|
||||
"""Trainingsgruppe anlegen u.Ä.; Vereins-rollentrainer, Content-Editor, Spartenleitung …
|
||||
|
||||
Hinweis: ``content_editor`` ist derzeit zusammen mit ``trainer``/``division_lead`` in diesem
|
||||
gemeinsamen Strang gebündelt — u.a. Vereinsübungen bearbeiten (s. exercises) und
|
||||
Trainingsgruppen unter ``clubs``. Es gibt noch keine eigene Nur-Content-Guard pro Endpunkt.
|
||||
"""
|
||||
if is_platform_admin(global_role):
|
||||
return True
|
||||
return has_club_role(
|
||||
|
|
|
|||
|
|
@ -94,7 +94,7 @@ def get_current_profile(
|
|||
session=Depends(require_auth),
|
||||
x_active_club_id: Optional[str] = Header(default=None, alias="X-Active-Club-Id"),
|
||||
):
|
||||
"""Profil inkl. Vereinsmitgliedschaften; effective_club_id = aufgelöster Request-Kontext (Header vor Profilfeld)."""
|
||||
"""Profil inkl. Vereinsmitgliedschaften (aktive und temporär deaktivierte Zugänge); effective_club_id nur bei aktivem Vereinszugang."""
|
||||
profile_id = session["profile_id"]
|
||||
with get_db() as conn:
|
||||
cur = get_cursor(conn)
|
||||
|
|
@ -104,7 +104,7 @@ def get_current_profile(
|
|||
raise HTTPException(404, "Profil nicht gefunden")
|
||||
data = r2d(row)
|
||||
data.pop("pin_hash", None)
|
||||
clubs = memberships_with_roles(cur, profile_id)
|
||||
clubs = memberships_with_roles(cur, profile_id, active_only=False)
|
||||
data["clubs"] = clubs
|
||||
ac_raw = data.get("active_club_id")
|
||||
stored_ac = int(ac_raw) if ac_raw is not None and ac_raw != "" else None
|
||||
|
|
|
|||
|
|
@ -21,6 +21,22 @@ def _club_exists(cur, club_id: int) -> bool:
|
|||
return cur.fetchone() is not None
|
||||
|
||||
|
||||
def memberships_for_tenant_resolution(
|
||||
memberships: List[Dict[str, Any]],
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Nur Zeilen mit aktivem Vereinszugang (cm.status = 'active').
|
||||
Wird genutzt, wenn /profiles/me alle Mitgliedschaften inkl. inaktiver liefert.
|
||||
"""
|
||||
out: List[Dict[str, Any]] = []
|
||||
for r in memberships:
|
||||
st_raw = r.get("membership_status")
|
||||
st = str(st_raw if st_raw is not None else "active").strip().lower()
|
||||
if st == "active":
|
||||
out.append(r)
|
||||
return out
|
||||
|
||||
|
||||
def parse_active_club_header(raw: Optional[str]) -> Optional[int]:
|
||||
"""Parst X-Active-Club-Id; leer → None. Ungültig → HTTP 400."""
|
||||
if raw is None:
|
||||
|
|
@ -97,7 +113,9 @@ def resolve_tenant_context(
|
|||
invalid_header_policy: str = "reject",
|
||||
) -> TenantContext:
|
||||
"""
|
||||
Mitgliedschaften: wenn nicht übergeben, wird aus der DB geladen (aktive Mitgliedschaften).
|
||||
Mitgliedschaften: wenn nicht übergeben, lädt ``active_only=True`` aus der DB.
|
||||
Übergabe z. B. von ``/profiles/me``: Liste darf auch **deaktivierte** Vereinszugänge
|
||||
(`membership_status` = inactive) enthalten — für ``club_ids`` und Mandantenwahl werden nur aktive verwendet.
|
||||
|
||||
Auflösung effective_club_id:
|
||||
- Plattform-Admin: Header setzt beliebigen existierenden Verein; ohne Header → gespeichertes
|
||||
|
|
@ -110,9 +128,11 @@ def resolve_tenant_context(
|
|||
header_cid = parse_active_club_header(header_raw)
|
||||
|
||||
if memberships is None:
|
||||
memberships = memberships_with_roles(cur, profile_id, active_only=True)
|
||||
membership_rows = memberships_with_roles(cur, profile_id, active_only=True)
|
||||
else:
|
||||
membership_rows = memberships_for_tenant_resolution(memberships)
|
||||
|
||||
club_ids = frozenset(int(r["id"]) for r in memberships if r.get("id") is not None)
|
||||
club_ids = frozenset(int(r["id"]) for r in membership_rows if r.get("id") is not None)
|
||||
|
||||
if is_platform_admin(role_lc):
|
||||
if header_cid is not None:
|
||||
|
|
@ -131,7 +151,7 @@ def resolve_tenant_context(
|
|||
global_role=role_lc,
|
||||
effective_club_id=effective,
|
||||
club_ids=club_ids,
|
||||
memberships=memberships,
|
||||
memberships=membership_rows,
|
||||
)
|
||||
|
||||
chosen_header = header_cid
|
||||
|
|
@ -159,7 +179,7 @@ def resolve_tenant_context(
|
|||
global_role=role_lc,
|
||||
effective_club_id=effective,
|
||||
club_ids=club_ids,
|
||||
memberships=memberships,
|
||||
memberships=membership_rows,
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -125,3 +125,37 @@ def test_resolve_platform_admin_no_header_stored_invalid(monkeypatch):
|
|||
stored_active_club_id=123,
|
||||
)
|
||||
assert ctx.effective_club_id is None
|
||||
|
||||
|
||||
def test_resolve_trainer_club_ids_excludes_inactive_memberships():
|
||||
"""Nur aktive Vereinszugänge zählen für Mandant / Header-Validierung."""
|
||||
cur = object()
|
||||
ctx = resolve_tenant_context(
|
||||
cur,
|
||||
profile_id=9,
|
||||
global_role="user",
|
||||
header_raw=None,
|
||||
memberships=[
|
||||
{"id": 10, "membership_status": "inactive"},
|
||||
{"id": 20, "membership_status": "active"},
|
||||
],
|
||||
stored_active_club_id=None,
|
||||
invalid_header_policy="ignore",
|
||||
)
|
||||
assert ctx.club_ids == frozenset({20})
|
||||
assert ctx.effective_club_id == 20
|
||||
|
||||
|
||||
def test_resolve_all_memberships_inactive_no_effective_club():
|
||||
cur = object()
|
||||
ctx = resolve_tenant_context(
|
||||
cur,
|
||||
profile_id=9,
|
||||
global_role="user",
|
||||
header_raw=None,
|
||||
memberships=[{"id": 10, "membership_status": "inactive"}],
|
||||
stored_active_club_id=10,
|
||||
invalid_header_policy="ignore",
|
||||
)
|
||||
assert ctx.club_ids == frozenset()
|
||||
assert ctx.effective_club_id is None
|
||||
|
|
|
|||
|
|
@ -38,13 +38,15 @@ import AdminHomeRedirect from './components/AdminHomeRedirect'
|
|||
import PlatformAdminRoute from './components/PlatformAdminRoute'
|
||||
import MediaLibraryPage from './pages/MediaLibraryPage'
|
||||
import ActiveClubSwitcher from './components/ActiveClubSwitcher'
|
||||
import InactiveMembershipBanner from './components/InactiveMembershipBanner'
|
||||
import { activeClubMemberships } from './utils/activeClub'
|
||||
import './app.css'
|
||||
|
||||
/** Shield-„Admin“: Portal-Admins oder Vereinsorganisation (Zugriff mindestens /admin/users). */
|
||||
function computeShowAdminNav(currentUser) {
|
||||
const plat = currentUser?.role === 'admin' || currentUser?.role === 'superadmin'
|
||||
if (plat) return true
|
||||
return (currentUser?.clubs || []).some((c) => (c.roles || []).includes('club_admin'))
|
||||
return activeClubMemberships(currentUser?.clubs).some((c) => (c.roles || []).includes('club_admin'))
|
||||
}
|
||||
|
||||
// Bottom Navigation (Mobile)
|
||||
|
|
@ -126,6 +128,7 @@ function ProtectedLayout() {
|
|||
<ActiveClubSwitcher variant="mobile" />
|
||||
</div>
|
||||
<div className="app-main">
|
||||
<InactiveMembershipBanner />
|
||||
<Outlet />
|
||||
</div>
|
||||
<Nav showAdminNav={showAdminNav} />
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { useAuth } from '../context/AuthContext'
|
||||
import { getResolvedActiveClubIdForUi } from '../utils/activeClub'
|
||||
import { activeClubMemberships, getResolvedActiveClubIdForUi } from '../utils/activeClub'
|
||||
|
||||
/**
|
||||
* Zeigt einen Vereins-Umschalter, wenn der Nutzer mehreren Vereinen zugeordnet ist.
|
||||
|
|
@ -7,7 +7,7 @@ import { getResolvedActiveClubIdForUi } from '../utils/activeClub'
|
|||
*/
|
||||
export default function ActiveClubSwitcher({ variant = 'sidebar' }) {
|
||||
const { user, setActiveClub } = useAuth()
|
||||
const clubs = user?.clubs || []
|
||||
const clubs = activeClubMemberships(user?.clubs)
|
||||
if (clubs.length <= 1) return null
|
||||
|
||||
const selectClubId = getResolvedActiveClubIdForUi(user)
|
||||
|
|
|
|||
39
frontend/src/components/InactiveMembershipBanner.jsx
Normal file
39
frontend/src/components/InactiveMembershipBanner.jsx
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
import { useAuth } from '../context/AuthContext'
|
||||
|
||||
/**
|
||||
* Hinweis, wenn der Vereinszugang (Mitgliedschaft) deaktiviert wurde — Login bleibt möglich.
|
||||
*/
|
||||
export default function InactiveMembershipBanner() {
|
||||
const { user } = useAuth()
|
||||
const inactive = (user?.clubs || []).filter(
|
||||
(c) => (c.membership_status || '').toString().trim().toLowerCase() === 'inactive'
|
||||
)
|
||||
if (!inactive.length) return null
|
||||
|
||||
const names = inactive.map((c) => c.name || `Verein #${c.id}`).join(', ')
|
||||
|
||||
return (
|
||||
<div
|
||||
role="status"
|
||||
className="inactive-membership-banner"
|
||||
style={{
|
||||
marginBottom: '0.75rem',
|
||||
padding: '0.65rem 0.85rem',
|
||||
borderRadius: '8px',
|
||||
background: 'var(--surface2, #2a2a2a)',
|
||||
border: '1px solid color-mix(in srgb, var(--warning, #d4a012) 45%, transparent)',
|
||||
color: 'var(--text1)',
|
||||
fontSize: '0.88rem',
|
||||
lineHeight: 1.45,
|
||||
}}
|
||||
>
|
||||
<strong>Vereinszugang vorübergehend deaktiviert</strong>
|
||||
<span style={{ display: 'block', marginTop: '0.35rem' }}>
|
||||
Für {inactive.length === 1 ? 'den Verein' : 'die Vereine'}{' '}
|
||||
<strong>{names}</strong>{' '}
|
||||
ist der Zugang zu Vereinsinhalten ausgesetzt — du kannst dich weiterhin anmelden und z. B.
|
||||
öffentliche Inhalte oder andere Vereine nutzen. Bei Fragen wende dich an eine:n Vereinsadministrator:in.
|
||||
</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,13 +1,13 @@
|
|||
import { Link, useLocation, useNavigate } from 'react-router-dom'
|
||||
import { useAuth } from '../context/AuthContext'
|
||||
import { getResolvedActiveClubIdForUi } from '../utils/activeClub'
|
||||
import { getResolvedActiveClubIdForUi, activeClubMemberships } from '../utils/activeClub'
|
||||
|
||||
function Navigation() {
|
||||
const location = useLocation()
|
||||
const navigate = useNavigate()
|
||||
const { user, logout, setActiveClub } = useAuth()
|
||||
|
||||
const clubs = user?.clubs || []
|
||||
const clubs = activeClubMemberships(user?.clubs)
|
||||
const selectClubId = getResolvedActiveClubIdForUi(user)
|
||||
|
||||
const handleLogout = async () => {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import React, { useCallback, useMemo, useState } from 'react'
|
||||
import { Link } from 'react-router-dom'
|
||||
import api from '../utils/api'
|
||||
import { activeClubMemberships } from '../utils/activeClub'
|
||||
|
||||
const VIS_LABELS = {
|
||||
private: 'Privat',
|
||||
|
|
@ -48,7 +49,7 @@ function userMayPromote(user, targetClubId, createdBy) {
|
|||
const role = String(user.role || '').toLowerCase()
|
||||
if (role === 'admin' || role === 'superadmin') return true
|
||||
if (createdBy != null && Number(createdBy) === Number(user.id)) return true
|
||||
const row = (user.clubs || []).find((c) => Number(c.id) === Number(targetClubId))
|
||||
const row = activeClubMemberships(user?.clubs).find((c) => Number(c.id) === Number(targetClubId))
|
||||
if (!row || !Array.isArray(row.roles)) return false
|
||||
return row.roles.includes('club_admin')
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
import { createContext, useContext, useState, useEffect, useCallback, useRef } from 'react'
|
||||
import api, { ACTIVE_CLUB_STORAGE_KEY } from '../utils/api'
|
||||
import { activeClubMemberships } from '../utils/activeClub'
|
||||
|
||||
const AuthContext = createContext(null)
|
||||
|
||||
function syncStoredActiveClub(profile) {
|
||||
const clubs = profile?.clubs || []
|
||||
const clubs = activeClubMemberships(profile?.clubs)
|
||||
const ids = new Set(clubs.map((c) => String(c.id)))
|
||||
const eff = profile?.effective_club_id
|
||||
if (eff != null && eff !== '' && ids.has(String(eff))) {
|
||||
|
|
@ -24,6 +25,12 @@ function syncStoredActiveClub(profile) {
|
|||
}
|
||||
if (clubs.length >= 1) {
|
||||
localStorage.setItem(ACTIVE_CLUB_STORAGE_KEY, String(clubs[0].id))
|
||||
return
|
||||
}
|
||||
try {
|
||||
localStorage.removeItem(ACTIVE_CLUB_STORAGE_KEY)
|
||||
} catch {
|
||||
/* ignore */
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,13 +7,14 @@ import {
|
|||
useState,
|
||||
} from 'react'
|
||||
import api from '../utils/api'
|
||||
import { activeClubMemberships } from '../utils/activeClub'
|
||||
|
||||
const OrgInboxContext = createContext(null)
|
||||
|
||||
export function canAccessOrgInbox(user) {
|
||||
if (!user?.id) return false
|
||||
if (user.role === 'admin' || user.role === 'superadmin') return true
|
||||
return (user.clubs || []).some((c) => (c.roles || []).includes('club_admin'))
|
||||
return activeClubMemberships(user.clubs).some((c) => (c.roles || []).includes('club_admin'))
|
||||
}
|
||||
|
||||
/** Nach Annahme/Ablehnung: Posteingang-Badges & Widget aktualisieren */
|
||||
|
|
|
|||
|
|
@ -252,13 +252,22 @@ function AccountSettingsPage() {
|
|||
<span style={{ lineHeight: 1.45 }}>
|
||||
{user?.clubs?.length ? (
|
||||
<>
|
||||
{user.clubs.map((c) => (
|
||||
<div key={c.id}>
|
||||
<strong style={{ color: 'var(--text1)' }}>{c.name}</strong>
|
||||
{': '}
|
||||
{(c.roles || []).length ? (c.roles || []).join(', ') : '—'}
|
||||
</div>
|
||||
))}
|
||||
{user.clubs.map((c) => {
|
||||
const mem = (c.membership_status || 'active').toString().trim().toLowerCase()
|
||||
const inactive = mem === 'inactive'
|
||||
return (
|
||||
<div key={c.id}>
|
||||
<strong style={{ color: 'var(--text1)' }}>{c.name}</strong>
|
||||
{inactive ? (
|
||||
<span style={{ color: 'var(--warning, #d4a012)', marginLeft: '0.35rem', fontSize: '0.82rem' }}>
|
||||
(Vereinszugang deaktiviert)
|
||||
</span>
|
||||
) : null}
|
||||
{': '}
|
||||
{(c.roles || []).length ? (c.roles || []).join(', ') : '—'}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</>
|
||||
) : (
|
||||
'—'
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import { useCallback, useEffect, useMemo, useState } from 'react'
|
|||
import { Navigate } from 'react-router-dom'
|
||||
import { useAuth } from '../context/AuthContext'
|
||||
import api from '../utils/api'
|
||||
import { activeClubMemberships } from '../utils/activeClub'
|
||||
import AdminPageNav from '../components/AdminPageNav'
|
||||
|
||||
const CLUB_ROLE_OPTIONS = [
|
||||
|
|
@ -19,7 +20,7 @@ const PORTAL_ROLE_LABEL = {
|
|||
}
|
||||
|
||||
function clubAdminClubIds(user) {
|
||||
return (user?.clubs || [])
|
||||
return activeClubMemberships(user?.clubs)
|
||||
.filter((c) => Array.isArray(c.roles) && c.roles.includes('club_admin'))
|
||||
.map((c) => c.id)
|
||||
}
|
||||
|
|
@ -206,6 +207,29 @@ export default function AdminUsersPage() {
|
|||
}
|
||||
}
|
||||
|
||||
const toggleMemberClubAccess = async (m, activate) => {
|
||||
if (!selectedClubId) return
|
||||
const st = activate ? 'active' : 'inactive'
|
||||
if (
|
||||
!activate &&
|
||||
!confirm(
|
||||
`Vereinszugang für „${m.name || m.email || '#' + m.profile_id}“ in ${selectedClubLabel} deaktivieren? ` +
|
||||
'Die Person bleibt anmeldbar, sieht aber keine Inhalte dieses Vereins mehr (Login bleibt unverändert).'
|
||||
)
|
||||
) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
await api.updateClubMember(selectedClubId, m.profile_id, {
|
||||
roles: [...(m.roles || [])],
|
||||
status: st,
|
||||
})
|
||||
await reloadClubMembers()
|
||||
} catch (e) {
|
||||
alert(e.message || String(e))
|
||||
}
|
||||
}
|
||||
|
||||
const removeClubMembership = async () => {
|
||||
if (!clubEditModal) return
|
||||
if (!confirm('Mitgliedschaft in diesem Verein wirklich entfernen?')) return
|
||||
|
|
@ -288,8 +312,9 @@ export default function AdminUsersPage() {
|
|||
|
||||
{clubOrgMode ? (
|
||||
<p style={{ color: 'var(--text2)', maxWidth: '52rem', lineHeight: 1.55, marginBottom: '1.25rem' }}>
|
||||
Du verwaltest nur Mitglieder des ausgewählten Vereins und deren <strong>Rollen in diesem Verein</strong>.
|
||||
Portal-Rollen und andere Vereine sind hier nicht sichtbar.
|
||||
Du verwaltest Mitglieder des ausgewählten Vereins. <strong>Vereinszugang deaktivieren</strong> sperrt nur die
|
||||
Sicht auf Vereinsinhalte — der Login des Nutzers bleibt möglich. Wiederherstellen über „aktivieren“ oder
|
||||
Bearbeiten.
|
||||
</p>
|
||||
) : (
|
||||
<>
|
||||
|
|
@ -368,7 +393,9 @@ export default function AdminUsersPage() {
|
|||
{!clubMembers.length ? (
|
||||
<p className="muted">Keine Mitglieder in diesem Verein.</p>
|
||||
) : (
|
||||
clubMembers.map((m) => (
|
||||
clubMembers.map((m) => {
|
||||
const memStatus = (m.status || 'active').toLowerCase()
|
||||
return (
|
||||
<div key={m.membership_id} className="card">
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', flexWrap: 'wrap', gap: '0.75rem' }}>
|
||||
<div>
|
||||
|
|
@ -377,7 +404,12 @@ export default function AdminUsersPage() {
|
|||
</strong>
|
||||
<div style={{ fontSize: '0.875rem', color: 'var(--text2)' }}>{m.email || '—'}</div>
|
||||
<div style={{ fontSize: '0.78rem', color: 'var(--text3)', marginTop: '0.35rem' }}>
|
||||
Status: {m.status} · Verifiziert: {m.email_verified ? 'ja' : 'nein'}
|
||||
Vereinszugang:{' '}
|
||||
<strong style={{ color: memStatus === 'active' ? 'var(--text1)' : 'var(--warning, #d4a012)' }}>
|
||||
{memStatus === 'active' ? 'aktiv' : 'deaktiviert'}
|
||||
</strong>
|
||||
{' '}
|
||||
· Verifiziert: {m.email_verified ? 'ja' : 'nein'}
|
||||
</div>
|
||||
<div style={{ fontSize: '0.82rem', marginTop: '0.35rem' }}>
|
||||
Rollen: {(m.roles || []).join(', ') || '—'}
|
||||
|
|
@ -394,12 +426,31 @@ export default function AdminUsersPage() {
|
|||
profileId: m.profile_id,
|
||||
profileLabel: m.name || m.email,
|
||||
roles: [...(m.roles || [])],
|
||||
status: (m.status || 'active').toLowerCase(),
|
||||
status: memStatus,
|
||||
})
|
||||
}
|
||||
>
|
||||
Bearbeiten
|
||||
</button>
|
||||
{m.profile_id !== user?.id ? (
|
||||
memStatus === 'inactive' ? (
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-primary"
|
||||
onClick={() => toggleMemberClubAccess(m, true)}
|
||||
>
|
||||
Vereinszugang aktivieren
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-secondary"
|
||||
onClick={() => toggleMemberClubAccess(m, false)}
|
||||
>
|
||||
Vereinszugang deaktivieren
|
||||
</button>
|
||||
)
|
||||
) : null}
|
||||
{m.profile_id !== user?.id && !isEscalatedPortalRole(m.portal_role) ? (
|
||||
<button
|
||||
type="button"
|
||||
|
|
@ -417,7 +468,8 @@ export default function AdminUsersPage() {
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)
|
||||
})
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
|
|
@ -721,8 +773,12 @@ export default function AdminUsersPage() {
|
|||
<p style={{ color: 'var(--text2)', fontSize: '0.9rem' }}>
|
||||
{clubEditModal.profileLabel} → {clubEditModal.clubName}
|
||||
</p>
|
||||
<p className="muted" style={{ fontSize: '0.82rem', lineHeight: 1.45 }}>
|
||||
„Deaktiviert“ betrifft nur den Zugriff auf Inhalte dieses Vereins; Login und andere Vereine bleiben
|
||||
unberührt.
|
||||
</p>
|
||||
<div className="form-row">
|
||||
<label className="form-label">Status</label>
|
||||
<label className="form-label">Vereinszugang</label>
|
||||
<select
|
||||
className="form-input"
|
||||
value={clubEditModal.status}
|
||||
|
|
@ -730,8 +786,8 @@ export default function AdminUsersPage() {
|
|||
setClubEditModal((prev) => (prev ? { ...prev, status: e.target.value } : prev))
|
||||
}
|
||||
>
|
||||
<option value="active">aktiv</option>
|
||||
<option value="inactive">inaktiv</option>
|
||||
<option value="active">aktiv — sieht Vereinsinhalte</option>
|
||||
<option value="inactive">deaktiviert — kein Zugriff auf Vereinsinhalte</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="form-row">
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import React, { useState, useEffect, useMemo } from 'react'
|
|||
import api from '../utils/api'
|
||||
import { notifyOrgInboxChanged } from '../context/OrgInboxContext'
|
||||
import { useAuth } from '../context/AuthContext'
|
||||
import { activeClubMemberships } from '../utils/activeClub'
|
||||
import PageSectionNav from '../components/PageSectionNav'
|
||||
|
||||
const CLUB_ROLE_OPTIONS = [
|
||||
|
|
@ -41,7 +42,7 @@ function ClubsPage() {
|
|||
const isPlatformAdmin = user?.role === 'admin' || user?.role === 'superadmin'
|
||||
const isSuperAdmin = user?.role === 'superadmin'
|
||||
const clubAdminClubIds = new Set(
|
||||
(user?.clubs || [])
|
||||
activeClubMemberships(user?.clubs)
|
||||
.filter((c) => (c.roles || []).includes('club_admin'))
|
||||
.map((c) => c.id)
|
||||
)
|
||||
|
|
@ -49,7 +50,7 @@ function ClubsPage() {
|
|||
const canCreateClub = isPlatformAdmin
|
||||
const canManageOrgSomewhere = isPlatformAdmin || clubAdminClubIds.size > 0
|
||||
const canCreateTrainingGroup =
|
||||
isPlatformAdmin || (Array.isArray(user?.clubs) && user.clubs.length > 0)
|
||||
isPlatformAdmin || activeClubMemberships(user?.clubs).length > 0
|
||||
|
||||
const canEditGroup = (g) =>
|
||||
isPlatformAdmin ||
|
||||
|
|
@ -1310,7 +1311,7 @@ function ClubsPage() {
|
|||
{editMemberModal.name || editMemberModal.email} (#{editMemberModal.profile_id})
|
||||
</p>
|
||||
<div className="form-row">
|
||||
<label className="form-label">Status</label>
|
||||
<label className="form-label">Vereinszugang</label>
|
||||
<select
|
||||
className="form-input"
|
||||
value={editMemberModal.status || 'active'}
|
||||
|
|
@ -1318,10 +1319,13 @@ function ClubsPage() {
|
|||
setEditMemberModal((prev) => (prev ? { ...prev, status: e.target.value } : prev))
|
||||
}
|
||||
>
|
||||
<option value="active">aktiv</option>
|
||||
<option value="inactive">inaktiv</option>
|
||||
<option value="active">aktiv — sieht Vereinsinhalte</option>
|
||||
<option value="inactive">deaktiviert — weiter anmeldbar, keine Vereinsinhalte</option>
|
||||
</select>
|
||||
</div>
|
||||
<p className="muted" style={{ fontSize: '0.82rem', lineHeight: 1.45 }}>
|
||||
Deaktivierung gilt nur für diesen Verein; der Login-Account bleibt aktiv.
|
||||
</p>
|
||||
<div className="form-row">
|
||||
<span className="form-label">Rollen</span>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.35rem' }}>
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import {
|
|||
} from 'lucide-react'
|
||||
import api from '../utils/api'
|
||||
import { useAuth } from '../context/AuthContext'
|
||||
import { activeClubMemberships } from '../utils/activeClub'
|
||||
import { SKILL_LEVEL_OPTIONS } from '../constants/skillLevels'
|
||||
import MultiSelectCombo from '../components/MultiSelectCombo'
|
||||
import ExerciseFocusRulePicker from '../components/ExerciseFocusRulePicker'
|
||||
|
|
@ -508,7 +509,7 @@ function ExercisesListPage() {
|
|||
|
||||
const clubNameById = useMemo(() => {
|
||||
const m = {}
|
||||
for (const c of user?.clubs || []) {
|
||||
for (const c of activeClubMemberships(user?.clubs)) {
|
||||
if (c?.id != null) m[Number(c.id)] = c.name || `#${c.id}`
|
||||
}
|
||||
return m
|
||||
|
|
@ -1206,7 +1207,7 @@ function ExercisesListPage() {
|
|||
onChange={(e) => setBulkClubSelect(e.target.value)}
|
||||
>
|
||||
<option value="">Aktiver Verein (Vereins-Umschalter / Header)</option>
|
||||
{(user?.clubs || []).map((c) => (
|
||||
{activeClubMemberships(user?.clubs).map((c) => (
|
||||
<option key={c.id} value={String(c.id)}>
|
||||
{c.name || `#${c.id}`}
|
||||
</option>
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ import {
|
|||
} from 'lucide-react'
|
||||
import { useAuth } from '../context/AuthContext'
|
||||
import api from '../utils/api'
|
||||
import { activeClubMemberships } from '../utils/activeClub'
|
||||
import { resolveMediaAssetFileUrl } from '../utils/exerciseMediaUrl'
|
||||
|
||||
const LC_OPTIONS = [
|
||||
|
|
@ -233,7 +234,7 @@ export default function MediaLibraryPage() {
|
|||
const { user } = useAuth()
|
||||
const isPlatformAdmin = user?.role === 'admin' || user?.role === 'superadmin'
|
||||
const isSuperadmin = user?.role === 'superadmin'
|
||||
const hasClubOrgAdmin = (user?.clubs || []).some((c) => (c.roles || []).includes('club_admin'))
|
||||
const hasClubOrgAdmin = activeClubMemberships(user?.clubs).some((c) => (c.roles || []).includes('club_admin'))
|
||||
|
||||
const archiveVisOptions = useMemo(
|
||||
() => VIS_OPTIONS.filter((o) => o.value !== 'official' || isSuperadmin),
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react'
|
|||
import { Link, useSearchParams } from 'react-router-dom'
|
||||
import api from '../utils/api'
|
||||
import { useAuth } from '../context/AuthContext'
|
||||
import { activeClubMemberships } from '../utils/activeClub'
|
||||
import ExercisePickerModal from '../components/ExercisePickerModal'
|
||||
import ExercisePeekModal from '../components/ExercisePeekModal'
|
||||
import TrainingUnitSectionsEditor from '../components/TrainingUnitSectionsEditor'
|
||||
|
|
@ -284,13 +285,13 @@ function TrainingPlanningPage() {
|
|||
const r = (user?.role || '').toLowerCase()
|
||||
if (r === 'admin' || r === 'superadmin') return true
|
||||
if (selectedGroupClubIdMemo == null || !Number.isFinite(selectedGroupClubIdMemo)) return false
|
||||
const row = (user?.clubs || []).find((c) => Number(c.id) === selectedGroupClubIdMemo)
|
||||
const row = activeClubMemberships(user?.clubs).find((c) => Number(c.id) === selectedGroupClubIdMemo)
|
||||
return Array.isArray(row?.roles) && row.roles.includes('club_admin')
|
||||
}, [user?.role, user?.clubs, selectedGroupClubIdMemo])
|
||||
|
||||
const clubAdminClubIdSet = useMemo(() => {
|
||||
const ids = []
|
||||
for (const c of user?.clubs || []) {
|
||||
for (const c of activeClubMemberships(user?.clubs)) {
|
||||
if (Array.isArray(c.roles) && c.roles.includes('club_admin')) {
|
||||
const id = Number(c.id)
|
||||
if (Number.isFinite(id)) ids.push(id)
|
||||
|
|
|
|||
|
|
@ -1,11 +1,18 @@
|
|||
import { ACTIVE_CLUB_STORAGE_KEY } from './api'
|
||||
|
||||
/** Nur Mitgliedschaften mit aktivem Vereinszugang (Backend: club_members.status). */
|
||||
export function activeClubMemberships(clubs) {
|
||||
return (clubs || []).filter(
|
||||
(c) => (c.membership_status || 'active').toString().trim().toLowerCase() === 'active'
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Einheitliche Anzeige des aktiven Vereins: Abgleich mit effective_club_id, active_club_id,
|
||||
* LocalStorage (Request-Header-Quelle), sonst erster Verein der Liste.
|
||||
* LocalStorage (Request-Header-Quelle), sonst erster **aktiver** Verein der Liste.
|
||||
*/
|
||||
export function getResolvedActiveClubIdForUi(user) {
|
||||
const clubs = user?.clubs || []
|
||||
const clubs = activeClubMemberships(user?.clubs)
|
||||
if (!clubs.length) return null
|
||||
|
||||
const idInClubs = (id) =>
|
||||
|
|
|
|||
|
|
@ -1,12 +1,14 @@
|
|||
import { activeClubMemberships } from './activeClub'
|
||||
|
||||
function userIsClubAdminForClub(user, clubId) {
|
||||
if (clubId == null || user == null) return false
|
||||
const cid = Number(clubId)
|
||||
const row = (user.clubs || []).find((c) => Number(c.id) === cid)
|
||||
const row = activeClubMemberships(user.clubs).find((c) => Number(c.id) === cid)
|
||||
return Array.isArray(row?.roles) && row.roles.includes('club_admin')
|
||||
}
|
||||
|
||||
function userHasAnyClubAdminRole(user) {
|
||||
return (user?.clubs || []).some((c) => Array.isArray(c.roles) && c.roles.includes('club_admin'))
|
||||
return activeClubMemberships(user?.clubs).some((c) => Array.isArray(c.roles) && c.roles.includes('club_admin'))
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user