diff --git a/frontend/src/app.css b/frontend/src/app.css index b094299..2503d53 100644 --- a/frontend/src/app.css +++ b/frontend/src/app.css @@ -6398,6 +6398,68 @@ a.analysis-split__nav-item { color: var(--text3); margin-right: 6px; } +.combo-plan-bracket__station-exercises--interactive { + display: flex; + flex-wrap: wrap; + align-items: baseline; + gap: 4px 6px; +} +.combo-plan-bracket__cand-inline { + display: inline-flex; + align-items: baseline; + gap: 4px; +} +.combo-plan-bracket__cand-sep { + color: var(--text3); + font-size: 0.78rem; + user-select: none; +} +.combo-plan-bracket__cand-btn { + margin: 0; + padding: 2px 8px; + font: inherit; + font-size: 0.84rem; + font-weight: 600; + color: var(--accent-dark); + background: var(--surface); + border: 1px solid var(--border); + border-radius: 8px; + cursor: pointer; + text-align: left; + line-height: 1.35; +} +.combo-plan-bracket__cand-btn:hover { + border-color: var(--accent); + background: var(--surface2); +} +.combo-plan-bracket__cand-link { + font-size: 0.84rem; + font-weight: 600; + color: var(--accent-dark); + text-decoration: underline; + text-underline-offset: 2px; +} +.combo-plan-bracket__cand-link:hover { + color: var(--accent); +} + +button.combo-coach-cand-link { + margin: 0; + padding: 0; + border: none; + background: none; + font: inherit; + font-size: 0.84rem; + font-weight: 600; + color: var(--accent); + text-decoration: underline; + cursor: pointer; + text-align: left; +} +button.combo-coach-cand-link:hover { + color: var(--accent-dark); +} + .training-run-combo-embed { margin-top: 0.65rem; } diff --git a/frontend/src/components/CombinationCoachSlots.jsx b/frontend/src/components/CombinationCoachSlots.jsx index a5286af..e2b5975 100644 --- a/frontend/src/components/CombinationCoachSlots.jsx +++ b/frontend/src/components/CombinationCoachSlots.jsx @@ -35,15 +35,26 @@ export default function CombinationCoachSlots({ 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) { - 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) + 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] @@ -282,9 +293,19 @@ export default function CombinationCoachSlots({ <>
{ex.title}
- - Im Katalog öffnen - + {typeof onOpenCandidatePeek === 'function' ? ( + + ) : ( + + Im Katalog öffnen + + )}
> ) : ( @@ -320,9 +341,19 @@ export default function CombinationCoachSlots({ ) : null}- - Volle Übungsseite - + {typeof onOpenCandidatePeek === 'function' ? ( + + ) : ( + + Volle Übungsseite + + )}
> ) diff --git a/frontend/src/components/CombinationPlanBracket.jsx b/frontend/src/components/CombinationPlanBracket.jsx index fa63cc6..28438ee 100644 --- a/frontend/src/components/CombinationPlanBracket.jsx +++ b/frontend/src/components/CombinationPlanBracket.jsx @@ -2,6 +2,7 @@ * 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, @@ -14,24 +15,22 @@ import { stationPrimaryLoadLabel, } from '../utils/combinationMethodProfileUi' -function candidateLine(slot) { - const cands = slot.candidates - if (Array.isArray(cands) && cands.length > 0) { - return cands - .map((c) => - ((c.title || '').trim() || (c.exercise_id != null ? `Übung #${c.exercise_id}` : '')).trim(), - ) - .filter(Boolean) - .join(' ↔ ') +/** @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 }) } - const ids = slot.candidate_exercise_ids || [] - return ids - .map((raw) => { - const n = typeof raw === 'number' ? raw : parseInt(String(raw), 10) - return Number.isFinite(n) ? `Übung #${n}` : '' - }) - .filter(Boolean) - .join(' ↔ ') + return out } export default function CombinationPlanBracket({ @@ -39,6 +38,9 @@ export default function CombinationPlanBracket({ 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 @@ -97,7 +99,8 @@ export default function CombinationPlanBracket({ const stationIx = Number.isFinite(ixParsed) ? ixParsed : si const displayStep = si + 1 const stationTitle = ((slot.title || '').trim() || `Station ${displayStep}`).trim() - const names = candidateLine(slot) + 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) @@ -112,7 +115,35 @@ export default function CombinationPlanBracket({