Enhance TrainingUnitSectionsEditor with advanced phase management features
All checks were successful
Deploy Development / deploy (push) Successful in 39s
Test Suite / pytest-backend (push) Successful in 37s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 12s
Test Suite / k6 /health Baseline (push) Successful in 34s
Test Suite / playwright-tests (push) Successful in 1m13s
All checks were successful
Deploy Development / deploy (push) Successful in 39s
Test Suite / pytest-backend (push) Successful in 37s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 12s
Test Suite / k6 /health Baseline (push) Successful in 34s
Test Suite / playwright-tests (push) Successful in 1m13s
- 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.
This commit is contained in:
parent
613fedfaff
commit
a0a0be8bef
|
|
@ -21,6 +21,11 @@ import {
|
||||||
reorderWithoutIndices,
|
reorderWithoutIndices,
|
||||||
parallelStreamBucketHasContent,
|
parallelStreamBucketHasContent,
|
||||||
dissolveParallelPhaseToWholeGroup,
|
dissolveParallelPhaseToWholeGroup,
|
||||||
|
phaseRunsFromSections,
|
||||||
|
swapAdjacentPhaseRuns,
|
||||||
|
reorderBlocksImmutableWithPlanLoc,
|
||||||
|
movePhaseRunUpByPhaseOrder,
|
||||||
|
movePhaseRunDownByPhaseOrder,
|
||||||
exerciseRow,
|
exerciseRow,
|
||||||
noteRow,
|
noteRow,
|
||||||
sectionPlannedMinutes,
|
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 = () => {
|
const addParallelPhaseTwoStreams = () => {
|
||||||
patch((prev) => {
|
patch((prev) => {
|
||||||
const nextPo = maxPhaseOrderIndexFromSections(prev) + 1
|
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) => {
|
const moveSection = (sIdx, dir) => {
|
||||||
patch((prev) => {
|
patch((prev) => {
|
||||||
const p = ensure(prev)
|
const p = ensure(prev)
|
||||||
|
|
@ -442,9 +438,27 @@ export default function TrainingUnitSectionsEditor({
|
||||||
const bucket = sectionIndicesForParallelStream(p, po, so)
|
const bucket = sectionIndicesForParallelStream(p, po, so)
|
||||||
const pos = bucket.indexOf(sIdx)
|
const pos = bucket.indexOf(sIdx)
|
||||||
if (pos < 0) return p
|
if (pos < 0) return p
|
||||||
const newPos = pos + dir
|
if (dir < 0 && pos > 0) {
|
||||||
if (newPos < 0 || newPos >= bucket.length) return p
|
const newPos = pos + dir
|
||||||
return reorderWithinBucketIndices(p, bucket, pos, newPos)
|
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 arr = [...p]
|
||||||
const ta = sIdx + dir
|
const ta = sIdx + dir
|
||||||
|
|
@ -686,7 +700,12 @@ export default function TrainingUnitSectionsEditor({
|
||||||
return
|
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) => {
|
const onItemDragStart = (e, sIdx, iIdx) => {
|
||||||
|
|
@ -829,6 +848,8 @@ export default function TrainingUnitSectionsEditor({
|
||||||
|
|
||||||
const list = ensure(sections)
|
const list = ensure(sections)
|
||||||
|
|
||||||
|
const planningPhaseRuns = useMemo(() => phaseRunsFromSections(list), [list])
|
||||||
|
|
||||||
const firstSectionIndexByParallelPhase = useMemo(() => {
|
const firstSectionIndexByParallelPhase = useMemo(() => {
|
||||||
const m = new Map()
|
const m = new Map()
|
||||||
list.forEach((s, i) => {
|
list.forEach((s, i) => {
|
||||||
|
|
@ -883,7 +904,11 @@ export default function TrainingUnitSectionsEditor({
|
||||||
const so = L.parallelStreamOrderIndex ?? 0
|
const so = L.parallelStreamOrderIndex ?? 0
|
||||||
const bucket = sectionIndicesForParallelStream(list, po, so)
|
const bucket = sectionIndicesForParallelStream(list, po, so)
|
||||||
const pos = bucket.indexOf(sIdx)
|
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
|
return sIdx === 0
|
||||||
}
|
}
|
||||||
|
|
@ -896,7 +921,11 @@ export default function TrainingUnitSectionsEditor({
|
||||||
const so = L.parallelStreamOrderIndex ?? 0
|
const so = L.parallelStreamOrderIndex ?? 0
|
||||||
const bucket = sectionIndicesForParallelStream(list, po, so)
|
const bucket = sectionIndicesForParallelStream(list, po, so)
|
||||||
const pos = bucket.indexOf(sIdx)
|
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
|
return sIdx === list.length - 1
|
||||||
}
|
}
|
||||||
|
|
@ -1013,8 +1042,7 @@ export default function TrainingUnitSectionsEditor({
|
||||||
enableParallelPhaseControls && pl?.phaseKind === 'parallel'
|
enableParallelPhaseControls && pl?.phaseKind === 'parallel'
|
||||||
? parallelStreamVisual(pl.parallelStreamOrderIndex ?? 0)
|
? parallelStreamVisual(pl.parallelStreamOrderIndex ?? 0)
|
||||||
: null
|
: null
|
||||||
const allowSectionDragGrip =
|
const allowSectionDragGrip = enableSectionDragReorder
|
||||||
enableSectionDragReorder && !(enableParallelPhaseControls && pl?.phaseKind === 'parallel')
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment key={`secFrag-${sIdx}`}>
|
<Fragment key={`secFrag-${sIdx}`}>
|
||||||
|
|
@ -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 (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'inline-flex',
|
||||||
|
gap: '4px',
|
||||||
|
alignItems: 'center',
|
||||||
|
marginRight: '2px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-secondary framework-ctrl framework-ctrl--xs"
|
||||||
|
disabled={chipPhaseUpDis}
|
||||||
|
aria-label="Parallelen Block nach oben"
|
||||||
|
title="Gesamten parallelen Block im Plan nach oben schieben"
|
||||||
|
onClick={() =>
|
||||||
|
patch((p) => movePhaseRunUpByPhaseOrder(p, parallelPhaseOrder))
|
||||||
|
}
|
||||||
|
style={{ padding: '4px 10px' }}
|
||||||
|
>
|
||||||
|
▲
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-secondary framework-ctrl framework-ctrl--xs"
|
||||||
|
disabled={chipPhaseDownDis}
|
||||||
|
aria-label="Parallelen Block nach unten"
|
||||||
|
title="Gesamten parallelen Block im Plan nach unten schieben"
|
||||||
|
onClick={() =>
|
||||||
|
patch((p) => movePhaseRunDownByPhaseOrder(p, parallelPhaseOrder))
|
||||||
|
}
|
||||||
|
style={{ padding: '4px 10px' }}
|
||||||
|
>
|
||||||
|
▼
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})()}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="btn btn-secondary framework-ctrl framework-ctrl--xs"
|
className="btn btn-secondary framework-ctrl framework-ctrl--xs"
|
||||||
|
|
@ -1979,9 +2052,6 @@ export default function TrainingUnitSectionsEditor({
|
||||||
>
|
>
|
||||||
{enableParallelPhaseControls ? (
|
{enableParallelPhaseControls ? (
|
||||||
<>
|
<>
|
||||||
<button type="button" className="btn btn-secondary framework-ctrl framework-ctrl--xs" onClick={addWholeGroupPhase}>
|
|
||||||
Neue Ganzgruppen-Phase
|
|
||||||
</button>
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="btn btn-secondary framework-ctrl framework-ctrl--xs"
|
className="btn btn-secondary framework-ctrl framework-ctrl--xs"
|
||||||
|
|
@ -2002,20 +2072,21 @@ export default function TrainingUnitSectionsEditor({
|
||||||
type="button"
|
type="button"
|
||||||
className="btn btn-secondary framework-ctrl framework-ctrl--xs"
|
className="btn btn-secondary framework-ctrl framework-ctrl--xs"
|
||||||
onClick={addWholeGroupSection}
|
onClick={addWholeGroupSection}
|
||||||
title="Neuer Abschnitt für die gemeinsame Gruppe (nicht für einen parallelen Stream)"
|
title="Neuer Abschnitt für die gemeinsame Gruppe (legt bei Bedarf eine neue Ganzgruppen-Phase an)"
|
||||||
>
|
>
|
||||||
+ Ganzgruppen-Abschnitt
|
+ Abschnitt (Ganzgruppe)
|
||||||
</button>
|
</button>
|
||||||
</>
|
</>
|
||||||
) : null}
|
) : (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="btn btn-secondary framework-ctrl framework-ctrl--xs"
|
className="btn btn-secondary framework-ctrl framework-ctrl--xs"
|
||||||
onClick={addSection}
|
onClick={addSection}
|
||||||
title="Letzte Zuordnung übernehmen (Phase / Stream)"
|
title="Abschnitt am Ende anfügen"
|
||||||
>
|
>
|
||||||
+ Abschnitt hinzufügen
|
+ Abschnitt hinzufügen
|
||||||
</button>
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{insertChooser ? (
|
{insertChooser ? (
|
||||||
|
|
|
||||||
|
|
@ -721,7 +721,7 @@ function stripPlanLoc(sec) {
|
||||||
return rest
|
return rest
|
||||||
}
|
}
|
||||||
|
|
||||||
function inheritPlanLocForPhasedSave(sections) {
|
export function inheritPlanLocForPhasedSave(sections) {
|
||||||
let prev = {
|
let prev = {
|
||||||
phaseKind: 'whole_group',
|
phaseKind: 'whole_group',
|
||||||
phaseOrderIndex: 0,
|
phaseOrderIndex: 0,
|
||||||
|
|
@ -732,7 +732,7 @@ function inheritPlanLocForPhasedSave(sections) {
|
||||||
streamNotes: null,
|
streamNotes: null,
|
||||||
streamAssignedTrainerProfileIds: null,
|
streamAssignedTrainerProfileIds: null,
|
||||||
}
|
}
|
||||||
return sections.map((s) => {
|
return (sections || []).map((s) => {
|
||||||
if (s?.planLoc && s.planLoc.phaseKind) {
|
if (s?.planLoc && s.planLoc.phaseKind) {
|
||||||
prev = { ...s.planLoc }
|
prev = { ...s.planLoc }
|
||||||
return { ...s, planLoc: prev }
|
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) {
|
function buildPhasesPayloadFromFlat(sections) {
|
||||||
const norm = inheritPlanLocForPhasedSave(sections)
|
const norm = inheritPlanLocForPhasedSave(sections)
|
||||||
const phases = []
|
const phases = []
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user