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 SkillProfilePanel from './skills/SkillProfilePanel'
|
||||
import { useAuth } from '../context/AuthContext'
|
||||
import { activeClubMemberships, getTenantClubDependencyKey } from '../utils/activeClub'
|
||||
import { activeClubMemberships, getDefaultClubIdForGovernanceForms, getTenantClubDependencyKey } from '../utils/activeClub'
|
||||
import ProgressionGraphEditor from './ProgressionGraphEditor'
|
||||
import ProgressionGraphListCard from './ProgressionGraphListCard'
|
||||
import { EXERCISE_VISIBILITY_FIELD_LABEL } from '../constants/exerciseGovernanceLabels'
|
||||
|
|
@ -41,6 +41,8 @@ function ExerciseProgressionGraphPanel(
|
|||
const { user } = useAuth()
|
||||
const location = useLocation()
|
||||
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 filteredGraphVisOptions = useMemo(
|
||||
|
|
@ -61,6 +63,8 @@ function ExerciseProgressionGraphPanel(
|
|||
const [metaName, setMetaName] = useState('')
|
||||
const [metaDescription, setMetaDescription] = useState('')
|
||||
const [metaVisibility, setMetaVisibility] = useState('private')
|
||||
const [metaClubSelect, setMetaClubSelect] = useState('')
|
||||
const [metaClubManual, setMetaClubManual] = useState('')
|
||||
|
||||
const [filterAnchorOnly, setFilterAnchorOnly] = useState(!!anchorExerciseId)
|
||||
const [editingEdgeNotes, setEditingEdgeNotes] = useState(null)
|
||||
|
|
@ -157,6 +161,8 @@ function ExerciseProgressionGraphPanel(
|
|||
setMetaName('')
|
||||
setMetaDescription('')
|
||||
setMetaVisibility('private')
|
||||
setMetaClubSelect('')
|
||||
setMetaClubManual('')
|
||||
return
|
||||
}
|
||||
const g = graphs.find((x) => x.id === selectedGraphId)
|
||||
|
|
@ -164,6 +170,13 @@ function ExerciseProgressionGraphPanel(
|
|||
setMetaName(g.name || '')
|
||||
setMetaDescription(g.description || '')
|
||||
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
|
||||
;(async () => {
|
||||
|
|
@ -176,7 +189,20 @@ function ExerciseProgressionGraphPanel(
|
|||
return () => {
|
||||
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(() => {
|
||||
if (!filterAnchorOnly || anchorExerciseId == null) return edges
|
||||
|
|
@ -226,13 +252,7 @@ function ExerciseProgressionGraphPanel(
|
|||
}
|
||||
}
|
||||
|
||||
const resolvePromoteClubId = () => {
|
||||
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 resolvePromoteClubId = resolveGovernanceClubId
|
||||
|
||||
const handleSaveMeta = async () => {
|
||||
if (!selectedGraphId) return
|
||||
|
|
@ -268,7 +288,9 @@ function ExerciseProgressionGraphPanel(
|
|||
if (promote) {
|
||||
const clubId = resolvePromoteClubId()
|
||||
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 {
|
||||
const ids = privateExercises.map((ex) => ex.id).filter((id) => id != null)
|
||||
const res = await api.bulkPatchExercisesMetadata({
|
||||
|
|
@ -286,6 +308,11 @@ function ExerciseProgressionGraphPanel(
|
|||
}
|
||||
|
||||
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, {
|
||||
name,
|
||||
description: metaDescription.trim() || null,
|
||||
|
|
@ -541,7 +568,14 @@ function ExerciseProgressionGraphPanel(
|
|||
<select
|
||||
className="form-input"
|
||||
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) => (
|
||||
<option key={o.value} value={o.value}>
|
||||
|
|
@ -550,6 +584,38 @@ function ExerciseProgressionGraphPanel(
|
|||
))}
|
||||
</select>
|
||||
</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' }}>
|
||||
<button type="button" className="btn btn-secondary" disabled={busy} onClick={handleSaveMeta}>
|
||||
Metadaten speichern
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user