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.