From 805ad3c5a577433f0bc3472ece877031fffa8e2b Mon Sep 17 00:00:00 2001 From: Lars Date: Wed, 13 May 2026 09:58:59 +0200 Subject: [PATCH] feat(training-unit-editor): enhance combination slots handling and UI improvements - Added support for `compactPlanningView` and `omitGlobalKeyValueBlock` in the CombinationCoachSlots component to improve display options. - Updated the TrainingUnitSectionsEditor to fetch and manage combination slots more effectively, including new state management for modal interactions. - Introduced a new utility function `comboSlotsOutlineForProfileEditor` to streamline the display of combination slots in the editor. - Enhanced UI elements for better user experience when managing combination exercises and their associated slots. Co-Authored-By: Claude Sonnet 4.6 --- .../src/components/CombinationCoachSlots.jsx | 25 +++- .../components/TrainingUnitSectionsEditor.jsx | 118 ++++++++++++++++-- .../src/utils/trainingUnitSectionsForm.js | 25 +++- 3 files changed, 154 insertions(+), 14 deletions(-) diff --git a/frontend/src/components/CombinationCoachSlots.jsx b/frontend/src/components/CombinationCoachSlots.jsx index c8ca19d..99d5260 100644 --- a/frontend/src/components/CombinationCoachSlots.jsx +++ b/frontend/src/components/CombinationCoachSlots.jsx @@ -52,7 +52,13 @@ function summarizeSlotProfilesRow(r) { return bits.join(' · ') } -export default function CombinationCoachSlots({ combinationSlots, methodArchetype, methodProfile }) { +export default function CombinationCoachSlots({ + combinationSlots, + methodArchetype, + methodProfile, + compactPlanningView = false, + omitGlobalKeyValueBlock = false, +}) { const slots = useMemo(() => sortCombinationSlotsForDisplay(combinationSlots), [combinationSlots]) const candidateIds = useMemo(() => { @@ -153,7 +159,7 @@ export default function CombinationCoachSlots({ combinationSlots, methodArchetyp letterSpacing: '0.04em', }} > - Kombination · Stationen & Einzelübungen + {compactPlanningView ? 'Stationen & Einzelübungen (Katalog)' : 'Kombination · Stationen & Einzelübungen'} {archDisplay ? (

@@ -165,11 +171,13 @@ export default function CombinationCoachSlots({ combinationSlots, methodArchetyp ) : null}

) : null} + {compactPlanningView ? null : (

{archetypeCoachHint(archeKey)}

+ )} - {methodProfile && typeof methodProfile === 'object' && !Array.isArray(methodProfile) && Object.keys(methodProfile).length ? ( + {methodProfile && typeof methodProfile === 'object' && !Array.isArray(methodProfile) && Object.keys(methodProfile).length && !omitGlobalKeyValueBlock ? (
) : ex ? ( + compactPlanningView ? ( + <> +

{ex.title}

+

+ + Im Katalog öffnen + +

+ + ) : ( <>

{ex.title}

{ex.summary ? ( @@ -312,6 +330,7 @@ export default function CombinationCoachSlots({ combinationSlots, methodArchetyp

+ ) ) : (

{candTitleFallback || `Übung #${cid}`} diff --git a/frontend/src/components/TrainingUnitSectionsEditor.jsx b/frontend/src/components/TrainingUnitSectionsEditor.jsx index 201fc13..4c5f759 100644 --- a/frontend/src/components/TrainingUnitSectionsEditor.jsx +++ b/frontend/src/components/TrainingUnitSectionsEditor.jsx @@ -1,14 +1,17 @@ -import React, { Fragment, useCallback, useEffect, useState } from 'react' +import React, { Fragment, useCallback, useEffect, useMemo, useState } from 'react' import { GripVertical, Pencil } from 'lucide-react' import CombinationMethodProfileEditor from './CombinationMethodProfileEditor' -import { comboPlanningProfileJsonForEditor } from '../utils/comboPlanningMethodProfile' +import CombinationCoachSlots from './CombinationCoachSlots' +import { comboPlanningProfileJsonForEditor, effectiveComboMethodProfile } from '../utils/comboPlanningMethodProfile' import { combinationArchetypeLabel } from '../constants/combinationArchetypes' import { + comboSlotsOutlineForProfileEditor, defaultSection, exerciseRow, noteRow, sectionPlannedMinutes, } from '../utils/trainingUnitSectionsForm' +import api from '../utils/api' import { isCompactTagLegendMode } from '../config/planningModuleUx' import { useAuth } from '../context/AuthContext' @@ -307,6 +310,8 @@ export default function TrainingUnitSectionsEditor({ const [textEdit, setTextEdit] = useState(null) /** Kombi: Ablaufprofil in Modal statt einzuklappender Karte */ const [comboPlanningModal, setComboPlanningModal] = useState(null) + /** Katalog-Stationen, falls Zeile noch keine `combination_slots` (vor Enrich o. Ä.) */ + const [modalComboSlotsFetched, setModalComboSlotsFetched] = useState(null) /** { sIdx: number, beforeIx: number } – Einfüge-Popup („+“ zwischen Zeilen) */ const [insertChooser, setInsertChooser] = useState(null) const [draggingPos, setDraggingPos] = useState(null) @@ -354,6 +359,38 @@ export default function TrainingUnitSectionsEditor({ if (!ok) setComboPlanningModal(null) }, [sections, comboPlanningModal]) + useEffect(() => { + if (!comboPlanningModal) { + setModalComboSlotsFetched(null) + return + } + const L = ensure(sections) + const { sIdx, iIdx } = comboPlanningModal + const cand = L[sIdx]?.items?.[iIdx] + if ( + !cand || + String(cand.exercise_kind || '').toLowerCase().trim() !== 'combination' || + !cand.exercise_id + ) { + setModalComboSlotsFetched(null) + return + } + const cached = cand.combination_slots + if (Array.isArray(cached) && cached.length > 0) { + setModalComboSlotsFetched(cached) + return + } + let cancelled = false + setModalComboSlotsFetched([]) + api.getExercise(cand.exercise_id).then((ex) => { + if (cancelled) return + setModalComboSlotsFetched(Array.isArray(ex?.combination_slots) ? ex.combination_slots : []) + }) + return () => { + cancelled = true + } + }, [sections, comboPlanningModal]) + const closeInsertChooser = useCallback(() => setInsertChooser(null), []) const insertSlotKeyPrefix = @@ -571,10 +608,10 @@ export default function TrainingUnitSectionsEditor({ const list = ensure(sections) - let comboPlanningModalItem = null - let comboPlanningModalSX = null - let comboPlanningModalIX = null - if (comboPlanningModal) { + const comboPlanningModalDerived = useMemo(() => { + if (!comboPlanningModal) { + return { item: null, sIdx: null, iIdx: null } + } const { sIdx, iIdx } = comboPlanningModal const cand = list[sIdx]?.items?.[iIdx] if ( @@ -582,11 +619,34 @@ export default function TrainingUnitSectionsEditor({ String(cand.exercise_kind || '').toLowerCase().trim() === 'combination' && cand.exercise_id ) { - comboPlanningModalItem = cand - comboPlanningModalSX = sIdx - comboPlanningModalIX = iIdx + return { item: cand, sIdx, iIdx } } - } + return { item: null, sIdx: null, iIdx: null } + }, [list, comboPlanningModal]) + + const comboPlanningModalItem = comboPlanningModalDerived.item + const comboPlanningModalSX = comboPlanningModalDerived.sIdx + const comboPlanningModalIX = comboPlanningModalDerived.iIdx + + const comboPlanningResolvedSlots = useMemo(() => { + if (!comboPlanningModalItem) return [] + const c = comboPlanningModalItem.combination_slots + if (Array.isArray(c) && c.length > 0) return c + return Array.isArray(modalComboSlotsFetched) ? modalComboSlotsFetched : [] + }, [comboPlanningModalItem, modalComboSlotsFetched]) + + const comboPlanningSlotsOutline = useMemo( + () => comboSlotsOutlineForProfileEditor(comboPlanningResolvedSlots), + [comboPlanningResolvedSlots] + ) + + const comboPlanningEffectiveProfile = useMemo(() => { + if (!comboPlanningModalItem) return {} + return effectiveComboMethodProfile( + comboPlanningModalItem.catalog_method_profile || {}, + comboPlanningModalItem.planning_method_profile + ) + }, [comboPlanningModalItem]) return (

+

+ Stationen und Einzelübungen entsprechen der Kombination im Katalog. Einzelübungen hier auszutauschen ist + derzeit nicht vorgesehen (würde die Katalog-Übung ändern). Die Bereiche unten überschreiben nur diesen + Termin, sofern du von den Katalogvorgaben abweichst. +

+ {comboPlanningResolvedSlots.length > 0 ? ( +
+ +
+ ) : ( +

+ Stationen werden geladen … oder die Kombination hat im Katalog keine Stationsliste. +

+ )} +
+ Zeiten und Steuerung für diesen Termin +