import { useCallback, useEffect, useMemo, useState } from 'react'
import { Navigate } from 'react-router-dom'
import { useAuth } from '../context/AuthContext'
import api from '../utils/api'
import AdminPageNav from '../components/AdminPageNav'
const TABS = [
{ id: 'portal', label: 'Portal-Rollen' },
{ id: 'club_roles', label: 'Vereinsrollen' },
{ id: 'bypass', label: 'Kontingent-Bypass' },
{ id: 'quotas', label: 'Vereins-Kontingente' },
]
const PORTAL_ROLE_LABEL = {
user: 'Nutzer',
trainer: 'Portal-Trainer',
admin: 'Portal-Admin',
superadmin: 'Superadmin',
}
const CLUB_ROLE_LABEL = {
club_admin: 'Vereinsadmin',
trainer: 'Trainer',
division_lead: 'Spartenleitung',
content_editor: 'Inhalte',
}
function limitInputValue(v) {
if (v === null || v === undefined) return ''
return String(v)
}
function parseLimitInput(raw, limitType) {
const s = String(raw ?? '').trim()
if (s === '' || s === '∞') return null
const n = parseInt(s, 10)
if (Number.isNaN(n)) return limitType === 'boolean' ? 0 : null
return n
}
function formatLimitHint(feature) {
if (feature.limit_type === 'boolean') return '0 = aus, 1 = an'
if (feature.reset_period === 'monthly') return 'pro Monat'
if (feature.reset_period === 'never') return 'Bestand'
return ''
}
/**
* Superadmin: Rollen → Fähigkeiten (Capabilities) und Vereins-Kontingente konfigurieren.
*/
export default function AdminRightsPage() {
const { user } = useAuth()
const isSuperadmin = user?.role === 'superadmin'
const [tab, setTab] = useState('portal')
const [loading, setLoading] = useState(true)
const [error, setError] = useState('')
const [busy, setBusy] = useState(false)
const [plansData, setPlansData] = useState({ plans: [], features: [], limits: {} })
const [limitDraft, setLimitDraft] = useState({})
const [clubSubs, setClubSubs] = useState([])
const [capMatrix, setCapMatrix] = useState(null)
const [bypassData, setBypassData] = useState(null)
const [newClubGrant, setNewClubGrant] = useState({ role_code: 'trainer', capability_id: '' })
const [newBypassPortal, setNewBypassPortal] = useState({ portal_role: 'helpdesk', feature_id: '' })
const [newBypassProfile, setNewBypassProfile] = useState({
profile_id: '',
feature_id: '',
reason: '',
})
const loadPlans = useCallback(async () => {
const data = await api.getAdminRightsClubPlansMatrix()
setPlansData(data)
const draft = {}
for (const plan of data.plans || []) {
draft[plan.id] = {}
for (const f of data.features || []) {
draft[plan.id][f.id] = limitInputValue(data.limits?.[plan.id]?.[f.id])
}
}
setLimitDraft(draft)
}, [])
const loadClubs = useCallback(async () => {
const rows = await api.listAdminRightsClubSubscriptions()
setClubSubs(Array.isArray(rows) ? rows : [])
}, [])
const loadCapMatrix = useCallback(async () => {
setCapMatrix(await api.getAdminRightsCapabilityMatrix())
}, [])
const loadBypass = useCallback(async () => {
setBypassData(await api.listAdminRightsQuotaBypass())
}, [])
const reloadTab = useCallback(async () => {
setError('')
if (tab === 'portal' || tab === 'club_roles') await loadCapMatrix()
else if (tab === 'bypass') await loadBypass()
else if (tab === 'quotas') await Promise.all([loadPlans(), loadClubs()])
}, [tab, loadPlans, loadClubs, loadCapMatrix, loadBypass])
useEffect(() => {
if (!isSuperadmin) return
let cancelled = false
;(async () => {
setLoading(true)
try {
await reloadTab()
} catch (e) {
if (!cancelled) setError(e.message || String(e))
} finally {
if (!cancelled) setLoading(false)
}
})()
return () => {
cancelled = true
}
}, [isSuperadmin, reloadTab])
const portalCapabilities = useMemo(() => {
if (!capMatrix?.capabilities) return []
return capMatrix.capabilities.filter(
(c) => c.domain === 'platform' || String(c.id).startsWith('platform.'),
)
}, [capMatrix])
const clubScopedCapabilities = useMemo(() => {
if (!capMatrix?.capabilities) return []
return capMatrix.capabilities.filter(
(c) =>
c.domain !== 'platform' &&
c.domain !== 'quota_bypass' &&
c.domain !== 'account' &&
c.domain !== 'club',
)
}, [capMatrix])
const portalGrantSet = useMemo(() => {
const s = new Set()
for (const g of capMatrix?.portal_grants || []) {
s.add(`${g.portal_role}::${g.capability_id}`)
}
return s
}, [capMatrix])
const clubGrantSet = useMemo(() => {
const s = new Set()
for (const g of capMatrix?.club_role_grants || []) {
s.add(`${g.role_code}::${g.capability_id}`)
}
return s
}, [capMatrix])
if (!isSuperadmin) return
Rollen → Fähigkeiten (Capabilities): Wer darf welche Funktion nutzen?
Kontingente: Wie viel darf ein Verein verbrauchen (an Fähigkeiten gekoppelt
über linked_feature_id)?
Vereinspläne bündeln nur Kontingent-Werte — sie ersetzen keine Berechtigungen.
{error}
) : null} {loading ? (Laden…
) : null} {!loading && tab === 'portal' && capMatrix ? (
Plattform-Funktionen (domain=platform). Jede Funktion im Produkt soll sich
hier anmelden und bei Anzeige und Ausführung prüfen.
| Fähigkeit | {(capMatrix.portal_roles || []).map((r) => ({PORTAL_ROLE_LABEL[r] || r} | ))}
|---|---|
{cap.id}
{cap.name}
{cap.linked_feature_id ? (
Kontingent: {cap.linked_feature_id}
) : null}
|
{(capMatrix.portal_roles || []).map((role) => {
const on = portalGrantSet.has(`${role}::${cap.id}`)
return (
togglePortalGrant(role, cap.id, on)} /> | ) })}
Vereinsrollen → Fähigkeiten. Ohne Grant-Eintrag gilt die Fähigkeit für alle aktiven Vereinsmitglieder; gesetzte Grants schränken auf die angehakten Rollen ein.
| Fähigkeit | {(capMatrix.club_roles || []).map((r) => ({CLUB_ROLE_LABEL[r] || r} | ))}
|---|---|
{cap.id}
{cap.name}
{cap.linked_feature_id ? (
Kontingent: {cap.linked_feature_id}
) : null}
|
{(capMatrix.club_roles || []).map((role) => {
const on = clubGrantSet.has(`${role}::${cap.id}`)
return (
toggleClubGrant(role, cap.id, on)} /> | ) })}
Capability platform.club_quota.bypass — umgeht Vereins-Kontingente (z. B.
Superadmin, Helpdesk). Kein separates Rechtemodell.
Kontingent-Bündel pro Plan. Leeres Feld = unbegrenzt. Ersetzt keine Rollen-Grants.
| Feature | {(plansData.plans || []).map((p) => (
{p.name}
{p.id}
|
))}
|---|---|
|
{f.name}
{f.id} · {formatLimitHint(f)}
|
{(plansData.plans || []).map((p) => (
setLimitDraft((prev) => ({ ...prev, [p.id]: { ...prev[p.id], [f.id]: e.target.value }, })) } /> | ))}
| Verein | Plan | Status |
|---|---|---|
| {row.club_name || `Verein #${row.club_id}`} |
Keine Vereine.
) : null}