From a0a0be8beff09ca20722a85942dc23aa7a073047 Mon Sep 17 00:00:00 2001 From: Lars Date: Fri, 15 May 2026 08:20:43 +0200 Subject: [PATCH] Enhance TrainingUnitSectionsEditor with advanced phase management features - Introduced new utility functions for managing phase runs, including the ability to swap adjacent phase runs and move phase runs up or down by their order. - Updated the TrainingUnitSectionsEditor to incorporate these new functions, allowing for improved reordering of sections within parallel phases. - Enhanced the moveSection logic to support movement across different phase types, ensuring better user experience when managing sections. - Refactored the reorderBlocksImmutable function to accommodate plan location adjustments during section reordering, improving overall functionality. --- .../components/TrainingUnitSectionsEditor.jsx | 135 +++++++++++++----- .../src/utils/trainingUnitSectionsForm.js | 85 ++++++++++- 2 files changed, 186 insertions(+), 34 deletions(-) diff --git a/frontend/src/components/TrainingUnitSectionsEditor.jsx b/frontend/src/components/TrainingUnitSectionsEditor.jsx index 6f45923..ffbac2e 100644 --- a/frontend/src/components/TrainingUnitSectionsEditor.jsx +++ b/frontend/src/components/TrainingUnitSectionsEditor.jsx @@ -21,6 +21,11 @@ import { reorderWithoutIndices, parallelStreamBucketHasContent, dissolveParallelPhaseToWholeGroup, + phaseRunsFromSections, + swapAdjacentPhaseRuns, + reorderBlocksImmutableWithPlanLoc, + movePhaseRunUpByPhaseOrder, + movePhaseRunDownByPhaseOrder, exerciseRow, noteRow, sectionPlannedMinutes, @@ -263,15 +268,6 @@ export default function TrainingUnitSectionsEditor({ }) } - const addWholeGroupPhase = () => { - patch((prev) => { - const nextPo = maxPhaseOrderIndexFromSections(prev) + 1 - const pl = defaultPlanLocWholeGroup(nextPo) - const base = defaultSection(`Abschnitt ${prev.length + 1}`) - return [...prev, { ...base, planLoc: pl }] - }) - } - const addParallelPhaseTwoStreams = () => { patch((prev) => { const nextPo = maxPhaseOrderIndexFromSections(prev) + 1 @@ -430,7 +426,7 @@ export default function TrainingUnitSectionsEditor({ }) } - /** Ganzgruppe: global tauschen; parallele Phase: nur innerhalb desselben Streams sortieren. */ + /** Ganzgruppe: global tauschen; parallele Phase: innerhalb Stream oder ganze Parallel-Phase am Stück */ const moveSection = (sIdx, dir) => { patch((prev) => { const p = ensure(prev) @@ -442,9 +438,27 @@ export default function TrainingUnitSectionsEditor({ const bucket = sectionIndicesForParallelStream(p, po, so) const pos = bucket.indexOf(sIdx) if (pos < 0) return p - const newPos = pos + dir - if (newPos < 0 || newPos >= bucket.length) return p - return reorderWithinBucketIndices(p, bucket, pos, newPos) + if (dir < 0 && pos > 0) { + const newPos = pos + dir + return reorderWithinBucketIndices(p, bucket, pos, newPos) + } + if (dir > 0 && pos < bucket.length - 1) { + const newPos = pos + dir + return reorderWithinBucketIndices(p, bucket, pos, newPos) + } + if (dir < 0 && pos === 0) { + const runs = phaseRunsFromSections(p) + const rIdx = runs.findIndex((r) => r.phaseKind === 'parallel' && r.phaseOrderIndex === po) + if (rIdx <= 0) return p + return swapAdjacentPhaseRuns(p, rIdx - 1) + } + if (dir > 0 && pos === bucket.length - 1) { + const runs = phaseRunsFromSections(p) + const rIdx = runs.findIndex((r) => r.phaseKind === 'parallel' && r.phaseOrderIndex === po) + if (rIdx < 0 || rIdx >= runs.length - 1) return p + return swapAdjacentPhaseRuns(p, rIdx) + } + return p } const arr = [...p] const ta = sIdx + dir @@ -686,7 +700,12 @@ export default function TrainingUnitSectionsEditor({ return } - patch((prev) => reorderBlocksImmutable(prev, fromSi, insertBeforeIdx)) + patch((prev) => { + if (enableParallelPhaseControls) { + return reorderBlocksImmutableWithPlanLoc(prev, fromSi, insertBeforeIdx) + } + return reorderBlocksImmutable(prev, fromSi, insertBeforeIdx) + }) } const onItemDragStart = (e, sIdx, iIdx) => { @@ -829,6 +848,8 @@ export default function TrainingUnitSectionsEditor({ const list = ensure(sections) + const planningPhaseRuns = useMemo(() => phaseRunsFromSections(list), [list]) + const firstSectionIndexByParallelPhase = useMemo(() => { const m = new Map() list.forEach((s, i) => { @@ -883,7 +904,11 @@ export default function TrainingUnitSectionsEditor({ const so = L.parallelStreamOrderIndex ?? 0 const bucket = sectionIndicesForParallelStream(list, po, so) const pos = bucket.indexOf(sIdx) - return pos <= 0 + if (pos > 0) return false + const rIdx = planningPhaseRuns.findIndex( + (r) => r.phaseKind === 'parallel' && r.phaseOrderIndex === po + ) + return rIdx <= 0 } return sIdx === 0 } @@ -896,7 +921,11 @@ export default function TrainingUnitSectionsEditor({ const so = L.parallelStreamOrderIndex ?? 0 const bucket = sectionIndicesForParallelStream(list, po, so) const pos = bucket.indexOf(sIdx) - return pos < 0 || pos >= bucket.length - 1 + if (pos >= 0 && pos < bucket.length - 1) return false + const rIdx = planningPhaseRuns.findIndex( + (r) => r.phaseKind === 'parallel' && r.phaseOrderIndex === po + ) + return rIdx < 0 || rIdx >= planningPhaseRuns.length - 1 } return sIdx === list.length - 1 } @@ -1013,8 +1042,7 @@ export default function TrainingUnitSectionsEditor({ enableParallelPhaseControls && pl?.phaseKind === 'parallel' ? parallelStreamVisual(pl.parallelStreamOrderIndex ?? 0) : null - const allowSectionDragGrip = - enableSectionDragReorder && !(enableParallelPhaseControls && pl?.phaseKind === 'parallel') + const allowSectionDragGrip = enableSectionDragReorder return ( @@ -1124,6 +1152,51 @@ export default function TrainingUnitSectionsEditor({ ) })()} + {(() => { + const prRunIdx = planningPhaseRuns.findIndex( + (r) => r.phaseKind === 'parallel' && r.phaseOrderIndex === parallelPhaseOrder + ) + const chipPhaseUpDis = prRunIdx <= 0 + const chipPhaseDownDis = + prRunIdx < 0 || prRunIdx >= planningPhaseRuns.length - 1 + return ( +
+ + +
+ ) + })()} - ) : null} - + ) : ( + + )} {insertChooser ? ( diff --git a/frontend/src/utils/trainingUnitSectionsForm.js b/frontend/src/utils/trainingUnitSectionsForm.js index 1c912a4..dc5dffb 100644 --- a/frontend/src/utils/trainingUnitSectionsForm.js +++ b/frontend/src/utils/trainingUnitSectionsForm.js @@ -721,7 +721,7 @@ function stripPlanLoc(sec) { return rest } -function inheritPlanLocForPhasedSave(sections) { +export function inheritPlanLocForPhasedSave(sections) { let prev = { phaseKind: 'whole_group', phaseOrderIndex: 0, @@ -732,7 +732,7 @@ function inheritPlanLocForPhasedSave(sections) { streamNotes: null, streamAssignedTrainerProfileIds: null, } - return sections.map((s) => { + return (sections || []).map((s) => { if (s?.planLoc && s.planLoc.phaseKind) { prev = { ...s.planLoc } return { ...s, planLoc: prev } @@ -741,6 +741,87 @@ function inheritPlanLocForPhasedSave(sections) { }) } +/** Phasen-„Runs“ in der flachen Abschnittsliste (Reihenfolge wie beim Speichern). */ +export function phaseRunsFromSections(sections) { + const norm = inheritPlanLocForPhasedSave(sections) + const runs = [] + let i = 0 + while (i < norm.length) { + const loc0 = norm[i]?.planLoc + if (!loc0?.phaseKind) { + i += 1 + continue + } + const pOi = loc0.phaseOrderIndex ?? 0 + const pk = loc0.phaseKind === 'parallel' ? 'parallel' : 'whole_group' + const start = i + while (i < norm.length) { + const L = norm[i]?.planLoc + if (!L?.phaseKind) break + const pk2 = L.phaseKind === 'parallel' ? 'parallel' : 'whole_group' + if ((L.phaseOrderIndex ?? 0) !== pOi || pk2 !== pk) break + i += 1 + } + runs.push({ phaseKind: pk, phaseOrderIndex: pOi, start, end: i }) + } + return runs +} + +/** Vertauscht zwei unmittelbar benachbarte Runs (upperRunIndex = erste der beiden). */ +export function swapAdjacentPhaseRuns(prev, upperRunIndex) { + const runs = phaseRunsFromSections(prev) + const a = upperRunIndex + const b = upperRunIndex + 1 + if (a < 0 || b >= runs.length) return prev + const rgA = runs[a] + const rgB = runs[b] + const head = prev.slice(0, rgA.start) + const blA = prev.slice(rgA.start, rgA.end) + const blB = prev.slice(rgB.start, rgB.end) + const tail = prev.slice(rgB.end) + return [...head, ...blB, ...blA, ...tail] +} + +export function movePhaseRunUpByPhaseOrder(prev, phaseOrderIndex) { + const po = Number(phaseOrderIndex) || 0 + const runs = phaseRunsFromSections(prev) + const rIdx = runs.findIndex((r) => r.phaseKind === 'parallel' && r.phaseOrderIndex === po) + if (rIdx <= 0) return prev + return swapAdjacentPhaseRuns(prev, rIdx - 1) +} + +export function movePhaseRunDownByPhaseOrder(prev, phaseOrderIndex) { + const po = Number(phaseOrderIndex) || 0 + const runs = phaseRunsFromSections(prev) + const rIdx = runs.findIndex((r) => r.phaseKind === 'parallel' && r.phaseOrderIndex === po) + if (rIdx < 0 || rIdx >= runs.length - 1) return prev + return swapAdjacentPhaseRuns(prev, rIdx) +} + +/** + * Abschnitt verschieben und planLoc an der Einfügestelle an Nachbarn anpassen + * (z. B. Ganzgruppe in parallelen Stream). + */ +export function reorderBlocksImmutableWithPlanLoc(prev, fromI, toBeforeIdx) { + const arr = [...prev] + if (fromI < 0 || fromI >= arr.length) return prev + const [moved] = arr.splice(fromI, 1) + let insertAt = toBeforeIdx + if (fromI < toBeforeIdx) insertAt = toBeforeIdx - 1 + insertAt = Math.max(0, Math.min(insertAt, arr.length)) + const below = arr[insertAt] + const above = arr[insertAt - 1] + const ref = below ?? above + let nextMoved = { ...moved } + if (ref?.planLoc?.phaseKind) { + nextMoved = { ...moved, planLoc: { ...ref.planLoc } } + } else { + nextMoved = stripPlanLoc(moved) + } + arr.splice(insertAt, 0, nextMoved) + return arr +} + function buildPhasesPayloadFromFlat(sections) { const norm = inheritPlanLocForPhasedSave(sections) const phases = []