e.target === e.currentTarget && onClose()}>
{err}}
{!loading && exercise && (
<>
+ {isCombination ? (
+ <>
+
+ Kombination
+
+ {(() => {
+ const ak = String(exercise.method_archetype || '').trim()
+ const lbl = ak ? combinationArchetypeLabel(ak) : null
+ return lbl || ak || 'Archetyp nicht gesetzt'
+ })()}
+
+ {peekExtras?.planning_method_profile != null &&
+ typeof peekExtras.planning_method_profile === 'object' &&
+ !Array.isArray(peekExtras.planning_method_profile) ? (
+
+ · Planung angepasst
+
+ ) : null}
+
+
+
+ >
+ ) : null}
+
{variant ? (
[Number(r.slot_index), r]))
+ const titles = it.combo_member_title_by_id || {}
+ return slots.map((slot, idx) => {
+ const siRaw = slot.slot_index
+ const siParsed =
+ siRaw === '' || siRaw == null ? idx : typeof siRaw === 'number' ? siRaw : parseInt(String(siRaw), 10)
+ const ix = Number.isFinite(siParsed) ? siParsed : idx
+ const stationLbl = ((slot.title || '').trim() || `Station ${ix}`)
+ const candIds = (slot.candidate_exercise_ids || [])
+ .map((raw) => (typeof raw === 'number' ? raw : parseInt(String(raw), 10)))
+ .filter((n) => Number.isFinite(n))
+ const namesJoined =
+ candIds.length === 0
+ ? '(keine Übung)'
+ : candIds.map((id) => titles[String(id)] || `Übung ${id}`).join(' ↔ ')
+ const timing = summarizeSlotProfileBrief(byIx.get(ix))
+ let line = `${stationLbl}: ${namesJoined}`
+ if (timing) line += ` · ${timing}`
+ return line
+ })
+}
+
/** Stabile Farbzurodnung aus Modul-ID (nur Darstellung). */
function planningModulePalette(moduleId) {
const id = normalizedPlanningModuleChainId(moduleId)
@@ -964,6 +1018,24 @@ export default function TrainingUnitSectionsEditor({
? undefined
: Number(it.exercise_variant_id)
+ const stripArchRaw =
+ isCombination && it.exercise_id ? String(it.catalog_method_archetype || '').trim() : ''
+ const stripArchLbl =
+ stripArchRaw && isCombination ? combinationArchetypeLabel(stripArchRaw) : null
+ const stripBullets =
+ isCombination && it.exercise_id ? comboPlanningStripBulletTexts(it) : []
+ const stripMpEff =
+ isCombination && it.exercise_id
+ ? effectiveComboMethodProfile(
+ it.catalog_method_profile || {},
+ it.planning_method_profile,
+ )
+ : null
+ const stripGlobalRough =
+ isCombination && it.exercise_id && stripMpEff
+ ? comboRoughGlobalTimingHint(stripMpEff, stripArchRaw)
+ : null
+
return (
{!planningCompactLegend &&
@@ -1047,7 +1119,16 @@ export default function TrainingUnitSectionsEditor({
type="button"
className="btn btn-secondary framework-ctrl framework-ctrl--xs"
onClick={() =>
- onPeekExercise(Number(it.exercise_id), variantIdPeek)
+ onPeekExercise(
+ Number(it.exercise_id),
+ variantIdPeek,
+ isCombination
+ ? {
+ catalog_method_profile: it.catalog_method_profile,
+ planning_method_profile: it.planning_method_profile,
+ }
+ : undefined,
+ )
}
>
Vorschau
@@ -1142,29 +1223,73 @@ export default function TrainingUnitSectionsEditor({
style={{
display: 'flex',
flexWrap: 'wrap',
- alignItems: 'center',
- gap: '8px 10px',
- padding: '6px 12px 8px',
+ alignItems: 'flex-start',
+ gap: '10px',
+ padding: '8px 12px 10px',
paddingLeft: enableItemDragReorder ? 44 : 12,
borderTop: '1px solid var(--border)',
background: 'var(--surface2)',
}}
>
-
- Ablauf:
- {compactComboPlanningCaption(it)}
-
+
+ Archetyp:
+
+ {stripArchLbl || stripArchRaw || '—'}
+ {stripArchRaw && stripArchLbl && stripArchLbl !== stripArchRaw ? (
+
+ ({stripArchRaw})
+
+ ) : null}
+
+ {compactComboPlanningCaption(it)}
+
+ {stripGlobalRough ? (
+
+ Block:
+ {stripGlobalRough}
+
+ ) : null}
+ {stripBullets.length > 0 ? (
+
+ {stripBullets.map((line, bi) => (
+ -
+ {line}
+
+ ))}
+
+ ) : (
+
+ Stationen laden oder noch keine Kombi-Stationen im Katalog …
+
+ )}
+
diff --git a/frontend/src/utils/combinationMethodProfileUi.js b/frontend/src/utils/combinationMethodProfileUi.js
index 719d435..986372e 100644
--- a/frontend/src/utils/combinationMethodProfileUi.js
+++ b/frontend/src/utils/combinationMethodProfileUi.js
@@ -242,6 +242,47 @@ export function readSlotProfilesV1(profileObj) {
}).filter(Boolean)
}
+/** Kurztext für Listen/strip (Coach „Plan:“ — gleiche Logik). */
+export function summarizeSlotProfileBrief(r) {
+ if (!r) return null
+ const adv = r.advance_mode || 'timed'
+ const bits = []
+ if (adv === 'timed') {
+ bits.push('Zeit')
+ if (r.load_sec != null) bits.push(`${r.load_sec}s Arbeit`)
+ } else if (adv === 'rep') {
+ bits.push('Ziel‑Wdh.')
+ const nSer = r.rep_series_count != null && r.rep_series_count >= 1 ? r.rep_series_count : 1
+ if (r.consecutive_reps != null) {
+ if (nSer >= 2) bits.push(`${nSer} Serien à ${r.consecutive_reps}×`)
+ else bits.push(`${r.consecutive_reps}×`)
+ }
+ } else {
+ bits.push('Coach')
+ const nSerMan = r.rep_series_count != null && r.rep_series_count >= 2 ? r.rep_series_count : 1
+ if (r.consecutive_reps != null) {
+ if (nSerMan >= 2) bits.push(`${nSerMan} Serien à Richtwert ${r.consecutive_reps}×`)
+ else bits.push(`Richtwert ${r.consecutive_reps}×`)
+ } else if (r.rep_series_count != null && r.rep_series_count >= 2) {
+ bits.push(`${r.rep_series_count} Serien`)
+ }
+ }
+ if (r.intra_rep_rest_sec != null) {
+ if (adv === 'timed') bits.push(`Pause ${r.intra_rep_rest_sec}s`)
+ else if (adv === 'rep' && r.rep_series_count != null && r.rep_series_count >= 2)
+ bits.push(`Pause zw. Serien ${r.intra_rep_rest_sec}s`)
+ else if (
+ adv === 'manual' &&
+ r.rep_series_count != null &&
+ r.rep_series_count >= 2
+ ) {
+ bits.push(`Pause zw. Serien ${r.intra_rep_rest_sec}s`)
+ }
+ }
+ if (r.transition_after_sec != null) bits.push(`Wechsel ${r.transition_after_sec}s`)
+ return bits.join(' · ')
+}
+
function normalizeOptionalNonNegInt(v) {
if (v === '' || v === undefined || v === null) return undefined
const n = typeof v === 'number' ? v : parseInt(String(v), 10)
diff --git a/frontend/src/utils/trainingUnitSectionsForm.js b/frontend/src/utils/trainingUnitSectionsForm.js
index ea48945..f0f2be9 100644
--- a/frontend/src/utils/trainingUnitSectionsForm.js
+++ b/frontend/src/utils/trainingUnitSectionsForm.js
@@ -276,6 +276,48 @@ export async function enrichSectionsWithVariants(sections) {
}
})
)
+
+ const titleById = new Map()
+ for (const id of unique) {
+ const row = cache.get(id)
+ const t = (row?.title || '').trim()
+ if (t) titleById.set(Number(id), t)
+ }
+ const comboCandidateExtra = new Set()
+ for (const id of unique) {
+ const row = cache.get(id)
+ if (String(row?.exercise_kind || '').toLowerCase().trim() !== 'combination') continue
+ for (const slot of row.combination_slots || []) {
+ for (const raw of slot.candidate_exercise_ids || []) {
+ const n = typeof raw === 'number' ? raw : parseInt(String(raw), 10)
+ if (Number.isFinite(n) && !titleById.has(n)) comboCandidateExtra.add(n)
+ }
+ }
+ }
+ await Promise.all(
+ [...comboCandidateExtra].map(async (cid) => {
+ try {
+ const ex = await api.getExercise(cid)
+ titleById.set(cid, ((ex.title || '').trim() || `Übung #${cid}`))
+ } catch {
+ titleById.set(cid, `Übung #${cid}`)
+ }
+ }),
+ )
+
+ function comboMemberTitleByIdForSlots(slots) {
+ const o = {}
+ for (const slot of slots || []) {
+ for (const raw of slot.candidate_exercise_ids || []) {
+ const n = typeof raw === 'number' ? raw : parseInt(String(raw), 10)
+ if (!Number.isFinite(n)) continue
+ const key = String(n)
+ if (!o[key]) o[key] = titleById.get(n) || `Übung #${n}`
+ }
+ }
+ return o
+ }
+
return sections.map((sec) => ({
...sec,
items: (sec.items || []).map((it) => {
@@ -306,7 +348,7 @@ export async function enrichSectionsWithVariants(sections) {
exercise_club_id: c.club_id,
exercise_created_by: c.created_by,
exercise_status: c.status,
- ...(isCombo ? { combination_slots: c.combination_slots || [] } : {}),
+ ...(isCombo ? { combination_slots: c.combination_slots || [], combo_member_title_by_id: comboMemberTitleByIdForSlots(c.combination_slots || []) } : {}),
}
}),
}))