diff --git a/backend/routers/training_framework_programs.py b/backend/routers/training_framework_programs.py
index 9855463..2b7e11c 100644
--- a/backend/routers/training_framework_programs.py
+++ b/backend/routers/training_framework_programs.py
@@ -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(
diff --git a/frontend/src/pages/TrainingFrameworkProgramEditPage.jsx b/frontend/src/pages/TrainingFrameworkProgramEditPage.jsx
index 588a6cb..3d29262 100644
--- a/frontend/src/pages/TrainingFrameworkProgramEditPage.jsx
+++ b/frontend/src/pages/TrainingFrameworkProgramEditPage.jsx
@@ -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() {
Rahmenprogramm (Bibliothek): Wiederverwendbare Vorlage mit
Zielen und Session‑Slots. Die Zuordnung zu Gruppe oder Kalendertermin erfolgt aus der{' '}
Gruppen‑Planung („Übernahme“). Pro Slot planst du den Ablauf wie bei einer Trainingsseinheit:{' '}
- Abschnitte, Übungen mit Varianten und Dauer, Zwischen‑Anmerkungen.
+ Abschnitte, optional Ganzgruppen- und parallele Phasen (Breakout), Übungen mit Varianten und Dauer, Zwischen‑Anmerkungen. Abschnitte kannst du per Ziehen auch in eine andere Session legen.