shinkan-jinkendo/frontend/src/components/CombinationPlanBracket.jsx
Lars 930a786315
All checks were successful
Deploy Development / deploy (push) Successful in 40s
Test Suite / pytest-backend (push) Successful in 36s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 12s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m5s
Test Suite / pytest-backend (pull_request) Successful in 34s
Test Suite / lint-backend (pull_request) Successful in 0s
Test Suite / build-frontend (pull_request) Successful in 11s
Test Suite / k6 /health Baseline (pull_request) Successful in 33s
Test Suite / playwright-tests (pull_request) Successful in 1m8s
refactor(ui): enhance styling and structure of training unit sections and combination plan bracket
- Updated CSS for training unit sections to improve layout and responsiveness, ensuring combo planning strips are displayed correctly.
- Refactored CombinationPlanBracket component to accept additional class names for better customization.
- Removed unused functions and streamlined imports in TrainingUnitSectionsEditor for cleaner code.
- Reintroduced ExercisePickerModal with improved placement in ExerciseFormPage for better user experience.
2026-05-14 09:05:15 +02:00

167 lines
7.4 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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,
className,
}) {
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', className].filter(Boolean).join(' ')}>
<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>
)
}