2026-05-13 14:24:55 +02:00
|
|
|
|
/**
|
|
|
|
|
|
* Kombination: konsolidierte Darstellung globales Profil + Stationen mit Zeiten (Vorschau, Plan-Ansicht, Druck).
|
|
|
|
|
|
*/
|
|
|
|
|
|
import React, { useMemo } from 'react'
|
2026-05-13 21:51:52 +02:00
|
|
|
|
import { Link } from 'react-router-dom'
|
2026-05-13 14:24:55 +02:00
|
|
|
|
import {
|
|
|
|
|
|
archetypeCoachHint,
|
|
|
|
|
|
combinationArchetypeLabel,
|
|
|
|
|
|
sortCombinationSlotsForDisplay,
|
|
|
|
|
|
} from '../constants/combinationArchetypes'
|
2026-05-13 15:28:37 +02:00
|
|
|
|
import {
|
|
|
|
|
|
describeGlobalComboProfile,
|
|
|
|
|
|
effectiveStationTimingSummary,
|
|
|
|
|
|
readSlotProfilesV1,
|
|
|
|
|
|
stationPrimaryLoadLabel,
|
|
|
|
|
|
} from '../utils/combinationMethodProfileUi'
|
2026-05-13 14:24:55 +02:00
|
|
|
|
|
2026-05-13 21:51:52 +02:00
|
|
|
|
/** @returns {{ exerciseId: number, label: string }[]} */
|
|
|
|
|
|
export function normalizeCombinationSlotCandidates(slot) {
|
|
|
|
|
|
const out = []
|
|
|
|
|
|
const cands =
|
|
|
|
|
|
slot.candidates && slot.candidates.length
|
|
|
|
|
|
? slot.candidates
|
|
|
|
|
|
: (slot.candidate_exercise_ids || []).map((id) => ({ exercise_id: id, title: null }))
|
|
|
|
|
|
for (const c of cands) {
|
|
|
|
|
|
const rawId = c.exercise_id
|
|
|
|
|
|
if (rawId == null) continue
|
|
|
|
|
|
const n = typeof rawId === 'number' ? rawId : parseInt(String(rawId), 10)
|
|
|
|
|
|
if (!Number.isFinite(n)) continue
|
|
|
|
|
|
const label = ((c.title || '').trim() || `Übung #${n}`).trim()
|
|
|
|
|
|
out.push({ exerciseId: n, label })
|
2026-05-13 14:24:55 +02:00
|
|
|
|
}
|
2026-05-13 21:51:52 +02:00
|
|
|
|
return out
|
2026-05-13 14:24:55 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export default function CombinationPlanBracket({
|
|
|
|
|
|
methodArchetype,
|
|
|
|
|
|
methodProfile,
|
|
|
|
|
|
combinationSlots,
|
|
|
|
|
|
planningAdjusted = false,
|
2026-05-13 21:51:52 +02:00
|
|
|
|
/** 'none' | 'link' (Router) | 'button' (z. B. ExercisePeekModal / PWA-sicher) */
|
|
|
|
|
|
candidateInteraction = 'none',
|
|
|
|
|
|
onCandidatePeek,
|
2026-05-14 09:05:15 +02:00
|
|
|
|
className,
|
2026-05-13 14:24:55 +02:00
|
|
|
|
}) {
|
|
|
|
|
|
const arch = typeof methodArchetype === 'string' ? methodArchetype.trim() : ''
|
|
|
|
|
|
const archLabel = arch ? combinationArchetypeLabel(arch) : null
|
|
|
|
|
|
const globals = describeGlobalComboProfile(arch, methodProfile || {})
|
|
|
|
|
|
const slotsSorted = useMemo(() => sortCombinationSlotsForDisplay(combinationSlots || []), [combinationSlots])
|
|
|
|
|
|
const timingByIx = useMemo(() => {
|
|
|
|
|
|
const mp = methodProfile || {}
|
|
|
|
|
|
const rows = readSlotProfilesV1(mp)
|
|
|
|
|
|
const m = new Map()
|
|
|
|
|
|
for (const r of rows) {
|
|
|
|
|
|
m.set(Number(r.slot_index), r)
|
|
|
|
|
|
}
|
|
|
|
|
|
return m
|
|
|
|
|
|
}, [methodProfile])
|
|
|
|
|
|
|
|
|
|
|
|
const coachHint = arch ? archetypeCoachHint(arch) : ''
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
2026-05-14 09:05:15 +02:00
|
|
|
|
<div className={['combo-plan-bracket', className].filter(Boolean).join(' ')}>
|
2026-05-13 14:24:55 +02:00
|
|
|
|
<div className="combo-plan-bracket__accent" aria-hidden />
|
|
|
|
|
|
<div className="combo-plan-bracket__body">
|
|
|
|
|
|
<header className="combo-plan-bracket__head">
|
|
|
|
|
|
<div className="combo-plan-bracket__head-main">
|
|
|
|
|
|
<span className="combo-plan-bracket__kicker">Kombinations‑Plan</span>
|
2026-05-13 14:42:01 +02:00
|
|
|
|
<span className="combo-plan-bracket__archetype">{archLabel || arch || 'Archetyp'}</span>
|
2026-05-13 14:24:55 +02:00
|
|
|
|
{planningAdjusted ? (
|
|
|
|
|
|
<span className="combo-plan-bracket__badge">Planung angepasst</span>
|
|
|
|
|
|
) : null}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
{coachHint ? <p className="combo-plan-bracket__hint">{coachHint}</p> : null}
|
|
|
|
|
|
</header>
|
|
|
|
|
|
|
|
|
|
|
|
{globals.length > 0 ? (
|
|
|
|
|
|
<section className="combo-plan-bracket__globals" aria-label="Globale Eckdaten">
|
|
|
|
|
|
<div className="combo-plan-bracket__globals-title">Runden · Zeiten · Pausen (global)</div>
|
|
|
|
|
|
<ul className="combo-plan-bracket__chip-row">
|
|
|
|
|
|
{globals.map((g) => (
|
|
|
|
|
|
<li key={g.key} className="combo-plan-bracket__chip" title={g.detailLabel}>
|
|
|
|
|
|
<span className="combo-plan-bracket__chip-cap">{g.caption}</span>
|
|
|
|
|
|
<span className="combo-plan-bracket__chip-val">{g.value}</span>
|
|
|
|
|
|
</li>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</ul>
|
|
|
|
|
|
</section>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<p className="combo-plan-bracket__globals-empty">
|
|
|
|
|
|
Keine globalen Zahlenfelder gesetzt — Steuerung erfolgt nur je Station oder über den Freitext der Kombination.
|
|
|
|
|
|
</p>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
<ol className="combo-plan-bracket__stations">
|
|
|
|
|
|
{slotsSorted.map((slot, si) => {
|
|
|
|
|
|
const siRaw = slot.slot_index
|
|
|
|
|
|
const ixParsed =
|
|
|
|
|
|
siRaw === '' || siRaw == null ? si : typeof siRaw === 'number' ? siRaw : parseInt(String(siRaw), 10)
|
|
|
|
|
|
const stationIx = Number.isFinite(ixParsed) ? ixParsed : si
|
2026-05-13 14:34:17 +02:00
|
|
|
|
const displayStep = si + 1
|
|
|
|
|
|
const stationTitle = ((slot.title || '').trim() || `Station ${displayStep}`).trim()
|
2026-05-13 21:51:52 +02:00
|
|
|
|
const candRows = normalizeCombinationSlotCandidates(slot)
|
|
|
|
|
|
const names = candRows.length ? candRows.map((r) => r.label).join(' ↔ ') : ''
|
2026-05-13 15:28:37 +02:00
|
|
|
|
const slotProfRow = timingByIx.get(stationIx)
|
|
|
|
|
|
const loadBadge = stationPrimaryLoadLabel(slotProfRow)
|
|
|
|
|
|
const timing = effectiveStationTimingSummary(arch, methodProfile || {}, slotProfRow)
|
2026-05-13 14:24:55 +02:00
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<li key={`slot-${stationIx}-${si}`} className="combo-plan-bracket__station">
|
2026-05-13 15:28:37 +02:00
|
|
|
|
<div
|
|
|
|
|
|
className="combo-plan-bracket__station-load"
|
|
|
|
|
|
title={loadBadge ? 'Belastung je Station (Sekunden oder Wiederholungen)' : undefined}
|
|
|
|
|
|
>
|
|
|
|
|
|
{loadBadge || '—'}
|
|
|
|
|
|
</div>
|
2026-05-13 14:24:55 +02:00
|
|
|
|
<div className="combo-plan-bracket__station-main">
|
|
|
|
|
|
<div className="combo-plan-bracket__station-title">{stationTitle}</div>
|
2026-05-13 21:51:52 +02:00
|
|
|
|
{candidateInteraction === 'button' && typeof onCandidatePeek === 'function' && candRows.length > 0 ? (
|
|
|
|
|
|
<div className="combo-plan-bracket__station-exercises combo-plan-bracket__station-exercises--interactive">
|
|
|
|
|
|
{candRows.map((c, ci) => (
|
|
|
|
|
|
<span key={`${c.exerciseId}-${ci}`} className="combo-plan-bracket__cand-inline">
|
|
|
|
|
|
{ci > 0 ? <span className="combo-plan-bracket__cand-sep">↔</span> : null}
|
|
|
|
|
|
<button
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
className="combo-plan-bracket__cand-btn"
|
|
|
|
|
|
onClick={() => onCandidatePeek(c.exerciseId, c.label)}
|
|
|
|
|
|
>
|
|
|
|
|
|
{c.label}
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
) : candidateInteraction === 'link' && candRows.length > 0 ? (
|
|
|
|
|
|
<div className="combo-plan-bracket__station-exercises combo-plan-bracket__station-exercises--interactive">
|
|
|
|
|
|
{candRows.map((c, ci) => (
|
|
|
|
|
|
<span key={`${c.exerciseId}-${ci}`} className="combo-plan-bracket__cand-inline">
|
|
|
|
|
|
{ci > 0 ? <span className="combo-plan-bracket__cand-sep">↔</span> : null}
|
|
|
|
|
|
<Link to={`/exercises/${c.exerciseId}`} className="combo-plan-bracket__cand-link">
|
|
|
|
|
|
{c.label}
|
|
|
|
|
|
</Link>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
))}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<div className="combo-plan-bracket__station-exercises">{names || '(keine Einzelübung)'}</div>
|
|
|
|
|
|
)}
|
2026-05-13 14:24:55 +02:00
|
|
|
|
{timing ? (
|
|
|
|
|
|
<div className="combo-plan-bracket__station-timing">
|
|
|
|
|
|
<span className="combo-plan-bracket__timing-label">Zeit / Steuerung</span>
|
|
|
|
|
|
<span>{timing}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<div className="combo-plan-bracket__station-timing combo-plan-bracket__station-timing--muted">
|
|
|
|
|
|
Keine eigene Stations‑Zeit im Profil — ggf. nur globale Vorgaben oder Freitext.
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</li>
|
|
|
|
|
|
)
|
|
|
|
|
|
})}
|
|
|
|
|
|
</ol>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|