diff --git a/frontend/src/pages/TrainingUnitRunPage.jsx b/frontend/src/pages/TrainingUnitRunPage.jsx index 1633731..cd4394b 100644 --- a/frontend/src/pages/TrainingUnitRunPage.jsx +++ b/frontend/src/pages/TrainingUnitRunPage.jsx @@ -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 = [] diff --git a/frontend/src/utils/trainingPlanUtils.js b/frontend/src/utils/trainingPlanUtils.js index f1e8b28..78771fd 100644 --- a/frontend/src/utils/trainingPlanUtils.js +++ b/frontend/src/utils/trainingPlanUtils.js @@ -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) => {