feat(training-unit-editor): integrate new summary function and enhance combination exercise display
All checks were successful
Deploy Development / deploy (push) Successful in 38s
Test Suite / pytest-backend (push) Successful in 35s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 11s
Test Suite / playwright-tests (push) Successful in 56s
All checks were successful
Deploy Development / deploy (push) Successful in 38s
Test Suite / pytest-backend (push) Successful in 35s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 11s
Test Suite / playwright-tests (push) Successful in 56s
- Introduced `summarizeSlotProfileBrief` utility for concise slot profile summaries, improving the display of combination exercises. - Updated `CombinationCoachSlots` and `ExercisePeekModal` components to utilize the new summary function for better user experience. - Enhanced `TrainingUnitSectionsEditor` to manage combination slots more effectively, including improved title handling and display options. - Adjusted `TrainingPlanningPage` to support additional peek context for combination exercises, streamlining the planning process. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
805ad3c5a5
commit
5dc93d9a8c
|
|
@ -10,47 +10,7 @@ import {
|
||||||
combinationArchetypeLabel,
|
combinationArchetypeLabel,
|
||||||
sortCombinationSlotsForDisplay,
|
sortCombinationSlotsForDisplay,
|
||||||
} from '../constants/combinationArchetypes'
|
} from '../constants/combinationArchetypes'
|
||||||
import { readSlotProfilesV1 } from '../utils/combinationMethodProfileUi'
|
import { readSlotProfilesV1, summarizeSlotProfileBrief } from '../utils/combinationMethodProfileUi'
|
||||||
|
|
||||||
function summarizeSlotProfilesRow(r) {
|
|
||||||
if (!r) return null
|
|
||||||
const adv = r.advance_mode || 'timed'
|
|
||||||
const bits = []
|
|
||||||
if (adv === 'timed') {
|
|
||||||
bits.push('Zeit')
|
|
||||||
if (r.load_sec != null) bits.push(`${r.load_sec}s Arbeit`)
|
|
||||||
} else if (adv === 'rep') {
|
|
||||||
bits.push('Ziel‑Wdh.')
|
|
||||||
const nSer = r.rep_series_count != null && r.rep_series_count >= 1 ? r.rep_series_count : 1
|
|
||||||
if (r.consecutive_reps != null) {
|
|
||||||
if (nSer >= 2) bits.push(`${nSer} Serien à ${r.consecutive_reps}×`)
|
|
||||||
else bits.push(`${r.consecutive_reps}×`)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
bits.push('Coach')
|
|
||||||
const nSerMan = r.rep_series_count != null && r.rep_series_count >= 2 ? r.rep_series_count : 1
|
|
||||||
if (r.consecutive_reps != null) {
|
|
||||||
if (nSerMan >= 2) bits.push(`${nSerMan} Serien à Richtwert ${r.consecutive_reps}×`)
|
|
||||||
else bits.push(`Richtwert ${r.consecutive_reps}×`)
|
|
||||||
} else if (r.rep_series_count != null && r.rep_series_count >= 2) {
|
|
||||||
bits.push(`${r.rep_series_count} Serien`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (r.intra_rep_rest_sec != null) {
|
|
||||||
if (adv === 'timed') bits.push(`Pause ${r.intra_rep_rest_sec}s`)
|
|
||||||
else if (adv === 'rep' && r.rep_series_count != null && r.rep_series_count >= 2)
|
|
||||||
bits.push(`Pause zw. Serien ${r.intra_rep_rest_sec}s`)
|
|
||||||
else if (
|
|
||||||
adv === 'manual' &&
|
|
||||||
r.rep_series_count != null &&
|
|
||||||
r.rep_series_count >= 2
|
|
||||||
) {
|
|
||||||
bits.push(`Pause zw. Serien ${r.intra_rep_rest_sec}s`)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (r.transition_after_sec != null) bits.push(`Wechsel ${r.transition_after_sec}s`)
|
|
||||||
return bits.join(' · ')
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function CombinationCoachSlots({
|
export default function CombinationCoachSlots({
|
||||||
combinationSlots,
|
combinationSlots,
|
||||||
|
|
@ -233,7 +193,7 @@ export default function CombinationCoachSlots({
|
||||||
`Station ${slot.slot_index != null ? Number(slot.slot_index) + 1 : si + 1}`
|
`Station ${slot.slot_index != null ? Number(slot.slot_index) + 1 : si + 1}`
|
||||||
|
|
||||||
const ix = slot.slot_index != null ? Number(slot.slot_index) : si
|
const ix = slot.slot_index != null ? Number(slot.slot_index) : si
|
||||||
const timingSummary = summarizeSlotProfilesRow(slotTimingByIx.get(ix))
|
const timingSummary = summarizeSlotProfileBrief(slotTimingByIx.get(ix))
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li key={`${slot.slot_index ?? si}-${slotTitle}`} style={{ lineHeight: 1.45 }}>
|
<li key={`${slot.slot_index ?? si}-${slotTitle}`} style={{ lineHeight: 1.45 }}>
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,13 @@
|
||||||
/**
|
/**
|
||||||
* Schnellansicht einer Übung aus dem Katalog (ohne die Planungsseite zu verlassen).
|
* Schnellansicht einer Übung aus dem Katalog (ohne die Planungsseite zu verlassen).
|
||||||
*/
|
*/
|
||||||
import React, { useEffect, useState } from 'react'
|
import React, { useEffect, useMemo, 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'
|
||||||
|
import CombinationCoachSlots from './CombinationCoachSlots'
|
||||||
|
import { combinationArchetypeLabel } from '../constants/combinationArchetypes'
|
||||||
|
import { effectiveComboMethodProfile } from '../utils/comboPlanningMethodProfile'
|
||||||
|
|
||||||
function TagMini({ exercise }) {
|
function TagMini({ exercise }) {
|
||||||
const parts = []
|
const parts = []
|
||||||
|
|
@ -29,6 +32,8 @@ export default function ExercisePeekModal({
|
||||||
variantId,
|
variantId,
|
||||||
onClose,
|
onClose,
|
||||||
titleFallback,
|
titleFallback,
|
||||||
|
/** Nur Planung: effektives method_profile aus Zeilen-Katalog + Planungs-Override */
|
||||||
|
peekExtras,
|
||||||
}) {
|
}) {
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const [err, setErr] = useState(null)
|
const [err, setErr] = useState(null)
|
||||||
|
|
@ -39,6 +44,22 @@ export default function ExercisePeekModal({
|
||||||
? exercise.variants.find((v) => String(v.id) === String(variantId)) || null
|
? exercise.variants.find((v) => String(v.id) === String(variantId)) || null
|
||||||
: 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)
|
setExercise(null)
|
||||||
|
|
@ -69,6 +90,8 @@ export default function ExercisePeekModal({
|
||||||
|
|
||||||
if (!open) return null
|
if (!open) return null
|
||||||
|
|
||||||
|
const sheetWide = Boolean(isCombination && exercise && !loading)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="admin-modal-backdrop" role="presentation" onClick={(e) => e.target === e.currentTarget && onClose()}>
|
<div className="admin-modal-backdrop" role="presentation" onClick={(e) => e.target === e.currentTarget && onClose()}>
|
||||||
<div
|
<div
|
||||||
|
|
@ -77,7 +100,7 @@ export default function ExercisePeekModal({
|
||||||
aria-modal="true"
|
aria-modal="true"
|
||||||
aria-labelledby="exercise-peek-title"
|
aria-labelledby="exercise-peek-title"
|
||||||
style={{
|
style={{
|
||||||
maxWidth: '620px',
|
maxWidth: sheetWide ? 'min(760px, 96vw)' : '620px',
|
||||||
width: '100%',
|
width: '100%',
|
||||||
maxHeight: '88vh',
|
maxHeight: '88vh',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
|
|
@ -103,6 +126,49 @@ export default function ExercisePeekModal({
|
||||||
{!loading && err && <p style={{ color: 'var(--danger)' }}>{err}</p>}
|
{!loading && err && <p style={{ color: 'var(--danger)' }}>{err}</p>}
|
||||||
{!loading && exercise && (
|
{!loading && exercise && (
|
||||||
<>
|
<>
|
||||||
|
{isCombination ? (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
marginBottom: '12px',
|
||||||
|
padding: '8px 10px',
|
||||||
|
borderRadius: '8px',
|
||||||
|
background: 'var(--surface2)',
|
||||||
|
border: '1px solid var(--border)',
|
||||||
|
fontSize: '0.82rem',
|
||||||
|
color: 'var(--text2)',
|
||||||
|
lineHeight: 1.45,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<strong style={{ color: 'var(--text1)' }}>Kombination</strong>
|
||||||
|
<span style={{ marginLeft: 8 }}>
|
||||||
|
{(() => {
|
||||||
|
const ak = String(exercise.method_archetype || '').trim()
|
||||||
|
const lbl = ak ? combinationArchetypeLabel(ak) : null
|
||||||
|
return lbl || ak || 'Archetyp nicht gesetzt'
|
||||||
|
})()}
|
||||||
|
</span>
|
||||||
|
{peekExtras?.planning_method_profile != null &&
|
||||||
|
typeof peekExtras.planning_method_profile === 'object' &&
|
||||||
|
!Array.isArray(peekExtras.planning_method_profile) ? (
|
||||||
|
<span style={{ marginLeft: 8, color: 'var(--accent-dark)', fontWeight: 600 }}>
|
||||||
|
· Planung angepasst
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
<CombinationCoachSlots
|
||||||
|
combinationSlots={
|
||||||
|
Array.isArray(exercise.combination_slots) ? exercise.combination_slots : []
|
||||||
|
}
|
||||||
|
methodArchetype={exercise.method_archetype || ''}
|
||||||
|
methodProfile={comboMethodProfileEffective}
|
||||||
|
compactPlanningView
|
||||||
|
omitGlobalKeyValueBlock={false}
|
||||||
|
/>
|
||||||
|
<hr style={{ border: 'none', borderTop: '1px solid var(--border)', margin: '1rem 0' }} />
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
|
||||||
{variant ? (
|
{variant ? (
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import { GripVertical, Pencil } from 'lucide-react'
|
||||||
import CombinationMethodProfileEditor from './CombinationMethodProfileEditor'
|
import CombinationMethodProfileEditor from './CombinationMethodProfileEditor'
|
||||||
import CombinationCoachSlots from './CombinationCoachSlots'
|
import CombinationCoachSlots from './CombinationCoachSlots'
|
||||||
import { comboPlanningProfileJsonForEditor, effectiveComboMethodProfile } from '../utils/comboPlanningMethodProfile'
|
import { comboPlanningProfileJsonForEditor, effectiveComboMethodProfile } from '../utils/comboPlanningMethodProfile'
|
||||||
import { combinationArchetypeLabel } from '../constants/combinationArchetypes'
|
import { combinationArchetypeLabel, sortCombinationSlotsForDisplay } from '../constants/combinationArchetypes'
|
||||||
import {
|
import {
|
||||||
comboSlotsOutlineForProfileEditor,
|
comboSlotsOutlineForProfileEditor,
|
||||||
defaultSection,
|
defaultSection,
|
||||||
|
|
@ -12,6 +12,7 @@ import {
|
||||||
sectionPlannedMinutes,
|
sectionPlannedMinutes,
|
||||||
} from '../utils/trainingUnitSectionsForm'
|
} from '../utils/trainingUnitSectionsForm'
|
||||||
import api from '../utils/api'
|
import api from '../utils/api'
|
||||||
|
import { readSlotProfilesV1, summarizeSlotProfileBrief } from '../utils/combinationMethodProfileUi'
|
||||||
import { isCompactTagLegendMode } from '../config/planningModuleUx'
|
import { isCompactTagLegendMode } from '../config/planningModuleUx'
|
||||||
import { useAuth } from '../context/AuthContext'
|
import { useAuth } from '../context/AuthContext'
|
||||||
|
|
||||||
|
|
@ -77,6 +78,59 @@ function compactComboPlanningCaption(it) {
|
||||||
return archLbl ? `${archLbl} · wie Katalog` : 'wie im Katalog'
|
return archLbl ? `${archLbl} · wie Katalog` : 'wie im Katalog'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Globale Eckdaten aus effective profile (optional unter Stationenliste). */
|
||||||
|
function comboRoughGlobalTimingHint(profileObj, archetypeKey) {
|
||||||
|
if (!profileObj || typeof profileObj !== 'object' || Array.isArray(profileObj)) return null
|
||||||
|
const bits = []
|
||||||
|
const rounds = profileObj.rounds
|
||||||
|
const ws = profileObj.work_seconds
|
||||||
|
const rb = profileObj.rest_between_rounds_sec
|
||||||
|
const hint = profileObj.hint_step_duration_sec
|
||||||
|
const globRest = profileObj.rest_between_sets_sec
|
||||||
|
if (rounds != null && rounds !== '') bits.push(`${rounds} Runden`)
|
||||||
|
if (ws != null && ws !== '') bits.push(`${ws}s Arbeit`)
|
||||||
|
if (rb != null && rb !== '') bits.push(`Pause ${rb}s`)
|
||||||
|
if (globRest != null && globRest !== '') bits.push(`Sets-Pause ${globRest}s`)
|
||||||
|
if (hint != null && hint !== '') bits.push(`Orientierung ~${hint}s`)
|
||||||
|
const arch = (archetypeKey || '').trim()
|
||||||
|
if (arch === 'time_domain_interval') {
|
||||||
|
const iw = profileObj.interval_work_sec
|
||||||
|
const ir = profileObj.interval_rest_sec
|
||||||
|
const ig = profileObj.interval_groups
|
||||||
|
if (iw != null && iw !== '') bits.push(`${iw}s Intervall`)
|
||||||
|
if (ir != null && ir !== '') bits.push(`${ir}s Erholung`)
|
||||||
|
if (ig != null && ig !== '') bits.push(`${ig} Gruppen`)
|
||||||
|
}
|
||||||
|
return bits.length ? bits.join(' · ') : null
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Pro Station eine kompakte Textzeile für die Planungsliste. */
|
||||||
|
function comboPlanningStripBulletTexts(it) {
|
||||||
|
const slots = sortCombinationSlotsForDisplay(it.combination_slots || [])
|
||||||
|
if (!slots.length) return []
|
||||||
|
const mp = effectiveComboMethodProfile(it.catalog_method_profile || {}, it.planning_method_profile)
|
||||||
|
const byIx = new Map(readSlotProfilesV1(mp).map((r) => [Number(r.slot_index), r]))
|
||||||
|
const titles = it.combo_member_title_by_id || {}
|
||||||
|
return slots.map((slot, idx) => {
|
||||||
|
const siRaw = slot.slot_index
|
||||||
|
const siParsed =
|
||||||
|
siRaw === '' || siRaw == null ? idx : typeof siRaw === 'number' ? siRaw : parseInt(String(siRaw), 10)
|
||||||
|
const ix = Number.isFinite(siParsed) ? siParsed : idx
|
||||||
|
const stationLbl = ((slot.title || '').trim() || `Station ${ix}`)
|
||||||
|
const candIds = (slot.candidate_exercise_ids || [])
|
||||||
|
.map((raw) => (typeof raw === 'number' ? raw : parseInt(String(raw), 10)))
|
||||||
|
.filter((n) => Number.isFinite(n))
|
||||||
|
const namesJoined =
|
||||||
|
candIds.length === 0
|
||||||
|
? '(keine Übung)'
|
||||||
|
: candIds.map((id) => titles[String(id)] || `Übung ${id}`).join(' ↔ ')
|
||||||
|
const timing = summarizeSlotProfileBrief(byIx.get(ix))
|
||||||
|
let line = `${stationLbl}: ${namesJoined}`
|
||||||
|
if (timing) line += ` · ${timing}`
|
||||||
|
return line
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/** Stabile Farbzurodnung aus Modul-ID (nur Darstellung). */
|
/** Stabile Farbzurodnung aus Modul-ID (nur Darstellung). */
|
||||||
function planningModulePalette(moduleId) {
|
function planningModulePalette(moduleId) {
|
||||||
const id = normalizedPlanningModuleChainId(moduleId)
|
const id = normalizedPlanningModuleChainId(moduleId)
|
||||||
|
|
@ -964,6 +1018,24 @@ export default function TrainingUnitSectionsEditor({
|
||||||
? undefined
|
? undefined
|
||||||
: Number(it.exercise_variant_id)
|
: Number(it.exercise_variant_id)
|
||||||
|
|
||||||
|
const stripArchRaw =
|
||||||
|
isCombination && it.exercise_id ? String(it.catalog_method_archetype || '').trim() : ''
|
||||||
|
const stripArchLbl =
|
||||||
|
stripArchRaw && isCombination ? combinationArchetypeLabel(stripArchRaw) : null
|
||||||
|
const stripBullets =
|
||||||
|
isCombination && it.exercise_id ? comboPlanningStripBulletTexts(it) : []
|
||||||
|
const stripMpEff =
|
||||||
|
isCombination && it.exercise_id
|
||||||
|
? effectiveComboMethodProfile(
|
||||||
|
it.catalog_method_profile || {},
|
||||||
|
it.planning_method_profile,
|
||||||
|
)
|
||||||
|
: null
|
||||||
|
const stripGlobalRough =
|
||||||
|
isCombination && it.exercise_id && stripMpEff
|
||||||
|
? comboRoughGlobalTimingHint(stripMpEff, stripArchRaw)
|
||||||
|
: null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment key={`${insertSlotKeyPrefix}sec-${sIdx}-blk-${iIdx}`}>
|
<Fragment key={`${insertSlotKeyPrefix}sec-${sIdx}-blk-${iIdx}`}>
|
||||||
{!planningCompactLegend &&
|
{!planningCompactLegend &&
|
||||||
|
|
@ -1047,7 +1119,16 @@ export default function TrainingUnitSectionsEditor({
|
||||||
type="button"
|
type="button"
|
||||||
className="btn btn-secondary framework-ctrl framework-ctrl--xs"
|
className="btn btn-secondary framework-ctrl framework-ctrl--xs"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
onPeekExercise(Number(it.exercise_id), variantIdPeek)
|
onPeekExercise(
|
||||||
|
Number(it.exercise_id),
|
||||||
|
variantIdPeek,
|
||||||
|
isCombination
|
||||||
|
? {
|
||||||
|
catalog_method_profile: it.catalog_method_profile,
|
||||||
|
planning_method_profile: it.planning_method_profile,
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
Vorschau
|
Vorschau
|
||||||
|
|
@ -1142,29 +1223,73 @@ export default function TrainingUnitSectionsEditor({
|
||||||
style={{
|
style={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexWrap: 'wrap',
|
flexWrap: 'wrap',
|
||||||
alignItems: 'center',
|
alignItems: 'flex-start',
|
||||||
gap: '8px 10px',
|
gap: '10px',
|
||||||
padding: '6px 12px 8px',
|
padding: '8px 12px 10px',
|
||||||
paddingLeft: enableItemDragReorder ? 44 : 12,
|
paddingLeft: enableItemDragReorder ? 44 : 12,
|
||||||
borderTop: '1px solid var(--border)',
|
borderTop: '1px solid var(--border)',
|
||||||
background: 'var(--surface2)',
|
background: 'var(--surface2)',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span
|
<div
|
||||||
style={{
|
style={{
|
||||||
|
flex: '1 1 200px',
|
||||||
|
minWidth: 0,
|
||||||
fontSize: '0.78rem',
|
fontSize: '0.78rem',
|
||||||
color: 'var(--text2)',
|
color: 'var(--text2)',
|
||||||
flex: '1 1 160px',
|
lineHeight: 1.45,
|
||||||
minWidth: 0,
|
|
||||||
}}
|
}}
|
||||||
title="Ablaufprofil für diese Kombi in diesem Termin — Editor öffnen zum Anpassen"
|
title="Stationen und grobe Zeiten aus Katalog bzw. Planungs-Anpassung — Details unter „Ablauf bearbeiten“ oder „Vorschau“"
|
||||||
>
|
>
|
||||||
<strong style={{ color: 'var(--text1)', fontWeight: 600 }}>Ablauf: </strong>
|
<div style={{ marginBottom: stripBullets.length || stripGlobalRough ? 6 : 0 }}>
|
||||||
<span>{compactComboPlanningCaption(it)}</span>
|
<strong style={{ color: 'var(--text1)', fontWeight: 600 }}>Archetyp: </strong>
|
||||||
</span>
|
<span style={{ color: 'var(--text1)' }}>
|
||||||
|
{stripArchLbl || stripArchRaw || '—'}
|
||||||
|
{stripArchRaw && stripArchLbl && stripArchLbl !== stripArchRaw ? (
|
||||||
|
<span style={{ marginLeft: 6, fontWeight: 400, color: 'var(--text3)', fontSize: '0.72rem' }}>
|
||||||
|
({stripArchRaw})
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
</span>
|
||||||
|
<span style={{ marginLeft: 10, fontWeight: 500 }}>{compactComboPlanningCaption(it)}</span>
|
||||||
|
</div>
|
||||||
|
{stripGlobalRough ? (
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
marginBottom: stripBullets.length ? 6 : 0,
|
||||||
|
fontSize: '0.74rem',
|
||||||
|
color: 'var(--text3)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<strong style={{ color: 'var(--text2)', fontWeight: 600 }}>Block: </strong>
|
||||||
|
{stripGlobalRough}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
{stripBullets.length > 0 ? (
|
||||||
|
<ul
|
||||||
|
style={{
|
||||||
|
margin: 0,
|
||||||
|
paddingLeft: '1.05rem',
|
||||||
|
fontSize: '0.74rem',
|
||||||
|
color: 'var(--text2)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{stripBullets.map((line, bi) => (
|
||||||
|
<li key={`combo-strip-${sIdx}-${iIdx}-${bi}`} style={{ marginBottom: 2 }}>
|
||||||
|
{line}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
) : (
|
||||||
|
<div style={{ fontSize: '0.74rem', color: 'var(--text3)', fontStyle: 'italic' }}>
|
||||||
|
Stationen laden oder noch keine Kombi-Stationen im Katalog …
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="btn btn-secondary framework-ctrl framework-ctrl--xs"
|
className="btn btn-secondary framework-ctrl framework-ctrl--xs"
|
||||||
|
style={{ flexShrink: 0 }}
|
||||||
aria-haspopup="dialog"
|
aria-haspopup="dialog"
|
||||||
aria-label="Ablaufprofil Kombination für diese Planung bearbeiten"
|
aria-label="Ablaufprofil Kombination für diese Planung bearbeiten"
|
||||||
onClick={() => setComboPlanningModal({ sIdx, iIdx })}
|
onClick={() => setComboPlanningModal({ sIdx, iIdx })}
|
||||||
|
|
|
||||||
|
|
@ -2892,8 +2892,12 @@ function TrainingPlanningPage() {
|
||||||
})
|
})
|
||||||
setExercisePickerOpen(true)
|
setExercisePickerOpen(true)
|
||||||
}}
|
}}
|
||||||
onPeekExercise={(id, variantId) =>
|
onPeekExercise={(id, variantId, peekExtras) =>
|
||||||
setPlanningPeekCtx({ exerciseId: id, variantId: variantId ?? null })
|
setPlanningPeekCtx({
|
||||||
|
exerciseId: id,
|
||||||
|
variantId: variantId ?? null,
|
||||||
|
peekExtras: peekExtras ?? null,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
showExecutionExtras={Boolean(editingUnit) && sectionsEditMode === 'debrief'}
|
showExecutionExtras={Boolean(editingUnit) && sectionsEditMode === 'debrief'}
|
||||||
/>
|
/>
|
||||||
|
|
@ -3078,6 +3082,7 @@ function TrainingPlanningPage() {
|
||||||
open={planningPeekCtx != null}
|
open={planningPeekCtx != null}
|
||||||
exerciseId={planningPeekCtx?.exerciseId}
|
exerciseId={planningPeekCtx?.exerciseId}
|
||||||
variantId={planningPeekCtx?.variantId ?? undefined}
|
variantId={planningPeekCtx?.variantId ?? undefined}
|
||||||
|
peekExtras={planningPeekCtx?.peekExtras ?? undefined}
|
||||||
onClose={() => setPlanningPeekCtx(null)}
|
onClose={() => setPlanningPeekCtx(null)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -242,6 +242,47 @@ export function readSlotProfilesV1(profileObj) {
|
||||||
}).filter(Boolean)
|
}).filter(Boolean)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Kurztext für Listen/strip (Coach „Plan:“ — gleiche Logik). */
|
||||||
|
export function summarizeSlotProfileBrief(r) {
|
||||||
|
if (!r) return null
|
||||||
|
const adv = r.advance_mode || 'timed'
|
||||||
|
const bits = []
|
||||||
|
if (adv === 'timed') {
|
||||||
|
bits.push('Zeit')
|
||||||
|
if (r.load_sec != null) bits.push(`${r.load_sec}s Arbeit`)
|
||||||
|
} else if (adv === 'rep') {
|
||||||
|
bits.push('Ziel‑Wdh.')
|
||||||
|
const nSer = r.rep_series_count != null && r.rep_series_count >= 1 ? r.rep_series_count : 1
|
||||||
|
if (r.consecutive_reps != null) {
|
||||||
|
if (nSer >= 2) bits.push(`${nSer} Serien à ${r.consecutive_reps}×`)
|
||||||
|
else bits.push(`${r.consecutive_reps}×`)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
bits.push('Coach')
|
||||||
|
const nSerMan = r.rep_series_count != null && r.rep_series_count >= 2 ? r.rep_series_count : 1
|
||||||
|
if (r.consecutive_reps != null) {
|
||||||
|
if (nSerMan >= 2) bits.push(`${nSerMan} Serien à Richtwert ${r.consecutive_reps}×`)
|
||||||
|
else bits.push(`Richtwert ${r.consecutive_reps}×`)
|
||||||
|
} else if (r.rep_series_count != null && r.rep_series_count >= 2) {
|
||||||
|
bits.push(`${r.rep_series_count} Serien`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (r.intra_rep_rest_sec != null) {
|
||||||
|
if (adv === 'timed') bits.push(`Pause ${r.intra_rep_rest_sec}s`)
|
||||||
|
else if (adv === 'rep' && r.rep_series_count != null && r.rep_series_count >= 2)
|
||||||
|
bits.push(`Pause zw. Serien ${r.intra_rep_rest_sec}s`)
|
||||||
|
else if (
|
||||||
|
adv === 'manual' &&
|
||||||
|
r.rep_series_count != null &&
|
||||||
|
r.rep_series_count >= 2
|
||||||
|
) {
|
||||||
|
bits.push(`Pause zw. Serien ${r.intra_rep_rest_sec}s`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (r.transition_after_sec != null) bits.push(`Wechsel ${r.transition_after_sec}s`)
|
||||||
|
return bits.join(' · ')
|
||||||
|
}
|
||||||
|
|
||||||
function normalizeOptionalNonNegInt(v) {
|
function normalizeOptionalNonNegInt(v) {
|
||||||
if (v === '' || v === undefined || v === null) return undefined
|
if (v === '' || v === undefined || v === null) return undefined
|
||||||
const n = typeof v === 'number' ? v : parseInt(String(v), 10)
|
const n = typeof v === 'number' ? v : parseInt(String(v), 10)
|
||||||
|
|
|
||||||
|
|
@ -276,6 +276,48 @@ export async function enrichSectionsWithVariants(sections) {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const titleById = new Map()
|
||||||
|
for (const id of unique) {
|
||||||
|
const row = cache.get(id)
|
||||||
|
const t = (row?.title || '').trim()
|
||||||
|
if (t) titleById.set(Number(id), t)
|
||||||
|
}
|
||||||
|
const comboCandidateExtra = new Set()
|
||||||
|
for (const id of unique) {
|
||||||
|
const row = cache.get(id)
|
||||||
|
if (String(row?.exercise_kind || '').toLowerCase().trim() !== 'combination') continue
|
||||||
|
for (const slot of row.combination_slots || []) {
|
||||||
|
for (const raw of slot.candidate_exercise_ids || []) {
|
||||||
|
const n = typeof raw === 'number' ? raw : parseInt(String(raw), 10)
|
||||||
|
if (Number.isFinite(n) && !titleById.has(n)) comboCandidateExtra.add(n)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await Promise.all(
|
||||||
|
[...comboCandidateExtra].map(async (cid) => {
|
||||||
|
try {
|
||||||
|
const ex = await api.getExercise(cid)
|
||||||
|
titleById.set(cid, ((ex.title || '').trim() || `Übung #${cid}`))
|
||||||
|
} catch {
|
||||||
|
titleById.set(cid, `Übung #${cid}`)
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
function comboMemberTitleByIdForSlots(slots) {
|
||||||
|
const o = {}
|
||||||
|
for (const slot of slots || []) {
|
||||||
|
for (const raw of slot.candidate_exercise_ids || []) {
|
||||||
|
const n = typeof raw === 'number' ? raw : parseInt(String(raw), 10)
|
||||||
|
if (!Number.isFinite(n)) continue
|
||||||
|
const key = String(n)
|
||||||
|
if (!o[key]) o[key] = titleById.get(n) || `Übung #${n}`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return o
|
||||||
|
}
|
||||||
|
|
||||||
return sections.map((sec) => ({
|
return sections.map((sec) => ({
|
||||||
...sec,
|
...sec,
|
||||||
items: (sec.items || []).map((it) => {
|
items: (sec.items || []).map((it) => {
|
||||||
|
|
@ -306,7 +348,7 @@ export async function enrichSectionsWithVariants(sections) {
|
||||||
exercise_club_id: c.club_id,
|
exercise_club_id: c.club_id,
|
||||||
exercise_created_by: c.created_by,
|
exercise_created_by: c.created_by,
|
||||||
exercise_status: c.status,
|
exercise_status: c.status,
|
||||||
...(isCombo ? { combination_slots: c.combination_slots || [] } : {}),
|
...(isCombo ? { combination_slots: c.combination_slots || [], combo_member_title_by_id: comboMemberTitleByIdForSlots(c.combination_slots || []) } : {}),
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
}))
|
}))
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user