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,
_optional_positive_int,
_insert_sections_from_legacy_exercises,
_replace_unit_phases,
_replace_unit_sections,
_validate_variant_for_exercise,
)
@ -132,6 +133,7 @@ def _hydrate_framework(cur, row: Dict[str, Any]) -> Dict[str, Any]:
row_b = cur.fetchone()
if not row_b:
s["blueprint_training_unit_id"] = None
s["phases"] = []
s["sections"] = []
s["exercises"] = []
continue
@ -139,6 +141,7 @@ def _hydrate_framework(cur, row: Dict[str, Any]) -> Dict[str, Any]:
s["blueprint_training_unit_id"] = uid
unit_min: Dict[str, Any] = {"id": uid}
_hydrate_training_unit_payload(cur, unit_min)
s["phases"] = unit_min.get("phases", [])
s["sections"] = unit_min.get("sections", [])
s["exercises"] = unit_min.get("exercises", [])
row["slots"] = slots
@ -250,6 +253,7 @@ def _insert_slots_and_blueprints(
framework_id: int,
slots_in: Optional[List[Any]],
profile_id: int,
role: str,
) -> None:
if slots_in is None:
return
@ -296,10 +300,13 @@ def _insert_slots_and_blueprints(
)
bid = cur.fetchone()["id"]
phases_in = slot.get("phases")
sections_in = slot.get("sections")
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:
_insert_default_blueprint_section(cur, bid)
else:
@ -432,7 +439,7 @@ def create_training_framework_program(
)
fid = cur.fetchone()["id"]
_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_target_groups(cur, fid, tg_ids)
conn.commit()
@ -543,7 +550,9 @@ def update_training_framework_program(
"DELETE FROM training_framework_slots WHERE framework_program_id = %s",
(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:
cur.execute(

View File

@ -12,7 +12,7 @@ import {
defaultSection,
normalizeUnitToForm,
enrichSectionsWithVariants,
buildSectionsPayload,
buildPlanPayloadForSave,
hydrateExercisePlanningRow,
reorderBlockIntoParallelStreamEnd,
} from '../utils/trainingUnitSectionsForm'
@ -48,7 +48,11 @@ function emptySlot() {
async function enrichFrameworkSlotSections(slots) {
const out = []
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({
...s,
sections: await enrichSectionsWithVariants(sec),
@ -132,7 +136,11 @@ function serverFrameworkToForm(fw) {
slots: (fw.slots || []).map((s) => ({
title: s.title || '',
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 secList = s.sections && s.sections.length ? s.sections : [defaultSection('Ablauf')]
const sectionsPayload = buildSectionsPayload(secList)
return {
const plan = buildPlanPayloadForSave(secList)
const base = {
sort_order: si,
title: (s.title || '').trim() || null,
notes: (s.notes || '').trim() || null,
sections: sectionsPayload,
}
if (plan.phases) {
return { ...base, phases: plan.phases }
}
return { ...base, sections: plan.sections }
})
const focusAreaId =
@ -710,6 +721,7 @@ export default function TrainingFrameworkProgramEditPage() {
showExecutionExtras={false}
wideExerciseGrid
slotIndex={si}
enableParallelPhaseControls
onMoveSectionsAcrossSlots={moveSectionsAcrossFrameworkSlots}
onSectionsChange={(updater) => {
setForm((prev) => ({
@ -774,7 +786,7 @@ export default function TrainingFrameworkProgramEditPage() {
<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{' '}
<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>
</details>