diff --git a/frontend/src/components/CombinationCoachSlots.jsx b/frontend/src/components/CombinationCoachSlots.jsx index 99d5260..fbcf995 100644 --- a/frontend/src/components/CombinationCoachSlots.jsx +++ b/frontend/src/components/CombinationCoachSlots.jsx @@ -10,47 +10,7 @@ import { combinationArchetypeLabel, sortCombinationSlotsForDisplay, } from '../constants/combinationArchetypes' -import { readSlotProfilesV1 } from '../utils/combinationMethodProfileUi' - -function summarizeSlotProfilesRow(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(' · ') -} +import { readSlotProfilesV1, summarizeSlotProfileBrief } from '../utils/combinationMethodProfileUi' export default function CombinationCoachSlots({ combinationSlots, @@ -233,7 +193,7 @@ export default function CombinationCoachSlots({ `Station ${slot.slot_index != null ? Number(slot.slot_index) + 1 : si + 1}` const ix = slot.slot_index != null ? Number(slot.slot_index) : si - const timingSummary = summarizeSlotProfilesRow(slotTimingByIx.get(ix)) + const timingSummary = summarizeSlotProfileBrief(slotTimingByIx.get(ix)) return (
  • diff --git a/frontend/src/components/ExercisePeekModal.jsx b/frontend/src/components/ExercisePeekModal.jsx index 208cac4..b87613b 100644 --- a/frontend/src/components/ExercisePeekModal.jsx +++ b/frontend/src/components/ExercisePeekModal.jsx @@ -1,10 +1,13 @@ /** * Schnellansicht einer Übung aus dem Katalog (ohne die Planungsseite zu verlassen). */ -import React, { useEffect, useState } from 'react' +import React, { useEffect, useMemo, useState } from 'react' import { Link } from 'react-router-dom' import api from '../utils/api' import ExerciseRichTextBlock from './ExerciseRichTextBlock' +import CombinationCoachSlots from './CombinationCoachSlots' +import { combinationArchetypeLabel } from '../constants/combinationArchetypes' +import { effectiveComboMethodProfile } from '../utils/comboPlanningMethodProfile' function TagMini({ exercise }) { const parts = [] @@ -29,6 +32,8 @@ export default function ExercisePeekModal({ variantId, onClose, titleFallback, + /** Nur Planung: effektives method_profile aus Zeilen-Katalog + Planungs-Override */ + peekExtras, }) { const [loading, setLoading] = useState(false) const [err, setErr] = useState(null) @@ -39,6 +44,22 @@ export default function ExercisePeekModal({ ? exercise.variants.find((v) => String(v.id) === String(variantId)) || null : null + const isCombination = + exercise && + String(exercise.exercise_kind || 'simple').toLowerCase().trim() === 'combination' + + const comboMethodProfileEffective = useMemo(() => { + if (!exercise || !isCombination) return {} + const fromPeek = + peekExtras?.catalog_method_profile && + typeof peekExtras.catalog_method_profile === 'object' && + !Array.isArray(peekExtras.catalog_method_profile) && + Object.keys(peekExtras.catalog_method_profile).length > 0 + ? peekExtras.catalog_method_profile + : exercise.method_profile || {} + return effectiveComboMethodProfile(fromPeek, peekExtras?.planning_method_profile ?? null) + }, [exercise, isCombination, peekExtras]) + useEffect(() => { if (!open) { setExercise(null) @@ -69,6 +90,8 @@ export default function ExercisePeekModal({ if (!open) return null + const sheetWide = Boolean(isCombination && exercise && !loading) + return (
    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 || []) } : {}), } }), }))