Enhance Exercise Progression Graph Panel with Governance Club Management
All checks were successful
Deploy Development / deploy (push) Successful in 42s
Test Suite / pytest-backend (push) Successful in 44s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 14s
Test Suite / k6 /health Baseline (push) Successful in 38s
Test Suite / playwright-tests (push) Successful in 1m12s

- Introduced functionality to manage governance clubs for superadmins, allowing for better club selection and organization within the Exercise Progression Graph Panel.
- Implemented state management for clubs, including sorting and filtering options, to improve user experience and accessibility.
- Enhanced the useEffect hook to fetch governance clubs dynamically, ensuring up-to-date club information is available for selection.
- Updated the club selection dropdown to categorize clubs into "My Clubs" and "Other Clubs," improving clarity and usability for users.
This commit is contained in:
Lars 2026-06-14 07:19:35 +02:00
parent 87d9fa9b65
commit 1c67a50ce4

View File

@ -41,9 +41,9 @@ function ExerciseProgressionGraphPanel(
const { user } = useAuth() const { user } = useAuth()
const location = useLocation() const location = useLocation()
const isSuperadmin = user?.role === 'superadmin' const isSuperadmin = user?.role === 'superadmin'
const isPlatformAdmin = isSuperadmin || user?.role === 'admin'
const memberClubs = useMemo(() => activeClubMemberships(user?.clubs), [user?.clubs]) const memberClubs = useMemo(() => activeClubMemberships(user?.clubs), [user?.clubs])
const tenantClubDepKey = useMemo(() => getTenantClubDependencyKey(user), [user]) const tenantClubDepKey = useMemo(() => getTenantClubDependencyKey(user), [user])
const [clubsForGovernanceForms, setClubsForGovernanceForms] = useState([])
const filteredGraphVisOptions = useMemo( const filteredGraphVisOptions = useMemo(
() => VIS_OPTIONS.filter((o) => o.value !== 'official' || isSuperadmin), () => VIS_OPTIONS.filter((o) => o.value !== 'official' || isSuperadmin),
@ -64,7 +64,36 @@ function ExerciseProgressionGraphPanel(
const [metaDescription, setMetaDescription] = useState('') const [metaDescription, setMetaDescription] = useState('')
const [metaVisibility, setMetaVisibility] = useState('private') const [metaVisibility, setMetaVisibility] = useState('private')
const [metaClubSelect, setMetaClubSelect] = useState('') const [metaClubSelect, setMetaClubSelect] = useState('')
const [metaClubManual, setMetaClubManual] = useState('')
const memberClubIdSet = useMemo(
() => new Set(memberClubs.map((c) => Number(c.id))),
[memberClubs],
)
const sortedMemberClubs = useMemo(
() =>
[...memberClubs].sort((a, b) =>
String(a.name || '').localeCompare(String(b.name || ''), 'de'),
),
[memberClubs],
)
const sortedOtherGovernanceClubs = useMemo(() => {
if (!isSuperadmin || clubsForGovernanceForms.length === 0) return []
return clubsForGovernanceForms
.filter((c) => !memberClubIdSet.has(Number(c.id)))
.sort((a, b) => String(a.name || '').localeCompare(String(b.name || ''), 'de'))
}, [isSuperadmin, clubsForGovernanceForms, memberClubIdSet])
const showGovernanceClubOptgroups =
isSuperadmin && sortedMemberClubs.length > 0 && sortedOtherGovernanceClubs.length > 0
const governanceClubSelectOptions = useMemo(() => {
if (isSuperadmin && clubsForGovernanceForms.length > 0) {
return [...sortedMemberClubs, ...sortedOtherGovernanceClubs]
}
return sortedMemberClubs
}, [isSuperadmin, clubsForGovernanceForms.length, sortedMemberClubs, sortedOtherGovernanceClubs])
const [filterAnchorOnly, setFilterAnchorOnly] = useState(!!anchorExerciseId) const [filterAnchorOnly, setFilterAnchorOnly] = useState(!!anchorExerciseId)
const [editingEdgeNotes, setEditingEdgeNotes] = useState(null) const [editingEdgeNotes, setEditingEdgeNotes] = useState(null)
@ -129,6 +158,25 @@ function ExerciseProgressionGraphPanel(
} }
}, [refreshGraphs, tenantClubDepKey]) }, [refreshGraphs, tenantClubDepKey])
useEffect(() => {
if (!isSuperadmin) {
setClubsForGovernanceForms([])
return undefined
}
let cancelled = false
;(async () => {
try {
const list = await api.listClubs()
if (!cancelled) setClubsForGovernanceForms(Array.isArray(list) ? list : [])
} catch {
if (!cancelled) setClubsForGovernanceForms([])
}
})()
return () => {
cancelled = true
}
}, [isSuperadmin, tenantClubDepKey])
useEffect(() => { useEffect(() => {
if (!selectedGraphId) { if (!selectedGraphId) {
setSkillProfileData(null) setSkillProfileData(null)
@ -162,7 +210,6 @@ function ExerciseProgressionGraphPanel(
setMetaDescription('') setMetaDescription('')
setMetaVisibility('private') setMetaVisibility('private')
setMetaClubSelect('') setMetaClubSelect('')
setMetaClubManual('')
return return
} }
const g = graphs.find((x) => x.id === selectedGraphId) const g = graphs.find((x) => x.id === selectedGraphId)
@ -176,7 +223,6 @@ function ExerciseProgressionGraphPanel(
const fallback = getDefaultClubIdForGovernanceForms(user) const fallback = getDefaultClubIdForGovernanceForms(user)
setMetaClubSelect(fallback != null ? String(fallback) : '') setMetaClubSelect(fallback != null ? String(fallback) : '')
} }
setMetaClubManual('')
} }
let cancelled = false let cancelled = false
;(async () => { ;(async () => {
@ -195,14 +241,11 @@ function ExerciseProgressionGraphPanel(
const g = graphs.find((x) => x.id === selectedGraphId) const g = graphs.find((x) => x.id === selectedGraphId)
if (g?.club_id != null) return Number(g.club_id) if (g?.club_id != null) return Number(g.club_id)
const manual = String(metaClubManual || '').trim()
if (manual && /^\d+$/.test(manual)) return Number(manual)
const sel = String(metaClubSelect || '').trim() const sel = String(metaClubSelect || '').trim()
if (sel && /^\d+$/.test(sel)) return Number(sel) if (sel && /^\d+$/.test(sel)) return Number(sel)
return getDefaultClubIdForGovernanceForms(user) return getDefaultClubIdForGovernanceForms(user)
}, [graphs, selectedGraphId, metaClubManual, metaClubSelect, user]) }, [graphs, selectedGraphId, metaClubSelect, user])
const filteredEdges = useMemo(() => { const filteredEdges = useMemo(() => {
if (!filterAnchorOnly || anchorExerciseId == null) return edges if (!filterAnchorOnly || anchorExerciseId == null) return edges
@ -593,27 +636,31 @@ function ExerciseProgressionGraphPanel(
onChange={(e) => setMetaClubSelect(e.target.value)} onChange={(e) => setMetaClubSelect(e.target.value)}
> >
<option value="">Aktiver Verein (Vereins-Umschalter / Header)</option> <option value="">Aktiver Verein (Vereins-Umschalter / Header)</option>
{memberClubs.map((c) => ( {showGovernanceClubOptgroups ? (
<option key={c.id} value={String(c.id)}> <>
{c.name || `Verein #${c.id}`} <optgroup label="Meine Vereine">
</option> {sortedMemberClubs.map((c) => (
))} <option key={c.id} value={String(c.id)}>
{c.name || `Verein #${c.id}`}
</option>
))}
</optgroup>
<optgroup label="Weitere Vereine">
{sortedOtherGovernanceClubs.map((c) => (
<option key={c.id} value={String(c.id)}>
{c.name || `Verein #${c.id}`}
</option>
))}
</optgroup>
</>
) : (
governanceClubSelectOptions.map((c) => (
<option key={c.id} value={String(c.id)}>
{c.name || `Verein #${c.id}`}
</option>
))
)}
</select> </select>
{isPlatformAdmin ? (
<>
<label className="form-label" style={{ marginTop: '10px' }}>
Oder Vereins-ID (Plattform-Admin)
</label>
<input
type="number"
min={1}
className="form-input"
placeholder="Leer = wie Dropdown / aktiver Verein"
value={metaClubManual}
onChange={(e) => setMetaClubManual(e.target.value)}
/>
</>
) : null}
</div> </div>
) : null} ) : null}
<div style={{ display: 'flex', gap: '8px', flexWrap: 'wrap' }}> <div style={{ display: 'flex', gap: '8px', flexWrap: 'wrap' }}>