feat(access): enhance visibility handling for club-related content
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 1s
Test Suite / build-frontend (push) Successful in 8s
Test Suite / playwright-tests (push) Successful in 23s
Test Suite / pytest-backend (pull_request) Successful in 23s
Test Suite / lint-backend (pull_request) Successful in 0s
Test Suite / build-frontend (pull_request) Successful in 6s
Test Suite / playwright-tests (pull_request) Successful in 30s
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 1s
Test Suite / build-frontend (push) Successful in 8s
Test Suite / playwright-tests (push) Successful in 23s
Test Suite / pytest-backend (pull_request) Successful in 23s
Test Suite / lint-backend (pull_request) Successful in 0s
Test Suite / build-frontend (pull_request) Successful in 6s
Test Suite / playwright-tests (pull_request) Successful in 30s
- Updated visibility logic for exercises, media assets, and training programs to ensure access is correctly managed based on active club memberships. - Refactored SQL queries to streamline visibility checks for platform admins and club members, ensuring only relevant content is displayed. - Improved user interface elements to reflect the status of club memberships, including visual indicators for inactive memberships. - Enhanced test cases to validate the new visibility logic and ensure proper access control across various components.
This commit is contained in:
parent
24c70c5ea0
commit
30c1c259d2
|
|
@ -203,25 +203,45 @@ def exercise_visible_to_profile(
|
|||
created_by: Optional[int],
|
||||
global_role: Optional[str],
|
||||
) -> bool:
|
||||
"""Leserechte einer Übung. Für neue Codepfade lieber `library_content_visible_to_profile` verwenden."""
|
||||
if is_platform_admin(global_role):
|
||||
"""
|
||||
Leserechte einer Übung (und analoger Bibliotheksobjekte).
|
||||
|
||||
Vereinsbezogene Inhalte (visibility club): aktiv nur mit **aktiver** Mitgliedschaft in diesem Verein.
|
||||
Mitgliedschaft mit status **inactive** sperrt — auch für Plattform-/Super-Admins — solange eine
|
||||
Mitgliedschaft existiert.
|
||||
|
||||
Ist man kein Mitglied dieses Vereins, behalten Plattform-Admins den bisherigen „Audit“-Zugang
|
||||
zum Vereinskontext ohne eigene Mitgliedschaft.
|
||||
"""
|
||||
vis = (visibility or "").strip().lower()
|
||||
plat = is_platform_admin(global_role)
|
||||
pid = int(profile_id)
|
||||
|
||||
if vis == "official":
|
||||
return True
|
||||
if visibility == "official":
|
||||
if created_by is not None and int(created_by) == pid:
|
||||
return True
|
||||
if created_by is not None and created_by == profile_id:
|
||||
return True
|
||||
if visibility == "private":
|
||||
if vis == "private":
|
||||
return plat
|
||||
if vis != "club":
|
||||
return False
|
||||
if visibility == "club":
|
||||
if exercise_club_id is None:
|
||||
return False
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT 1 FROM club_members
|
||||
WHERE profile_id = %s AND club_id = %s AND status = 'active'
|
||||
LIMIT 1
|
||||
""",
|
||||
(profile_id, exercise_club_id),
|
||||
)
|
||||
return cur.fetchone() is not None
|
||||
return False
|
||||
if exercise_club_id is None:
|
||||
return False
|
||||
try:
|
||||
ecid = int(exercise_club_id)
|
||||
except (TypeError, ValueError):
|
||||
return False
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT cm.status
|
||||
FROM club_members cm
|
||||
WHERE cm.profile_id = %s AND cm.club_id = %s
|
||||
LIMIT 1
|
||||
""",
|
||||
(pid, ecid),
|
||||
)
|
||||
row = cur.fetchone()
|
||||
if row is None:
|
||||
return plat
|
||||
st_raw = row["status"] if isinstance(row, dict) else row[0]
|
||||
return str(st_raw or "active").strip().lower() == "active"
|
||||
|
|
|
|||
|
|
@ -206,32 +206,22 @@ def list_progression_graphs(tenant: TenantContext = Depends(get_tenant_context))
|
|||
role = tenant.global_role
|
||||
with get_db() as conn:
|
||||
cur = get_cursor(conn)
|
||||
if is_platform_admin(role):
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT g.*,
|
||||
(SELECT COUNT(*) FROM exercise_progression_edges e WHERE e.graph_id = g.id) AS edges_count
|
||||
FROM exercise_progression_graphs g
|
||||
ORDER BY g.updated_at DESC NULLS LAST, g.name
|
||||
"""
|
||||
)
|
||||
else:
|
||||
vis_sql, vis_params = library_content_visibility_sql(
|
||||
alias="g",
|
||||
profile_id=profile_id,
|
||||
role=role,
|
||||
effective_club_id=tenant.effective_club_id,
|
||||
)
|
||||
cur.execute(
|
||||
f"""
|
||||
SELECT g.*,
|
||||
(SELECT COUNT(*) FROM exercise_progression_edges e WHERE e.graph_id = g.id) AS edges_count
|
||||
FROM exercise_progression_graphs g
|
||||
WHERE ({vis_sql})
|
||||
ORDER BY g.updated_at DESC NULLS LAST, g.name
|
||||
""",
|
||||
vis_params,
|
||||
)
|
||||
vis_sql, vis_params = library_content_visibility_sql(
|
||||
alias="g",
|
||||
profile_id=profile_id,
|
||||
role=role,
|
||||
effective_club_id=tenant.effective_club_id,
|
||||
)
|
||||
cur.execute(
|
||||
f"""
|
||||
SELECT g.*,
|
||||
(SELECT COUNT(*) FROM exercise_progression_edges e WHERE e.graph_id = g.id) AS edges_count
|
||||
FROM exercise_progression_graphs g
|
||||
WHERE ({vis_sql})
|
||||
ORDER BY g.updated_at DESC NULLS LAST, g.name
|
||||
""",
|
||||
vis_params,
|
||||
)
|
||||
return [r2d(r) for r in cur.fetchall()]
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1542,15 +1542,14 @@ def list_exercises(
|
|||
params = []
|
||||
|
||||
role = tenant.global_role
|
||||
if not is_platform_admin(role):
|
||||
vis_sql, vis_params = library_content_visibility_sql(
|
||||
alias="e",
|
||||
profile_id=profile_id,
|
||||
role=role,
|
||||
effective_club_id=tenant.effective_club_id,
|
||||
)
|
||||
where.append(vis_sql)
|
||||
params.extend(vis_params)
|
||||
vis_sql, vis_params = library_content_visibility_sql(
|
||||
alias="e",
|
||||
profile_id=profile_id,
|
||||
role=role,
|
||||
effective_club_id=tenant.effective_club_id,
|
||||
)
|
||||
where.append(vis_sql)
|
||||
params.extend(vis_params)
|
||||
|
||||
if created_by_me:
|
||||
where.append("e.created_by = %s")
|
||||
|
|
|
|||
|
|
@ -380,25 +380,42 @@ def _relocate_asset_file_if_governance_changed(
|
|||
|
||||
|
||||
def _list_active_visibility_clause(is_plat: bool, profile_id: int) -> tuple[str, list[Any]]:
|
||||
"""Sichtbare aktive Einträge: Plattform-Admin alles; sonst official + eigene private + Verein als Mitglied."""
|
||||
sql = """(
|
||||
%s
|
||||
OR lower(trim(ma.visibility)) = 'official'
|
||||
OR (
|
||||
lower(trim(ma.visibility)) = 'private'
|
||||
AND ma.uploaded_by_profile_id = %s
|
||||
)
|
||||
OR (
|
||||
lower(trim(ma.visibility)) = 'club'
|
||||
AND EXISTS (
|
||||
SELECT 1 FROM club_members cm
|
||||
WHERE cm.profile_id = %s
|
||||
AND cm.club_id = ma.club_id
|
||||
AND cm.status = 'active'
|
||||
)
|
||||
)
|
||||
"""Sichtbare aktive Einträge: official; private (eigen oder Plattform-Admin); Verein wie Bibliotheks-SQL."""
|
||||
parts = ["lower(trim(ma.visibility)) = 'official'"]
|
||||
vals: list[Any] = []
|
||||
|
||||
if is_plat:
|
||||
parts.append("lower(trim(ma.visibility)) = 'private'")
|
||||
else:
|
||||
parts.append("(lower(trim(ma.visibility)) = 'private' AND ma.uploaded_by_profile_id = %s)")
|
||||
vals.append(profile_id)
|
||||
|
||||
club_plat = (
|
||||
"(lower(trim(ma.visibility)) = 'club' AND ma.club_id IS NOT NULL AND ("
|
||||
"EXISTS (SELECT 1 FROM club_members cm WHERE cm.profile_id = %s AND cm.club_id = ma.club_id "
|
||||
"AND cm.status = 'active') OR NOT EXISTS (SELECT 1 FROM club_members cm2 "
|
||||
"WHERE cm2.profile_id = %s AND cm2.club_id = ma.club_id)))"
|
||||
)
|
||||
|
||||
if is_plat:
|
||||
parts.append(club_plat)
|
||||
vals.extend([profile_id, profile_id])
|
||||
else:
|
||||
parts.append(
|
||||
"""(
|
||||
lower(trim(ma.visibility)) = 'club'
|
||||
AND EXISTS (
|
||||
SELECT 1 FROM club_members cm
|
||||
WHERE cm.profile_id = %s
|
||||
AND cm.club_id = ma.club_id
|
||||
AND cm.status = 'active'
|
||||
)
|
||||
)"""
|
||||
return sql, [is_plat, profile_id, profile_id]
|
||||
)
|
||||
vals.append(profile_id)
|
||||
|
||||
sql = "(" + " OR ".join(parts) + ")"
|
||||
return sql, vals
|
||||
|
||||
|
||||
def _list_trash_visibility_clause(
|
||||
|
|
|
|||
|
|
@ -350,21 +350,18 @@ def list_training_framework_programs(tenant: TenantContext = Depends(get_tenant_
|
|||
LEFT JOIN focus_areas fa ON fa.id = fp.focus_area_id
|
||||
LEFT JOIN style_directions sd ON sd.id = fp.style_direction_id
|
||||
"""
|
||||
if is_platform_admin(role):
|
||||
cur.execute(base_sel + " ORDER BY fp.updated_at DESC NULLS LAST, fp.title")
|
||||
else:
|
||||
vis_clause, vis_params = library_content_visibility_sql(
|
||||
alias="fp",
|
||||
profile_id=profile_id,
|
||||
role=role,
|
||||
effective_club_id=tenant.effective_club_id,
|
||||
)
|
||||
cur.execute(
|
||||
base_sel
|
||||
+ f""" WHERE ({vis_clause})
|
||||
vis_clause, vis_params = library_content_visibility_sql(
|
||||
alias="fp",
|
||||
profile_id=profile_id,
|
||||
role=role,
|
||||
effective_club_id=tenant.effective_club_id,
|
||||
)
|
||||
cur.execute(
|
||||
base_sel
|
||||
+ f""" WHERE ({vis_clause})
|
||||
ORDER BY fp.updated_at DESC NULLS LAST, fp.title""",
|
||||
vis_params,
|
||||
)
|
||||
vis_params,
|
||||
)
|
||||
return [r2d(r) for r in cur.fetchall()]
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -896,25 +896,14 @@ def list_training_plan_templates(tenant: TenantContext = Depends(get_tenant_cont
|
|||
role = tenant.global_role
|
||||
with get_db() as conn:
|
||||
cur = get_cursor(conn)
|
||||
if is_platform_admin(role):
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT t.*,
|
||||
(SELECT COUNT(*) FROM training_plan_template_sections s WHERE s.template_id = t.id)
|
||||
AS sections_count
|
||||
FROM training_plan_templates t
|
||||
ORDER BY t.updated_at DESC NULLS LAST, t.name
|
||||
"""
|
||||
)
|
||||
else:
|
||||
vis_clause, vis_params = library_content_visibility_sql(
|
||||
alias="t",
|
||||
profile_id=profile_id,
|
||||
role=role,
|
||||
effective_club_id=tenant.effective_club_id,
|
||||
)
|
||||
cur.execute(
|
||||
f"""
|
||||
vis_clause, vis_params = library_content_visibility_sql(
|
||||
alias="t",
|
||||
profile_id=profile_id,
|
||||
role=role,
|
||||
effective_club_id=tenant.effective_club_id,
|
||||
)
|
||||
cur.execute(
|
||||
f"""
|
||||
SELECT t.*,
|
||||
(SELECT COUNT(*) FROM training_plan_template_sections s WHERE s.template_id = t.id)
|
||||
AS sections_count
|
||||
|
|
@ -922,8 +911,8 @@ def list_training_plan_templates(tenant: TenantContext = Depends(get_tenant_cont
|
|||
WHERE ({vis_clause})
|
||||
ORDER BY t.updated_at DESC NULLS LAST, t.name
|
||||
""",
|
||||
vis_params,
|
||||
)
|
||||
vis_params,
|
||||
)
|
||||
return [r2d(r) for r in cur.fetchall()]
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -61,21 +61,36 @@ def library_content_visibility_sql(
|
|||
effective_club_id: Optional[int],
|
||||
) -> tuple[str, List[Any]]:
|
||||
"""
|
||||
WHERE-Baustein für Bibliothekslisten (Übungen, Vorlagen, Rahmenprogramme):
|
||||
official, eigene private, club nur im aktiven Vereinskontext (effective_club_id).
|
||||
Plattform-Admin: keine Einschränkung (TRUE).
|
||||
Ohne effective_club_id: kein club-Zweig (nur official + private).
|
||||
WHERE-Baustein für Bibliothekslisten (Übungen, Vorlagen, Rahmenprogramme, …):
|
||||
|
||||
- official immer
|
||||
- private: eigene (Norm) bzw. alle (Plattform-Admin/Superadmin)
|
||||
- club: nur mit **aktivem** Vereinszugang; Existenz einer nur **inactive** Mitgliedschaft schließt
|
||||
aus — auch bei Plattform-Rolle. Ist man **kein** Mitglied des Vereins, behalten Plattform-Admins
|
||||
Zugriff (Audit).
|
||||
Für Nicht-Plattform: club-Zweig nur mit effective_club_id (Mandantenfilter).
|
||||
"""
|
||||
if is_platform_admin(role):
|
||||
return "TRUE", []
|
||||
plat = is_platform_admin(role)
|
||||
parts: List[str] = [f"{alias}.visibility = 'official'"]
|
||||
params: List[Any] = []
|
||||
|
||||
parts: List[str] = [
|
||||
f"{alias}.visibility = 'official'",
|
||||
f"({alias}.visibility = 'private' AND {alias}.created_by = %s)",
|
||||
]
|
||||
params: List[Any] = [profile_id]
|
||||
if plat:
|
||||
parts.append(f"({alias}.visibility = 'private')")
|
||||
else:
|
||||
parts.append(f"({alias}.visibility = 'private' AND {alias}.created_by = %s)")
|
||||
params.append(profile_id)
|
||||
|
||||
if effective_club_id is not None:
|
||||
club_ok_plat = (
|
||||
f"({alias}.visibility = 'club' AND {alias}.club_id IS NOT NULL AND ("
|
||||
f"EXISTS (SELECT 1 FROM club_members cm WHERE cm.profile_id = %s AND cm.club_id = {alias}.club_id "
|
||||
f"AND cm.status = 'active') OR NOT EXISTS (SELECT 1 FROM club_members cm2 WHERE cm2.profile_id = %s "
|
||||
f"AND cm2.club_id = {alias}.club_id)))"
|
||||
)
|
||||
|
||||
if plat:
|
||||
parts.append(club_ok_plat)
|
||||
params.extend([profile_id, profile_id])
|
||||
elif effective_club_id is not None:
|
||||
parts.append(
|
||||
f"""(
|
||||
{alias}.visibility = 'club'
|
||||
|
|
|
|||
|
|
@ -5,26 +5,27 @@ from fastapi import HTTPException
|
|||
from tenant_context import library_content_visibility_sql, parse_active_club_header, resolve_tenant_context
|
||||
|
||||
|
||||
def test_library_visibility_sql_platform_admin_no_filter():
|
||||
def test_library_visibility_sql_platform_admin_restricts_club_by_membership():
|
||||
sql, params = library_content_visibility_sql(
|
||||
alias="e",
|
||||
profile_id=1,
|
||||
role="admin",
|
||||
effective_club_id=None,
|
||||
)
|
||||
assert sql == "TRUE"
|
||||
assert params == []
|
||||
assert "e.visibility = 'official'" in sql
|
||||
assert "club_members" in sql
|
||||
assert params == [1, 1]
|
||||
|
||||
|
||||
def test_library_visibility_sql_superadmin():
|
||||
def test_library_visibility_sql_superadmin_uses_membership_clause_for_club_visibility():
|
||||
sql, params = library_content_visibility_sql(
|
||||
alias="fp",
|
||||
profile_id=2,
|
||||
role="superadmin",
|
||||
effective_club_id=100,
|
||||
)
|
||||
assert sql == "TRUE"
|
||||
assert params == []
|
||||
assert "club_members" in sql
|
||||
assert params == [2, 2]
|
||||
|
||||
|
||||
def test_library_visibility_sql_trainer_without_active_club_no_shared_club_branch():
|
||||
|
|
|
|||
|
|
@ -562,7 +562,10 @@ export default function AdminUsersPage() {
|
|||
{c.abbreviation ? ` (${c.abbreviation})` : ''} —{' '}
|
||||
{(c.roles || []).join(', ') || '—'}
|
||||
{c.membership_status === 'inactive' ? (
|
||||
<span style={{ color: 'var(--text3)', fontSize: '0.8rem' }}> (inaktiv)</span>
|
||||
<span style={{ color: 'var(--warning, #d4a012)', fontSize: '0.8rem', fontWeight: 600 }}>
|
||||
{' '}
|
||||
(Vereinszugang deaktiviert)
|
||||
</span>
|
||||
) : null}{' '}
|
||||
<button
|
||||
type="button"
|
||||
|
|
@ -588,6 +591,33 @@ export default function AdminUsersPage() {
|
|||
>
|
||||
bearbeiten
|
||||
</button>
|
||||
{isSuperadminViewer && c.membership_status === 'inactive' ? (
|
||||
<button
|
||||
type="button"
|
||||
style={{
|
||||
marginLeft: '0.35rem',
|
||||
fontSize: '0.75rem',
|
||||
padding: '0.12rem 0.45rem',
|
||||
borderRadius: '6px',
|
||||
border: '1px solid var(--accent, #0366d6)',
|
||||
background: 'var(--surface)',
|
||||
cursor: 'pointer',
|
||||
}}
|
||||
onClick={async () => {
|
||||
try {
|
||||
await api.updateClubMember(c.id, row.id, {
|
||||
roles: [...(c.roles || [])],
|
||||
status: 'active',
|
||||
})
|
||||
await loadPlatform()
|
||||
} catch (e) {
|
||||
alert(e.message || String(e))
|
||||
}
|
||||
}}
|
||||
>
|
||||
Zugang aktivieren
|
||||
</button>
|
||||
) : null}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
|
|
|||
|
|
@ -196,6 +196,28 @@ function ClubsPage() {
|
|||
}
|
||||
}
|
||||
|
||||
const toggleMembersAdminClubAccess = async (m, activate) => {
|
||||
if (!membersAdminClubId || !canManageClub(membersAdminClubId)) return
|
||||
const st = activate ? 'active' : 'inactive'
|
||||
if (
|
||||
!activate &&
|
||||
!confirm(
|
||||
`Vereinszugang für „${m.name || m.email || '#' + m.profile_id}“ hier deaktivieren? Login bleibt möglich, Vereinsinhalte nicht — auch bei Super-Admins.`,
|
||||
)
|
||||
) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
await api.updateClubMember(membersAdminClubId, m.profile_id, {
|
||||
roles: [...(m.roles || [])],
|
||||
status: st,
|
||||
})
|
||||
await reloadMembersAdmin()
|
||||
} catch (err) {
|
||||
alert(err.message || String(err))
|
||||
}
|
||||
}
|
||||
|
||||
const handleEdit = (item, type) => {
|
||||
setEditing(item)
|
||||
setModalType(type)
|
||||
|
|
@ -667,17 +689,41 @@ function ClubsPage() {
|
|||
|
||||
<div className="card">
|
||||
<h3 style={{ marginTop: 0 }}>Mitglieder</h3>
|
||||
{isPlatformAdmin ? (
|
||||
<p
|
||||
className="muted"
|
||||
style={{ fontSize: '0.85rem', marginTop: '-0.35rem', marginBottom: '0.85rem', lineHeight: 1.45 }}
|
||||
>
|
||||
Liste enthält <strong style={{ color: 'var(--text1)' }}>aktive und deaktivierte</strong> Vereinszugänge.
|
||||
Deaktiviert gilt pro Verein (ohne Kontosperre) — auch für Super-Admins ohne aktive Mitgliedschaft in diesem
|
||||
Verein kein Zugriff auf dessen Vereinsinhalte. Wiederherstellen über die Schaltflächen oder Mitglied
|
||||
bearbeiten.
|
||||
</p>
|
||||
) : (
|
||||
<p
|
||||
className="muted"
|
||||
style={{ fontSize: '0.85rem', marginTop: '-0.35rem', marginBottom: '0.85rem', lineHeight: 1.45 }}
|
||||
>
|
||||
Deaktivierte Vereinszugänge sind hervorgehoben —{' '}
|
||||
<strong>Anmeldung</strong> bleibt möglich, <strong>Vereinsinhalte</strong> dieser Zuordnung nicht.
|
||||
</p>
|
||||
)}
|
||||
{clubMembersAdmin.length === 0 ? (
|
||||
<p style={{ color: 'var(--text2)' }}>Noch keine Mitglieder erfasst.</p>
|
||||
) : (
|
||||
<div style={{ display: 'grid', gap: '0.65rem' }}>
|
||||
{clubMembersAdmin.map((m) => (
|
||||
{clubMembersAdmin.map((m) => {
|
||||
const memStatus = (m.status || 'active').toLowerCase()
|
||||
const inactiveRow = memStatus === 'inactive'
|
||||
const portalLabel = (m.portal_role || '').trim()
|
||||
return (
|
||||
<div
|
||||
key={m.membership_id}
|
||||
style={{
|
||||
padding: '0.65rem',
|
||||
borderRadius: '8px',
|
||||
background: 'var(--surface2)',
|
||||
background: inactiveRow ? 'color-mix(in srgb, var(--warning, #884400) 12%, var(--surface2))' : 'var(--surface2)',
|
||||
border: inactiveRow ? '1px solid color-mix(in srgb, var(--warning, #d4a012) 40%, transparent)' : undefined,
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
flexWrap: 'wrap',
|
||||
|
|
@ -687,21 +733,49 @@ function ClubsPage() {
|
|||
<div>
|
||||
<strong>{m.name || m.email}</strong>
|
||||
<div style={{ fontSize: '0.8rem', color: 'var(--text2)' }}>
|
||||
{m.email} · #{m.profile_id} · {m.status}
|
||||
{m.email} · #{m.profile_id}
|
||||
{' · '}
|
||||
<span style={{ fontWeight: 600 }}>
|
||||
Vereinszugang:{' '}
|
||||
<span style={{ color: inactiveRow ? 'var(--warning, #d4a012)' : 'inherit' }}>
|
||||
{inactiveRow ? 'deaktiviert' : 'aktiv'}
|
||||
</span>
|
||||
</span>
|
||||
{portalLabel ? (
|
||||
<span style={{ color: 'var(--text3)', marginLeft: '0.35rem' }}> · Portal: {portalLabel}</span>
|
||||
) : null}
|
||||
</div>
|
||||
<div style={{ fontSize: '0.8rem', marginTop: '0.25rem' }}>
|
||||
Rollen: {(m.roles || []).join(', ') || '—'}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-secondary"
|
||||
onClick={() => setEditMemberModal(m)}
|
||||
>
|
||||
Bearbeiten
|
||||
</button>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '0.35rem', alignItems: 'stretch' }}>
|
||||
<button type="button" className="btn btn-secondary" onClick={() => setEditMemberModal(m)}>
|
||||
Mitglied bearbeiten
|
||||
</button>
|
||||
{m.profile_id !== user?.id ? (
|
||||
inactiveRow ? (
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-primary"
|
||||
onClick={() => toggleMembersAdminClubAccess(m, true)}
|
||||
>
|
||||
Vereinszugang aktivieren
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-secondary"
|
||||
onClick={() => toggleMembersAdminClubAccess(m, false)}
|
||||
>
|
||||
Vereinszugang deaktivieren
|
||||
</button>
|
||||
)
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user