/** * Verwaltung mehrerer Progressionsgraphen (Übung → Übung) mit Kantentypen Nachfolger / Schwester. * Varianten-Ketten bleiben unter „Übungsvarianten“ der jeweiligen Übung. */ import React, { useCallback, useEffect, useMemo, useState } from 'react' import { Link } from 'react-router-dom' import api from '../utils/api' import ExercisePickerModal from './ExercisePickerModal' const VIS_OPTIONS = [ { value: 'private', label: 'Privat' }, { value: 'club', label: 'Verein' }, { value: 'official', label: 'Offiziell' }, ] function edgeTypeLabel(type) { if (type === 'next_exercise') return 'Nachfolger' if (type === 'sibling') return 'Schwester' return type || '—' } export default function ExerciseProgressionGraphPanel({ anchorExerciseId = null, anchorTitle = null, }) { const [graphs, setGraphs] = useState([]) const [selectedGraphId, setSelectedGraphId] = useState(null) const [edges, setEdges] = useState([]) const [busy, setBusy] = useState(false) const [loadErr, setLoadErr] = useState(null) const [newGraphName, setNewGraphName] = useState('') const [newGraphVisibility, setNewGraphVisibility] = useState('private') const [metaName, setMetaName] = useState('') const [metaDescription, setMetaDescription] = useState('') const [metaVisibility, setMetaVisibility] = useState('private') const [relationKind, setRelationKind] = useState('progression') const [firstEx, setFirstEx] = useState(null) const [secondEx, setSecondEx] = useState(null) const [edgeNotes, setEdgeNotes] = useState('') const [pickSlot, setPickSlot] = useState(null) const [filterAnchorOnly, setFilterAnchorOnly] = useState(!!anchorExerciseId) const [editingEdgeNotes, setEditingEdgeNotes] = useState(null) const [notesDraft, setNotesDraft] = useState('') const refreshGraphs = useCallback(async () => { const list = await api.listExerciseProgressionGraphs() setGraphs(Array.isArray(list) ? list : []) return list }, []) const refreshEdges = useCallback(async (gid) => { if (!gid) { setEdges([]) return } const list = await api.listExerciseProgressionEdges(gid) setEdges(Array.isArray(list) ? list : []) }, []) useEffect(() => { let cancelled = false ;(async () => { setBusy(true) setLoadErr(null) try { await refreshGraphs() } catch (e) { if (!cancelled) setLoadErr(e.message || String(e)) } finally { if (!cancelled) setBusy(false) } })() return () => { cancelled = true } }, [refreshGraphs]) useEffect(() => { if (!selectedGraphId) { setEdges([]) setMetaName('') setMetaDescription('') setMetaVisibility('private') return } const g = graphs.find((x) => x.id === selectedGraphId) if (g) { setMetaName(g.name || '') setMetaDescription(g.description || '') setMetaVisibility(g.visibility || 'private') } let cancelled = false ;(async () => { try { await refreshEdges(selectedGraphId) } catch (e) { if (!cancelled) alert(e.message || String(e)) } })() return () => { cancelled = true } }, [selectedGraphId, graphs, refreshEdges]) const filteredEdges = useMemo(() => { if (!filterAnchorOnly || anchorExerciseId == null) return edges return edges.filter( (e) => e.from_exercise_id === anchorExerciseId || e.to_exercise_id === anchorExerciseId, ) }, [edges, filterAnchorOnly, anchorExerciseId]) const handleCreateGraph = async (e) => { e.preventDefault() const name = newGraphName.trim() if (!name) { alert('Name für den Graphen eingeben') return } setBusy(true) try { const created = await api.createExerciseProgressionGraph({ name, visibility: newGraphVisibility, }) setNewGraphName('') await refreshGraphs() if (created?.id != null) setSelectedGraphId(created.id) } catch (err) { alert(err.message || String(err)) } finally { setBusy(false) } } const handleSaveMeta = async () => { if (!selectedGraphId) return const name = metaName.trim() if (!name) { alert('Name ist Pflicht') return } setBusy(true) try { await api.updateExerciseProgressionGraph(selectedGraphId, { name, description: metaDescription.trim() || null, visibility: metaVisibility, }) await refreshGraphs() alert('Graph gespeichert.') } catch (err) { alert(err.message || String(err)) } finally { setBusy(false) } } const handleDeleteGraph = async () => { if (!selectedGraphId) return if (!confirm('Diesen Progressionsgraphen und alle Kanten wirklich löschen?')) return setBusy(true) try { await api.deleteExerciseProgressionGraph(selectedGraphId) setSelectedGraphId(null) await refreshGraphs() } catch (err) { alert(err.message || String(err)) } finally { setBusy(false) } } const handleAddEdge = async () => { if (!selectedGraphId) { alert('Zuerst einen Graphen wählen oder anlegen.') return } if (!firstEx?.id || !secondEx?.id) { alert('Beide Übungen auswählen.') return } if (firstEx.id === secondEx.id) { alert('Es müssen zwei verschiedene Übungen sein.') return } const edge_type = relationKind === 'sibling' ? 'sibling' : 'next_exercise' const notes = edgeNotes.trim() || null const body = relationKind === 'sibling' ? { from_exercise_id: firstEx.id, to_exercise_id: secondEx.id, edge_type, notes, } : { from_exercise_id: firstEx.id, to_exercise_id: secondEx.id, edge_type: 'next_exercise', notes, } setBusy(true) try { await api.createExerciseProgressionEdge(selectedGraphId, body) setEdgeNotes('') await refreshEdges(selectedGraphId) } catch (err) { alert(err.message || String(err)) } finally { setBusy(false) } } const handleDeleteEdge = async (edgeId) => { if (!selectedGraphId) return if (!confirm('Kante löschen?')) return setBusy(true) try { await api.deleteExerciseProgressionEdge(selectedGraphId, edgeId) await refreshEdges(selectedGraphId) } catch (err) { alert(err.message || String(err)) } finally { setBusy(false) } } const startEditNotes = (edge) => { setEditingEdgeNotes(edge.id) setNotesDraft(edge.notes || '') } const saveNotes = async (edgeId) => { if (!selectedGraphId) return setBusy(true) try { await api.updateExerciseProgressionEdge(selectedGraphId, edgeId, { notes: notesDraft.trim() || null, }) setEditingEdgeNotes(null) await refreshEdges(selectedGraphId) } catch (err) { alert(err.message || String(err)) } finally { setBusy(false) } } const swapEnds = () => { const a = firstEx setFirstEx(secondEx) setSecondEx(a) } const onPicked = (ex) => { const row = { id: ex.id, title: ex.title || `Übung #${ex.id}` } if (pickSlot === 'first') setFirstEx(row) else if (pickSlot === 'second') setSecondEx(row) setPickSlot(null) } return (
{anchorExerciseId != null && (

Kontext:{' '} {anchorTitle?.trim() || `Übung #${anchorExerciseId}`} {' · '} Ansehen

)}

Graphen bilden einen gerichteten Wald aus Übungen: Nachfolger ist „zuerst A, dann B“; Schwestern markieren Alternativen oder parallele Entwicklungsschritte. Fortschritt{' '} innerhalb einer Übung (Varianten, Progressionsstufen) pflegst du unter „Übungsvarianten“.

{loadErr && (

{loadErr}

)}

Graph auswählen

Neuen Graphen anlegen

setNewGraphName(e.target.value)} placeholder="z. B. Kumite-Einstieg Verein Nord" />
{selectedGraphId && (

Graph bearbeiten

setMetaName(e.target.value)} />