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
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:
parent
cf9932990e
commit
13efce6e36
|
|
@ -2,6 +2,7 @@ import React, { Fragment, useCallback, useEffect, useState } from 'react'
|
|||
import { GripVertical, Pencil } from 'lucide-react'
|
||||
import CombinationMethodProfileEditor from './CombinationMethodProfileEditor'
|
||||
import { comboPlanningProfileJsonForEditor } from '../utils/comboPlanningMethodProfile'
|
||||
import { combinationArchetypeLabel } from '../constants/combinationArchetypes'
|
||||
import {
|
||||
defaultSection,
|
||||
exerciseRow,
|
||||
|
|
@ -59,6 +60,20 @@ function gatherPlanningModuleOutline(items, startIdx, moduleId) {
|
|||
|
||||
const MODULE_OUTLINE_PREVIEW_MAX = 8
|
||||
|
||||
/** Statuszeile: Planungs‑Override vs. Katalog, inkl. Archetyp‑Label 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). */
|
||||
function planningModulePalette(moduleId) {
|
||||
const id = normalizedPlanningModuleChainId(moduleId)
|
||||
|
|
@ -290,6 +305,8 @@ export default function TrainingUnitSectionsEditor({
|
|||
}
|
||||
|
||||
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) */
|
||||
const [insertChooser, setInsertChooser] = useState(null)
|
||||
const [draggingPos, setDraggingPos] = useState(null)
|
||||
|
|
@ -316,6 +333,27 @@ export default function TrainingUnitSectionsEditor({
|
|||
return () => window.removeEventListener('keydown', onKey)
|
||||
}, [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 insertSlotKeyPrefix =
|
||||
|
|
@ -533,6 +571,23 @@ export default function TrainingUnitSectionsEditor({
|
|||
|
||||
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 (
|
||||
<div
|
||||
className={
|
||||
|
|
@ -1023,68 +1078,39 @@ export default function TrainingUnitSectionsEditor({
|
|||
|
||||
{isCombination && it.exercise_id ? (
|
||||
<div
|
||||
className="tu-combo-planning-profile"
|
||||
className="tu-combo-planning-strip"
|
||||
style={{
|
||||
padding: '4px 12px 4px',
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
alignItems: 'center',
|
||||
gap: '8px 10px',
|
||||
padding: '6px 12px 8px',
|
||||
paddingLeft: enableItemDragReorder ? 44 : 12,
|
||||
borderTop: '1px solid var(--border)',
|
||||
background: 'var(--surface2)',
|
||||
}}
|
||||
>
|
||||
<details className="card" style={{ padding: '12px 14px', background: 'var(--surface2)' }}>
|
||||
<summary
|
||||
style={{ cursor: 'pointer', fontSize: '0.88rem', color: 'var(--text2)', fontWeight: 600 }}
|
||||
>
|
||||
Ablaufprofil für diese Planung (Kombination)
|
||||
<span style={{ marginLeft: 10, fontWeight: 400, fontSize: '0.82rem' }}>
|
||||
{it.planning_method_profile != null &&
|
||||
typeof it.planning_method_profile === 'object' &&
|
||||
!Array.isArray(it.planning_method_profile)
|
||||
? '— Anpassung aktiv'
|
||||
: '— wie im Katalog'}
|
||||
</span>
|
||||
</summary>
|
||||
<div style={{ marginTop: 12 }}>
|
||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 8, marginBottom: 10 }}>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-secondary framework-ctrl framework-ctrl--xs"
|
||||
onClick={() => updateItem(sIdx, iIdx, '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(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>
|
||||
<span
|
||||
style={{
|
||||
fontSize: '0.78rem',
|
||||
color: 'var(--text2)',
|
||||
flex: '1 1 160px',
|
||||
minWidth: 0,
|
||||
}}
|
||||
title="Ablaufprofil für diese Kombi in diesem Termin — Editor öffnen zum Anpassen"
|
||||
>
|
||||
<strong style={{ color: 'var(--text1)', fontWeight: 600 }}>Ablauf: </strong>
|
||||
<span>{compactComboPlanningCaption(it)}</span>
|
||||
</span>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-secondary framework-ctrl framework-ctrl--xs"
|
||||
aria-haspopup="dialog"
|
||||
aria-label="Ablaufprofil Kombination für diese Planung bearbeiten"
|
||||
onClick={() => setComboPlanningModal({ sIdx, iIdx })}
|
||||
>
|
||||
Ablauf bearbeiten…
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
|
|
@ -1322,6 +1348,101 @@ export default function TrainingUnitSectionsEditor({
|
|||
</div>
|
||||
) : 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 ? (
|
||||
<div
|
||||
className="tu-textedit-backdrop"
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user