shinkan-jinkendo/frontend/src/components/CombinationPlanBracket.jsx

166 lines
7.4 KiB
React
Raw Normal View History

/**
* Kombination: konsolidierte Darstellung globales Profil + Stationen mit Zeiten (Vorschau, Plan-Ansicht, Druck).
*/
import React, { useMemo } from 'react'
import { Link } from 'react-router-dom'
import {
archetypeCoachHint,
combinationArchetypeLabel,
sortCombinationSlotsForDisplay,
} from '../constants/combinationArchetypes'
import {
describeGlobalComboProfile,
effectiveStationTimingSummary,
readSlotProfilesV1,
stationPrimaryLoadLabel,
} from '../utils/combinationMethodProfileUi'
/** @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 })
}
return out
}
export default function CombinationPlanBracket({
methodArchetype,
methodProfile,
combinationSlots,
planningAdjusted = false,
/** 'none' | 'link' (Router) | 'button' (z. B. ExercisePeekModal / PWA-sicher) */
candidateInteraction = 'none',
onCandidatePeek,
}) {
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 (
<div className="combo-plan-bracket">
<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>
<span className="combo-plan-bracket__archetype">{archLabel || arch || 'Archetyp'}</span>
{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
const displayStep = si + 1
const stationTitle = ((slot.title || '').trim() || `Station ${displayStep}`).trim()
const candRows = normalizeCombinationSlotCandidates(slot)
const names = candRows.length ? candRows.map((r) => r.label).join(' ↔ ') : ''
const slotProfRow = timingByIx.get(stationIx)
const loadBadge = stationPrimaryLoadLabel(slotProfRow)
const timing = effectiveStationTimingSummary(arch, methodProfile || {}, slotProfRow)
return (
<li key={`slot-${stationIx}-${si}`} className="combo-plan-bracket__station">
<div
className="combo-plan-bracket__station-load"
title={loadBadge ? 'Belastung je Station (Sekunden oder Wiederholungen)' : undefined}
>
{loadBadge || '—'}
</div>
<div className="combo-plan-bracket__station-main">
<div className="combo-plan-bracket__station-title">{stationTitle}</div>
{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>
)}
{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>
)
}