Enhance Exercise Progression Graph Panel with Club Management Features
All checks were successful
Deploy Development / deploy (push) Successful in 43s
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 43s
Test Suite / playwright-tests (push) Successful in 1m13s
All checks were successful
Deploy Development / deploy (push) Successful in 43s
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 43s
Test Suite / playwright-tests (push) Successful in 1m13s
- Added functionality to select and manage clubs within the Exercise Progression Graph Panel, allowing users to assign clubs to exercises. - Introduced state management for club selection and manual entry, improving user experience for platform admins. - Updated visibility handling to ensure proper governance and club association during exercise promotion. - Enhanced error handling to provide clearer feedback when no club is selected, ensuring users are guided to make necessary selections.
This commit is contained in:
parent
4b9374765b
commit
87d9fa9b65
|
|
@ -13,7 +13,7 @@ import { Link, useLocation } from 'react-router-dom'
|
||||||
import api from '../utils/api'
|
import api from '../utils/api'
|
||||||
import SkillProfilePanel from './skills/SkillProfilePanel'
|
import SkillProfilePanel from './skills/SkillProfilePanel'
|
||||||
import { useAuth } from '../context/AuthContext'
|
import { useAuth } from '../context/AuthContext'
|
||||||
import { activeClubMemberships, getTenantClubDependencyKey } from '../utils/activeClub'
|
import { activeClubMemberships, getDefaultClubIdForGovernanceForms, getTenantClubDependencyKey } from '../utils/activeClub'
|
||||||
import ProgressionGraphEditor from './ProgressionGraphEditor'
|
import ProgressionGraphEditor from './ProgressionGraphEditor'
|
||||||
import ProgressionGraphListCard from './ProgressionGraphListCard'
|
import ProgressionGraphListCard from './ProgressionGraphListCard'
|
||||||
import { EXERCISE_VISIBILITY_FIELD_LABEL } from '../constants/exerciseGovernanceLabels'
|
import { EXERCISE_VISIBILITY_FIELD_LABEL } from '../constants/exerciseGovernanceLabels'
|
||||||
|
|
@ -41,6 +41,8 @@ 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 tenantClubDepKey = useMemo(() => getTenantClubDependencyKey(user), [user])
|
const tenantClubDepKey = useMemo(() => getTenantClubDependencyKey(user), [user])
|
||||||
|
|
||||||
const filteredGraphVisOptions = useMemo(
|
const filteredGraphVisOptions = useMemo(
|
||||||
|
|
@ -61,6 +63,8 @@ function ExerciseProgressionGraphPanel(
|
||||||
const [metaName, setMetaName] = useState('')
|
const [metaName, setMetaName] = useState('')
|
||||||
const [metaDescription, setMetaDescription] = useState('')
|
const [metaDescription, setMetaDescription] = useState('')
|
||||||
const [metaVisibility, setMetaVisibility] = useState('private')
|
const [metaVisibility, setMetaVisibility] = useState('private')
|
||||||
|
const [metaClubSelect, setMetaClubSelect] = useState('')
|
||||||
|
const [metaClubManual, setMetaClubManual] = useState('')
|
||||||
|
|
||||||
const [filterAnchorOnly, setFilterAnchorOnly] = useState(!!anchorExerciseId)
|
const [filterAnchorOnly, setFilterAnchorOnly] = useState(!!anchorExerciseId)
|
||||||
const [editingEdgeNotes, setEditingEdgeNotes] = useState(null)
|
const [editingEdgeNotes, setEditingEdgeNotes] = useState(null)
|
||||||
|
|
@ -157,6 +161,8 @@ function ExerciseProgressionGraphPanel(
|
||||||
setMetaName('')
|
setMetaName('')
|
||||||
setMetaDescription('')
|
setMetaDescription('')
|
||||||
setMetaVisibility('private')
|
setMetaVisibility('private')
|
||||||
|
setMetaClubSelect('')
|
||||||
|
setMetaClubManual('')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
const g = graphs.find((x) => x.id === selectedGraphId)
|
const g = graphs.find((x) => x.id === selectedGraphId)
|
||||||
|
|
@ -164,6 +170,13 @@ function ExerciseProgressionGraphPanel(
|
||||||
setMetaName(g.name || '')
|
setMetaName(g.name || '')
|
||||||
setMetaDescription(g.description || '')
|
setMetaDescription(g.description || '')
|
||||||
setMetaVisibility(g.visibility || 'private')
|
setMetaVisibility(g.visibility || 'private')
|
||||||
|
if (g.club_id != null) {
|
||||||
|
setMetaClubSelect(String(g.club_id))
|
||||||
|
} else {
|
||||||
|
const fallback = getDefaultClubIdForGovernanceForms(user)
|
||||||
|
setMetaClubSelect(fallback != null ? String(fallback) : '')
|
||||||
|
}
|
||||||
|
setMetaClubManual('')
|
||||||
}
|
}
|
||||||
let cancelled = false
|
let cancelled = false
|
||||||
;(async () => {
|
;(async () => {
|
||||||
|
|
@ -176,7 +189,20 @@ function ExerciseProgressionGraphPanel(
|
||||||
return () => {
|
return () => {
|
||||||
cancelled = true
|
cancelled = true
|
||||||
}
|
}
|
||||||
}, [selectedGraphId, graphs, refreshEdges])
|
}, [selectedGraphId, graphs, refreshEdges, user])
|
||||||
|
|
||||||
|
const resolveGovernanceClubId = useCallback(() => {
|
||||||
|
const g = graphs.find((x) => x.id === selectedGraphId)
|
||||||
|
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()
|
||||||
|
if (sel && /^\d+$/.test(sel)) return Number(sel)
|
||||||
|
|
||||||
|
return getDefaultClubIdForGovernanceForms(user)
|
||||||
|
}, [graphs, selectedGraphId, metaClubManual, metaClubSelect, user])
|
||||||
|
|
||||||
const filteredEdges = useMemo(() => {
|
const filteredEdges = useMemo(() => {
|
||||||
if (!filterAnchorOnly || anchorExerciseId == null) return edges
|
if (!filterAnchorOnly || anchorExerciseId == null) return edges
|
||||||
|
|
@ -226,13 +252,7 @@ function ExerciseProgressionGraphPanel(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const resolvePromoteClubId = () => {
|
const resolvePromoteClubId = resolveGovernanceClubId
|
||||||
const g = graphs.find((x) => x.id === selectedGraphId)
|
|
||||||
if (g?.club_id != null) return Number(g.club_id)
|
|
||||||
const memberships = activeClubMemberships(user?.clubs)
|
|
||||||
const active = memberships.find((c) => c.is_active) || memberships[0]
|
|
||||||
return active?.club_id != null ? Number(active.club_id) : null
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleSaveMeta = async () => {
|
const handleSaveMeta = async () => {
|
||||||
if (!selectedGraphId) return
|
if (!selectedGraphId) return
|
||||||
|
|
@ -268,7 +288,9 @@ function ExerciseProgressionGraphPanel(
|
||||||
if (promote) {
|
if (promote) {
|
||||||
const clubId = resolvePromoteClubId()
|
const clubId = resolvePromoteClubId()
|
||||||
if (!clubId) {
|
if (!clubId) {
|
||||||
alert('Kein aktiver Verein — Übungen können nicht auf Verein promoted werden.')
|
throw new Error(
|
||||||
|
'Kein Verein gewählt — bitte unter „Verein zuordnen“ einen Verein auswählen oder den Vereins-Umschalter nutzen.',
|
||||||
|
)
|
||||||
} else {
|
} else {
|
||||||
const ids = privateExercises.map((ex) => ex.id).filter((id) => id != null)
|
const ids = privateExercises.map((ex) => ex.id).filter((id) => id != null)
|
||||||
const res = await api.bulkPatchExercisesMetadata({
|
const res = await api.bulkPatchExercisesMetadata({
|
||||||
|
|
@ -286,6 +308,11 @@ function ExerciseProgressionGraphPanel(
|
||||||
}
|
}
|
||||||
|
|
||||||
const promoteClubId = nextVis === 'club' ? resolvePromoteClubId() : null
|
const promoteClubId = nextVis === 'club' ? resolvePromoteClubId() : null
|
||||||
|
if (nextVis === 'club' && !promoteClubId) {
|
||||||
|
throw new Error(
|
||||||
|
'Vereins-Sichtbarkeit: Bitte einen Verein unter „Verein zuordnen“ wählen oder den Vereins-Umschalter setzen.',
|
||||||
|
)
|
||||||
|
}
|
||||||
await api.updateExerciseProgressionGraph(selectedGraphId, {
|
await api.updateExerciseProgressionGraph(selectedGraphId, {
|
||||||
name,
|
name,
|
||||||
description: metaDescription.trim() || null,
|
description: metaDescription.trim() || null,
|
||||||
|
|
@ -541,7 +568,14 @@ function ExerciseProgressionGraphPanel(
|
||||||
<select
|
<select
|
||||||
className="form-input"
|
className="form-input"
|
||||||
value={metaVisibility}
|
value={metaVisibility}
|
||||||
onChange={(e) => setMetaVisibility(e.target.value)}
|
onChange={(e) => {
|
||||||
|
const v = e.target.value
|
||||||
|
setMetaVisibility(v)
|
||||||
|
if (v === 'club' && !metaClubSelect) {
|
||||||
|
const fb = getDefaultClubIdForGovernanceForms(user)
|
||||||
|
if (fb != null) setMetaClubSelect(String(fb))
|
||||||
|
}
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{filteredGraphVisOptions.map((o) => (
|
{filteredGraphVisOptions.map((o) => (
|
||||||
<option key={o.value} value={o.value}>
|
<option key={o.value} value={o.value}>
|
||||||
|
|
@ -550,6 +584,38 @@ function ExerciseProgressionGraphPanel(
|
||||||
))}
|
))}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
{metaVisibility === 'club' ? (
|
||||||
|
<div className="form-row">
|
||||||
|
<label className="form-label">Verein zuordnen</label>
|
||||||
|
<select
|
||||||
|
className="form-input"
|
||||||
|
value={metaClubSelect}
|
||||||
|
onChange={(e) => setMetaClubSelect(e.target.value)}
|
||||||
|
>
|
||||||
|
<option value="">Aktiver Verein (Vereins-Umschalter / Header)</option>
|
||||||
|
{memberClubs.map((c) => (
|
||||||
|
<option key={c.id} value={String(c.id)}>
|
||||||
|
{c.name || `Verein #${c.id}`}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</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>
|
||||||
|
) : null}
|
||||||
<div style={{ display: 'flex', gap: '8px', flexWrap: 'wrap' }}>
|
<div style={{ display: 'flex', gap: '8px', flexWrap: 'wrap' }}>
|
||||||
<button type="button" className="btn btn-secondary" disabled={busy} onClick={handleSaveMeta}>
|
<button type="button" className="btn btn-secondary" disabled={busy} onClick={handleSaveMeta}>
|
||||||
Metadaten speichern
|
Metadaten speichern
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user