Refactor TrainingUnitRunPage and trainingPlanUtils for improved section handling
All checks were successful
Deploy Development / deploy (push) Successful in 38s
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 1m26s

- Updated TrainingUnitRunPage to utilize new utility functions for managing sections with plan locations, enhancing data representation.
- Refactored trainingPlanUtils to introduce functions for building view models from sections and for displaying sections with plan locations, streamlining the data flow.
- Improved logic for handling phase runs and section indices, ensuring accurate representation of training units during rendering.
This commit is contained in:
Lars 2026-05-15 14:00:46 +02:00
parent 5338871f36
commit c182ced7cd
2 changed files with 87 additions and 11 deletions

View File

@ -8,9 +8,9 @@ import api from '../utils/api'
import ExercisePeekModal from '../components/ExercisePeekModal'
import CombinationPlanBracket from '../components/CombinationPlanBracket'
import {
buildPlanRunViewModel,
buildPlanRunViewModelFromSections,
itemStableKey,
sortedSections,
sectionsWithPlanLocForDisplay,
sortedItems,
} from '../utils/trainingPlanUtils'
import { effectiveComboMethodProfile } from '../utils/comboPlanningMethodProfile'
@ -115,8 +115,8 @@ export default function TrainingUnitRunPage() {
}
}, [idNum, persistChecked])
const sections = useMemo(() => sortedSections(unit), [unit])
const planModel = useMemo(() => buildPlanRunViewModel(unit), [unit])
const sections = useMemo(() => sectionsWithPlanLocForDisplay(unit), [unit])
const planModel = useMemo(() => buildPlanRunViewModelFromSections(sections), [sections])
const printStreamOptions = useMemo(() => {
const opts = []

View File

@ -4,6 +4,7 @@
import {
cloneJsonSerializablePlanningProfile,
inheritPlanLocForPhasedSave,
phaseRunsFromSections,
sectionIndicesForParallelStream,
streamsForParallelPhaseOrders,
@ -37,11 +38,79 @@ export function sumExerciseMinutesInSection(sec) {
}
/**
* Lesemodell für Plan & Ablauf / Druck / Coach: Phasenläufe mit Ganzgruppe vs. Split.
* Legacy ohne planLoc: ein Block.
* GET liefert `planLoc` oft nicht auf flachen `sections`, aber `unit.phases` (verschachtelt).
* Baut pro Abschnitt `planLoc` für phaseRuns / Darstellung (camelCase wie im Editor).
*/
export function buildPlanRunViewModel(unit) {
const sections = sortedSections(unit)
function planLocBySectionIdFromPhases(phases) {
const byId = new Map()
if (!Array.isArray(phases)) return byId
for (const ph of phases) {
const po = Number(ph.order_index ?? ph.orderIndex ?? 0) || 0
const pk = String(ph.phase_kind ?? ph.phaseKind ?? '')
.toLowerCase()
.trim()
const phaseTitle = ph.title ?? ph.phaseTitle ?? null
const phaseGuidanceNotes = ph.guidance_notes ?? ph.guidanceNotes ?? null
if (pk === 'whole_group') {
for (const sec of ph.sections || []) {
const sid = sec.id != null ? Number(sec.id) : NaN
if (!Number.isFinite(sid)) continue
byId.set(sid, {
phaseKind: 'whole_group',
phaseOrderIndex: po,
parallelStreamOrderIndex: null,
phaseTitle,
phaseGuidanceNotes,
streamTitle: null,
streamNotes: null,
streamAssignedTrainerProfileIds: null,
})
}
} else if (pk === 'parallel') {
for (const st of ph.streams || []) {
const so = Number(st.order_index ?? st.orderIndex ?? 0) || 0
const streamTitle = st.title ?? st.streamTitle ?? null
const streamNotes = st.notes ?? st.streamNotes ?? null
const streamAssignedTrainerProfileIds =
st.assigned_trainer_profile_ids ?? st.streamAssignedTrainerProfileIds ?? null
for (const sec of st.sections || []) {
const sid = sec.id != null ? Number(sec.id) : NaN
if (!Number.isFinite(sid)) continue
byId.set(sid, {
phaseKind: 'parallel',
phaseOrderIndex: po,
parallelStreamOrderIndex: so,
phaseTitle,
phaseGuidanceNotes,
streamTitle,
streamNotes,
streamAssignedTrainerProfileIds,
})
}
}
}
}
return byId
}
export function sectionsWithPlanLocForDisplay(unit) {
const sorted = sortedSections(unit)
const byId = planLocBySectionIdFromPhases(unit?.phases)
const merged = sorted.map((s) => {
const sid = s.id != null ? Number(s.id) : NaN
if (Number.isFinite(sid) && byId.has(sid)) {
return { ...s, planLoc: { ...byId.get(sid) } }
}
if (s.planLoc && s.planLoc.phaseKind) return s
return { ...s }
})
return inheritPlanLocForPhasedSave(merged)
}
/**
* Läuft auf bereits angereichter Abschnittsliste (gleiche Objektreferenzen wie in Slices).
*/
export function buildPlanRunViewModelFromSections(sections) {
if (!sections.length) {
return { mode: 'empty', runs: [], totalMin: 0, runCumulativeEnds: [] }
}
@ -88,7 +157,9 @@ export function buildPlanRunViewModel(unit) {
const streamOrders = streamsForParallelPhaseOrders(slice, po)
const streams = streamOrders.map((so) => {
const idxs = sectionIndicesForParallelStream(slice, po, so)
const streamSecs = idxs.map((i) => slice[i])
const streamSecs = idxs
.map((i) => slice[i])
.sort((a, b) => (a.order_index ?? 0) - (b.order_index ?? 0))
const first = streamSecs[0]
const streamTitle = first?.planLoc?.streamTitle ?? null
const minutes = streamSecs.reduce((s, sec) => s + sumExerciseMinutesInSection(sec), 0)
@ -119,6 +190,11 @@ export function buildPlanRunViewModel(unit) {
return { mode: 'phased', runs, totalMin: cum, runCumulativeEnds }
}
/** @param {object} unit Trainingseinheit inkl. `sections`, optional `phases` (GET) */
export function buildPlanRunViewModel(unit) {
return buildPlanRunViewModelFromSections(sectionsWithPlanLocForDisplay(unit))
}
function coachContextLabelForSection(sec, sectionsList) {
const pl = sec?.planLoc
if (!pl?.phaseKind) return 'Ablauf'
@ -134,10 +210,10 @@ function coachContextLabelForSection(sec, sectionsList) {
/** Flache Reihenfolge für Coach-Timeline (global wie im Editor, inkl. gemischter Split-Abschnitte). */
export function flattenPlanTimeline(unit) {
const model = buildPlanRunViewModel(unit)
const sections = sectionsWithPlanLocForDisplay(unit)
const model = buildPlanRunViewModelFromSections(sections)
if (model.mode === 'empty') return []
const sections = sortedSections(unit)
const list = []
const pushSectionItems = (sec, coachCtx) => {