From 4080088d42a8a8be261fa8a6a11d4735b2395071 Mon Sep 17 00:00:00 2001 From: Lars Date: Tue, 5 May 2026 14:47:38 +0200 Subject: [PATCH] feat: enhance TrainingUnitSectionsEditor with heading accessory support - Added a new `headingAccessory` prop to TrainingUnitSectionsEditor for customizable header content. - Updated the component to conditionally render the heading and accessory in a flexible toolbar layout. - Refactored TrainingFrameworkProgramEditPage and TrainingPlanningPage to utilize the new heading accessory feature, improving user interaction and layout consistency. --- .../components/TrainingUnitSectionsEditor.jsx | 36 +++++++++++- .../TrainingFrameworkProgramEditPage.jsx | 27 +-------- frontend/src/pages/TrainingPlanningPage.jsx | 57 +++++-------------- .../src/utils/trainingUnitSectionsForm.js | 22 +++++++ 4 files changed, 72 insertions(+), 70 deletions(-) diff --git a/frontend/src/components/TrainingUnitSectionsEditor.jsx b/frontend/src/components/TrainingUnitSectionsEditor.jsx index 4a4cb10..960932a 100644 --- a/frontend/src/components/TrainingUnitSectionsEditor.jsx +++ b/frontend/src/components/TrainingUnitSectionsEditor.jsx @@ -46,6 +46,7 @@ export default function TrainingUnitSectionsEditor({ showExecutionExtras = false, heading = 'Abschnitte & Übungen', hideHeading = false, + headingAccessory = null, wideExerciseGrid = false, enableItemDragReorder = true, enableSectionDragReorder = true, @@ -353,8 +354,39 @@ export default function TrainingUnitSectionsEditor({ (wideExerciseGrid ? ' training-unit-sections-editor--wide' : '') } > - {!hideHeading ? ( -

{heading}

+ {(!hideHeading || headingAccessory) ? ( +
+ {!hideHeading ? ( +

+ {heading} +

+ ) : headingAccessory ? ( + + ) : null} + {headingAccessory ? ( +
+ {headingAccessory} +
+ ) : null} +
) : null} {list.map((sec, sIdx) => { const planMin = sectionPlannedMinutes(sec) diff --git a/frontend/src/pages/TrainingFrameworkProgramEditPage.jsx b/frontend/src/pages/TrainingFrameworkProgramEditPage.jsx index 72f9446..bced89d 100644 --- a/frontend/src/pages/TrainingFrameworkProgramEditPage.jsx +++ b/frontend/src/pages/TrainingFrameworkProgramEditPage.jsx @@ -6,10 +6,10 @@ import ExercisePeekModal from '../components/ExercisePeekModal' import TrainingUnitSectionsEditor from '../components/TrainingUnitSectionsEditor' import { defaultSection, - exerciseRow, normalizeUnitToForm, enrichSectionsWithVariants, buildSectionsPayload, + hydrateExercisePlanningRow, } from '../utils/trainingUnitSectionsForm' const DND_FW_SLOT = 'application/x-shinkan-framework-slot' @@ -52,27 +52,6 @@ async function enrichFrameworkSlotSections(slots) { return out } -async function hydrateExerciseForSlotRow(exercise) { - let variants = Array.isArray(exercise?.variants) ? exercise.variants : [] - let title = exercise?.title || '' - const id = exercise?.id - if (!id) return null - if (!variants.length) { - try { - const full = await api.getExercise(id) - variants = Array.isArray(full?.variants) ? full.variants : [] - title = full?.title || title - } catch { - variants = [] - } - } - const row = exerciseRow() - row.exercise_id = id - row.exercise_variant_id = '' - row.exercise_title = title - row.variants = variants - return row -} function goalHoverText(g) { const t = (g.title || '').trim() || 'Ohne Titel' const n = (g.notes || '').trim() @@ -1138,7 +1117,7 @@ export default function TrainingFrameworkProgramEditPage() { const { slotIdx, sectionIndex: sIdx } = sectionPickerCtx const rows = [] for (const ex of picked) { - const row = await hydrateExerciseForSlotRow(ex) + const row = await hydrateExercisePlanningRow(ex) if (row) rows.push(row) } if (!rows.length) return @@ -1166,7 +1145,7 @@ export default function TrainingFrameworkProgramEditPage() { if (sectionPickerCtx.multi) return if (typeof sectionPickerCtx.itemIndex !== 'number') return const { slotIdx, sectionIndex: sIdx, itemIndex: iIdx } = sectionPickerCtx - const row = await hydrateExerciseForSlotRow(exercise) + const row = await hydrateExercisePlanningRow(exercise) if (!row) return setForm((prev) => ({ ...prev, diff --git a/frontend/src/pages/TrainingPlanningPage.jsx b/frontend/src/pages/TrainingPlanningPage.jsx index fac61ad..6363bbe 100644 --- a/frontend/src/pages/TrainingPlanningPage.jsx +++ b/frontend/src/pages/TrainingPlanningPage.jsx @@ -7,34 +7,12 @@ import ExercisePeekModal from '../components/ExercisePeekModal' import TrainingUnitSectionsEditor from '../components/TrainingUnitSectionsEditor' import { defaultSection, - exerciseRow, normalizeUnitToForm, enrichSectionsWithVariants, buildSectionsPayload, + hydrateExercisePlanningRow, } from '../utils/trainingUnitSectionsForm' -async function hydrateExerciseForPickerRow(exercise) { - let variants = Array.isArray(exercise?.variants) ? exercise.variants : [] - let title = exercise?.title || '' - const id = exercise?.id - if (!id) return null - if (!variants.length) { - try { - const full = await api.getExercise(id) - variants = Array.isArray(full?.variants) ? full.variants : [] - title = full?.title || title - } catch { - variants = [] - } - } - const row = exerciseRow() - row.exercise_id = id - row.exercise_variant_id = '' - row.exercise_title = title - row.variants = variants - return row -} - function TrainingPlanningPage() { const { user } = useAuth() const [groups, setGroups] = useState([]) @@ -634,7 +612,7 @@ function TrainingPlanningPage() { background: 'var(--surface)', borderRadius: '12px', padding: 'clamp(12px, 3vw, 2rem)', - maxWidth: 'min(960px, 100%)', + maxWidth: 'min(1100px, 100%)', width: '100%', maxHeight: '92vh', overflowY: 'auto', @@ -715,26 +693,16 @@ function TrainingPlanningPage() { /> -
-

Abschnitte & Übungen

- -
- +
+ Vorlage aus Aufbau speichern + + } sections={formData.sections} + wideExerciseGrid onSectionsChange={(updater) => setFormData((prev) => ({ ...prev, @@ -754,6 +722,7 @@ function TrainingPlanningPage() { } showExecutionExtras={!!editingUnit} /> +
@@ -867,7 +836,7 @@ function TrainingPlanningPage() { const { sIdx } = exercisePickerTarget const rows = [] for (const ex of picked) { - const row = await hydrateExerciseForPickerRow(ex) + const row = await hydrateExercisePlanningRow(ex) if (row) rows.push(row) } if (!rows.length) return @@ -884,7 +853,7 @@ function TrainingPlanningPage() { } onSelectExercise={async (ex) => { if (!exercisePickerTarget || exercisePickerTarget.multi) return - const row = await hydrateExerciseForPickerRow(ex) + const row = await hydrateExercisePlanningRow(ex) if (!row) return const { sIdx, iIdx } = exercisePickerTarget if (typeof iIdx !== 'number') return diff --git a/frontend/src/utils/trainingUnitSectionsForm.js b/frontend/src/utils/trainingUnitSectionsForm.js index e41ef40..01c9b4e 100644 --- a/frontend/src/utils/trainingUnitSectionsForm.js +++ b/frontend/src/utils/trainingUnitSectionsForm.js @@ -18,6 +18,28 @@ export function exerciseRow() { } } +export async function hydrateExercisePlanningRow(exercise) { + let variants = Array.isArray(exercise?.variants) ? exercise.variants : [] + let title = exercise?.title || '' + const id = exercise?.id + if (!id) return null + if (!variants.length) { + try { + const full = await api.getExercise(id) + variants = Array.isArray(full?.variants) ? full.variants : [] + title = full?.title || title + } catch { + variants = [] + } + } + const row = exerciseRow() + row.exercise_id = id + row.exercise_variant_id = '' + row.exercise_title = title + row.variants = variants + return row +} + export function noteRow() { return { item_type: 'note', note_body: '' } }