minor improvements. Darstellung, Handlung, Popups #32

Merged
Lars merged 5 commits from develop into main 2026-05-13 22:02:43 +02:00
10 changed files with 270 additions and 109 deletions
Showing only changes of commit 502dddd3b3 - Show all commits

View File

@ -6398,6 +6398,68 @@ a.analysis-split__nav-item {
color: var(--text3); color: var(--text3);
margin-right: 6px; 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 { .training-run-combo-embed {
margin-top: 0.65rem; margin-top: 0.65rem;
} }

View File

@ -35,17 +35,28 @@ export default function CombinationCoachSlots({
methodProfile, methodProfile,
compactPlanningView = false, compactPlanningView = false,
omitGlobalKeyValueBlock = false, omitGlobalKeyValueBlock = false,
/** Wenn gesetzt: Kandidaten als Button → Peek (kein Router-Wechsel, PWA-sicher) */
onOpenCandidatePeek,
}) { }) {
const slots = useMemo(() => sortCombinationSlotsForDisplay(combinationSlots), [combinationSlots]) const slots = useMemo(() => sortCombinationSlotsForDisplay(combinationSlots), [combinationSlots])
const candidateIds = useMemo(() => { const candidateIds = useMemo(() => {
const set = new Set() const set = new Set()
for (const s of slots) { 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 || []) { for (const id of s.candidate_exercise_ids || []) {
const n = typeof id === 'number' ? id : parseInt(String(id), 10) const n = typeof id === 'number' ? id : parseInt(String(id), 10)
if (Number.isFinite(n)) set.add(n) if (Number.isFinite(n)) set.add(n)
} }
} }
}
return [...set] return [...set]
}, [slots]) }, [slots])
@ -282,9 +293,19 @@ export default function CombinationCoachSlots({
<> <>
<p style={{ margin: '0 0 4px', fontSize: '0.96rem', fontWeight: 700 }}>{ex.title}</p> <p style={{ margin: '0 0 4px', fontSize: '0.96rem', fontWeight: 700 }}>{ex.title}</p>
<p style={{ margin: 0 }}> <p style={{ margin: 0 }}>
{typeof onOpenCandidatePeek === 'function' ? (
<button
type="button"
className="combo-coach-cand-link"
onClick={() => onOpenCandidatePeek(cid)}
>
Details anzeigen
</button>
) : (
<Link to={`/exercises/${cid}`} style={{ fontSize: '0.82rem', color: 'var(--accent)' }}> <Link to={`/exercises/${cid}`} style={{ fontSize: '0.82rem', color: 'var(--accent)' }}>
Im Katalog öffnen Im Katalog öffnen
</Link> </Link>
)}
</p> </p>
</> </>
) : ( ) : (
@ -320,9 +341,19 @@ export default function CombinationCoachSlots({
</details> </details>
) : null} ) : null}
<p style={{ marginTop: '8px', marginBottom: 0 }}> <p style={{ marginTop: '8px', marginBottom: 0 }}>
{typeof onOpenCandidatePeek === 'function' ? (
<button
type="button"
className="combo-coach-cand-link"
onClick={() => onOpenCandidatePeek(cid)}
>
Volle Übungsansicht
</button>
) : (
<Link to={`/exercises/${cid}`} style={{ fontSize: '0.84rem', color: 'var(--accent)' }}> <Link to={`/exercises/${cid}`} style={{ fontSize: '0.84rem', color: 'var(--accent)' }}>
Volle Übungsseite Volle Übungsseite
</Link> </Link>
)}
</p> </p>
</> </>
) )

View File

