diff --git a/frontend/src/components/ExerciseProgressionGraphPanel.jsx b/frontend/src/components/ExerciseProgressionGraphPanel.jsx index a8be046..a0a2988 100644 --- a/frontend/src/components/ExerciseProgressionGraphPanel.jsx +++ b/frontend/src/components/ExerciseProgressionGraphPanel.jsx @@ -1,13 +1,21 @@ /** - * Progressionsgraphen — eine Oberfläche: Graph wählen, Roadmap-Slots bearbeiten, KI & Speichern. + * Progressionsgraphen — Kachel-Übersicht (wie Übungen) + Editor-Detailansicht. */ -import React, { useCallback, useEffect, useMemo, useState } from 'react' +import React, { + forwardRef, + useCallback, + useEffect, + useImperativeHandle, + useMemo, + useState, +} from 'react' import { Link, useLocation } from 'react-router-dom' import api from '../utils/api' import SkillProfilePanel from './skills/SkillProfilePanel' import { useAuth } from '../context/AuthContext' import { getTenantClubDependencyKey } from '../utils/activeClub' import ProgressionGraphEditor from './ProgressionGraphEditor' +import ProgressionGraphListCard from './ProgressionGraphListCard' import { EXERCISE_VISIBILITY_FIELD_LABEL } from '../constants/exerciseGovernanceLabels' const VIS_OPTIONS = [ @@ -22,10 +30,14 @@ function edgeTypeLabel(type) { return type || '—' } -export default function ExerciseProgressionGraphPanel({ - anchorExerciseId = null, - anchorTitle = null, -}) { +function ExerciseProgressionGraphPanel( + { + anchorExerciseId = null, + anchorTitle = null, + initialGraphId = null, + }, + ref, +) { const { user } = useAuth() const location = useLocation() const isSuperadmin = user?.role === 'superadmin' @@ -42,6 +54,7 @@ export default function ExerciseProgressionGraphPanel({ const [busy, setBusy] = useState(false) const [loadErr, setLoadErr] = useState(null) + const [createModalOpen, setCreateModalOpen] = useState(false) const [newGraphName, setNewGraphName] = useState('') const [newGraphVisibility, setNewGraphVisibility] = useState('private') @@ -56,16 +69,28 @@ export default function ExerciseProgressionGraphPanel({ const [skillProfileLoading, setSkillProfileLoading] = useState(false) const [skillProfileError, setSkillProfileError] = useState('') + useImperativeHandle(ref, () => ({ + openCreateDialog: () => { + setNewGraphName('') + setNewGraphVisibility('private') + setCreateModalOpen(true) + }, + })) + useEffect(() => { - const gid = location.state?.progressionGraphId + const gid = + initialGraphId ?? + location.state?.progressionGraphId if (gid != null && Number.isFinite(Number(gid))) { setSelectedGraphId(Number(gid)) } - }, [location.state?.progressionGraphId]) + }, [location.state?.progressionGraphId, initialGraphId]) useEffect(() => { - setSelectedGraphId(null) - }, [tenantClubDepKey]) + if (!initialGraphId && !location.state?.progressionGraphId) { + setSelectedGraphId(null) + } + }, [tenantClubDepKey, initialGraphId, location.state?.progressionGraphId]) const refreshGraphs = useCallback(async () => { const list = await api.listExerciseProgressionGraphs() @@ -174,6 +199,7 @@ export default function ExerciseProgressionGraphPanel({ name, visibility: newGraphVisibility, }) + setCreateModalOpen(false) setNewGraphName('') await refreshGraphs() if (created?.id != null) setSelectedGraphId(created.id) @@ -184,6 +210,22 @@ export default function ExerciseProgressionGraphPanel({ } } + const handleDeleteGraph = async (graph) => { + const gid = graph?.id ?? selectedGraphId + if (!gid) return + if (!window.confirm(`Progressionsgraph „${graph?.name || gid}" wirklich löschen?`)) return + setBusy(true) + try { + await api.deleteExerciseProgressionGraph(gid) + if (selectedGraphId === gid) setSelectedGraphId(null) + await refreshGraphs() + } catch (err) { + alert(err.message || String(err)) + } finally { + setBusy(false) + } + } + const handleSaveMeta = async () => { if (!selectedGraphId) return const name = metaName.trim() @@ -207,21 +249,6 @@ export default function ExerciseProgressionGraphPanel({ } } - const handleDeleteGraph = async () => { - if (!selectedGraphId) return - if (!window.confirm('Diesen Progressionsgraph 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 handleDeleteEdge = async (edgeId) => { if (!selectedGraphId) return setBusy(true) @@ -261,8 +288,159 @@ export default function ExerciseProgressionGraphPanel({ await Promise.all([refreshEdges(selectedGraphId), refreshGraphs()]) }, [selectedGraphId, refreshEdges, refreshGraphs]) + const selectedGraph = graphs.find((g) => g.id === selectedGraphId) + + if (loadErr) { + return ( +
+

{loadErr}

+
+ ) + } + + if (!selectedGraphId) { + return ( +
+ {anchorExerciseId != null && ( +

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

+ )} + +

+ Progressionsgraphen planen didaktische Entwicklungspfade mit Roadmap-Slots, KI-Match und + Bewertung — analog zur Übungsbibliothek als eigene Sammlung. +

+ + {busy && graphs.length === 0 ? ( +
+
+

+ Lade Progressionsgraphen… +

+
+ ) : graphs.length === 0 ? ( +
+

+ Noch keine Progressionsgraphen — lege den ersten an. +

+ +
+ ) : ( +
+ {graphs.map((g) => ( + setSelectedGraphId(row.id)} + onDelete={handleDeleteGraph} + /> + ))} +
+ )} + + {createModalOpen ? ( +
{ + if (e.target === e.currentTarget && !busy) setCreateModalOpen(false) + }} + > +
e.stopPropagation()} + style={{ maxWidth: '480px' }} + > +
+

+ Neuer Progressionsgraph +

+ +
+
+
+
+ + setNewGraphName(e.target.value)} + placeholder="z. B. Kumite-Einstieg Verein Nord" + autoFocus + /> +
+
+ + +
+
+
+ + +
+
+
+
+ ) : null} +
+ ) + } + return (
+
+ +

+ {selectedGraph?.name || `Graph #${selectedGraphId}`} +

+
+ {anchorExerciseId != null && (

Kontext:{' '} @@ -272,84 +450,6 @@ export default function ExerciseProgressionGraphPanel({

)} -

- Ein Progressionsgraph = Roadmap mit Slots (Lernziel + Hauptübung + Schwestern). - Roadmap, KI-Match und Bewertung sind in einer Ansicht — kein separater Wizard. -

- - {loadErr && ( -
-

{loadErr}

-
- )} - -
-

Graph auswählen

-
-
- - -
- - -
- -
-

Neuen Graphen anlegen

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