From 79dabbca5a24b0b64c120e5fbd6a498326a031c5 Mon Sep 17 00:00:00 2001 From: Lars Date: Wed, 13 May 2026 14:34:17 +0200 Subject: [PATCH] feat(combo-planning): replace summarizeSlotProfileBrief with effectiveStationTimingSummary - 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. --- .../src/components/CombinationCoachSlots.jsx | 6 +- .../src/components/CombinationPlanBracket.jsx | 14 ++-- .../components/TrainingUnitSectionsEditor.jsx | 7 +- .../src/utils/combinationMethodProfileUi.js | 76 +++++++++++++++++++ 4 files changed, 92 insertions(+), 11 deletions(-) diff --git a/frontend/src/components/CombinationCoachSlots.jsx b/frontend/src/components/CombinationCoachSlots.jsx index fbcf995..d9a1954 100644 --- a/frontend/src/components/CombinationCoachSlots.jsx +++ b/frontend/src/components/CombinationCoachSlots.jsx @@ -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 (
  • diff --git a/frontend/src/components/CombinationPlanBracket.jsx b/frontend/src/components/CombinationPlanBracket.jsx index 043146a..fa8e497 100644 --- a/frontend/src/components/CombinationPlanBracket.jsx +++ b/frontend/src/components/CombinationPlanBracket.jsx @@ -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 (
  • -
    - S{stationIx} +
    + S{displayStep}
    {stationTitle}
    diff --git a/frontend/src/components/TrainingUnitSectionsEditor.jsx b/frontend/src/components/TrainingUnitSectionsEditor.jsx index 78ecf1c..9493f42 100644 --- a/frontend/src/components/TrainingUnitSectionsEditor.jsx +++ b/frontend/src/components/TrainingUnitSectionsEditor.jsx @@ -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 diff --git a/frontend/src/utils/combinationMethodProfileUi.js b/frontend/src/utils/combinationMethodProfileUi.js index c3ce328..d5a8b9f 100644 --- a/frontend/src/utils/combinationMethodProfileUi.js +++ b/frontend/src/utils/combinationMethodProfileUi.js @@ -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} Umlauf‑Runden`) + 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} Sequenz‑Durchläufe`) + if (mp.block_intro_sec != null && mp.block_intro_sec !== '') bits.push(`Block‑Intro ${mp.block_intro_sec}s`) + break + case 'time_domain_interval': + if (mp.work_seconds != null && mp.work_seconds !== '') bits.push(`${mp.work_seconds}s Intervall‑Arbeit`) + 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} Intervall‑Zyklen`) + 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} Parcours‑Runden`) + 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 === 'Ziel‑Wdh.' +} + +/** + * Stationszeile für Lesetext: Slot‑Zeiten + bei Bedarf globale Eckdaten (Zirkel‑Sekunden, 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)