/** * Bearbeitbare Darstellung linearer Progressions-Reihen im Graphen. */ import React, { forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useState } from 'react' import { Link } from 'react-router-dom' import api from '../utils/api' function emptyNode() { return { exerciseId: null, exerciseTitle: '', variantId: null, variantName: null, variants: [], } } function chainToDraft(chain) { return { key: `chain-${chain.edges[0]?.id ?? 'x'}`, edgeIds: chain.edges.map((e) => e.id), segmentNotes: chain.edges.map((e) => e.notes || ''), nodes: chain.nodes.map((n) => ({ exerciseId: n.exercise_id, exerciseTitle: n.title || `Übung #${n.exercise_id}`, variantId: n.variant_id ?? null, variantName: n.variant_name ?? null, variants: [], })), dirty: false, isNew: false, } } function newChainDraft() { const key = `new-${Date.now()}` return { key, edgeIds: [], segmentNotes: [], nodes: [emptyNode(), emptyNode()], dirty: true, isNew: true, } } function formatNodeLabel(node) { if (!node.exerciseId) return '— Übung wählen —' return ( <> {node.exerciseTitle} {node.variantName ? ( {` · ${node.variantName}`} ) : null} > ) } const ProgressionChainEditor = forwardRef(function ProgressionChainEditor( { graphId, chains = [], busy = false, anchorExerciseId = null, anchorTitle = null, onRefresh, onPickExercise, loadVariantsForExercise, singlePathMode = false, }, ref, ) { const [drafts, setDrafts] = useState([]) const [savingKey, setSavingKey] = useState(null) const chainSignature = useMemo( () => chains .map((c) => c.edges.map((e) => e.id).join(',')) .join('|'), [chains], ) useEffect(() => { setDrafts(chains.map(chainToDraft)) }, [chainSignature, chains]) const patchDraft = useCallback((key, patchFn) => { setDrafts((prev) => prev.map((d) => { if (d.key !== key) return d const next = patchFn(d) return { ...next, dirty: true } }), ) }, []) const moveNode = (key, idx, dir) => { patchDraft(key, (d) => { const j = idx + dir if (j < 0 || j >= d.nodes.length) return d const nodes = [...d.nodes] const t = nodes[idx] nodes[idx] = nodes[j] nodes[j] = t return { ...d, nodes } }) } const removeNode = (key, idx) => { patchDraft(key, (d) => { if (d.nodes.length <= 2) return d const nodes = d.nodes.filter((_, i) => i !== idx) const segmentNotes = d.segmentNotes.slice(0, Math.max(0, nodes.length - 1)) return { ...d, nodes, segmentNotes } }) } const setVariant = (key, idx, variantId) => { patchDraft(key, (d) => ({ ...d, nodes: d.nodes.map((n, i) => { if (i !== idx) return n const v = (n.variants || []).find((x) => Number(x.id) === Number(variantId)) return { ...n, variantId: variantId === '' || variantId == null ? null : Number(variantId), variantName: v?.variant_name || null, } }), })) } const applyExerciseToNode = useCallback(async (key, idx, ex) => { const title = ex.title || `Übung #${ex.id}` const variants = Array.isArray(ex.variants) && ex.variants.length ? ex.variants : await loadVariantsForExercise(ex.id) const variantId = ex.exercise_variant_id ?? ex.suggested_variant_id ?? null patchDraft(key, (d) => ({ ...d, nodes: d.nodes.map((n, i) => i === idx ? { exerciseId: ex.id, exerciseTitle: title, variantId: variantId != null ? Number(variantId) : null, variantName: variantId != null ? variants.find((v) => Number(v.id) === Number(variantId))?.variant_name || null : null, variants, } : n, ), })) }, [patchDraft, loadVariantsForExercise]) const insertNodeAfter = (key, idx) => { patchDraft(key, (d) => { const nodes = [...d.nodes] nodes.splice(idx + 1, 0, emptyNode()) return { ...d, nodes } }) onPickExercise({ kind: 'chain', draftKey: key, nodeIndex: idx + 1 }) } const addNewChain = () => { setDrafts((prev) => [...prev, newChainDraft()]) } const discardDraft = (key) => { setDrafts((prev) => { const draft = prev.find((d) => d.key === key) if (!draft) return prev if (draft.isNew) return prev.filter((d) => d.key !== key) const original = chains.find((c) => `chain-${c.edges[0]?.id}` === key) if (!original) return prev.filter((d) => d.key !== key) return prev.map((d) => (d.key === key ? chainToDraft(original) : d)) }) } const deleteChain = async (draft) => { if (!graphId) return if (draft.isNew) { setDrafts((prev) => prev.filter((d) => d.key !== draft.key)) return } if (!draft.edgeIds.length) return if (!window.confirm(`Reihe mit ${draft.nodes.length} Schritten löschen?`)) return setSavingKey(draft.key) try { await api.deleteExerciseProgressionEdgesBatch(graphId, draft.edgeIds) await onRefresh() } catch (e) { alert(e.message || String(e)) } finally { setSavingKey(null) } } const saveDraft = async (draft) => { if (!graphId) return const steps = draft.nodes.filter((n) => n.exerciseId != null) if (steps.length < 2) { alert('Mindestens zwei Schritte mit gewählter Übung.') return } const n = steps.length - 1 let segment_notes = draft.segmentNotes.slice(0, n) while (segment_notes.length < n) segment_notes.push(null) segment_notes = segment_notes.slice(0, n) setSavingKey(draft.key) try { if (!draft.isNew && draft.edgeIds.length) { await api.deleteExerciseProgressionEdgesBatch(graphId, draft.edgeIds) } await api.createExerciseProgressionSequence(graphId, { steps: steps.map((s) => ({ exercise_id: s.exerciseId, variant_id: s.variantId || null, })), segment_notes, }) await onRefresh() } catch (e) { alert(e.message || String(e)) } finally { setSavingKey(null) } } const ensureVariantsLoaded = async (key, idx) => { const draft = drafts.find((d) => d.key === key) const node = draft?.nodes[idx] if (!node?.exerciseId || (node.variants || []).length) return const variants = await loadVariantsForExercise(node.exerciseId) setDrafts((prev) => prev.map((d) => { if (d.key !== key) return d return { ...d, nodes: d.nodes.map((n, i) => (i === idx ? { ...n, variants } : n)), } }), ) } useImperativeHandle( ref, () => ({ applyExercise: applyExerciseToNode, }), [applyExerciseToNode], ) if (!graphId) return null return (
Schritt 1 → 2 → …: Reihenfolge ändern, Übungen tauschen oder dazwischen einfügen, dann speichern.
{singlePathMode ? 'Noch kein gespeicherter Pfad — manuell anlegen oder mit dem KI-Planer unten.' : 'Noch keine Reihen in diesem Graph.'}
) : ( drafts.map((draft, chainIdx) => (