feat(combo-planning): replace summarizeSlotProfileBrief with effectiveStationTimingSummary
All checks were successful
Deploy Development / deploy (push) Successful in 43s
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 / playwright-tests (push) Successful in 57s

- Updated CombinationCoachSlots, CombinationPlanBracket, and TrainingUnitSectionsEditor components to utilize effectiveStationTimingSummary for improved timing display.
- Adjusted station title handling to enhance clarity and consistency across components.
- Refactored utility functions to streamline slot timing summaries and improve overall user experience in combination planning.
This commit is contained in:
Lars 2026-05-13 14:34:17 +02:00
parent ed15f73727
commit 79dabbca5a
4 changed files with 92 additions and 11 deletions

View File

@ -10,7 +10,7 @@ import {
combinationArchetypeLabel,
sortCombinationSlotsForDisplay,
} from '../constants/combinationArchetypes'
import { readSlotProfilesV1, summarizeSlotProfileBrief } from '../utils/combinationMethodProfileUi'
import { effectiveStationTimingSummary, readSlotProfilesV1 } from '../utils/combinationMethodProfileUi'
export default function CombinationCoachSlots({
combinationSlots,
@ -190,10 +190,10 @@ export default function CombinationCoachSlots({
const slotTitle =
(slot.title && String(slot.title).trim()) ||
(candIds.length <= 1 && slot.candidates?.[0]?.title) ||
`Station ${slot.slot_index != null ? Number(slot.slot_index) + 1 : si + 1}`
`Station ${si + 1}`
const ix = slot.slot_index != null ? Number(slot.slot_index) : si
const timingSummary = summarizeSlotProfileBrief(slotTimingByIx.get(ix))
const timingSummary = effectiveStationTimingSummary(archeKey, methodProfile || {}, slotTimingByIx.get(ix))
return (
<li key={`${slot.slot_index ?? si}-${slotTitle}`} style={{ lineHeight: 1.45 }}>

View File

@ -7,7 +7,7 @@ import {
combinationArchetypeLabel,
sortCombinationSlotsForDisplay,
} from '../constants/combinationArchetypes'
import { describeGlobalComboProfile, readSlotProfilesV1, summarizeSlotProfileBrief } from '../utils/combinationMethodProfileUi'
import { describeGlobalComboProfile, effectiveStationTimingSummary, readSlotProfilesV1 } from '../utils/combinationMethodProfileUi'
function candidateLine(slot) {
const cands = slot.candidates
@ -95,14 +95,18 @@ export default function CombinationPlanBracket({
const ixParsed =
siRaw === '' || siRaw == null ? si : typeof siRaw === 'number' ? siRaw : parseInt(String(siRaw), 10)
const stationIx = Number.isFinite(ixParsed) ? ixParsed : si
const stationTitle = ((slot.title || '').trim() || `Station ${stationIx}`).trim()
const displayStep = si + 1
const stationTitle = ((slot.title || '').trim() || `Station ${displayStep}`).trim()
const names = candidateLine(slot)
const timing = summarizeSlotProfileBrief(timingByIx.get(stationIx))
const timing = effectiveStationTimingSummary(arch, methodProfile || {}, timingByIx.get(stationIx))
return (
<li key={`slot-${stationIx}-${si}`} className="combo-plan-bracket__station">
<div className="combo-plan-bracket__station-index" title={`slot_index ${stationIx}`}>
S{stationIx}
<div
className="combo-plan-bracket__station-index"
title={`Technischer Slot-Index (slot_index): ${stationIx}`}
>
S{displayStep}
</div>
<div className="combo-plan-bracket__station-main">
<div className="combo-plan-bracket__station-title">{stationTitle}</div>

View File

@ -12,7 +12,7 @@ import {
sectionPlannedMinutes,
} from '../utils/trainingUnitSectionsForm'
import api from '../utils/api'
import { readSlotProfilesV1, summarizeSlotProfileBrief } from '../utils/combinationMethodProfileUi'
import { effectiveStationTimingSummary, readSlotProfilesV1 } from '../utils/combinationMethodProfileUi'
import { isCompactTagLegendMode } from '../config/planningModuleUx'
import { useAuth } from '../context/AuthContext'
@ -109,6 +109,7 @@ 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 archRaw = String(it.catalog_method_archetype || '').trim()
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) => {
@ -116,7 +117,7 @@ function comboPlanningStripBulletTexts(it) {
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 stationLbl = ((slot.title || '').trim() || `Station ${idx + 1}`)
const candIds = (slot.candidate_exercise_ids || [])
.map((raw) => (typeof raw === 'number' ? raw : parseInt(String(raw), 10)))
.filter((n) => Number.isFinite(n))
@ -124,7 +125,7 @@ function comboPlanningStripBulletTexts(it) {
candIds.length === 0
? '(keine Übung)'
: candIds.map((id) => titles[String(id)] || `Übung ${id}`).join(' ↔ ')
const timing = summarizeSlotProfileBrief(byIx.get(ix))
const timing = effectiveStationTimingSummary(archRaw, mp, byIx.get(ix))
let line = `${stationLbl}: ${namesJoined}`
if (timing) line += ` · ${timing}`
return line

View File

@ -322,6 +322,82 @@ export function summarizeSlotProfileBrief(r) {
return bits.join(' · ')
}
function globalTimingHintsForArchetype(arch, mp) {
if (!mp || typeof mp !== 'object' || Array.isArray(mp)) return []
const bits = []
switch (arch) {
case 'circuit_rotate_time':
if (mp.work_seconds != null && mp.work_seconds !== '') bits.push(`${mp.work_seconds}s Arbeit je Station`)
if (mp.transition_seconds != null && mp.transition_seconds !== '')
bits.push(`Rotation ${mp.transition_seconds}s`)
if (mp.rest_seconds != null && mp.rest_seconds !== '') bits.push(`Pause ${mp.rest_seconds}s`)
if (mp.rounds != null && mp.rounds !== '') bits.push(`${mp.rounds} UmlaufRunden`)
break
case 'sequence_linear':
if (mp.hint_step_duration_sec != null && mp.hint_step_duration_sec !== '')
bits.push(`~${mp.hint_step_duration_sec}s je Station`)
if (mp.rounds != null && mp.rounds !== '') bits.push(`${mp.rounds} SequenzDurchläufe`)
if (mp.block_intro_sec != null && mp.block_intro_sec !== '') bits.push(`BlockIntro ${mp.block_intro_sec}s`)
break
case 'time_domain_interval':
if (mp.work_seconds != null && mp.work_seconds !== '') bits.push(`${mp.work_seconds}s IntervallArbeit`)
if (mp.rest_seconds != null && mp.rest_seconds !== '') bits.push(`${mp.rest_seconds}s Erholung`)
if (mp.interval_rounds != null && mp.interval_rounds !== '') bits.push(`${mp.interval_rounds} IntervallZyklen`)
break
case 'pair_superset':
if (mp.work_seconds_per_side != null && mp.work_seconds_per_side !== '')
bits.push(`${mp.work_seconds_per_side}s Arbeit`)
if (mp.switch_seconds != null && mp.switch_seconds !== '') bits.push(`Wechsel ${mp.switch_seconds}s`)
break
case 'station_parcour':
if (mp.rounds != null && mp.rounds !== '') bits.push(`${mp.rounds} ParcoursRunden`)
break
case 'circuit_all_parallel':
if (mp.rounds != null && mp.rounds !== '') bits.push(`${mp.rounds} Runden`)
if (mp.explain_before_seconds != null && mp.explain_before_seconds !== '')
bits.push(`Erklärung ${mp.explain_before_seconds}s`)
break
default:
break
}
return bits
}
function isWeakSlotTimingSummary(txt) {
if (!txt || typeof txt !== 'string') return true
const t = txt.trim()
return t === 'Zeit' || t === 'Coach' || t === 'ZielWdh.'
}
/**
* Stationszeile für Lesetext: SlotZeiten + bei Bedarf globale Eckdaten (ZirkelSekunden, Runden ).
*/
export function effectiveStationTimingSummary(archetypeKey, profileObj, slotRow) {
const arch = typeof archetypeKey === 'string' ? archetypeKey.trim() : ''
const mp = profileObj && typeof profileObj === 'object' && !Array.isArray(profileObj) ? profileObj : {}
const slotTxt = summarizeSlotProfileBrief(slotRow)
const hints = globalTimingHintsForArchetype(arch, mp)
const hintStr = hints.join(' · ')
if (!isWeakSlotTimingSummary(slotTxt)) {
const extras = []
for (const h of hints) {
if (
/Runden|Durchläufe|Zyklen|Umlauf/i.test(h) &&
slotTxt &&
!/Runden|Serien|×|\d+s Arbeit|\d+s Erholung|\d+s Intervall/i.test(slotTxt)
) {
extras.push(h)
}
}
return extras.length ? `${slotTxt} · ${extras.join(' · ')}` : slotTxt
}
if (hintStr) return hintStr
if (slotTxt) return slotTxt
return null
}
function normalizeOptionalNonNegInt(v) {
if (v === '' || v === undefined || v === null) return undefined
const n = typeof v === 'number' ? v : parseInt(String(v), 10)