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 { 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: 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). */
|
/** 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: </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"
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user