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,
|
||||
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 (
|
||||
<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
|
||||
type="button"
|
||||
className="btn btn-secondary framework-ctrl framework-ctrl--xs"
|
||||
|
|
@ -1979,9 +2052,6 @@ export default function TrainingUnitSectionsEditor({
|
|||
>
|
||||
{enableParallelPhaseControls ? (
|
||||
<>
|
||||
<button type="button" className="btn btn-secondary framework-ctrl framework-ctrl--xs" onClick={addWholeGroupPhase}>
|
||||
Neue Ganzgruppen-Phase
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-secondary framework-ctrl framework-ctrl--xs"
|
||||
|
|
@ -2002,20 +2072,21 @@ export default function TrainingUnitSectionsEditor({
|
|||
type="button"
|
||||
className="btn btn-secondary framework-ctrl framework-ctrl--xs"
|
||||
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>
|
||||
</>
|
||||
) : null}
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-secondary framework-ctrl framework-ctrl--xs"
|
||||
onClick={addSection}
|
||||
title="Letzte Zuordnung übernehmen (Phase / Stream)"
|
||||
>
|
||||
+ Abschnitt hinzufügen
|
||||
</button>
|
||||
) : (
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-secondary framework-ctrl framework-ctrl--xs"
|
||||
onClick={addSection}
|
||||
title="Abschnitt am Ende anfügen"
|
||||
>
|
||||
+ Abschnitt hinzufügen
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{insertChooser ? (
|
||||
|
|
|
|||
|
|
@ -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 = []
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user