diff --git a/frontend/src/app.css b/frontend/src/app.css index 64c3d7a..2b3891d 100644 --- a/frontend/src/app.css +++ b/frontend/src/app.css @@ -5935,6 +5935,45 @@ a.analysis-split__nav-item { box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent) 42%, transparent); } +/* Planungseditor: Einfügebänder — Split vs. Ganzgruppe (inaktiv sichtbar, aktiv weiterhin Akzent-Ring) */ +.tu-section-dropband--region-split { + height: 12px; + background: hsl(200 38% 92%); + border: 1px dashed hsl(200 42% 58%); +} + +.tu-section-dropband--region-whole { + height: 12px; + background: color-mix(in srgb, var(--accent) 10%, var(--surface2)); + border: 1px dashed color-mix(in srgb, var(--accent) 32%, transparent); +} + +.tu-section-dropband--region-split-to-whole { + height: 12px; + background: linear-gradient( + 90deg, + hsl(200 38% 92%) 0%, + color-mix(in srgb, var(--accent) 12%, var(--surface2)) 100% + ); + border: 1px dashed hsl(200 36% 50%); +} + +.tu-section-dropband--region-whole-to-split { + height: 12px; + background: linear-gradient( + 90deg, + color-mix(in srgb, var(--accent) 12%, var(--surface2)) 0%, + hsl(200 38% 92%) 100% + ); + border: 1px dashed hsl(200 36% 50%); +} + +.tu-section-dropband--region-neutral { + height: 11px; + border: 1px dashed color-mix(in srgb, var(--border) 70%, transparent); + background: color-mix(in srgb, var(--surface2) 55%, transparent); +} + .tu-sec-drag-grip { flex-shrink: 0; display: inline-flex; diff --git a/frontend/src/components/TrainingUnitSectionsEditor.jsx b/frontend/src/components/TrainingUnitSectionsEditor.jsx index ffbac2e..0f45c04 100644 --- a/frontend/src/components/TrainingUnitSectionsEditor.jsx +++ b/frontend/src/components/TrainingUnitSectionsEditor.jsx @@ -26,6 +26,9 @@ import { reorderBlocksImmutableWithPlanLoc, movePhaseRunUpByPhaseOrder, movePhaseRunDownByPhaseOrder, + moveParallelPhaseRunToInsertBefore, + afterSectionReorderParallelGuard, + indicesOfParallelPhase, exerciseRow, noteRow, sectionPlannedMinutes, @@ -75,6 +78,31 @@ function dtHasType(e, mime) { return Array.from(t).includes(mime) } +/** Visuelle Zuordnung der Einfügezeile zu Split- vs. Ganzgruppen-Bereich (nur Darstellung). */ +function sectionDropBandRegionClass(sections, beforeIdx, enableParallel) { + if (!enableParallel) return '' + const n = sections?.length ?? 0 + const below = beforeIdx < n ? sections[beforeIdx] : null + const above = beforeIdx > 0 ? sections[beforeIdx - 1] : null + const po = (s) => s?.planLoc?.phaseOrderIndex ?? 0 + const aboveP = above?.planLoc?.phaseKind === 'parallel' + const belowP = below?.planLoc?.phaseKind === 'parallel' + const aboveW = above?.planLoc?.phaseKind === 'whole_group' + const belowW = below?.planLoc?.phaseKind === 'whole_group' + + if (aboveP && belowP && po(above) === po(below)) { + return ' tu-section-dropband--region-split' + } + if (aboveW && belowW) return ' tu-section-dropband--region-whole' + if (aboveP && belowW) return ' tu-section-dropband--region-split-to-whole' + if (aboveW && belowP) return ' tu-section-dropband--region-whole-to-split' + if (!above && belowP) return ' tu-section-dropband--region-split' + if (!below && aboveP) return ' tu-section-dropband--region-split-to-whole' + if (!above && belowW) return ' tu-section-dropband--region-whole' + if (!below && aboveW) return ' tu-section-dropband--region-whole' + return ' tu-section-dropband--region-neutral' +} + function truncatePreview(text, max = 160) { const t = (text || '').replace(/\s+/g, ' ').trim() if (t.length <= max) return t @@ -298,7 +326,13 @@ export default function TrainingUnitSectionsEditor({ streamAssignedTrainerProfileIds: null, } const base = defaultSection(`Abschnitt ${prev.length + 1}`) - return [...prev, { ...base, planLoc: tmpl }] + const inPhaseIdx = indicesOfParallelPhase(prev, po) + const insertAfter = inPhaseIdx.length ? Math.max(...inPhaseIdx) : prev.length - 1 + return [ + ...prev.slice(0, insertAfter + 1), + { ...base, planLoc: tmpl }, + ...prev.slice(insertAfter + 1), + ] }) } @@ -633,6 +667,25 @@ export default function TrainingUnitSectionsEditor({ const clearSectionDnD = () => setDropSectionBand(null) + const onParallelPhaseDragStart = (e, phaseOrderIndex) => { + if (!enableSectionDragReorder || !enableParallelPhaseControls) return + e.stopPropagation() + try { + e.dataTransfer.effectAllowed = 'move' + e.dataTransfer.setData( + DND_TU_SECTION, + JSON.stringify({ + fromSlot: sectionToSlot, + fromSectionIdx: null, + phaseRunMove: { phaseOrderIndex }, + }) + ) + } catch { + /* ignore */ + } + setDropSectionBand(null) + } + const onSectionDragStart = (e, sIdx) => { if (!enableSectionDragReorder) return e.stopPropagation() @@ -683,7 +736,19 @@ export default function TrainingUnitSectionsEditor({ return } const fromSi = data.fromSectionIdx + const phaseRunMove = data.phaseRunMove const fromSlot = typeof data.fromSlot === 'number' ? data.fromSlot : -1 + + if (phaseRunMove != null && phaseRunMove.phaseOrderIndex != null) { + patch((prev) => { + const po = Number(phaseRunMove.phaseOrderIndex) || 0 + let next = moveParallelPhaseRunToInsertBefore(prev, po, insertBeforeIdx) + if (enableParallelPhaseControls) next = afterSectionReorderParallelGuard(prev, next) + return next + }) + return + } + if (typeof fromSi !== 'number') return if ( @@ -701,10 +766,11 @@ export default function TrainingUnitSectionsEditor({ } patch((prev) => { - if (enableParallelPhaseControls) { - return reorderBlocksImmutableWithPlanLoc(prev, fromSi, insertBeforeIdx) - } - return reorderBlocksImmutable(prev, fromSi, insertBeforeIdx) + let next = enableParallelPhaseControls + ? reorderBlocksImmutableWithPlanLoc(prev, fromSi, insertBeforeIdx) + : reorderBlocksImmutable(prev, fromSi, insertBeforeIdx) + if (enableParallelPhaseControls) next = afterSectionReorderParallelGuard(prev, next) + return next }) } @@ -1048,7 +1114,11 @@ export default function TrainingUnitSectionsEditor({ {enableSectionDragReorder ? (
{ if (!enableSectionDragReorder) return @@ -1083,6 +1153,19 @@ export default function TrainingUnitSectionsEditor({ marginBottom: '10px', }} > + {enableSectionDragReorder ? ( + onParallelPhaseDragStart(e, parallelPhaseOrder)} + role="button" + tabIndex={0} + aria-label="Parallele Phase ziehen" + title="Gesamte parallele Phase an neue Planposition ziehen" + > + + + ) : null}