From 800189ff8f614135e95ac8b11153a5375c9ccc6e Mon Sep 17 00:00:00 2001 From: Lars Date: Wed, 10 Jun 2026 07:55:51 +0200 Subject: [PATCH] Enhance Exercise Progression Graph Panel and Path Builder with New Features - Added `ProgressionChainEditor` to the Exercise Progression Graph Panel for improved management of exercise chains. - Refactored state management to utilize `useRef` for chain editor references and removed unused sequence step logic. - Introduced a path insert notice in the Exercise Progression Path Builder to inform users about unsaved changes. - Updated UI elements to enhance clarity regarding the status of paths before saving. - Incremented application version to reflect these updates. --- .../ExerciseProgressionGraphPanel.jsx | 440 ++++----------- .../ExerciseProgressionPathBuilder.jsx | 55 +- .../src/components/ProgressionChainEditor.jsx | 508 ++++++++++++++++++ 3 files changed, 657 insertions(+), 346 deletions(-) create mode 100644 frontend/src/components/ProgressionChainEditor.jsx diff --git a/frontend/src/components/ExerciseProgressionGraphPanel.jsx b/frontend/src/components/ExerciseProgressionGraphPanel.jsx index cd92620..a124300 100644 --- a/frontend/src/components/ExerciseProgressionGraphPanel.jsx +++ b/frontend/src/components/ExerciseProgressionGraphPanel.jsx @@ -2,7 +2,7 @@ * Progressionsgraphen: Sequenz-Editor (mehrere Nachfolger auf einmal), Ketten-Ansicht, * Varianten als eigene Knoten-Endpunkte, Schwester-Kanten gesondert, Tabelle als Fallback. */ -import React, { useCallback, useEffect, useMemo, useState } from 'react' +import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { Link } from 'react-router-dom' import api from '../utils/api' import SkillProfilePanel from './skills/SkillProfilePanel' @@ -10,6 +10,7 @@ import { useAuth } from '../context/AuthContext' import { getTenantClubDependencyKey } from '../utils/activeClub' import ExercisePickerModal from './ExercisePickerModal' import ExerciseProgressionPathBuilder from './ExerciseProgressionPathBuilder' +import ProgressionChainEditor from './ProgressionChainEditor' import { EXERCISE_VISIBILITY_FIELD_LABEL } from '../constants/exerciseGovernanceLabels' const VIS_OPTIONS = [ @@ -92,10 +93,6 @@ function maximalLinearChains(nextEdges) { return chains } -function emptySeqStep() { - return { exerciseId: null, exerciseTitle: '', variantId: null, variants: [] } -} - function emptyEndpoint() { return { exerciseId: null, exerciseTitle: '', variantId: null, variants: [] } } @@ -126,9 +123,8 @@ export default function ExerciseProgressionGraphPanel({ const [metaDescription, setMetaDescription] = useState('') const [metaVisibility, setMetaVisibility] = useState('private') - const [sequenceSteps, setSequenceSteps] = useState([emptySeqStep(), emptySeqStep()]) - const [sequenceBulkNotes, setSequenceBulkNotes] = useState('') const [pickContext, setPickContext] = useState(null) + const chainEditorRef = useRef(null) const [relationKind, setRelationKind] = useState('progression') const [firstEp, setFirstEp] = useState(emptyEndpoint) @@ -138,7 +134,6 @@ export default function ExerciseProgressionGraphPanel({ const [filterAnchorOnly, setFilterAnchorOnly] = useState(!!anchorExerciseId) const [editingEdgeNotes, setEditingEdgeNotes] = useState(null) const [notesDraft, setNotesDraft] = useState('') - const [uiTab, setUiTab] = useState('overview') const [skillProfileData, setSkillProfileData] = useState(null) const [skillProfileLoading, setSkillProfileLoading] = useState(false) const [skillProfileError, setSkillProfileError] = useState('') @@ -343,81 +338,6 @@ export default function ExerciseProgressionGraphPanel({ } } - const patchSeqStep = (idx, patch) => { - setSequenceSteps((prev) => prev.map((s, i) => (i === idx ? { ...s, ...patch } : s))) - } - - const addSeqStep = () => setSequenceSteps((prev) => [...prev, emptySeqStep()]) - - const removeSeqStep = (idx) => { - setSequenceSteps((prev) => { - if (prev.length <= 2) return prev - return prev.filter((_, i) => i !== idx) - }) - } - - const moveSeqStep = (idx, dir) => { - setSequenceSteps((prev) => { - const j = idx + dir - if (j < 0 || j >= prev.length) return prev - const next = [...prev] - const t = next[idx] - next[idx] = next[j] - next[j] = t - return next - }) - } - - const submitSequence = async () => { - if (!selectedGraphId) { - alert('Zuerst einen Graphen wählen.') - return - } - const steps = sequenceSteps.filter((s) => s.exerciseId != null) - if (steps.length < 2) { - alert('Mindestens zwei Schritte mit gewählter Übung.') - return - } - const n = steps.length - 1 - const noteRaw = sequenceBulkNotes.trim() - const segment_notes = Array.from({ length: n }, () => (noteRaw ? noteRaw : null)) - - setBusy(true) - try { - await api.createExerciseProgressionSequence(selectedGraphId, { - steps: steps.map((s) => ({ - exercise_id: s.exerciseId, - variant_id: s.variantId || null, - })), - segment_notes, - }) - setSequenceBulkNotes('') - await refreshEdges(selectedGraphId) - alert(`${n} Nachfolger-Kante(n) angelegt.`) - } catch (err) { - alert(err.message || String(err)) - } finally { - setBusy(false) - } - } - - const deleteChain = async (edgeObjs) => { - if (!selectedGraphId || !edgeObjs?.length) return - if (!confirm(`${edgeObjs.length} Kante(n) dieser Reihe löschen?`)) return - setBusy(true) - try { - await api.deleteExerciseProgressionEdgesBatch( - selectedGraphId, - edgeObjs.map((e) => e.id), - ) - await refreshEdges(selectedGraphId) - } catch (err) { - alert(err.message || String(err)) - } finally { - setBusy(false) - } - } - const handleAddEdge = async () => { if (!selectedGraphId) { alert('Zuerst einen Graphen wählen.') @@ -507,13 +427,16 @@ export default function ExerciseProgressionGraphPanel({ const variantId = ex.exercise_variant_id ?? ex.suggested_variant_id ?? null - if (pickContext?.kind === 'sequence') { - patchSeqStep(pickContext.index, { - exerciseId: ex.id, - exerciseTitle: title, - variantId: variantId != null ? Number(variantId) : null, - variants, - }) + if (pickContext?.kind === 'chain') { + await chainEditorRef.current?.applyExercise( + pickContext.draftKey, + pickContext.nodeIndex, + { + ...ex, + variants, + exercise_variant_id: variantId, + }, + ) setPickContext(null) return } @@ -555,10 +478,9 @@ export default function ExerciseProgressionGraphPanel({ )}

- Pro Graph mehrere Reihen und Alternativen: eine{' '} - Sequenz legt automatisch alle Schritte Übung1 → Übung2 → … als Nachfolger-Kanten an. - Optional pro Schritt eine Variante — sie wirkt wie ein eigener Knoten. Verzweigungen und - Schwestern trennst du weiterhin mit Einzelkanten oder mehreren Sequenzen aus dem gleichen Knoten. + Ein Graph enthält eine oder mehrere Reihen (lineare Pfade Übung → Übung) sowie optional{' '} + Schwester-Alternativen. Reihen bearbeiten Sie direkt in der Liste; mit dem KI-Planer legen + Sie neue Pfade in vier Schritten an.

{loadErr && ( @@ -631,236 +553,30 @@ export default function ExerciseProgressionGraphPanel({ - {selectedGraphId && ( -
-

Graph bearbeiten

-
- - setMetaName(e.target.value)} /> -
-
- -