diff --git a/frontend/src/components/TrainingUnitSectionsEditor.jsx b/frontend/src/components/TrainingUnitSectionsEditor.jsx index 505dbee..8785f7f 100644 --- a/frontend/src/components/TrainingUnitSectionsEditor.jsx +++ b/frontend/src/components/TrainingUnitSectionsEditor.jsx @@ -1,5 +1,5 @@ import React, { Fragment, useCallback, useEffect, useMemo, useState } from 'react' -import { GripVertical, Pencil } from 'lucide-react' +import { GripVertical, Pencil, X } from 'lucide-react' import CombinationMethodProfileEditor from './CombinationMethodProfileEditor' import CombinationPlanBracket from './CombinationPlanBracket' import { comboPlanningProfileJsonForEditor, effectiveComboMethodProfile } from '../utils/comboPlanningMethodProfile' @@ -15,10 +15,12 @@ import { planLocKey, MAX_PARALLEL_STREAMS_PER_PHASE, parallelStreamVisual, - streamTabLabelFromIndices, streamsForParallelPhaseOrders, sectionIndicesForParallelStream, reorderWithinBucketIndices, + reorderWithoutIndices, + parallelStreamBucketHasContent, + dissolveParallelPhaseToWholeGroup, exerciseRow, noteRow, sectionPlannedMinutes, @@ -270,27 +272,30 @@ export default function TrainingUnitSectionsEditor({ }) } - const addParallelPhase = () => { + const addParallelPhaseTwoStreams = () => { patch((prev) => { const nextPo = maxPhaseOrderIndexFromSections(prev) + 1 - const pl = defaultPlanLocParallel(nextPo, 0) - const base = defaultSection(`Abschnitt ${prev.length + 1}`) - return [...prev, { ...base, planLoc: pl }] + const pl0 = defaultPlanLocParallel(nextPo, 0) + const pl1 = defaultPlanLocParallel(nextPo, 1) + const base0 = defaultSection(`Abschnitt ${prev.length + 1}`) + const base1 = defaultSection(`Abschnitt ${prev.length + 2}`) + return [...prev, { ...base0, planLoc: pl0 }, { ...base1, planLoc: pl1 }] }) } - const addStreamToLastParallelPhase = () => { + const addStreamToParallelPhase = (phaseOrder) => { patch((prev) => { - const par = (prev || []).filter((s) => s?.planLoc?.phaseKind === 'parallel') + const po = Number(phaseOrder) || 0 + const par = (prev || []).filter( + (s) => s?.planLoc?.phaseKind === 'parallel' && (s.planLoc.phaseOrderIndex ?? 0) === po + ) if (!par.length) return prev - const maxP = Math.max(...par.map((s) => s.planLoc.phaseOrderIndex ?? 0)) - const inPhase = par.filter((s) => (s.planLoc.phaseOrderIndex ?? 0) === maxP) - const distinctStreams = new Set(inPhase.map((s) => s.planLoc.parallelStreamOrderIndex ?? 0)) - if (distinctStreams.size >= MAX_PARALLEL_STREAMS_PER_PHASE) return prev - const maxS = Math.max(...inPhase.map((s) => s.planLoc.parallelStreamOrderIndex ?? 0)) + const distinct = new Set(par.map((s) => s.planLoc.parallelStreamOrderIndex ?? 0)) + if (distinct.size >= MAX_PARALLEL_STREAMS_PER_PHASE) return prev + const maxS = Math.max(...par.map((s) => s.planLoc.parallelStreamOrderIndex ?? 0)) const newSo = maxS + 1 const tmpl = { - ...inPhase[0].planLoc, + ...par[0].planLoc, parallelStreamOrderIndex: newSo, streamTitle: null, streamNotes: null, @@ -301,6 +306,110 @@ export default function TrainingUnitSectionsEditor({ }) } + const addWholeGroupSection = () => { + patch((prev) => { + const L = ensure(prev) + const wgs = L.filter((s) => s?.planLoc?.phaseKind === 'whole_group') + let pl + if (wgs.length) { + const maxPo = Math.max(...wgs.map((s) => s.planLoc.phaseOrderIndex ?? 0)) + const sample = wgs.find((s) => (s.planLoc.phaseOrderIndex ?? 0) === maxPo) + pl = { ...sample.planLoc } + } else { + const nextPo = maxPhaseOrderIndexFromSections(L) + 1 + pl = defaultPlanLocWholeGroup(nextPo) + } + const base = defaultSection(`Abschnitt ${L.length + 1}`) + return [...L, { ...base, planLoc: pl }] + }) + } + + const addSectionToParallelStream = (phaseOrder, streamOrder) => { + patch((prev) => { + const L = ensure(prev) + const po = Number(phaseOrder) || 0 + const so = Number(streamOrder) || 0 + const idxs = sectionIndicesForParallelStream(L, po, so) + const tmpl = idxs.length ? L[idxs[0]].planLoc : defaultPlanLocParallel(po, so) + const pl = { + ...tmpl, + phaseKind: 'parallel', + phaseOrderIndex: po, + parallelStreamOrderIndex: so, + } + const base = defaultSection(`Abschnitt ${L.length + 1}`) + if (!idxs.length) { + return [...L, { ...base, planLoc: pl }] + } + const insertAfter = Math.max(...idxs) + return [...L.slice(0, insertAfter + 1), { ...base, planLoc: pl }, ...L.slice(insertAfter + 1)] + }) + } + + const updateParallelPhaseTitleAll = (phaseOrder, title) => { + const po = Number(phaseOrder) || 0 + const v = title.trim() ? title.trim() : null + patch((prev) => + prev.map((s) => { + const L = s?.planLoc + if (L?.phaseKind !== 'parallel' || (L.phaseOrderIndex ?? 0) !== po) return s + return { ...s, planLoc: { ...L, phaseTitle: v } } + }) + ) + } + + const updateParallelStreamTitleAll = (phaseOrder, streamOrder, title) => { + const po = Number(phaseOrder) || 0 + const so = Number(streamOrder) || 0 + const v = title.trim() ? title.trim() : null + patch((prev) => + prev.map((s) => { + const L = s?.planLoc + if ( + L?.phaseKind !== 'parallel' || + (L.phaseOrderIndex ?? 0) !== po || + (L.parallelStreamOrderIndex ?? 0) !== so + ) { + return s + } + return { ...s, planLoc: { ...L, streamTitle: v } } + }) + ) + } + + const removeParallelStream = (phaseOrder, streamOrder) => { + const po = Number(phaseOrder) || 0 + const so = Number(streamOrder) || 0 + const idxs = sectionIndicesForParallelStream(list, po, so) + if (!idxs.length) return + if ( + parallelStreamBucketHasContent(list, idxs, SECTION_INSERT_SEPARATOR_BODY) && + !window.confirm( + 'In diesem Stream sind Übungen oder Anmerkungen geplant. Stream wirklich löschen?' + ) + ) { + return + } + patch((prev) => { + const L = ensure(prev) + const beforeOrders = streamsForParallelPhaseOrders(L, po) + const rm = sectionIndicesForParallelStream(L, po, so) + if (!rm.length) return prev + let next = reorderWithoutIndices(L, rm) + const afterOrders = streamsForParallelPhaseOrders(next, po) + if (beforeOrders.length >= 2 && afterOrders.length <= 1) { + if ( + window.confirm( + 'Nur noch eine Gruppe in dieser Phase übrig. Parallelen Aufbau auflösen und alle Abschnitte als gemeinsame Ganzgruppen-Phase weiterführen?' + ) + ) { + next = dissolveParallelPhaseToWholeGroup(next, po) + } + } + return next + }) + } + const applySectionPlanTarget = (sIdx, rawKey) => { patch((prev) => { if (!rawKey) { @@ -713,11 +822,6 @@ export default function TrainingUnitSectionsEditor({ const list = ensure(sections) - const hasParallelPhase = useMemo( - () => list.some((s) => s?.planLoc?.phaseKind === 'parallel'), - [list] - ) - const firstSectionIndexByParallelPhase = useMemo(() => { const m = new Map() list.forEach((s, i) => { @@ -737,15 +841,6 @@ export default function TrainingUnitSectionsEditor({ return [...set].sort((a, b) => a - b) }, [list]) - const cannotAddMoreStreams = useMemo(() => { - const par = list.filter((s) => s?.planLoc?.phaseKind === 'parallel') - if (!par.length) return true - const maxP = Math.max(...par.map((s) => s.planLoc.phaseOrderIndex ?? 0)) - const inPhase = par.filter((s) => (s.planLoc.phaseOrderIndex ?? 0) === maxP) - const distinct = new Set(inPhase.map((s) => s.planLoc.parallelStreamOrderIndex ?? 0)) - return distinct.size >= MAX_PARALLEL_STREAMS_PER_PHASE - }, [list]) - useEffect(() => { if (!enableParallelPhaseControls || !parallelPhaseOrdersPresent.length) return setParallelStreamTabByPhase((prev) => { @@ -881,51 +976,6 @@ export default function TrainingUnitSectionsEditor({ ) : null} ) : null} - {enableParallelPhaseControls ? ( -
- Legt fest, ob Abschnitte zur ganzen Gruppe oder zu parallelen Gruppen gehören. Pro paralleler Phase erscheinen
- Reiter je Stream — nur der aktive Stream ist sichtbar (farbig am linken Rand). „Abschnitt hinzufügen“ übernimmt
- die Zuordnung des letzten Abschnitts. Speichern erzeugt bei Bedarf automatisch den{' '}
- phases-Payload fürs Backend.
-