/** * Kombinationsübung im Coach: Archetyp-Hinweis + Katalog-Inhalt je Slot/Kandidat. */ import React, { useEffect, useMemo, useState } from 'react' import { Link } from 'react-router-dom' import api from '../utils/api' import ExerciseRichTextBlock from './ExerciseRichTextBlock' import { archetypeCoachHint, 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.') if (r.consecutive_reps != null) bits.push(`${r.consecutive_reps}×`) } else { bits.push('Coach') if (r.consecutive_reps != null) bits.push(`Richtwert ${r.consecutive_reps}×`) } if (r.intra_rep_rest_sec != null) bits.push(`Pause ${r.intra_rep_rest_sec}s`) if (r.transition_after_sec != null) bits.push(`Wechsel ${r.transition_after_sec}s`) return bits.join(' · ') } export default function CombinationCoachSlots({ combinationSlots, methodArchetype, methodProfile }) { const candidateIds = useMemo(() => { const set = new Set() for (const s of slots) { for (const id of s.candidate_exercise_ids || []) { const n = typeof id === 'number' ? id : parseInt(String(id), 10) if (Number.isFinite(n)) set.add(n) } } return [...set] }, [slots]) const [byId, setById] = useState({}) const [errById, setErrById] = useState({}) const [loadingIds, setLoadingIds] = useState(false) const sig = candidateIds.slice().sort((a, b) => a - b).join(',') useEffect(() => { let cancelled = false setById({}) setErrById({}) if (candidateIds.length === 0) { setLoadingIds(false) return () => { cancelled = true } } setLoadingIds(true) Promise.all( candidateIds.map((id) => api.getExercise(id).then( (ex) => ({ id, ok: true, ex }), (e) => ({ id, ok: false, err: e?.message || String(e), }), ), ), ).then((results) => { if (cancelled) return const map = {} const emap = {} for (const r of results) { if (r.ok) map[r.id] = r.ex else emap[r.id] = r.err } setById(map) setErrById(emap) setLoadingIds(false) }) return () => { cancelled = true } }, [sig]) const archeKey = methodArchetype != null ? String(methodArchetype).trim() : '' const archDisplay = archeKey ? combinationArchetypeLabel(archeKey) : null const slotTimingByIx = useMemo(() => { if (!methodProfile || typeof methodProfile !== 'object' || Array.isArray(methodProfile)) return new Map() const rows = readSlotProfilesV1(methodProfile) const m = new Map() for (const r of rows) { m.set(Number(r.slot_index), r) } return m }, [methodProfile]) const methodProfileKvSansSlots = useMemo(() => { if (!methodProfile || typeof methodProfile !== 'object' || Array.isArray(methodProfile)) return [] return Object.entries(methodProfile) .filter(([k]) => k !== 'slot_profiles_v1') .sort(([a], [b]) => a.localeCompare(b, 'de')) }, [methodProfile]) return (

Kombination · Stationen & Einzelübungen

{archDisplay ? (

{archDisplay} {archeKey && archDisplay !== archeKey ? ( ({archeKey}) ) : null}

) : null}

{archetypeCoachHint(archeKey)}

{methodProfile && typeof methodProfile === 'object' && !Array.isArray(methodProfile) && Object.keys(methodProfile).length ? (
Geplantes Ablaufprofil (Katalog)
{methodProfileKvSansSlots.length === 0 ? (

Nur stationsbezogene Daten (Zeiten/Zähl‑Steuerung) — siehe je Station unter der Überschrift „Plan:“.

) : (
{methodProfileKvSansSlots.map(([k, val]) => (
{k}
{typeof val === 'boolean' ? val ? 'ja' : 'nein' : typeof val === 'number' ? String(val) : typeof val === 'string' ? val : JSON.stringify(val)}
))}
)}
) : null} {!slots.length ? (

Keine Stationen hinterlegt.

) : (
    {slots.map((slot, si) => { const candIdsRaw = slot.candidate_exercise_ids || [] const candIds = candIdsRaw .map((id) => (typeof id === 'number' ? id : parseInt(String(id), 10))) .filter((n) => Number.isFinite(n)) const slotTitle = (slot.title && String(slot.title).trim()) || (candIds.length <= 1 && slot.candidates?.[0]?.title) || `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)) return (
  1. 1 ? '6px' : '8px' }}> {slotTitle}
    {timingSummary ? (

    Plan: {timingSummary}

    ) : null} {candIds.length === 0 ? (

    Keine Übung zugeordnet.

    ) : (
    {candIds.map((cid, ci) => { const ex = byId[cid] const err = errById[cid] const candTitleFallback = slot.candidates?.find((c) => Number(c.exercise_id) === cid)?.title || slot.candidates?.[ci]?.title const isAlt = candIds.length > 1 return (
    {isAlt ? (
    {candIds.length > 2 ? `Alternative ${ci + 1}` : ci === 0 ? 'Alternative A' : 'Alternative B'}
    ) : null} {!ex && loadingIds ? (
    Übung #{cid} laden…
    ) : err ? (

    Übung #{cid}: {err}

    ) : ex ? ( <>

    {ex.title}

    {ex.summary ? (
    ) : (

    Keine Kurzbeschreibung im Katalog.

    )} {ex.execution ? (
    Ablauf (Detail)
    ) : null} {ex.trainer_notes ? (
    Hinweise Trainer
    ) : null}

    Volle Übungsseite

    ) : (

    {candTitleFallback || `Übung #${cid}`}

    )}
    ) })}
    )}
  2. ) })}
)}
) }