@ -2,6 +2,7 @@
* Kombination: konsolidierte Darstellung globales Profil + Stationen mit Zeiten (Vorschau, Plan-Ansicht, Druck). * Kombination: konsolidierte Darstellung globales Profil + Stationen mit Zeiten (Vorschau, Plan-Ansicht, Druck).
*/ */
import React, { useMemo } from 'react' import React, { useMemo } from 'react'
import { Link } from 'react-router-dom'
import { import {
archetypeCoachHint, archetypeCoachHint,
combinationArchetypeLabel, combinationArchetypeLabel,
@ -14,24 +15,22 @@ import {
stationPrimaryLoadLabel, stationPrimaryLoadLabel,
} from '../utils/combinationMethodProfileUi' } from '../utils/combinationMethodProfileUi'
function candidateLine(slot) { /** @returns {{ exerciseId: number, label: string }[]} */
const cands = slot.candidates export function normalizeCombinationSlotCandidates(slot) {
if (Array.isArray(cands) && cands.length > 0) { const out = []
return cands const cands =
.map((c) => slot.candidates && slot.candidates.length
((c.title || '').trim() || (c.exercise_id != null ? `Übung #${c.exercise_id}` : '')).trim(), ? slot.candidates
) : (slot.candidate_exercise_ids || []).map((id) => ({ exercise_id: id, title: null }))
.filter(Boolean) for (const c of cands) {
.join(' ↔ ') 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 out
return ids
.map((raw) => {
const n = typeof raw === 'number' ? raw : parseInt(String(raw), 10)
return Number.isFinite(n) ? `Übung #${n}` : ''
})
.filter(Boolean)
.join(' ↔ ')
} }
export default function CombinationPlanBracket({ export default function CombinationPlanBracket({
@ -39,6 +38,9 @@ export default function CombinationPlanBracket({
methodProfile, methodProfile,
combinationSlots, combinationSlots,
planningAdjusted = false, planningAdjusted = false,
/** 'none' | 'link' (Router) | 'button' (z. B. ExercisePeekModal / PWA-sicher) */
candidateInteraction = 'none',
onCandidatePeek,
}) { }) {
const arch = typeof methodArchetype === 'string' ? methodArchetype.trim() : '' const arch = typeof methodArchetype === 'string' ? methodArchetype.trim() : ''
const archLabel = arch ? combinationArchetypeLabel(arch) : null const archLabel = arch ? combinationArchetypeLabel(arch) : null
@ -97,7 +99,8 @@ export default function CombinationPlanBracket({
const stationIx = Number.isFinite(ixParsed) ? ixParsed : si const stationIx = Number.isFinite(ixParsed) ? ixParsed : si
const displayStep = si + 1 const displayStep = si + 1
const stationTitle = ((slot.title || '').trim() || `Station ${displayStep}`).trim() 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 slotProfRow = timingByIx.get(stationIx)
const loadBadge = stationPrimaryLoadLabel(slotProfRow) const loadBadge = stationPrimaryLoadLabel(slotProfRow)
const timing = effectiveStationTimingSummary(arch, methodProfile || {}, slotProfRow) const timing = effectiveStationTimingSummary(arch, methodProfile || {}, slotProfRow)
@ -112,7 +115,35 @@ export default function CombinationPlanBracket({
</div> </div>
<div className="combo-plan-bracket__station-main"> <div className="combo-plan-bracket__station-main">
<div className="combo-plan-bracket__station-title">{stationTitle}</div> <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> <div className="combo-plan-bracket__station-exercises">{names || '(keine Einzelübung)'}</div>
)}
{timing ? ( {timing ? (
<div className="combo-plan-bracket__station-timing"> <div className="combo-plan-bracket__station-timing">
<span className="combo-plan-bracket__timing-label">Zeit / Steuerung</span> <span className="combo-plan-bracket__timing-label">Zeit / Steuerung</span>

View File

@ -54,9 +54,18 @@ function metaParts(exercise) {
} }
/** /**
* @param {{ exercise?: object|null, loading?: boolean, error?: string|null, exerciseId?: number, variantId?: number|string|null, planningComboMethodProfile?: object|null, catalogMethodProfileSnapshot?: object|null }} props * @param {{ exercise?: object|null, loading?: boolean, error?: string|null, exerciseId?: number, variantId?: number|string|null, planningComboMethodProfile?: object|null, catalogMethodProfileSnapshot?: object|null, onCandidateExercisePeek?: (exerciseId: number) => void }} props
*/ */
export default function ExerciseFullContent({ exercise, loading, error, exerciseId, variantId, planningComboMethodProfile, catalogMethodProfileSnapshot }) { export default function ExerciseFullContent({
exercise,
loading,
error,
exerciseId,
variantId,
planningComboMethodProfile,
catalogMethodProfileSnapshot,
onCandidateExercisePeek,
}) {
if (loading) { if (loading) {
return ( return (
<div style={{ textAlign: 'center', padding: '1rem' }}> <div style={{ textAlign: 'center', padding: '1rem' }}>
@ -129,6 +138,7 @@ export default function ExerciseFullContent({ exercise, loading, error, exercise
combinationSlots={exercise.combination_slots} combinationSlots={exercise.combination_slots}
methodArchetype={exercise.method_archetype} methodArchetype={exercise.method_archetype}
methodProfile={coachComboProfile} methodProfile={coachComboProfile}
onOpenCandidatePeek={onCandidateExercisePeek}
/> />
) : null} ) : null}
<h2 style={{ margin: '0 0 8px', fontSize: '1.2rem', lineHeight: 1.35 }}>{exercise.title}</h2> <h2 style={{ margin: '0 0 8px', fontSize: '1.2rem', lineHeight: 1.35 }}>{exercise.title}</h2>

View File

@ -1,7 +1,8 @@
/** /**
* Schnellansicht einer Übung aus dem Katalog (ohne die Planungsseite zu verlassen). * Schnellansicht einer Übung aus dem Katalog (ohne die Planungsseite zu verlassen).
* Unterstützt Drill-down zu Kandidaten-Übungen bei Kombinationen inkl. Zurück (PWA-sicher).
*/ */
import React, { useEffect, useMemo, useState } from 'react' import React, { useEffect, useMemo, useRef, useState } from 'react'
import { Link } from 'react-router-dom' import { Link } from 'react-router-dom'
import api from '../utils/api' import api from '../utils/api'
import ExerciseRichTextBlock from './ExerciseRichTextBlock' import ExerciseRichTextBlock from './ExerciseRichTextBlock'
@ -25,6 +26,8 @@ function TagMini({ exercise }) {
) )
} }
/** @typedef {{ exerciseId: number, variantId?: number | null, peekExtras?: object | null }} PeekStackEntry */
export default function ExercisePeekModal({ export default function ExercisePeekModal({
open, open,
exerciseId, exerciseId,
@ -37,36 +40,37 @@ export default function ExercisePeekModal({
const [loading, setLoading] = useState(false) const [loading, setLoading] = useState(false)
const [err, setErr] = useState(null) const [err, setErr] = useState(null)
const [exercise, setExercise] = useState(null) const [exercise, setExercise] = useState(null)
/** @type {[PeekStackEntry[], React.Dispatch<React.SetStateAction<PeekStackEntry[]>>]} */
const [stack, setStack] = useState([])
const variant = /** @type {React.MutableRefObject<boolean>} */
variantId != null && variantId !== '' && exercise?.variants?.length const wasOpenRef = useRef(false)
? exercise.variants.find((v) => String(v.id) === String(variantId)) || null
: null
const isCombination =
exercise &&
String(exercise.exercise_kind || 'simple').toLowerCase().trim() === 'combination'
const comboMethodProfileEffective = useMemo(() => {
if (!exercise || !isCombination) return {}
const fromPeek =
peekExtras?.catalog_method_profile &&
typeof peekExtras.catalog_method_profile === 'object' &&
!Array.isArray(peekExtras.catalog_method_profile) &&
Object.keys(peekExtras.catalog_method_profile).length > 0
? peekExtras.catalog_method_profile
: exercise.method_profile || {}
return effectiveComboMethodProfile(fromPeek, peekExtras?.planning_method_profile ?? null)
}, [exercise, isCombination, peekExtras])
useEffect(() => { useEffect(() => {
if (!open) { if (!open) {
setExercise(null) setStack([])
setErr(null) wasOpenRef.current = false
return return
} }
if (!exerciseId) { if (exerciseId == null || exerciseId === '') return
setErr('Keine Übung gewählt') if (!wasOpenRef.current) {
wasOpenRef.current = true
setStack([
{
exerciseId: Number(exerciseId),
variantId: variantId ?? null,
peekExtras: peekExtras ?? null,
},
])
}
}, [open, exerciseId, variantId, peekExtras])
const top = stack.length ? stack[stack.length - 1] : null
useEffect(() => {
if (!open || !top?.exerciseId) {
setExercise(null)
setErr(null)
return return
} }
let cancelled = false let cancelled = false
@ -74,7 +78,7 @@ export default function ExercisePeekModal({
setLoading(true) setLoading(true)
setErr(null) setErr(null)
try { try {
const data = await api.getExercise(exerciseId) const data = await api.getExercise(top.exerciseId)
if (!cancelled) setExercise(data) if (!cancelled) setExercise(data)
} catch (e) { } catch (e) {
if (!cancelled) setErr(e.message || 'Laden fehlgeschlagen') if (!cancelled) setErr(e.message || 'Laden fehlgeschlagen')
@ -85,7 +89,40 @@ export default function ExercisePeekModal({
return () => { return () => {
cancelled = true cancelled = true
} }
}, [open, exerciseId, variantId]) }, [open, top?.exerciseId])
const variant =
top?.variantId != null &&
top.variantId !== '' &&
exercise?.variants?.length
? exercise.variants.find((v) => String(v.id) === String(top.variantId)) || null
: null
const isCombination =
exercise && String(exercise.exercise_kind || 'simple').toLowerCase().trim() === 'combination'
const comboMethodProfileEffective = useMemo(() => {
if (!exercise || !isCombination) return {}
const fromPeek =
top?.peekExtras?.catalog_method_profile &&
typeof top.peekExtras.catalog_method_profile === 'object' &&
!Array.isArray(top.peekExtras.catalog_method_profile) &&
Object.keys(top.peekExtras.catalog_method_profile).length > 0
? top.peekExtras.catalog_method_profile
: exercise.method_profile || {}
return effectiveComboMethodProfile(fromPeek, top?.peekExtras?.planning_method_profile ?? null)
}, [exercise, isCombination, top?.peekExtras])
const planningAdjustedBadge =
top?.peekExtras?.planning_method_profile != null &&
typeof top.peekExtras.planning_method_profile === 'object' &&
!Array.isArray(top.peekExtras.planning_method_profile)
const pushCandidatePeek = (id) => {
const n = Number(id)
if (!Number.isFinite(n)) return
setStack((s) => [...s, { exerciseId: n, variantId: null, peekExtras: null }])
}
if (!open) return null if (!open) return null
@ -107,9 +144,19 @@ export default function ExercisePeekModal({
}} }}
onClick={(e) => e.stopPropagation()} onClick={(e) => e.stopPropagation()}
> >
<div className="admin-modal-sheet__header"> <div className="admin-modal-sheet__header" style={{ gap: '8px', flexWrap: 'wrap' }}>
<h3 id="exercise-peek-title" className="admin-modal-sheet__title"> {stack.length > 1 ? (
{loading ? '…' : exercise?.title || titleFallback || `Übung #${exerciseId}`} <button
type="button"
className="btn btn-secondary"
style={{ flexShrink: 0, fontWeight: 600 }}
onClick={() => setStack((s) => (s.length > 1 ? s.slice(0, -1) : s))}
>
Zurück
</button>
) : null}
<h3 id="exercise-peek-title" className="admin-modal-sheet__title" style={{ flex: '1 1 200px', minWidth: 0 }}>
{loading ? '…' : exercise?.title || titleFallback || (top?.exerciseId != null ? `Übung #${top.exerciseId}` : 'Übung')}
</h3> </h3>
<button type="button" className="btn btn-secondary admin-modal-sheet__close" onClick={onClose}> <button type="button" className="btn btn-secondary admin-modal-sheet__close" onClick={onClose}>
Schließen Schließen
@ -130,14 +177,10 @@ export default function ExercisePeekModal({
<CombinationPlanBracket <CombinationPlanBracket
methodArchetype={exercise.method_archetype || ''} methodArchetype={exercise.method_archetype || ''}
methodProfile={comboMethodProfileEffective} methodProfile={comboMethodProfileEffective}
combinationSlots={ combinationSlots={Array.isArray(exercise.combination_slots) ? exercise.combination_slots : []}
Array.isArray(exercise.combination_slots) ? exercise.combination_slots : [] planningAdjusted={planningAdjustedBadge}
} candidateInteraction="button"
planningAdjusted={ onCandidatePeek={pushCandidatePeek}
peekExtras?.planning_method_profile != null &&
typeof peekExtras.planning_method_profile === 'object' &&
!Array.isArray(peekExtras.planning_method_profile)
}
/> />
<hr style={{ border: 'none', borderTop: '1px solid var(--border)', margin: '1rem 0' }} /> <hr style={{ border: 'none', borderTop: '1px solid var(--border)', margin: '1rem 0' }} />
</> </>
@ -210,13 +253,17 @@ export default function ExercisePeekModal({
</> </>
)} )}
</div> </div>
{exerciseId && ( {top?.exerciseId != null ? (
<div style={{ padding: '0 1rem 1rem', flexShrink: 0 }}> <div style={{ padding: '0 1rem 1rem', flexShrink: 0 }}>
<Link to={`/exercises/${exerciseId}`} className="btn btn-secondary" style={{ width: '100%', textDecoration: 'none', textAlign: 'center', display: 'block' }}> <Link
to={`/exercises/${top.exerciseId}`}
className="btn btn-secondary"
style={{ width: '100%', textDecoration: 'none', textAlign: 'center', display: 'block' }}
>
Vollständige Übungsseite öffnen Vollständige Übungsseite öffnen
</Link> </Link>
</div> </div>
)} ) : null}
</div> </div>
</div> </div>
) )

View File

@ -1596,6 +1596,12 @@ export default function TrainingUnitSectionsEditor({
typeof comboPlanningModalItem.planning_method_profile === 'object' && typeof comboPlanningModalItem.planning_method_profile === 'object' &&
!Array.isArray(comboPlanningModalItem.planning_method_profile) !Array.isArray(comboPlanningModalItem.planning_method_profile)
} }
candidateInteraction={onPeekExercise ? 'button' : 'none'}
onCandidatePeek={
onPeekExercise
? (exId) => onPeekExercise(Number(exId), null, undefined)
: undefined
}
/> />
</div> </div>
) : ( ) : (

View File

@ -5,7 +5,6 @@ import ExerciseRichTextBlock from '../components/ExerciseRichTextBlock'
import ExerciseAttachmentMediaStrip from '../components/ExerciseAttachmentMediaStrip' import ExerciseAttachmentMediaStrip from '../components/ExerciseAttachmentMediaStrip'
import CombinationPlanBracket from '../components/CombinationPlanBracket' import CombinationPlanBracket from '../components/CombinationPlanBracket'
import { formatSkillLevelSlug } from '../constants/skillLevels' import { formatSkillLevelSlug } from '../constants/skillLevels'
import { sortCombinationSlotsForDisplay } from '../constants/combinationArchetypes'
function TagRow({ exercise }) { function TagRow({ exercise }) {
const tags = [] const tags = []
@ -52,28 +51,6 @@ function metaParts(exercise) {
return parts return parts
} }
/** Eindeutige Kandidaten-Übungen für Schnellnavigation unter der Klammerdarstellung */
function flattenCombinationCandidateLinks(slots) {
const rows = []
const seen = new Set()
sortCombinationSlotsForDisplay(slots || []).forEach((s) => {
const cands =
s.candidates && s.candidates.length
? s.candidates
: (s.candidate_exercise_ids || []).map((id) => ({
exercise_id: id,
title: null,
}))
cands.forEach((c) => {
const eid = c.exercise_id
if (eid == null || seen.has(eid)) return
seen.add(eid)
rows.push({ exercise_id: eid, title: (c.title || '').trim() || null })
})
})
return rows
}
function ExerciseDetailPage() { function ExerciseDetailPage() {
const { id } = useParams() const { id } = useParams()
const navigate = useNavigate() const navigate = useNavigate()
@ -135,9 +112,6 @@ function ExerciseDetailPage() {
(exercise.exercise_kind || '').toLowerCase().trim() === 'combination' && (exercise.exercise_kind || '').toLowerCase().trim() === 'combination' &&
Array.isArray(exercise.combination_slots) && Array.isArray(exercise.combination_slots) &&
exercise.combination_slots.length > 0 exercise.combination_slots.length > 0
const combinationCandidateLinks = isCombinationDetail
? flattenCombinationCandidateLinks(exercise.combination_slots)
: []
const catalogMethodProfileForBracket = const catalogMethodProfileForBracket =
exercise.method_profile && exercise.method_profile &&
typeof exercise.method_profile === 'object' && typeof exercise.method_profile === 'object' &&
@ -186,20 +160,9 @@ function ExerciseDetailPage() {
methodProfile={catalogMethodProfileForBracket} methodProfile={catalogMethodProfileForBracket}
combinationSlots={exercise.combination_slots} combinationSlots={exercise.combination_slots}
planningAdjusted={false} planningAdjusted={false}
candidateInteraction="link"
/> />
</div> </div>
{combinationCandidateLinks.length > 0 ? (
<div style={{ marginTop: '14px', fontSize: '0.88rem' }}>
<div style={{ fontWeight: 600, marginBottom: '6px', color: 'var(--text2)' }}>Verknüpfte Einzelübungen</div>
<ul style={{ margin: 0, paddingLeft: '1.2rem' }}>
{combinationCandidateLinks.map((c) => (
<li key={c.exercise_id}>
<Link to={`/exercises/${c.exercise_id}`}>{c.title || `Übung #${c.exercise_id}`}</Link>
</li>
))}
</ul>
</div>
) : null}
</section> </section>
) : null} ) : null}

View File

@ -5,6 +5,7 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react'
import { Link, useNavigate, useParams } from 'react-router-dom' import { Link, useNavigate, useParams } from 'react-router-dom'
import api from '../utils/api' import api from '../utils/api'
import ExerciseFullContent from '../components/ExerciseFullContent' import ExerciseFullContent from '../components/ExerciseFullContent'
import ExercisePeekModal from '../components/ExercisePeekModal'
import { import {
flattenPlanTimeline, flattenPlanTimeline,
itemStableKey, itemStableKey,
@ -178,6 +179,7 @@ export default function TrainingCoachPage() {
const [trainerAppend, setTrainerAppend] = useState('') const [trainerAppend, setTrainerAppend] = useState('')
const [saveMarkDone, setSaveMarkDone] = useState(true) const [saveMarkDone, setSaveMarkDone] = useState(true)
const [saving, setSaving] = useState(false) const [saving, setSaving] = useState(false)
const [candidatePeekId, setCandidatePeekId] = useState(null)
const [saveOk, setSaveOk] = useState(null) const [saveOk, setSaveOk] = useState(null)
const reloadUnit = useCallback(async () => { const reloadUnit = useCallback(async () => {
@ -460,6 +462,12 @@ export default function TrainingCoachPage() {
return ( return (
<div className="training-coach-page training-coach-layout"> <div className="training-coach-page training-coach-layout">
<ExercisePeekModal
key={candidatePeekId != null ? String(candidatePeekId) : 'coach-peek-closed'}
open={candidatePeekId != null}
exerciseId={candidatePeekId}
onClose={() => setCandidatePeekId(null)}
/>
<nav <nav
className="no-print training-coach-meta-nav" className="no-print training-coach-meta-nav"
style={{ style={{
@ -750,6 +758,7 @@ export default function TrainingCoachPage() {
? currentEntry?.item?.planning_method_profile ?? null ? currentEntry?.item?.planning_method_profile ?? null
: null : null
} }
onCandidateExercisePeek={(id) => setCandidatePeekId(id)}
/> />
</div> </div>
</> </>

View File

@ -1223,6 +1223,7 @@ export default function TrainingFrameworkProgramEditPage() {
/> />
<ExercisePeekModal <ExercisePeekModal
key={peekCtx != null ? String(peekCtx.exerciseId) : 'fw-peek-closed'}
open={peekCtx != null} open={peekCtx != null}
exerciseId={peekCtx?.exerciseId || 0} exerciseId={peekCtx?.exerciseId || 0}
variantId={peekCtx?.variantId ?? undefined} variantId={peekCtx?.variantId ?? undefined}

View File

@ -3087,6 +3087,7 @@ function TrainingPlanningPage() {
}} }}
/> />
<ExercisePeekModal <ExercisePeekModal
key={planningPeekCtx != null ? String(planningPeekCtx.exerciseId) : 'plan-peek-closed'}
open={planningPeekCtx != null} open={planningPeekCtx != null}
exerciseId={planningPeekCtx?.exerciseId} exerciseId={planningPeekCtx?.exerciseId}
variantId={planningPeekCtx?.variantId ?? undefined} variantId={planningPeekCtx?.variantId ?? undefined}