From 613fedfaff433ca7f31c416dc66edd7e6abd073a Mon Sep 17 00:00:00 2001 From: Lars Date: Fri, 15 May 2026 08:02:18 +0200 Subject: [PATCH] Refactor training phase handling in backend and enhance TrainingUnitSectionsEditor - Updated the backend logic to ensure strict ordering of phase indices, preventing UNIQUE constraint violations when phases are duplicated. - Enhanced the TrainingUnitSectionsEditor component with new state management for editing phase titles and stream names, improving user interaction. - Implemented conditional rendering for input fields to facilitate inline editing of phase titles and stream names, streamlining the editing process. --- backend/routers/training_planning.py | 8 +- .../components/TrainingUnitSectionsEditor.jsx | 202 ++++++++++++++---- 2 files changed, 167 insertions(+), 43 deletions(-) diff --git a/backend/routers/training_planning.py b/backend/routers/training_planning.py index 69cf66d..acd9449 100644 --- a/backend/routers/training_planning.py +++ b/backend/routers/training_planning.py @@ -1261,9 +1261,9 @@ def _replace_unit_phases( status_code=400, detail="phase_kind muss whole_group oder parallel sein", ) - p_oix = ph.get("order_index") - if p_oix is None: - p_oix = pi + # Reihenfolge strikt aus der Liste (pi): vermeidet UNIQUE(tu, order_index)-Kollisionen, + # wenn der Client dieselbe phase_order_index mehrfach trägt (z. B. nach Zuordnungswechseln). + p_oix = int(pi) cur.execute( """ INSERT INTO training_unit_phases (training_unit_id, order_index, phase_kind, title, guidance_notes) @@ -1272,7 +1272,7 @@ def _replace_unit_phases( """, ( unit_id, - int(p_oix), + p_oix, kind, ph.get("title"), ph.get("guidance_notes"), diff --git a/frontend/src/components/TrainingUnitSectionsEditor.jsx b/frontend/src/components/TrainingUnitSectionsEditor.jsx index 8785f7f..6f45923 100644 --- a/frontend/src/components/TrainingUnitSectionsEditor.jsx +++ b/frontend/src/components/TrainingUnitSectionsEditor.jsx @@ -1,4 +1,4 @@ -import React, { Fragment, useCallback, useEffect, useMemo, useState } from 'react' +import React, { Fragment, useCallback, useEffect, useMemo, useRef, useState } from 'react' import { GripVertical, Pencil, X } from 'lucide-react' import CombinationMethodProfileEditor from './CombinationMethodProfileEditor' import CombinationPlanBracket from './CombinationPlanBracket' @@ -532,6 +532,13 @@ export default function TrainingUnitSectionsEditor({ const [dropSectionBand, setDropSectionBand] = useState(null) /** Aktiver Reiter pro paralleler Phase (phaseOrder → streamOrder). */ const [parallelStreamTabByPhase, setParallelStreamTabByPhase] = useState({}) + /** `${phaseOrder}:${streamOrder}` während Stream-Name bearbeitet wird */ + const [streamNameEditKey, setStreamNameEditKey] = useState(null) + const [streamNameDraft, setStreamNameDraft] = useState('') + const [phaseTitleEditPo, setPhaseTitleEditPo] = useState(null) + const [phaseTitleDraft, setPhaseTitleDraft] = useState('') + const skipStreamNameBlurSave = useRef(false) + const skipPhaseTitleBlurSave = useRef(false) /** { slot: number, beforeIdx: number } */ useEffect(() => { @@ -1054,19 +1061,69 @@ export default function TrainingUnitSectionsEditor({ > Phase - { - const hi = firstSectionIndexByParallelPhase.get(parallelPhaseOrder) - const t = hi != null ? list[hi]?.planLoc?.phaseTitle : null - return t != null ? String(t) : '' - })() - } - onChange={(e) => updateParallelPhaseTitleAll(parallelPhaseOrder, e.target.value)} - placeholder={`Bezeichnung (z. B. Drill runden · Phase ${parallelPhaseOrder})`} - /> + {(() => { + const hi = firstSectionIndexByParallelPhase.get(parallelPhaseOrder) + const phaseTitleStr = + hi != null && list[hi]?.planLoc?.phaseTitle != null + ? String(list[hi].planLoc.phaseTitle) + : '' + const editingPhase = phaseTitleEditPo === parallelPhaseOrder + return editingPhase ? ( + setPhaseTitleDraft(e.target.value)} + onBlur={() => { + if (!skipPhaseTitleBlurSave.current) { + updateParallelPhaseTitleAll(parallelPhaseOrder, phaseTitleDraft) + } + skipPhaseTitleBlurSave.current = false + setPhaseTitleEditPo(null) + }} + onKeyDown={(e) => { + if (e.key === 'Enter') e.currentTarget.blur() + if (e.key === 'Escape') { + skipPhaseTitleBlurSave.current = true + setPhaseTitleEditPo(null) + e.currentTarget.blur() + } + }} + placeholder="Bezeichnung der Phase (z. B. Drill-Runde)" + /> + ) : ( + <> + + {(phaseTitleStr || '').trim() || + `Phase ${parallelPhaseOrder} · Namen per Stift bearbeiten`} + + + + ) + })()} + )} +