Enhance training framework with phase handling and payload adjustments
All checks were successful
Deploy Development / deploy (push) Successful in 39s
Test Suite / pytest-backend (push) Successful in 36s
Test Suite / lint-backend (push) Successful in 1s
Test Suite / build-frontend (push) Successful in 12s
Test Suite / k6 /health Baseline (push) Successful in 34s
Test Suite / playwright-tests (push) Successful in 1m9s

- Updated backend logic to include phases in training unit hydration and insertion processes, improving data integrity.
- Modified frontend components to support phases in training framework slots, ensuring consistent data representation.
- Refactored payload building functions to accommodate phases, enhancing the save functionality for training plans.
- Improved user interface to enable controls for parallel phases, optimizing the user experience during training program edits.
This commit is contained in:
Lars 2026-05-16 07:02:12 +02:00
parent 76cc81a385
commit 6e1cc62065
2 changed files with 31 additions and 10 deletions

View File

@ -21,6 +21,7 @@ from routers.training_planning import (
_hydrate_training_unit_payload, _hydrate_training_unit_payload,
_optional_positive_int, _optional_positive_int,
_insert_sections_from_legacy_exercises, _insert_sections_from_legacy_exercises,
_replace_unit_phases,
_replace_unit_sections, _replace_unit_sections,
_validate_variant_for_exercise, _validate_variant_for_exercise,
) )
@ -132,6 +133,7 @@ def _hydrate_framework(cur, row: Dict[str, Any]) -> Dict[str, Any]:
row_b = cur.fetchone() row_b = cur.fetchone()
if not row_b: if not row_b:
s["blueprint_training_unit_id"] = None s["blueprint_training_unit_id"] = None
s["phases"] = []
s["sections"] = [] s["sections"] = []
s["exercises"] = [] s["exercises"] = []
continue continue
@ -139,6 +141,7 @@ def _hydrate_framework(cur, row: Dict[str, Any]) -> Dict[str, Any]:
s["blueprint_training_unit_id"] = uid s["blueprint_training_unit_id"] = uid
unit_min: Dict[str, Any] = {"id": uid} unit_min: Dict[str, Any] = {"id": uid}
_hydrate_training_unit_payload(cur, unit_min) _hydrate_training_unit_payload(cur, unit_min)
s["phases"] = unit_min.get("phases", [])
s["sections"] = unit_min.get("sections", []) s["sections"] = unit_min.get("sections", [])
s["exercises"] = unit_min.get("exercises", []) s["exercises"] = unit_min.get("exercises", [])
row["slots"] = slots row["slots"] = slots
@ -250,6 +253,7 @@ def _insert_slots_and_blueprints(
framework_id: int, framework_id: int,
slots_in: Optional[List[Any]], slots_in: Optional[List[Any]],
profile_id: int, profile_id: int,
role: str,
) -> None: ) -> None:
if slots_in is None: if slots_in is None:
return return
@ -296,10 +300,13 @@ def _insert_slots_and_blueprints(
) )
bid = cur.fetchone()["id"] bid = cur.fetchone()["id"]
phases_in = slot.get("phases")
sections_in = slot.get("sections") sections_in = slot.get("sections")
exercises_in = slot.get("exercises") exercises_in = slot.get("exercises")
if sections_in is not None: if phases_in is not None and isinstance(phases_in, list) and len(phases_in) > 0:
_replace_unit_phases(cur, bid, phases_in, profile_id, role, profile_id)
elif sections_in is not None:
if len(sections_in) == 0: if len(sections_in) == 0:
_insert_default_blueprint_section(cur, bid) _insert_default_blueprint_section(cur, bid)
else: else:
@ -432,7 +439,7 @@ def create_training_framework_program(
) )
fid = cur.fetchone()["id"] fid = cur.fetchone()["id"]
_insert_goal_rows(cur, fid, goals_in) _insert_goal_rows(cur, fid, goals_in)
_insert_slots_and_blueprints(cur, fid, slots_in, profile_id) _insert_slots_and_blueprints(cur, fid, slots_in, profile_id, role)
_replace_training_types(cur, fid, tt_ids) _replace_training_types(cur, fid, tt_ids)
_replace_target_groups(cur, fid, tg_ids) _replace_target_groups(cur, fid, tg_ids)
conn.commit() conn.commit()
@ -543,7 +550,9 @@ def update_training_framework_program(
"DELETE FROM training_framework_slots WHERE framework_program_id = %s", "DELETE FROM training_framework_slots WHERE framework_program_id = %s",
(framework_id,), (framework_id,),
) )
_insert_slots_and_blueprints(cur, framework_id, data.get("slots") or [], profile_id) _insert_slots_and_blueprints(
cur, framework_id, data.get("slots") or [], profile_id, role
)
if header_fields or "goals" in data or "slots" in data or "training_type_ids" in data or "target_group_ids" in data: if header_fields or "goals" in data or "slots" in data or "training_type_ids" in data or "target_group_ids" in data:
cur.execute( cur.execute(

View File

@ -12,7 +12,7 @@ import {
defaultSection, defaultSection,
normalizeUnitToForm, normalizeUnitToForm,
enrichSectionsWithVariants, enrichSectionsWithVariants,
buildSectionsPayload, buildPlanPayloadForSave,
hydrateExercisePlanningRow, hydrateExercisePlanningRow,
reorderBlockIntoParallelStreamEnd, reorderBlockIntoParallelStreamEnd,
} from '../utils/trainingUnitSectionsForm' } from '../utils/trainingUnitSectionsForm'
@ -48,7 +48,11 @@ function emptySlot() {
async function enrichFrameworkSlotSections(slots) { async function enrichFrameworkSlotSections(slots) {
const out = [] const out = []
for (const s of slots || []) { for (const s of slots || []) {
const sec = normalizeUnitToForm({ sections: s.sections, exercises: s.exercises }) const sec = normalizeUnitToForm({
sections: s.sections,
exercises: s.exercises,
phases: s.phases,
})
out.push({ out.push({
...s, ...s,
sections: await enrichSectionsWithVariants(sec), sections: await enrichSectionsWithVariants(sec),
@ -132,7 +136,11 @@ function serverFrameworkToForm(fw) {
slots: (fw.slots || []).map((s) => ({ slots: (fw.slots || []).map((s) => ({
title: s.title || '', title: s.title || '',
notes: s.notes || '', notes: s.notes || '',
sections: normalizeUnitToForm({ sections: s.sections, exercises: s.exercises }), sections: normalizeUnitToForm({
sections: s.sections,
exercises: s.exercises,
phases: s.phases,
}),
})), })),
} }
} }
@ -151,13 +159,16 @@ function buildApiPayload(form) {
const slots = (form.slots || []).map((s, si) => { const slots = (form.slots || []).map((s, si) => {
const secList = s.sections && s.sections.length ? s.sections : [defaultSection('Ablauf')] const secList = s.sections && s.sections.length ? s.sections : [defaultSection('Ablauf')]
const sectionsPayload = buildSectionsPayload(secList) const plan = buildPlanPayloadForSave(secList)
return { const base = {
sort_order: si, sort_order: si,
title: (s.title || '').trim() || null, title: (s.title || '').trim() || null,
notes: (s.notes || '').trim() || null, notes: (s.notes || '').trim() || null,
sections: sectionsPayload,
} }
if (plan.phases) {
return { ...base, phases: plan.phases }
}
return { ...base, sections: plan.sections }
}) })
const focusAreaId = const focusAreaId =
@ -710,6 +721,7 @@ export default function TrainingFrameworkProgramEditPage() {
showExecutionExtras={false} showExecutionExtras={false}
wideExerciseGrid wideExerciseGrid
slotIndex={si} slotIndex={si}
enableParallelPhaseControls
onMoveSectionsAcrossSlots={moveSectionsAcrossFrameworkSlots} onMoveSectionsAcrossSlots={moveSectionsAcrossFrameworkSlots}
onSectionsChange={(updater) => { onSectionsChange={(updater) => {
setForm((prev) => ({ setForm((prev) => ({
@ -774,7 +786,7 @@ export default function TrainingFrameworkProgramEditPage() {
<strong style={{ color: 'var(--text1)' }}>Rahmenprogramm (Bibliothek):</strong> Wiederverwendbare Vorlage mit <strong style={{ color: 'var(--text1)' }}>Rahmenprogramm (Bibliothek):</strong> Wiederverwendbare Vorlage mit
Zielen und SessionSlots. Die <strong>Zuordnung zu Gruppe oder Kalendertermin</strong> erfolgt aus der{' '} Zielen und SessionSlots. Die <strong>Zuordnung zu Gruppe oder Kalendertermin</strong> erfolgt aus der{' '}
<strong>GruppenPlanung</strong> (Übernahme). Pro Slot planst du den Ablauf wie bei einer Trainingsseinheit:{' '} <strong>GruppenPlanung</strong> (Übernahme). Pro Slot planst du den Ablauf wie bei einer Trainingsseinheit:{' '}
<strong>Abschnitte</strong>, Übungen mit Varianten und Dauer, <strong>ZwischenAnmerkungen</strong>. <strong>Abschnitte</strong>, optional <strong>Ganzgruppen- und parallele Phasen (Breakout)</strong>, Übungen mit Varianten und Dauer, <strong>ZwischenAnmerkungen</strong>. Abschnitte kannst du per Ziehen auch in eine andere Session legen.
</div> </div>
</details> </details>