feat(training-unit-editor): enhance combo planning features and UI updates
All checks were successful
Deploy Development / deploy (push) Successful in 37s
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 1m0s

- Added a new function `compactComboPlanningCaption` to display planning status with archetype labels.
- Introduced state management for a modal to edit combination planning profiles, improving user interaction.
- Updated UI components to enhance the display and interaction of combination exercises, including styling adjustments for better usability.
- Implemented keyboard event handling for modal closure, enhancing user experience.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Lars 2026-05-13 09:06:10 +02:00
parent cf9932990e
commit 13efce6e36

View File

@ -2,6 +2,7 @@ import React, { Fragment, useCallback, useEffect, useState } from 'react'
import { GripVertical, Pencil } from 'lucide-react' import { GripVertical, Pencil } from 'lucide-react'
import CombinationMethodProfileEditor from './CombinationMethodProfileEditor' import CombinationMethodProfileEditor from './CombinationMethodProfileEditor'
import { comboPlanningProfileJsonForEditor } from '../utils/comboPlanningMethodProfile' import { comboPlanningProfileJsonForEditor } from '../utils/comboPlanningMethodProfile'
import { combinationArchetypeLabel } from '../constants/combinationArchetypes'
import { import {
defaultSection, defaultSection,
exerciseRow, exerciseRow,
@ -59,6 +60,20 @@ function gatherPlanningModuleOutline(items, startIdx, moduleId) {
const MODULE_OUTLINE_PREVIEW_MAX = 8 const MODULE_OUTLINE_PREVIEW_MAX = 8
/** Statuszeile: PlanungsOverride vs. Katalog, inkl. ArchetypLabel wenn bekannt. */
function compactComboPlanningCaption(it) {
const overridden =
it.planning_method_profile != null &&
typeof it.planning_method_profile === 'object' &&
!Array.isArray(it.planning_method_profile)
const archRaw = String(it.catalog_method_archetype || '').trim()
const archLbl = archRaw ? combinationArchetypeLabel(archRaw) : null
if (overridden) {
return archLbl ? `${archLbl} · Planung angepasst` : 'Planung angepasst'
}
return archLbl ? `${archLbl} · wie Katalog` : 'wie im Katalog'
}
/** 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)
@ -290,6 +305,8 @@ export default function TrainingUnitSectionsEditor({
} }
const [textEdit, setTextEdit] = useState(null) const [textEdit, setTextEdit] = useState(null)
/** Kombi: Ablaufprofil in Modal statt einzuklappender Karte */
const [comboPlanningModal, setComboPlanningModal] = useState(null)
/** { sIdx: number, beforeIx: number } Einfüge-Popup („+“ zwischen Zeilen) */ /** { sIdx: number, beforeIx: number } Einfüge-Popup („+“ zwischen Zeilen) */
const [insertChooser, setInsertChooser] = useState(null) const [insertChooser, setInsertChooser] = useState(null)
const [draggingPos, setDraggingPos] = useState(null) const [draggingPos, setDraggingPos] = useState(null)
@ -316,6 +333,27 @@ export default function TrainingUnitSectionsEditor({
return () => window.removeEventListener('keydown', onKey) return () => window.removeEventListener('keydown', onKey)
}, [insertChooser]) }, [insertChooser])
useEffect(() => {
if (!comboPlanningModal) return
const onKey = (e) => {
if (e.key === 'Escape') setComboPlanningModal(null)
}
window.addEventListener('keydown', onKey)
return () => window.removeEventListener('keydown', onKey)
}, [comboPlanningModal])
useEffect(() => {
if (!comboPlanningModal) return
const L = ensure(sections)
const { sIdx, iIdx } = comboPlanningModal
const row = L[sIdx]?.items?.[iIdx]
const ok =
row &&
String(row.exercise_kind || '').toLowerCase().trim() === 'combination' &&
row.exercise_id
if (!ok) setComboPlanningModal(null)
}, [sections, comboPlanningModal])
const closeInsertChooser = useCallback(() => setInsertChooser(null), []) const closeInsertChooser = useCallback(() => setInsertChooser(null), [])
const insertSlotKeyPrefix = const insertSlotKeyPrefix =
@ -533,6 +571,23 @@ export default function TrainingUnitSectionsEditor({
const list = ensure(sections) const list = ensure(sections)
let comboPlanningModalItem = null
let comboPlanningModalSX = null
let comboPlanningModalIX = null
if (comboPlanningModal) {
const { sIdx, iIdx } = comboPlanningModal
const cand = list[sIdx]?.items?.[iIdx]
if (
cand &&
String(cand.exercise_kind || '').toLowerCase().trim() === 'combination' &&
cand.exercise_id
) {
comboPlanningModalItem = cand
comboPlanningModalSX = sIdx
comboPlanningModalIX = iIdx
}
}
return ( return (
<div <div
className={ className={
@ -1023,68 +1078,39 @@ export default function TrainingUnitSectionsEditor({
{isCombination && it.exercise_id ? ( {isCombination && it.exercise_id ? (
<div <div
className="tu-combo-planning-profile" className="tu-combo-planning-strip"
style={{ style={{
padding: '4px 12px 4px', display: 'flex',
flexWrap: 'wrap',
alignItems: 'center',
gap: '8px 10px',
padding: '6px 12px 8px',
paddingLeft: enableItemDragReorder ? 44 : 12, paddingLeft: enableItemDragReorder ? 44 : 12,
borderTop: '1px solid var(--border)', borderTop: '1px solid var(--border)',
background: 'var(--surface2)',
}} }}
> >
<details className="card" style={{ padding: '12px 14px', background: 'var(--surface2)' }}> <span
<summary style={{
style={{ cursor: 'pointer', fontSize: '0.88rem', color: 'var(--text2)', fontWeight: 600 }} fontSize: '0.78rem',
> color: 'var(--text2)',
Ablaufprofil für diese Planung (Kombination) flex: '1 1 160px',
<span style={{ marginLeft: 10, fontWeight: 400, fontSize: '0.82rem' }}> minWidth: 0,
{it.planning_method_profile != null && }}
typeof it.planning_method_profile === 'object' && title="Ablaufprofil für diese Kombi in diesem Termin — Editor öffnen zum Anpassen"
!Array.isArray(it.planning_method_profile) >
? '— Anpassung aktiv' <strong style={{ color: 'var(--text1)', fontWeight: 600 }}>Ablauf:&nbsp;</strong>
: '— wie im Katalog'} <span>{compactComboPlanningCaption(it)}</span>
</span> </span>
</summary> <button
<div style={{ marginTop: 12 }}> type="button"
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 8, marginBottom: 10 }}> className="btn btn-secondary framework-ctrl framework-ctrl--xs"
<button aria-haspopup="dialog"
type="button" aria-label="Ablaufprofil Kombination für diese Planung bearbeiten"
className="btn btn-secondary framework-ctrl framework-ctrl--xs" onClick={() => setComboPlanningModal({ sIdx, iIdx })}
onClick={() => updateItem(sIdx, iIdx, 'planning_method_profile', null)} >
> Ablauf bearbeiten
Planung wie Katalog </button>
</button>
<button
type="button"
className="btn btn-secondary framework-ctrl framework-ctrl--xs"
title="Bearbeitbare Kopie der Katalog-Vorgaben setzen"
onClick={() =>
updateItem(sIdx, iIdx, 'planning_method_profile', {
...(it.catalog_method_profile || {}),
})
}
>
Aus Katalog kopieren
</button>
</div>
<CombinationMethodProfileEditor
plannerMode
methodArchetype={(it.catalog_method_archetype || '').trim()}
methodProfileJson={comboPlanningProfileJsonForEditor(
it.catalog_method_profile || {},
it.planning_method_profile
)}
onChangeMethodProfileJson={(json) => {
try {
const obj = JSON.parse(json || '{}')
if (obj && typeof obj === 'object' && !Array.isArray(obj)) {
updateItem(sIdx, iIdx, 'planning_method_profile', obj)
}
} catch {
/* Ungültiges JSON — Hinweis im Editor */
}
}}
/>
</div>
</details>
</div> </div>
) : null} ) : null}
@ -1322,6 +1348,101 @@ export default function TrainingUnitSectionsEditor({
</div> </div>
) : null} ) : null}
{comboPlanningModalItem != null &&
comboPlanningModalSX != null &&
comboPlanningModalIX != null ? (
<div
className="tu-textedit-backdrop"
role="presentation"
onMouseDown={(e) => {
if (e.target === e.currentTarget) setComboPlanningModal(null)
}}
>
<div
className="tu-textedit-panel tu-textedit-panel--combo-planning"
role="dialog"
aria-modal="true"
aria-labelledby="tu-combo-planning-title"
onMouseDown={(e) => e.stopPropagation()}
style={{
maxWidth: 'min(920px, 96vw)',
maxHeight: 'min(800px, 88vh)',
overflow: 'auto',
}}
>
<h4 id="tu-combo-planning-title" className="tu-textedit-title">
Ablaufprofil dieser Kombination für diese Planung
</h4>
<p
style={{
margin: '0 0 0.85rem',
fontSize: '0.82rem',
color: 'var(--text2)',
lineHeight: 1.45,
}}
>
<strong style={{ fontWeight: 600, color: 'var(--text1)' }}>
{(comboPlanningModalItem.exercise_title || '').trim() ||
`Kombination #${comboPlanningModalItem.exercise_id}`}
</strong>
<span style={{ marginLeft: 8 }}>
({compactComboPlanningCaption(comboPlanningModalItem)})
</span>
</p>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 8, marginBottom: 12 }}>
<button
type="button"
className="btn btn-secondary framework-ctrl framework-ctrl--xs"
onClick={() =>
updateItem(comboPlanningModalSX, comboPlanningModalIX, 'planning_method_profile', null)
}
>
Planung wie Katalog
</button>
<button
type="button"
className="btn btn-secondary framework-ctrl framework-ctrl--xs"
title="Bearbeitbare Kopie der Katalog-Vorgaben setzen"
onClick={() =>
updateItem(comboPlanningModalSX, comboPlanningModalIX, 'planning_method_profile', {
...(comboPlanningModalItem.catalog_method_profile || {}),
})
}
>
Aus Katalog kopieren
</button>
</div>
<CombinationMethodProfileEditor
plannerMode
methodArchetype={(comboPlanningModalItem.catalog_method_archetype || '').trim()}
methodProfileJson={comboPlanningProfileJsonForEditor(
comboPlanningModalItem.catalog_method_profile || {},
comboPlanningModalItem.planning_method_profile
)}
onChangeMethodProfileJson={(json) => {
try {
const obj = JSON.parse(json || '{}')
if (obj && typeof obj === 'object' && !Array.isArray(obj)) {
updateItem(comboPlanningModalSX, comboPlanningModalIX, 'planning_method_profile', obj)
}
} catch {
/* Ungültiges JSON — Hinweis im Editor */
}
}}
/>
<div className="tu-textedit-actions" style={{ marginTop: '0.95rem', paddingTop: '0.25rem' }}>
<button
type="button"
className="btn btn-primary"
onClick={() => setComboPlanningModal(null)}
>
Schließen
</button>
</div>
</div>
</div>
) : null}
{textEdit ? ( {textEdit ? (
<div <div
className="tu-textedit-backdrop" className="tu-textedit-backdrop"