+ Ungültige Stream-Parameter in der Adresszeile — es wird der Gesamtplan angezeigt.
+
+ ) : null}
+
- · ca. {st.minutes} Min. (Üb.)
+
+ {st.streamTitle ? String(st.streamTitle).trim() : `Gruppe ${st.streamOrder + 1}`}
+
+ · ca. {st.minutes} Min. (Üb.)
+
+
+ Coach · nur diese Gruppe
+
{st.sections.map((sec) => renderSectionCard(sec, siUnit(sec)))}
diff --git a/frontend/src/utils/trainingPlanUtils.js b/frontend/src/utils/trainingPlanUtils.js
index 78771fd..1844d73 100644
--- a/frontend/src/utils/trainingPlanUtils.js
+++ b/frontend/src/utils/trainingPlanUtils.js
@@ -208,8 +208,9 @@ function coachContextLabelForSection(sec, sectionsList) {
return `Parallel · ${pt} · ${st}`
}
-/** Flache Reihenfolge für Coach-Timeline (global wie im Editor, inkl. gemischter Split-Abschnitte). */
-export function flattenPlanTimeline(unit) {
+/** Flache Reihenfolge für Coach-Timeline.
+ * @param {object|null} coachFocus `{ phaseOrder, streamOrder }` = nur dieser Stream in dieser parallelen Phase; andere Split-Phasen weiterhin voll (alle Streams verschränkt). Null = Gesamtplan.*/
+export function flattenPlanTimeline(unit, coachFocus = null) {
const sections = sectionsWithPlanLocForDisplay(unit)
const model = buildPlanRunViewModelFromSections(sections)
if (model.mode === 'empty') return []
@@ -232,15 +233,78 @@ export function flattenPlanTimeline(unit) {
})
}
+ const f = coachFocus
for (const run of model.runs) {
- for (const sec of run.globalOrderSections) {
- pushSectionItems(sec, coachContextLabelForSection(sec, sections))
+ if (run.kind === 'legacy' || run.kind === 'whole_group') {
+ for (const sec of run.globalOrderSections) {
+ pushSectionItems(sec, coachContextLabelForSection(sec, sections))
+ }
+ continue
+ }
+ if (run.kind === 'parallel') {
+ if (f == null || run.phaseOrderIndex !== f.phaseOrder) {
+ for (const sec of run.globalOrderSections) {
+ pushSectionItems(sec, coachContextLabelForSection(sec, sections))
+ }
+ } else {
+ const st = run.streams?.find((x) => x.streamOrder === f.streamOrder)
+ if (st?.sections?.length) {
+ for (const sec of st.sections) {
+ pushSectionItems(sec, coachContextLabelForSection(sec, sections))
+ }
+ }
+ }
}
}
return list
}
+/** Optionen für Coach-Stream-Auswahl (phases · Gruppe). */
+export function listCoachStreamFocusOptions(unit) {
+ const model = buildPlanRunViewModelFromSections(sectionsWithPlanLocForDisplay(unit))
+ const opts = []
+ for (const run of model.runs) {
+ if (run.kind !== 'parallel' || !run.streams?.length) continue
+ const phaseLabel =
+ run.phaseTitle != null && String(run.phaseTitle).trim()
+ ? String(run.phaseTitle).trim()
+ : `Phase ${run.phaseOrderIndex}`
+ for (const st of run.streams) {
+ const gl =
+ st.streamTitle != null && String(st.streamTitle).trim()
+ ? String(st.streamTitle).trim()
+ : `Gruppe ${st.streamOrder + 1}`
+ opts.push({
+ phaseOrder: run.phaseOrderIndex,
+ streamOrder: st.streamOrder,
+ label: `${phaseLabel} · ${gl}`,
+ valueKey: `${run.phaseOrderIndex}-${st.streamOrder}`,
+ })
+ }
+ }
+ return opts
+}
+
+/** Alle Ist-Minuten-Overrides aus lokalem Delta-State für PUT (unabhängig von gefilterter Coach-Timeline). */
+export function durationOverridesMapFromDeltas(unit, deltas) {
+ const out = {}
+ if (!unit || !deltas || typeof deltas !== 'object') return out
+ const sections = sortedSections(unit)
+ sections.forEach((sec, si) => {
+ const secOrder = sec.order_index ?? si
+ sortedItems(sec).forEach((it, ii) => {
+ if (it.item_type !== 'exercise' || it.id == null) return
+ const k = itemStableKey(it, secOrder, ii)
+ const dv = deltas[k]?.actual_duration_min
+ if (dv !== undefined && dv !== '' && dv !== null && !Number.isNaN(Number(dv))) {
+ out[String(it.id)] = { actual_duration_min: Number(dv) }
+ }
+ })
+ })
+ return out
+}
+
export function summarizeTimelineEntry({ item }) {
if (!item) return ''
if (item.item_type === 'note') {