2026-04-30 11:47:50 +02:00
/ * *
2026-06-10 16:17:40 +02:00
* Progressionsgraphen — Kachel - Übersicht ( wie Übungen ) + Editor - Detailansicht .
2026-04-30 11:47:50 +02:00
* /
2026-06-10 16:17:40 +02:00
import React , {
forwardRef ,
useCallback ,
useEffect ,
useImperativeHandle ,
useMemo ,
useState ,
} from 'react'
2026-06-10 15:42:29 +02:00
import { Link , useLocation } from 'react-router-dom'
2026-04-30 11:47:50 +02:00
import api from '../utils/api'
2026-05-20 16:42:25 +02:00
import SkillProfilePanel from './skills/SkillProfilePanel'
2026-05-08 10:20:41 +02:00
import { useAuth } from '../context/AuthContext'
2026-06-11 12:10:46 +02:00
import { activeClubMemberships , getTenantClubDependencyKey } from '../utils/activeClub'
2026-06-10 15:42:29 +02:00
import ProgressionGraphEditor from './ProgressionGraphEditor'
2026-06-10 16:17:40 +02:00
import ProgressionGraphListCard from './ProgressionGraphListCard'
2026-05-21 15:08:35 +02:00
import { EXERCISE _VISIBILITY _FIELD _LABEL } from '../constants/exerciseGovernanceLabels'
2026-04-30 11:47:50 +02:00
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 || '—'
}
2026-06-10 16:17:40 +02:00
function ExerciseProgressionGraphPanel (
{
anchorExerciseId = null ,
anchorTitle = null ,
initialGraphId = null ,
} ,
ref ,
) {
2026-05-08 10:20:41 +02:00
const { user } = useAuth ( )
2026-06-10 15:42:29 +02:00
const location = useLocation ( )
2026-05-08 10:20:41 +02:00
const isSuperadmin = user ? . role === 'superadmin'
2026-05-13 16:34:38 +02:00
const tenantClubDepKey = useMemo ( ( ) => getTenantClubDependencyKey ( user ) , [ user ] )
2026-05-08 10:20:41 +02:00
const filteredGraphVisOptions = useMemo (
( ) => VIS _OPTIONS . filter ( ( o ) => o . value !== 'official' || isSuperadmin ) ,
[ isSuperadmin ] ,
)
2026-04-30 11:47:50 +02:00
const [ graphs , setGraphs ] = useState ( [ ] )
const [ selectedGraphId , setSelectedGraphId ] = useState ( null )
const [ edges , setEdges ] = useState ( [ ] )
const [ busy , setBusy ] = useState ( false )
const [ loadErr , setLoadErr ] = useState ( null )
2026-06-10 16:17:40 +02:00
const [ createModalOpen , setCreateModalOpen ] = useState ( false )
2026-04-30 11:47:50 +02:00
const [ newGraphName , setNewGraphName ] = useState ( '' )
const [ newGraphVisibility , setNewGraphVisibility ] = useState ( 'private' )
const [ metaName , setMetaName ] = useState ( '' )
const [ metaDescription , setMetaDescription ] = useState ( '' )
const [ metaVisibility , setMetaVisibility ] = useState ( 'private' )
const [ filterAnchorOnly , setFilterAnchorOnly ] = useState ( ! ! anchorExerciseId )
const [ editingEdgeNotes , setEditingEdgeNotes ] = useState ( null )
const [ notesDraft , setNotesDraft ] = useState ( '' )
2026-05-20 16:42:25 +02:00
const [ skillProfileData , setSkillProfileData ] = useState ( null )
const [ skillProfileLoading , setSkillProfileLoading ] = useState ( false )
const [ skillProfileError , setSkillProfileError ] = useState ( '' )
2026-04-30 11:47:50 +02:00
2026-06-10 16:17:40 +02:00
useImperativeHandle ( ref , ( ) => ( {
openCreateDialog : ( ) => {
setNewGraphName ( '' )
setNewGraphVisibility ( 'private' )
setCreateModalOpen ( true )
} ,
} ) )
2026-06-10 15:42:29 +02:00
useEffect ( ( ) => {
2026-06-10 16:17:40 +02:00
const gid =
initialGraphId ? ?
location . state ? . progressionGraphId
2026-06-10 15:42:29 +02:00
if ( gid != null && Number . isFinite ( Number ( gid ) ) ) {
setSelectedGraphId ( Number ( gid ) )
}
2026-06-10 16:17:40 +02:00
} , [ location . state ? . progressionGraphId , initialGraphId ] )
2026-06-10 15:42:29 +02:00
2026-05-13 16:34:38 +02:00
useEffect ( ( ) => {
2026-06-10 16:17:40 +02:00
if ( ! initialGraphId && ! location . state ? . progressionGraphId ) {
setSelectedGraphId ( null )
}
} , [ tenantClubDepKey , initialGraphId , location . state ? . progressionGraphId ] )
2026-05-13 16:34:38 +02:00
2026-04-30 11:47:50 +02:00
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
}
2026-05-13 16:34:38 +02:00
} , [ refreshGraphs , tenantClubDepKey ] )
2026-04-30 11:47:50 +02:00
2026-05-20 16:42:25 +02:00
useEffect ( ( ) => {
if ( ! selectedGraphId ) {
setSkillProfileData ( null )
return undefined
}
let cancelled = false
; ( async ( ) => {
setSkillProfileLoading ( true )
setSkillProfileError ( '' )
try {
const data = await api . getProgressionGraphSkillProfile ( selectedGraphId )
if ( ! cancelled ) setSkillProfileData ( data )
} catch ( e ) {
if ( ! cancelled ) {
setSkillProfileData ( null )
setSkillProfileError ( e . message || 'Fähigkeiten-Profil nicht geladen' )
}
} finally {
if ( ! cancelled ) setSkillProfileLoading ( false )
}
} ) ( )
return ( ) => {
cancelled = true
}
} , [ selectedGraphId , edges . length ] )
2026-04-30 11:47:50 +02:00
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 ,
} )
2026-06-10 16:17:40 +02:00
setCreateModalOpen ( false )
2026-04-30 11:47:50 +02:00
setNewGraphName ( '' )
await refreshGraphs ( )
if ( created ? . id != null ) setSelectedGraphId ( created . id )
} catch ( err ) {
alert ( err . message || String ( err ) )
} finally {
setBusy ( false )
}
}
2026-06-10 16:17:40 +02:00
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 )
}
}
2026-06-11 12:10:46 +02:00
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
}
2026-04-30 11:47:50 +02:00
const handleSaveMeta = async ( ) => {
if ( ! selectedGraphId ) return
const name = metaName . trim ( )
if ( ! name ) {
alert ( 'Name ist Pflicht' )
return
}
2026-06-11 12:10:46 +02:00
const prevGraph = graphs . find ( ( x ) => x . id === selectedGraphId )
const prevVis = ( prevGraph ? . visibility || 'private' ) . trim ( ) . toLowerCase ( )
const nextVis = ( metaVisibility || 'private' ) . trim ( ) . toLowerCase ( )
2026-04-30 11:47:50 +02:00
setBusy ( true )
try {
2026-06-11 12:10:46 +02:00
if ( prevVis === 'private' && nextVis === 'club' ) {
const preview = await api . getProgressionGraphVisibilityPromotionCandidates (
selectedGraphId ,
{ targetVisibility : 'club' } ,
)
const privateExercises = Array . isArray ( preview ? . exercises ) ? preview . exercises : [ ]
if ( privateExercises . length > 0 ) {
const titles = privateExercises
. slice ( 0 , 8 )
. map ( ( ex ) => ` • ${ ex . title || ` Übung # ${ ex . id } ` } ` )
. join ( '\n' )
const more =
privateExercises . length > 8
? ` \ n… und ${ privateExercises . length - 8 } weitere `
: ''
const promote = window . confirm (
` Der Graph wird auf „Verein“ gestellt. Im Graph sind noch ${ privateExercises . length } private Übung(en): \ n \ n ${ titles } ${ more } \ n \ nDiese Übungen ebenfalls auf Vereins-Sichtbarkeit anheben? ` ,
)
if ( promote ) {
const clubId = resolvePromoteClubId ( )
if ( ! clubId ) {
alert ( 'Kein aktiver Verein — Übungen können nicht auf Verein promoted werden.' )
} else {
const ids = privateExercises . map ( ( ex ) => ex . id ) . filter ( ( id ) => id != null )
const res = await api . bulkPatchExercisesMetadata ( {
exercise _ids : ids ,
visibility : 'club' ,
club _id : clubId ,
} )
if ( ( res ? . failed || [ ] ) . length ) {
const f = res . failed [ 0 ]
throw new Error ( f ? . detail || 'Übungs-Promotion fehlgeschlagen' )
}
}
}
}
}
2026-04-30 11:47:50 +02:00
await api . updateExerciseProgressionGraph ( selectedGraphId , {
name ,
description : metaDescription . trim ( ) || null ,
visibility : metaVisibility ,
} )
await refreshGraphs ( )
2026-06-10 15:42:29 +02:00
alert ( 'Graph-Metadaten gespeichert.' )
2026-04-30 11:47:50 +02:00
} catch ( err ) {
alert ( err . message || String ( err ) )
} finally {
setBusy ( false )
}
}
const handleDeleteEdge = async ( edgeId ) => {
if ( ! selectedGraphId ) 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 )
}
}
2026-06-10 15:42:29 +02:00
const handleEditorSaved = useCallback ( async ( ) => {
if ( ! selectedGraphId ) return
await Promise . all ( [ refreshEdges ( selectedGraphId ) , refreshGraphs ( ) ] )
} , [ selectedGraphId , refreshEdges , refreshGraphs ] )
2026-05-03 18:07:52 +02:00
2026-06-10 16:17:40 +02:00
const selectedGraph = graphs . find ( ( g ) => g . id === selectedGraphId )
if ( loadErr ) {
return (
< div className = "card" style = { { borderColor : 'var(--danger)' } } >
< p style = { { margin : 0 , color : 'var(--danger)' } } > { loadErr } < / p >
< / div >
)
}
if ( ! selectedGraphId ) {
return (
< div className = "exercise-progression-panel" >
{ anchorExerciseId != null && (
< p style = { { fontSize : '13px' , color : 'var(--text2)' , marginBottom : '12px' } } >
Kontext : { ' ' }
< strong > { anchorTitle ? . trim ( ) || ` Übung # ${ anchorExerciseId } ` } < / strong >
{ ' · ' }
< Link to = { ` /exercises/ ${ anchorExerciseId } ` } > Ansehen < / Link >
< / p >
) }
< p style = { { fontSize : '13px' , color : 'var(--text2)' , marginBottom : '16px' } } >
Progressionsgraphen planen didaktische Entwicklungspfade mit Roadmap - Slots , KI - Match und
Bewertung — analog zur Übungsbibliothek als eigene Sammlung .
< / p >
{ busy && graphs . length === 0 ? (
< div className = "empty-state card" style = { { padding : '2rem 1rem' } } >
< div className = "spinner" / >
< p className = "muted" style = { { marginTop : '12px' } } >
Lade Progressionsgraphen …
< / p >
< / div >
) : graphs . length === 0 ? (
< div className = "card empty-state" style = { { padding : '2rem 1rem' , textAlign : 'center' } } >
< p style = { { margin : '0 0 12px' , color : 'var(--text2)' } } >
Noch keine Progressionsgraphen — lege den ersten an .
< / p >
< button
type = "button"
className = "btn btn-primary"
onClick = { ( ) => {
setCreateModalOpen ( true )
} }
>
+ Neu
< / button >
< / div >
) : (
< div className = "exercises-list-grid" >
{ graphs . map ( ( g ) => (
< ProgressionGraphListCard
key = { g . id }
graph = { g }
userId = { user ? . id }
disabled = { busy }
onOpen = { ( row ) => setSelectedGraphId ( row . id ) }
onDelete = { handleDeleteGraph }
/ >
) ) }
< / div >
) }
{ createModalOpen ? (
< div
className = "admin-modal-backdrop"
role = "presentation"
onClick = { ( e ) => {
if ( e . target === e . currentTarget && ! busy ) setCreateModalOpen ( false )
} }
>
< div
className = "admin-modal-sheet"
role = "dialog"
aria - modal = "true"
aria - labelledby = "new-progression-graph-title"
onClick = { ( e ) => e . stopPropagation ( ) }
style = { { maxWidth : '480px' } }
>
< div className = "admin-modal-sheet__header" >
< h3 id = "new-progression-graph-title" className = "admin-modal-sheet__title" >
Neuer Progressionsgraph
< / h3 >
< button
type = "button"
className = "btn btn-secondary admin-modal-sheet__close"
disabled = { busy }
onClick = { ( ) => setCreateModalOpen ( false ) }
>
Schließen
< / button >
< / div >
< form onSubmit = { handleCreateGraph } >
< div className = "admin-modal-sheet__body" >
< div className = "form-row" >
< label className = "form-label" > Name * < / label >
< input
className = "form-input"
value = { newGraphName }
onChange = { ( e ) => setNewGraphName ( e . target . value ) }
placeholder = "z. B. Kumite-Einstieg Verein Nord"
autoFocus
/ >
< / div >
< div className = "form-row" >
< label className = "form-label" > { EXERCISE _VISIBILITY _FIELD _LABEL } < / label >
< select
className = "form-input"
value = { newGraphVisibility }
onChange = { ( e ) => setNewGraphVisibility ( e . target . value ) }
>
{ filteredGraphVisOptions . map ( ( o ) => (
< option key = { o . value } value = { o . value } >
{ o . label }
< / option >
) ) }
< / select >
< / div >
< / div >
< div className = "admin-modal-sheet__footer" >
< button type = "button" className = "btn btn-secondary" disabled = { busy } onClick = { ( ) => setCreateModalOpen ( false ) } >
Abbrechen
< / button >
< button type = "submit" className = "btn btn-primary" disabled = { busy } >
Anlegen
< / button >
< / div >
< / form >
< / div >
< / div >
) : null }
< / div >
)
}
2026-04-30 11:47:50 +02:00
return (
< div className = "exercise-progression-panel" >
2026-06-10 16:17:40 +02:00
< div style = { { marginBottom : '12px' , display : 'flex' , flexWrap : 'wrap' , gap : '8px' , alignItems : 'center' } } >
< button
type = "button"
className = "btn btn-secondary"
style = { { fontSize : '12px' } }
disabled = { busy }
onClick = { ( ) => setSelectedGraphId ( null ) }
>
← Zur Übersicht
< / button >
< h2 style = { { margin : 0 , fontSize : '1.1rem' , flex : '1 1 auto' } } >
{ selectedGraph ? . name || ` Graph # ${ selectedGraphId } ` }
< / h2 >
< / div >
2026-04-30 11:47:50 +02:00
{ anchorExerciseId != null && (
< p style = { { fontSize : '13px' , color : 'var(--text2)' , marginBottom : '12px' } } >
Kontext : { ' ' }
< strong > { anchorTitle ? . trim ( ) || ` Übung # ${ anchorExerciseId } ` } < / strong >
{ ' · ' }
< Link to = { ` /exercises/ ${ anchorExerciseId } ` } > Ansehen < / Link >
< / p >
) }
2026-06-10 07:55:51 +02:00
{ selectedGraphId && anchorExerciseId != null && (
< label style = { { display : 'flex' , alignItems : 'center' , gap : '8px' , marginBottom : '12px' , fontSize : '13px' } } >
< input
type = "checkbox"
checked = { filterAnchorOnly }
onChange = { ( e ) => setFilterAnchorOnly ( e . target . checked ) }
/ >
2026-06-10 15:42:29 +02:00
Technische Kantenliste : nur Kanten mit dieser Übung
2026-06-10 07:55:51 +02:00
< / label >
2026-04-30 11:47:50 +02:00
) }
2026-06-10 16:17:40 +02:00
< ProgressionGraphEditor
key = { selectedGraphId }
graphId = { selectedGraphId }
embedded
onSaved = { handleEditorSaved }
/ >
< details className = "card" style = { { marginBottom : '12px' , marginTop : '12px' } } >
< summary style = { { cursor : 'pointer' , fontWeight : 600 } } > Graph - Einstellungen ( Name , Sichtbarkeit ) < / summary >
< div style = { { marginTop : '14px' } } >
< div className = "form-row" >
< label className = "form-label" > Name < / label >
< input className = "form-input" value = { metaName } onChange = { ( e ) => setMetaName ( e . target . value ) } / >
< / div >
< div className = "form-row" >
< label className = "form-label" > Beschreibung < / label >
< textarea
className = "form-input"
rows = { 2 }
value = { metaDescription }
onChange = { ( e ) => setMetaDescription ( e . target . value ) }
/ >
< / div >
< div className = "form-row" >
< label className = "form-label" > { EXERCISE _VISIBILITY _FIELD _LABEL } < / label >
< select
className = "form-input"
value = { metaVisibility }
onChange = { ( e ) => setMetaVisibility ( e . target . value ) }
>
{ filteredGraphVisOptions . map ( ( o ) => (
< option key = { o . value } value = { o . value } >
{ o . label }
< / option >
) ) }
< / select >
< / div >
< div style = { { display : 'flex' , gap : '8px' , flexWrap : 'wrap' } } >
< button type = "button" className = "btn btn-secondary" disabled = { busy } onClick = { handleSaveMeta } >
Metadaten speichern
< / button >
< button type = "button" className = "btn" disabled = { busy } onClick = { ( ) => handleDeleteGraph ( selectedGraph ) } >
Graph löschen
< / button >
< / div >
< / div >
< / details >
< SkillProfilePanel
title = "Fähigkeiten entlang des Pfads"
hint = "Alle Übungen als Knoten im Graph."
profile = { skillProfileData ? . overall }
loading = { skillProfileLoading }
error = { skillProfileError }
defaultExpanded = { false }
artifactType = "progression_graph"
/ >
< details className = "card" style = { { marginBottom : '12px' } } >
< summary style = { { cursor : 'pointer' , fontWeight : 600 } } >
Technische Kantenliste ( { filteredEdges . length }
{ edges . length !== filteredEdges . length ? ` von ${ edges . length } ` : '' } )
< / summary >
< div style = { { marginTop : '14px' } } >
{ filteredEdges . length === 0 ? (
< p style = { { color : 'var(--text2)' , margin : 0 } } > Keine Kanten . < / p >
) : (
< div style = { { overflowX : 'auto' } } >
< table style = { { width : '100%' , borderCollapse : 'collapse' , fontSize : '13px' } } >
< thead >
< tr style = { { textAlign : 'left' , borderBottom : '1px solid var(--border)' } } >
< th style = { { padding : '8px 6px' } } > Von < / th >
< th style = { { padding : '8px 6px' } } / >
< th style = { { padding : '8px 6px' } } > Nach < / th >
< th style = { { padding : '8px 6px' } } > Art < / th >
< th style = { { padding : '8px 6px' } } > Notiz < / th >
< th style = { { padding : '8px 6px' } } / >
< / tr >
< / thead >
< tbody >
{ filteredEdges . map ( ( row ) => (
< tr key = { row . id } style = { { borderBottom : '1px solid var(--border)' } } >
< td style = { { padding : '8px 6px' , verticalAlign : 'top' } } >
< Link to = { ` /exercises/ ${ row . from _exercise _id } ` } >
{ row . from _exercise _title || ` # ${ row . from _exercise _id } ` }
< / Link >
< / td >
< td style = { { padding : '8px 6px' , color : 'var(--text3)' , verticalAlign : 'top' } } >
{ row . edge _type === 'sibling' ? '·' : '→' }
< / td >
< td style = { { padding : '8px 6px' , verticalAlign : 'top' } } >
< Link to = { ` /exercises/ ${ row . to _exercise _id } ` } >
{ row . to _exercise _title || ` # ${ row . to _exercise _id } ` }
< / Link >
< / td >
< td style = { { padding : '8px 6px' , verticalAlign : 'top' } } > { edgeTypeLabel ( row . edge _type ) } < / td >
< td style = { { padding : '8px 6px' , verticalAlign : 'top' , maxWidth : '280px' } } >
{ editingEdgeNotes === row . id ? (
< >
< textarea
className = "form-input"
rows = { 2 }
value = { notesDraft }
onChange = { ( e ) => setNotesDraft ( e . target . value ) }
/ >
< button
type = "button"
className = "btn btn-primary"
style = { { marginTop : '6px' , fontSize : '12px' , padding : '4px 8px' } }
onClick = { ( ) => saveNotes ( row . id ) }
>
Speichern
< / button >
< / >
) : (
< >
{ row . notes || '—' }
2026-04-30 11:47:50 +02:00
< button
type = "button"
className = "btn"
2026-06-10 16:17:40 +02:00
style = { { marginLeft : '8px' , fontSize : '12px' , padding : '4px 8px' } }
onClick = { ( ) => startEditNotes ( row ) }
2026-04-30 11:47:50 +02:00
>
2026-06-10 16:17:40 +02:00
Bearbeiten
2026-04-30 11:47:50 +02:00
< / button >
2026-06-10 16:17:40 +02:00
< / >
) }
< / td >
< td style = { { padding : '8px 6px' , verticalAlign : 'top' } } >
< button
type = "button"
className = "btn"
style = { {
fontSize : '12px' ,
padding : '4px 8px' ,
background : 'var(--danger)' ,
color : '#fff' ,
border : 'none' ,
} }
onClick = { ( ) => handleDeleteEdge ( row . id ) }
>
Löschen
< / button >
< / td >
< / tr >
) ) }
< / tbody >
< / table >
2026-06-10 07:55:51 +02:00
< / div >
2026-06-10 16:17:40 +02:00
) }
< / div >
< / details >
2026-04-30 11:47:50 +02:00
< / div >
)
}
2026-06-10 16:17:40 +02:00
export default forwardRef ( ExerciseProgressionGraphPanel )