/** * 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 { describeGlobalComboProfile, effectiveStationTimingSummary, METHOD_PROFILE_GUI_FIELDS, readSlotProfilesV1, } from '../utils/combinationMethodProfileUi' function formatInlineProfileValue(val) { if (val === null || val === undefined) return '—' if (typeof val === 'boolean') return val ? 'ja' : 'nein' if (typeof val === 'number' && Number.isFinite(val)) return String(val) if (typeof val === 'string') return val.trim() === '' ? '—' : val try { return JSON.stringify(val) } catch { return String(val) } } export default function CombinationCoachSlots({ combinationSlots, methodArchetype, methodProfile, compactPlanningView = false, omitGlobalKeyValueBlock = false, /** Wenn gesetzt: Kandidaten als Button → Peek (kein Router-Wechsel, PWA-sicher) */ onOpenCandidatePeek, }) { const slots = useMemo(() => sortCombinationSlotsForDisplay(combinationSlots), [combinationSlots]) const candidateIds = useMemo(() => { const set = new Set() for (const s of slots) { if (Array.isArray(s.candidates) && s.candidates.length) { for (const c of s.candidates) { const raw = c.exercise_id if (raw == null) continue const n = typeof raw === 'number' ? raw : parseInt(String(raw), 10) if (Number.isFinite(n)) set.add(n) } } else { 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 globalComboRows = useMemo( () => describeGlobalComboProfile(archeKey, methodProfile || {}), [archeKey, methodProfile], ) const profileExtraEntries = useMemo(() => { if (!methodProfile || typeof methodProfile !== 'object' || Array.isArray(methodProfile)) return [] const known = new Set(['slot_profiles_v1']) for (const f of METHOD_PROFILE_GUI_FIELDS[archeKey] || []) { known.add(f.key) } const out = [] for (const [k, val] of Object.entries(methodProfile)) { if (known.has(k)) continue if (val === null || val === undefined || val === '') continue out.push([k, val]) } return out.sort((a, b) => String(a[0]).localeCompare(String(b[0]), 'de')) }, [methodProfile, archeKey]) return (

{compactPlanningView ? 'Stationen & Einzelübungen (Katalog)' : 'Kombination · Stationen & Einzelübungen'}

{archDisplay ? (

{archDisplay}

) : null} {compactPlanningView ? null : (

{archetypeCoachHint(archeKey)}

)} {methodProfile && typeof methodProfile === 'object' && !Array.isArray(methodProfile) && Object.keys(methodProfile).length && !omitGlobalKeyValueBlock ? (
Globale Eckdaten (wie im Editor)
{globalComboRows.length === 0 && profileExtraEntries.length === 0 ? (

Keine globalen Zahlenfelder gesetzt — Zeiten und Steuerung siehe je Station unter „Plan:“ (oder nur im Freitext der Kombination).

) : (
{globalComboRows.map((row) => (
{row.detailLabel}
{row.value}
))} {profileExtraEntries.map(([k, val]) => (
{k}
{formatInlineProfileValue(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 ${si + 1}` const ix = slot.slot_index != null ? Number(slot.slot_index) : si const timingSummary = effectiveStationTimingSummary(archeKey, methodProfile || {}, 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 ? ( compactPlanningView ? ( <>

    {ex.title}

    {typeof onOpenCandidatePeek === 'function' ? ( ) : ( Im Katalog öffnen )}

    ) : ( <>

    {ex.title}

    {ex.summary ? (
    ) : (

    Keine Kurzbeschreibung im Katalog.

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

    {typeof onOpenCandidatePeek === 'function' ? ( ) : ( Volle Übungsseite )}

    ) ) : (

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

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