Compare commits

...

12 Commits

Author SHA1 Message Date
b35a5ae216 Merge pull request 'SplitSession auch für Rahmenprogramme' (#36) from develop into main
All checks were successful
Deploy Production / deploy (push) Successful in 45s
Test Suite / pytest-backend (push) Successful in 41s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 12s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m8s
Reviewed-on: #36
2026-05-16 09:02:51 +02:00
8c07cf36ee Enhance section movement functionality in TrainingUnitSectionsEditor
All checks were successful
Deploy Development / deploy (push) Successful in 42s
Test Suite / pytest-backend (push) Successful in 38s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m8s
Test Suite / pytest-backend (pull_request) Successful in 34s
Test Suite / lint-backend (pull_request) Successful in 0s
Test Suite / build-frontend (pull_request) Successful in 12s
Test Suite / k6 /health Baseline (pull_request) Successful in 33s
Test Suite / playwright-tests (pull_request) Successful in 1m8s
- Updated the onMoveSectionsAcrossSlots function to support additional parameters for improved section movement across slots, including handling for parallel phase indices and insertion points.
- Refined logic for moving sections between slots, ensuring proper handling of parallel streams and enhancing the overall section management experience.
2026-05-16 08:34:30 +02:00
7d2661a8e8 Remove redundant boundary checks for section movement in TrainingUnitSectionsEditor
All checks were successful
Deploy Development / deploy (push) Successful in 42s
Test Suite / pytest-backend (push) Successful in 37s
Test Suite / lint-backend (push) Successful in 1s
Test Suite / build-frontend (push) Successful in 12s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m14s
- Eliminated the checks that prevented section movement across slots when crossing the boundary between 'parallel' and 'whole_group' phases, streamlining the section management logic and improving code clarity.
2026-05-16 08:17:35 +02:00
0fdee610ed Enhance section movement validation in TrainingUnitSectionsEditor
All checks were successful
Deploy Development / deploy (push) Successful in 42s
Test Suite / pytest-backend (push) Successful in 37s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 12s
Test Suite / k6 /health Baseline (push) Successful in 34s
Test Suite / playwright-tests (push) Successful in 1m8s
- Added a new check to prevent section movement across slots that crosses the boundary between 'parallel' and 'whole_group' phases, improving the logic for section management and ensuring valid operations during edits.
2026-05-16 08:09:24 +02:00
f1c470a8a3 Improve section movement validation in TrainingUnitSectionsEditor
All checks were successful
Deploy Development / deploy (push) Successful in 41s
Test Suite / pytest-backend (push) Successful in 38s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 12s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m12s
- Added a check to ensure that the source slot is not the same as the target slot when moving sections across slots, enhancing the logic for section management and preventing unnecessary operations.
2026-05-16 08:05:10 +02:00
736656bde8 Refactor enrichFrameworkSlotSections to improve section handling in training framework
All checks were successful
Deploy Development / deploy (push) Successful in 38s
Test Suite / pytest-backend (push) Successful in 37s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 12s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m8s
- Updated the enrichFrameworkSlotSections function to utilize default sections when no sections are provided, enhancing data integrity.
- Simplified the normalization process by directly using base sections, improving code clarity and maintainability.
2026-05-16 07:53:05 +02:00
e441f59bff Add delete functionality for training plan templates
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 0s
Test Suite / build-frontend (push) Successful in 12s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m22s
2026-05-16 07:45:53 +02:00
c3eb5a62c4 Update version to 0.8.141 and enhance training plan template handling
All checks were successful
Deploy Development / deploy (push) Successful in 44s
Test Suite / pytest-backend (push) Successful in 37s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 12s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m13s
- Incremented app version to 0.8.141 and updated build date to 2026-05-14.
- Modified the planning module version to 0.12.0, improving template section handling with phase metadata.
- Introduced new functions for normalizing and inserting training plan template sections, ensuring accurate phase representation during saves.
- Updated frontend components to utilize new utility functions for managing training plan templates, enhancing user experience and data integrity.
2026-05-16 07:41:08 +02:00
79e748b470 Refactor phase handling in training unit sections for improved data integrity
All checks were successful
Deploy Development / deploy (push) Successful in 40s
Test Suite / pytest-backend (push) Successful in 36s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 11s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m8s
- Introduced canonicalization for plan locations in phased saves to ensure consistent phase representation.
- Enhanced the `inheritPlanLocForPhasedSave` function to utilize the new canonicalization logic, improving data flow.
- Updated payload building logic to check for canonicalized plan locations, ensuring accurate phase detection during saves.
2026-05-16 07:35:35 +02:00
88c4201f80 Refactor training framework to improve phase management and user experience
All checks were successful
Deploy Development / deploy (push) Successful in 39s
Test Suite / pytest-backend (push) Successful in 37s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 12s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m8s
- Enhanced phase handling in training unit hydration and insertion processes, ensuring better data integrity.
- Updated frontend components to support phase representation in training framework slots.
- Improved user interface controls for managing parallel phases, optimizing user experience during training program edits.
- Refactored payload building functions to accommodate phase adjustments, enhancing save functionality for training plans.
2026-05-16 07:27:18 +02:00
6e1cc62065 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.
2026-05-16 07:02:12 +02:00
76cc81a385 Update project documentation and enhance training features for parallel streams
All checks were successful
Deploy Development / deploy (push) Successful in 42s
Test Suite / pytest-backend (push) Successful in 37s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 20s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m8s
- Updated CLAUDE.md and PROJECT_STATUS.md to reflect the latest app version (0.8.140) and database schema (20260515063) as of 2026-05-14.
- Enhanced DOMAIN_MODEL.md and PARALLEL_TRAINING_STREAMS_CONCEPT.md to clarify the implementation of phases and parallel streams in training units.
- Improved HANDOVER.md with detailed descriptions of the coaching and breakout functionalities, including rejoin logic and session management.
- Updated FACHLICHE_NUTZERFUNKTIONEN.md to include new features related to training planning and execution, emphasizing the integration of phases and parallel streams.
- Revised FEATURES_DELIVERED_2026-Q2.md to document the latest changes and improvements in the training framework and media management.
2026-05-15 22:11:05 +02:00
19 changed files with 924 additions and 166 deletions

View File

@ -1,8 +1,8 @@
# Shinkan Jinkendo - Projekt-Status # Shinkan Jinkendo - Projekt-Status
**Stand:** 2026-05-12 **Stand:** 2026-05-14
**Version (Code):** 0.8.110 (`backend/version.py`, APP_VERSION) **Version (Code):** 0.8.140 (`backend/version.py`, APP_VERSION)
**DB-Schema-Version:** `20260512057` (`backend/version.py`, DB_SCHEMA_VERSION) **DB-Schema-Version:** `20260515063` (`backend/version.py`, DB_SCHEMA_VERSION)
**Branch:** develop **Branch:** develop
--- ---
@ -15,7 +15,7 @@
**Plattform-Rechtstexte (P-01, 0.8.950.8.96):** Admin-Editor mit **Abschnitts- und Vollvorschau** (Markdown); fortlaufende Abschnittsnummerierung in der Anzeige/PDF (Darstellung, nicht DB-persistent). **Plattform-Rechtstexte (P-01, 0.8.950.8.96):** Admin-Editor mit **Abschnitts- und Vollvorschau** (Markdown); fortlaufende Abschnittsnummerierung in der Anzeige/PDF (Darstellung, nicht DB-persistent).
**Parallel weiter relevant:** **Trainingsrahmenprogramm** (036037), **Progressionsgraph** (032034) — siehe **`TRAINING_FRAMEWORK_SPEC.md`**. **Parallel weiter relevant:** **Trainingsplan Phasen & Streams** (Migration **063**, Coach + Planung **0.8.1370.8.140**; Handover **`docs/HANDOVER.md`** §3); **Trainingsrahmenprogramm** (036037), **Progressionsgraph** (032034) — siehe **`TRAINING_FRAMEWORK_SPEC.md`**.
**Referenz:** [`library/FEATURES_DELIVERED_2026-Q2.md`](library/FEATURES_DELIVERED_2026-Q2.md) Abschnitt 12 · Medien-Norm: [`technical/MEDIA_ASSETS_AND_ARCHIVE_SPEC.md`](technical/MEDIA_ASSETS_AND_ARCHIVE_SPEC.md) (inkl. **Abschnitt 11 Inline-Medien**, umgesetzt) · **Fachlicher Nutzerüberblick:** [`../../docs/FACHLICHE_NUTZERFUNKTIONEN.md`](../../docs/FACHLICHE_NUTZERFUNKTIONEN.md) **Referenz:** [`library/FEATURES_DELIVERED_2026-Q2.md`](library/FEATURES_DELIVERED_2026-Q2.md) Abschnitt 12 · Medien-Norm: [`technical/MEDIA_ASSETS_AND_ARCHIVE_SPEC.md`](technical/MEDIA_ASSETS_AND_ARCHIVE_SPEC.md) (inkl. **Abschnitt 11 Inline-Medien**, umgesetzt) · **Fachlicher Nutzerüberblick:** [`../../docs/FACHLICHE_NUTZERFUNKTIONEN.md`](../../docs/FACHLICHE_NUTZERFUNKTIONEN.md)
@ -36,7 +36,8 @@
1. KalenderUI: „Aus Rahmen übernehmen“ an **`from-framework-slot`** anbinden; ggf. Bulk. 1. KalenderUI: „Aus Rahmen übernehmen“ an **`from-framework-slot`** anbinden; ggf. Bulk.
2. Governance: Sichtbarkeit **club/official** für Rahmen so ausprägen, dass andere Trainer kopieren dürfen (Policy + API). 2. Governance: Sichtbarkeit **club/official** für Rahmen so ausprägen, dass andere Trainer kopieren dürfen (Policy + API).
3. Optional Backlog Graph: Alternativgruppen / bessere Visualisierung (**§4**). 3. Optional Backlog Graph: Alternativgruppen / bessere Visualisierung (**§4**).
4. **Kombinationsübungen / Coach (Fachspez §10.6):** Coach **Stufe B/C** (archetypgesteuerte Durchführung); **Archetyp-Verwaltung** jenseits Code-Konstanten; **Massen-Vorbelegung** aller Slot-Zeit/Anzahl-Felder; **serverseitige** Validierung Profil ↔ Archetyp — siehe `TRAINING_MODULES_IMPLEMENTATION_PLAN.md` (Pakete **4e4g**) und `COMBINATION_TIMING_PROFILE_PLAN.md`. 4. **Breakout / Coaching (Arbeitspaket):** Backend-Konsistenz `phases`↔`sections`, Run-UI vs. Spec (Stream-Tabs), Vorlagen phasenfähig, E2E-Smoke — siehe **`docs/HANDOVER.md`** (Tabelle „Coaching & Breakout“).
5. **Kombinationsübungen / Coach (Fachspez §10.6):** Coach **Stufe B/C** (archetypgesteuerte Durchführung); **Archetyp-Verwaltung** jenseits Code-Konstanten; **Massen-Vorbelegung** aller Slot-Zeit/Anzahl-Felder; **serverseitige** Validierung Profil ↔ Archetyp — siehe `TRAINING_MODULES_IMPLEMENTATION_PLAN.md` (Pakete **4e4g**) und `COMBINATION_TIMING_PROFILE_PLAN.md`.
--- ---
@ -92,6 +93,8 @@ Die exakten Zahlen hängen von der Umgebung ab (siehe Admin/DB). Die Skills/Übu
- [x] **Optionale Zuordnung einer Übungsvariante** pro Eintrag (`exercise_variant_id`) - [x] **Optionale Zuordnung einer Übungsvariante** pro Eintrag (`exercise_variant_id`)
- [x] **Trainingsrahmenprogramm Bibliothek** (Ziele, Slots, Kontext) + **SlotBlueprints** in `training_units` (036037) - [x] **Trainingsrahmenprogramm Bibliothek** (Ziele, Slots, Kontext) + **SlotBlueprints** in `training_units` (036037)
- [x] **Materialisierung** aus RahmenSlot (`POST …/training-units/from-framework-slot`; UIAnbindung optional) - [x] **Materialisierung** aus RahmenSlot (`POST …/training-units/from-framework-slot`; UIAnbindung optional)
- [x] **Phasenmodell & parallele Streams** pro Einheit (Migration **063**): `training_unit_phases`, `training_unit_parallel_streams`; GET mit **`phases`** + flachen **`sections`**; PUT mit **`phases`** (App **0.8.1370.8.140**)
- [x] **Coaching-Modus** für Breakout: Timeline mit Split-Wahl, Rejoin vor Ganzgruppe/nächstem Split, Nachbereitung speichern → Plan & Ablauf (`TrainingCoachPage`, `trainingPlanUtils.js`)
- [ ] Kalender-View / erweiterte Roadmap (Backlog) - [ ] Kalender-View / erweiterte Roadmap (Backlog)
**MediaWiki Import:** **MediaWiki Import:**
@ -155,18 +158,19 @@ Deployment der oben genannten Migrationen und Datenabgleich nach internem Prozes
| Dokument | Pfad | Stand | Status | | Dokument | Pfad | Stand | Status |
|----------|------|-------|--------| |----------|------|-------|--------|
| Fachliche Nutzerfunktionen (Design/Product) | `docs/FACHLICHE_NUTZERFUNKTIONEN.md` | 2026-05-12 | neu, Ist-Überblick | | Fachliche Nutzerfunktionen (Design/Product) | `docs/FACHLICHE_NUTZERFUNKTIONEN.md` | 2026-05-14 | Phasen/Coach/Rejoin |
| Lieferliste Q2 2026 | `library/FEATURES_DELIVERED_2026-Q2.md` | 2026-05-12 | Verweis Version siehe `version.py` | | Lieferliste Q2 2026 | `library/FEATURES_DELIVERED_2026-Q2.md` | 2026-05-14 | §11a Breakout |
| Trainingsrahmen + Graph | `technical/TRAINING_FRAMEWORK_SPEC.md` | 2026-05-05 | ✅ §2 Blueprint | | Trainingsrahmen + Graph | `technical/TRAINING_FRAMEWORK_SPEC.md` | 2026-05-05 | ✅ §2 Blueprint |
| Anforderungen (Index) | `functional/SHINKAN_REQUIREMENTS.md` | 2026-05-12 | Verweis Nutzerüberblick | | Anforderungen (Index) | `functional/SHINKAN_REQUIREMENTS.md` | 2026-05-12 | Verweis Nutzerüberblick |
| Database Schema | `technical/DATABASE_SCHEMA.md` | 2026-05-07 | ✅ Hinweis 040046 Medien (Kurz) | | Database Schema | `technical/DATABASE_SCHEMA.md` | 2026-05-07 | ✅ Hinweis 040046 Medien (Kurz) |
| Domain Model | `functional/DOMAIN_MODEL.md` | 2026-05-12 | Version 0.4.5, Verweis Nutzerüberblick | | Domain Model | `functional/DOMAIN_MODEL.md` | 2026-05-14 | Parallele Streams Ist 063 |
| API Übungen | `technical/EXERCISES_API_SPEC.md` | 2026-05-08 | ✅ Medien/Inline-Workflow ergänzt | | API Übungen | `technical/EXERCISES_API_SPEC.md` | 2026-05-08 | ✅ Medien/Inline-Workflow ergänzt |
| Frontend Routing | `technical/EXERCISES_FRONTEND_ROUTING.md` | 2026-04-30 | ✅ Ergänzt UI-Hinweise | | Frontend Routing | `technical/EXERCISES_FRONTEND_ROUTING.md` | 2026-04-30 | ✅ Ergänzt UI-Hinweise |
| Search & Filter | `technical/SEARCH_FILTER_SPEC.md` | 2026-04-27 | ✅ Aktualisiert (Liste UX) | | Search & Filter | `technical/SEARCH_FILTER_SPEC.md` | 2026-04-27 | ✅ Aktualisiert (Liste UX) |
| Media Upload | `technical/MEDIA_UPLOAD_SPEC.md` | 2026-05-07 | ✅ Verweis Archiv/Inline | | Media Upload | `technical/MEDIA_UPLOAD_SPEC.md` | 2026-05-07 | ✅ Verweis Archiv/Inline |
| Medien-Archiv & Lifecycle | `technical/MEDIA_ASSETS_AND_ARCHIVE_SPEC.md` | 2026-05-08 | ✅ Ist-Changelog + §11 Inline erweitert | | Medien-Archiv & Lifecycle | `technical/MEDIA_ASSETS_AND_ARCHIVE_SPEC.md` | 2026-05-08 | ✅ Ist-Changelog + §11 Inline erweitert |
| Projektstatus | `PROJECT_STATUS.md` | 2026-05-12 | auf 0.8.96 + P-13/P-01 + Nutzerüberblick | | Parallele Streams (Fach/Technik) | `functional/PARALLEL_TRAINING_STREAMS_CONCEPT.md`, `technical/PARALLEL_TRAINING_STREAMS_SPEC.md` | 2026-05-14 | Ist-Stand P1 teils |
| Projektstatus | `PROJECT_STATUS.md` | 2026-05-14 | Keyset, KPIs, Breakout/Coach Kurzverweis |
--- ---
@ -177,4 +181,4 @@ Deployment der oben genannten Migrationen und Datenabgleich nach internem Prozes
--- ---
**Letzte Aktualisierung:** 2026-05-12 (Version 0.8.96, Executive Summary P-13/P-01, `docs/FACHLICHE_NUTZERFUNKTIONEN.md`) **Letzte Aktualisierung:** 2026-05-14 (Version 0.8.140, DB 063, Handover Coaching/Breakout)

View File

@ -474,25 +474,23 @@ skill_level_definitions (
**Konkretisierung (037/API):** `POST /api/training-units/from-framework-slot` legt eine geplante Einheit aus dem SlotBlueprint an; **`origin_framework_slot_id`** dient als Herkunftsreferenz (**Lineage light**; weiteres Feedback/LineageKonzept: Konzeptpapier Schritt **E**). **Konkretisierung (037/API):** `POST /api/training-units/from-framework-slot` legt eine geplante Einheit aus dem SlotBlueprint an; **`origin_framework_slot_id`** dient als Herkunftsreferenz (**Lineage light**; weiteres Feedback/LineageKonzept: Konzeptpapier Schritt **E**).
### Parallele Trainingsstreams (Breakout, Entwurf) ### Parallele Trainingsstreams (Breakout)
**Fachlich:** Eine Kalender**Einheit** kann aus **Phasen** bestehen — z.B. gemeinsamer Block, dann **beliebig viele parallele** „Teilstrecken“ (**Streams**) mit je eigenem Miniplan (Abschnitte/Übungen), erneut gemeinsamer Block. Das ist **nicht** dasselbe wie ein **RahmenprogrammSlot** (SerienSession über Wochen): Slots strukturieren **mehrere Einheiten** in einem Programm; **Streams** strukturieren **gleichzeitige** Abläufe **innerhalb einer** Einheit. **Fachlich:** Eine Kalender**Einheit** kann aus **Phasen** bestehen — z.B. gemeinsamer Block, dann **beliebig viele parallele** „Teilstrecken“ (**Streams**) mit je eigenem Miniplan (Abschnitte/Übungen), erneut gemeinsamer Block. Das ist **nicht** dasselbe wie ein **RahmenprogrammSlot** (SerienSession über Wochen): Slots strukturieren **mehrere Einheiten** in einem Programm; **Streams** strukturieren **gleichzeitige** Abläufe **innerhalb einer** Einheit.
**Sonderfall Stationen:** Rotation kann **innerhalb** einer StreamPlanung über **Kombinationsübungen** (Methodenprofil/Archetyp) abgebildet werden; hallenweit **synchron** getaktete Rotation ist eine **erweiterte** Ausbaustufe (siehe Fachkonzept). **Sonderfall Stationen:** Rotation kann **innerhalb** einer StreamPlanung über **Kombinationsübungen** (Methodenprofil/Archetyp) abgebildet werden; hallenweit **synchron** getaktete Rotation ist eine **erweiterte** Ausbaustufe (siehe Fachkonzept).
**Umsetzung (2026-05, Migration 063, App 0.8.137 ff.):** Tabellen **`training_unit_phases`** und **`training_unit_parallel_streams`**; **`training_unit_sections`** mit **`phase_id`** und **`parallel_stream_id`** (exakt eine Zuordnung pro Sektion). **`GET /api/training-units/:id`** liefert **`phases`** (verschachtelt) und flache **`sections`**. **Coaching** und **Durchführung** nutzen dieselbe Phasenlogik im Frontend (`trainingPlanUtils.js`).
**Dokumentation:** `functional/PARALLEL_TRAINING_STREAMS_CONCEPT.md`, Umsetzung `technical/PARALLEL_TRAINING_STREAMS_SPEC.md`. **Dokumentation:** `functional/PARALLEL_TRAINING_STREAMS_CONCEPT.md`, Umsetzung `technical/PARALLEL_TRAINING_STREAMS_SPEC.md`.
**Schema-Hinweis (2026-05):** Tabelle `training_unit_sections` hat **`UNIQUE (training_unit_id, order_index)`** (Migration 031). Damit sind **zwei gleichzeitige „Spuren“ mit jeweils eigener Sektion auf derselben `order_index`** nicht abbildbar — Voraussetzung für Parallele Streams ist eine **geplante Migrations-/Constraint-Anpassung** (partielle Uniques pro Phase/Stream); siehe Arbeitsdokument `.claude/docs/working/PARALLEL_TRAINING_STREAMS_ANALYSIS_AND_IMPLEMENTATION_PLAN.md`. **Keine invasive Migration ohne explizite Freigabe.**
---
## Medien-Archiv & Übungs-Anhänge (Stand 2026-05-07) ## Medien-Archiv & Übungs-Anhänge (Stand 2026-05-07)
- **`media_assets`:** Zentrale Datei-/Asset-Zeile (technisch u.a. SHADedupe, Sichtbarkeit, `club_id`, Lifecycle, Copyright, Speicherreferenz unter `library/…`). Siehe **`DATABASE_SCHEMA.md`**, **`MEDIA_ASSETS_AND_ARCHIVE_SPEC.md`**. - **`media_assets`:** Zentrale Datei-/Asset-Zeile (technisch u.a. SHADedupe, Sichtbarkeit, `club_id`, Lifecycle, Copyright, Speicherreferenz unter `library/…`). Siehe **`DATABASE_SCHEMA.md`**, **`MEDIA_ASSETS_AND_ARCHIVE_SPEC.md`**.
- **`exercise_media`:** Verknüpfung **Übung ↔ Asset** (`media_asset_id`) oder **Embed** ohne Asset; Felder wie `context` (`ablauf` \| `detail` \| `trainer_hint`), Sortierung, Primär-Medium. - **`exercise_media`:** Verknüpfung **Übung ↔ Asset** (`media_asset_id`) oder **Embed** ohne Asset; Felder wie `context` (`ablauf` \| `detail` \| `trainer_hint`), Sortierung, Primär-Medium.
- **`platform_media_storage`:** Konfiguration effektiver Medienwurzel (Superadmin, relativ zu `MEDIA_ROOT`). - **`platform_media_storage`:** Konfiguration effektiver Medienwurzel (Superadmin, relativ zu `MEDIA_ROOT`).
- **Produkt:** Medienbibliothek **`/media`**; in der Übungsbearbeitung Upload, Entfernen der Verknüpfung, **Aus Archiv verknüpfen**; Governance **`official`** und Copyright-Regeln wie in der Norm beschrieben. - **Produkt:** Medienbibliothek **`/media`**; in der Übungsbearbeitung Upload, Entfernen der Verknüpfung, **Aus Archiv verknüpfen**; Governance **`official`** und Copyright-Regeln wie in der Norm beschrieben.
- **Geplant:** **Inline-Verweise** in Fließtextfeldern auf dieselbe Verknüpfung (`exercise_media.id`) — **`MEDIA_ASSETS_AND_ARCHIVE_SPEC.md` §11**, **`docs/HANDOVER.md`** §5. - **Inline-Verweise** in Fließtextfeldern: **`MEDIA_ASSETS_AND_ARCHIVE_SPEC.md` §11**, **`docs/HANDOVER.md`** §5.
--- ---

View File

@ -1,6 +1,6 @@
# Parallele Trainingsstreams (Breakout) — Fachkonzept # Parallele Trainingsstreams (Breakout) — Fachkonzept
**Status:** Entwurf zur Abstimmung · **Stand:** 2026-05-14 **Status:** MVP-Umsetzung **teilweise** (Code) · **Stand:** 2026-05-14
**Ziel:** Planung und Durchführung von Training mit **phasenweise gemeinsamem** Ablauf und **beliebig vielen parallelen Teilstrecken** (Breakout-Sessions), inkl. Sonderfall **rotierende Stationen**. **Ziel:** Planung und Durchführung von Training mit **phasenweise gemeinsamem** Ablauf und **beliebig vielen parallelen Teilstrecken** (Breakout-Sessions), inkl. Sonderfall **rotierende Stationen**.
**Technische Ausarbeitung:** `.claude/docs/technical/PARALLEL_TRAINING_STREAMS_SPEC.md` **Technische Ausarbeitung:** `.claude/docs/technical/PARALLEL_TRAINING_STREAMS_SPEC.md`
@ -95,7 +95,14 @@ Alle Streams (oder alle Kinder insgesamt) **wechseln gleichzeitig** zur nächste
--- ---
## 8. Verwandte Dokumente ## 9. Umsetzungsstand (kurz, 2026-05-14)
- **Erreicht:** Datenmodell Phasen/Streams (**063**), API **GET/PUT** mit **`phases`**, Planungs-Breakout-UI, Durchführung und Coach nutzen dieselbe Phasen-/Stream-Logik im Frontend (`trainingPlanUtils.js`). **Synchronisationspunkt** fachlich umgesetzt: vor nächster Ganzgruppenphase oder nächstem Split erscheint im Coach die **Rejoin-Karte** (mehrere Streams), sofern nicht am absoluten Planende.
- **Noch offen:** vollständige **Persistenz-Konsistenz** bei nachträglich geänderten Sektionen, **Vorlagen** mit Phasen, **Trainer pro Stream** in der UI, ggf. **Stream-Tabs** in der Durchführungsansicht wie in §5.2 skizziert — siehe **`docs/HANDOVER.md`** (Arbeitspaket-Tabelle).
---
## 10. Verwandte Dokumente
| Dokument | Bezug | | Dokument | Bezug |
|----------|--------| |----------|--------|
@ -103,4 +110,5 @@ Alle Streams (oder alle Kinder insgesamt) **wechseln gleichzeitig** zur nächste
| `technical/TRAINING_MODULES_AND_COMBINATION_EXERCISES_SPEC.md` | Kombinationsübungen, Archetypen, Stationslogik **im Item** | | `technical/TRAINING_MODULES_AND_COMBINATION_EXERCISES_SPEC.md` | Kombinationsübungen, Archetypen, Stationslogik **im Item** |
| `functional/Shinkan Trainingsmodule Kombinationsuebungen Spezifikation V2.md` | Fachliche Tiefe Kombi | | `functional/Shinkan Trainingsmodule Kombinationsuebungen Spezifikation V2.md` | Fachliche Tiefe Kombi |
| `docs/FACHLICHE_NUTZERFUNKTIONEN.md` | Nutzerüberblick | | `docs/FACHLICHE_NUTZERFUNKTIONEN.md` | Nutzerüberblick |
| `docs/HANDOVER.md` | Ist-Stand Coach, offene Breakout-Punkte |
| `technical/DATABASE_SCHEMA.md` | Aktueller Stand Tabellen | | `technical/DATABASE_SCHEMA.md` | Aktueller Stand Tabellen |

View File

@ -123,7 +123,16 @@ Hinweis: Es gibt **keine** separaten Routen `/exercises/:id/variants/...` — Be
--- ---
## 12. Medien-Archiv & Medienbibliothek (Migration **045** ff., App ca. **0.8.410.8.64**) ## 12. Trainingsplan: Phasen & parallele Streams (DB **063**, App **0.8.1370.8.140**)
- **063:** `training_unit_phases`, `training_unit_parallel_streams`; Sektionen mit `phase_id` / `parallel_stream_id`; Default-Ganzgruppenphase für Bestand.
- **API:** `GET /api/training-units/:id` mit **`phases`** + **`sections`**; `PUT`/`POST` mit **`phases`** für Breakout-Einheiten (**0.8.138**); Rahmen-Slot-Materialisierung kopiert Phasen (**0.8.138**).
- **Frontend:** Planung Breakout-Panel (**0.8.1390.8.140**); **`trainingPlanUtils.js`** — `sectionsWithPlanLocForDisplay`, `flattenPlanTimeline`, `buildCoachSavePlanPayload`, Split-Rejoin (`coachShouldPromptSplitRejoinTransition`); **`TrainingCoachPage`**, **`TrainingUnitRunPage`**.
- **Doku:** `.claude/docs/technical/PARALLEL_TRAINING_STREAMS_SPEC.md`, `docs/HANDOVER.md` §3, Arbeitspaket „offen“.
---
## 13. Medien-Archiv & Medienbibliothek (Migration **045** ff., App ca. **0.8.410.8.64**)
Einzelnorm: **`technical/MEDIA_ASSETS_AND_ARCHIVE_SPEC.md`**. Kurzüberblick geliefert: Einzelnorm: **`technical/MEDIA_ASSETS_AND_ARCHIVE_SPEC.md`**. Kurzüberblick geliefert:
@ -150,7 +159,7 @@ Einzelnorm: **`technical/MEDIA_ASSETS_AND_ARCHIVE_SPEC.md`**. Kurzüberblick gel
--- ---
## 13. Nächste sinnvolle Schritte (nicht Lieferstand) ## 14. Nächste sinnvolle Schritte (nicht Lieferstand)
- Trainingsplanung: KalenderUIAnbindung **„aus Rahmen übernehmen“**; Visibility/Policies für geteilte Rahmen (**CURR004** später). - Trainingsplanung: KalenderUIAnbindung **„aus Rahmen übernehmen“**; Visibility/Policies für geteilte Rahmen (**CURR004** später).
- Progressions-Serien als **Blöcke** (angekündigt; Voraussetzung: `prerequisite_variant_id` / `progression_level` vorhanden). - Progressions-Serien als **Blöcke** (angekündigt; Voraussetzung: `prerequisite_variant_id` / `progression_level` vorhanden).
@ -160,7 +169,7 @@ Einzelnorm: **`technical/MEDIA_ASSETS_AND_ARCHIVE_SPEC.md`**. Kurzüberblick gel
--- ---
## 14. Verweise ## 15. Verweise
| Thema | Dokument | | Thema | Dokument |
|--------|----------| |--------|----------|
@ -170,5 +179,7 @@ Einzelnorm: **`technical/MEDIA_ASSETS_AND_ARCHIVE_SPEC.md`**. Kurzüberblick gel
| Datenbank Überblick | `technical/DATABASE_SCHEMA.md` | | Datenbank Überblick | `technical/DATABASE_SCHEMA.md` |
| Medien Upload (Limits, MIME) | `technical/MEDIA_UPLOAD_SPEC.md` | | Medien Upload (Limits, MIME) | `technical/MEDIA_UPLOAD_SPEC.md` |
| Medien-Archiv & Lifecycle | `technical/MEDIA_ASSETS_AND_ARCHIVE_SPEC.md` | | Medien-Archiv & Lifecycle | `technical/MEDIA_ASSETS_AND_ARCHIVE_SPEC.md` |
| Parallele Phasen/Streams | `functional/PARALLEL_TRAINING_STREAMS_CONCEPT.md`, `technical/PARALLEL_TRAINING_STREAMS_SPEC.md` |
| Coaching/Breakout-Handover | `docs/HANDOVER.md` |
| Fachlicher Nutzerüberblick | `docs/FACHLICHE_NUTZERFUNKTIONEN.md` (Repo-Root) | | Fachlicher Nutzerüberblick | `docs/FACHLICHE_NUTZERFUNKTIONEN.md` (Repo-Root) |
| Projektstatus-Kachel | `../PROJECT_STATUS.md` | | Projektstatus-Kachel | `../PROJECT_STATUS.md` |

View File

@ -1,23 +1,26 @@
# Parallele Trainingsstreams — Technische Spezifikation (Umsetzung) # Parallele Trainingsstreams — Technische Spezifikation (Umsetzung)
**Status:** Entwurf · **Stand:** 2026-05-14 **Status:** Umsetzung **Phase 1 (teils)** · **Stand:** 2026-05-14
**Fachgrundlage:** `.claude/docs/functional/PARALLEL_TRAINING_STREAMS_CONCEPT.md` **Fachgrundlage:** `.claude/docs/functional/PARALLEL_TRAINING_STREAMS_CONCEPT.md`
Dieses Dokument beschreibt die **Umsetzung** auf Basis der **aktuellen Codebasis** (Stand Analyse 2026-05-14): eine `training_unit` mit **`training_unit_sections`** und **`training_unit_section_items`** (Übung/Notiz, optional `planning_method_profile` für Kombinationsübungen, Migration **057**); Rahmen-**Blueprint**-Einheiten mit `framework_slot_id` (**037**); Leitung **`lead_trainer_profile_id`** (**038**); Co-Trainer **`assistant_trainer_profile_ids`** JSONB (**042**); Durchführung **`TrainingUnitRunPage`** (sequentiell über Sektionen). Dieses Dokument beschreibt die **Umsetzung** auf Basis der **aktuellen Codebasis** (Stand 2026-05-14): **`training_unit_phases` / `training_unit_parallel_streams`** (Migration **063**) und **`training_unit_sections`** mit Phasen-/Stream-Bezug; **`training_unit_section_items`** (Übung/Notiz, optional `planning_method_profile` für Kombinationsübungen, Migration **057**); Rahmen-**Blueprint**-Einheiten mit `framework_slot_id` (**037**); Leitung **`lead_trainer_profile_id`** (**038**); Co-Trainer **`assistant_trainer_profile_ids`** JSONB (**042**); Durchführung und Coaching über **`TrainingUnitRunPage`**, **`TrainingCoachPage`** und **`trainingPlanUtils.js`**.
--- ---
## 1. Ist-Stand (relevant) ## 1. Ist-Stand (Code, 2026-05-14)
| Bereich | Aktuell | | Bereich | Aktuell |
|---------|---------| |---------|---------|
| Planstruktur | **Eine** lineare Liste `training_unit_sections` je `training_unit_id`; Items in `training_unit_section_items`. | | **Schema** | Migration **063:** `training_unit_phases`, `training_unit_parallel_streams`; Sektionen mit `phase_id` **oder** `parallel_stream_id`. |
| Rahmenprogramm | `training_framework_slots` verweisen auf **Blueprint**-`training_units` — Slots = **Serien-Spalten**, nicht simultane Breakouts in **einer** Halle. | | **API** | `GET /api/training-units/:id`**`phases`** (verschachtelt) + flache **`sections`**. `PUT/POST` mit **`phases`** für Breakout-Einheiten (**0.8.138**); höchstens eines von `phases`, `sections`, `exercises` pro Request (Planning-Router). Legacy-PUT mit nur `sections` erzeugt/ergänzt Ganzgruppen-Phase. |
| Kombinationsübung | Ein **Item** kann Kombi sein; `planning_method_profile` = Snapshot; Coaching-UI teilweise (`CombinationPlanBracket` in Run/Peek). | | **Planung (UI)** | Breakout-Panel: Ganzgruppen-/parallele Phasen, Streams; Speichern phasenbasiert (`trainingUnitSectionsForm.js`, `TrainingPlanningPage`). |
| Trainer-Zuweisung | `lead_trainer_profile_id`, `assistant_trainer_profile_ids` am **`training_units`**-Kopf; **keine** Zuordnung zu „welcher parallelen Spur“. | | **Durchführung** | `TrainingUnitRunPage.jsx` + `trainingPlanUtils.js` (`sectionsWithPlanLocForDisplay`, `buildPlanRunViewModelFromSections`) — Phasenfolge in „Plan & Ablauf“. |
| Run-Modus | `TrainingUnitRunPage`: sortierte Sektionen/Items, Checkliste, Fortschritt in `sessionStorage` pro Einheit. | | **Coaching** | `TrainingCoachPage.jsx` + `flattenPlanTimeline`, Stream-Picks, Rejoin vor Ganzgruppe/nächstem Split (`coachShouldPromptSplitRejoinTransition`), Nachbereitung mit `buildCoachSavePlanPayload`, danach Navigation zu `/planning/run/:id`. |
| **Kombinationsübung** | Unverändert je Item; `planning_method_profile`, Coach-Kombi-Stufe A. |
| **Trainer-Zuweisung** | `lead_trainer_profile_id`, `assistant_trainer_profile_ids` am Einheitskopf; **Stream-**`assigned_trainer_profile_ids` im Schema — UI/Policy noch nicht vollständig (siehe **§8 offen**). |
| **Rahmenprogramm** | Blueprint-`training_units` können dieselbe Phasenstruktur tragen; Kopie aus Slot (`from-framework-slot`, **0.8.138**). |
**Konsequenz:** Parallele Streams erfordern ein **erweitertes konzeptionelles „Gefäß“** unterhalb der Einheit (Phasen und/oder Streams) und eine **Verknüpfung** bestehender Sektionen mit diesem Gefäß — oder eine **Migration** zu einem neuen Pflicht-Container (siehe §3). **Hinweis:** Die frühere Planungsvariante „nur lineare `training_unit_sections` ohne Phasen“ gilt weiter für Alt-Daten; Migration **063** ordnet Bestand einer Default-Ganzgruppenphase zu.
--- ---
@ -37,9 +40,11 @@ training_unit (Kalender-Einheit)
## 3. Datenmodell — Optionen ## 3. Datenmodell — Optionen
**Ist (063):** Die unten skizzierte **empfohlene** Normalform ist unter den genannten Tabellennamen produktiv; die Abschnitte 3.1/3.2 bleiben zur Einordnung erhalten.
### 3.1 Empfohlen: explizite Phasen + Streams (normalisiert) ### 3.1 Empfohlen: explizite Phasen + Streams (normalisiert)
Neue Tabellen (Namen bei Implementierung final festlegen): Die Tabellen sind **umgesetzt** (Namen final):
| Tabelle | Zweck | | Tabelle | Zweck |
|---------|--------| |---------|--------|
@ -109,15 +114,15 @@ Nur **`training_unit_parallel_streams`** + `parallel_stream_id` auf Sektionen; P
--- ---
## 8. Implementierungsphasen (Vorschlag) ## 8. Implementierungsphasen (Abgleich)
| Phase | Inhalt | | Phase | Inhalt | Stand 2026-05-14 |
|-------|--------| |-------|--------|------------------|
| **P1** | Schema Phasen + Streams; Migration; GET/PATCH Einheit verschachtelt; Planungs-UI; Run-UI mit Stream-Tabs | | **P1** | Schema Phasen + Streams; Migration **063**; GET/PUT verschachtelt; Planungs-UI; Run + Coach phasenbasiert | **Teilweise erledigt** — Run-UI nutzt Phasen-Timeline in der Anzeige; **Stream-Tabs** optional noch zu vereinheitlichen (§5.2) |
| **P2** | Trainer-Zuordnung pro Stream + effektive Anzeige; Vorlagen erweitert | | **P2** | Trainer-Zuordnung pro Stream + effektive Anzeige; Vorlagen erweitert | **Offen** |
| **P3** | Synchroner Hallen-Takt / Rotationsmatrix (falls fachlich freigegeben) | | **P3** | Synchroner Hallen-Takt / Rotationsmatrix (falls fachlich freigegeben) | **Offen** |
--- **Offene Punkte (kurz):** siehe **`docs/HANDOVER.md`** Tabelle „Coaching & Breakout“.
## 9. Verwandte Dokumente ## 9. Verwandte Dokumente
@ -127,4 +132,5 @@ Nur **`training_unit_parallel_streams`** + `parallel_stream_id` auf Sektionen; P
| `technical/TRAINING_FRAMEWORK_SPEC.md` | Rahmen-Slot vs. Parallelität | | `technical/TRAINING_FRAMEWORK_SPEC.md` | Rahmen-Slot vs. Parallelität |
| `technical/TRAINING_MODULES_AND_COMBINATION_EXERCISES_SPEC.md` | Kombi, `planning_method_profile` | | `technical/TRAINING_MODULES_AND_COMBINATION_EXERCISES_SPEC.md` | Kombi, `planning_method_profile` |
| `technical/DATABASE_SCHEMA.md`, `backend/migrations/` | DDL-Historie | | `technical/DATABASE_SCHEMA.md`, `backend/migrations/` | DDL-Historie |
| `frontend/src/pages/TrainingPlanningPage.jsx`, `TrainingUnitRunPage.jsx`, `TrainingFrameworkProgramEditPage.jsx` | Ist-UI | | `TrainingPlanningPage.jsx`, `TrainingUnitRunPage.jsx`, `TrainingFrameworkProgramEditPage.jsx` | Planung, Durchführung, Rahmen |
| `frontend/src/utils/trainingPlanUtils.js`, `TrainingCoachPage.jsx` | Phasen-Timeline, Rejoin, Coach-Speichern |

View File

@ -86,7 +86,7 @@ frontend/src/
**Siehe:** `backend/version.py` (`APP_VERSION`, `DB_SCHEMA_VERSION`, `MODULE_VERSIONS`) und `.claude/docs/PROJECT_STATUS.md`. **Siehe:** `backend/version.py` (`APP_VERSION`, `DB_SCHEMA_VERSION`, `MODULE_VERSIONS`) und `.claude/docs/PROJECT_STATUS.md`.
Kurz (Stand 2026-05-12): App **0.8.96**, DBSchemaVersion siehe **`backend/version.py`**; Kern: Übungen, Varianten, **Medien-Archiv & Bibliothek (`/media`)**, **Inline-Medien im Rich-Text**, **Inhaltsmeldungen (P-13)** im Posteingang, Mandanten-Sync aktiver Verein, Planung mit Sektionen, **Trainingsrahmen Bibliothek + SlotBlueprint** (036037), Progressionsgraph, Reifegrad/MatrixStack — Details `PROJECT_STATUS.md`, `docs/HANDOVER.md`, Nutzerüberblick **`docs/FACHLICHE_NUTZERFUNKTIONEN.md`**, `MEDIA_ASSETS_AND_ARCHIVE_SPEC.md` (Abschnitt 11 umgesetzt). Kurz (Stand 2026-05-14): App- und DB-Version siehe **`backend/version.py`**; Kern: Übungen, Varianten, **Medien-Archiv & Bibliothek (`/media`)**, **Inline-Medien im Rich-Text**, **Inhaltsmeldungen (P-13)** im Posteingang, Mandanten-Sync aktiver Verein, Planung mit **Phasen & parallelen Streams (Breakout, 063)**, **Trainingsrahmen Bibliothek + SlotBlueprint** (036037), Progressionsgraph, Reifegrad/MatrixStack — Details `PROJECT_STATUS.md`, `docs/HANDOVER.md`, Nutzerüberblick **`docs/FACHLICHE_NUTZERFUNKTIONEN.md`**, `PARALLEL_TRAINING_STREAMS_SPEC.md`, `MEDIA_ASSETS_AND_ARCHIVE_SPEC.md` (Abschnitt 11 umgesetzt).
### Log (Auszug) ### Log (Auszug)

View File

@ -0,0 +1,8 @@
-- Vorlagen: Phasen/Parallel-Streams wie im Einheiten-Editor (planLoc-Abbild)
ALTER TABLE training_plan_template_sections
ADD COLUMN IF NOT EXISTS phase_kind VARCHAR(20) NOT NULL DEFAULT 'whole_group',
ADD COLUMN IF NOT EXISTS phase_order_index INT NOT NULL DEFAULT 0,
ADD COLUMN IF NOT EXISTS parallel_stream_order_index INT NULL;
COMMENT ON COLUMN training_plan_template_sections.parallel_stream_order_index IS
'NULL = Ganzgruppen-Abschnitt; 0..n = Stream innerhalb paralleler Phase';

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

@ -1523,32 +1523,187 @@ def _insert_sections_from_legacy_exercises(cur, unit_id: int, exercises_in: List
_insert_section_items(cur, sid, filtered, start_order=0) _insert_section_items(cur, sid, filtered, start_order=0)
def _instantiate_from_template(cur, unit_id: int, template_id: int): def _normalize_training_plan_template_section_payload(sec: Any, si: int) -> Dict[str, Any]:
_clear_unit_plan_content(cur, unit_id) title = (sec.get("title") or "").strip() or f"Abschnitt {si + 1}"
pid = _ensure_default_whole_group_phase(cur, unit_id, order_index=0) order_ix = sec.get("order_index")
if order_ix is None:
order_ix = si
try:
order_ix = int(order_ix)
except (TypeError, ValueError):
order_ix = si
pk = str(sec.get("phase_kind") or "whole_group").strip().lower()
if pk not in ("whole_group", "parallel"):
pk = "whole_group"
try:
p_oi = int(sec.get("phase_order_index") if sec.get("phase_order_index") is not None else 0)
except (TypeError, ValueError):
p_oi = 0
p_so: Optional[int] = None
if pk == "parallel":
raw_so = sec.get("parallel_stream_order_index")
try:
p_so = int(raw_so) if raw_so is not None and raw_so != "" else 0
except (TypeError, ValueError):
p_so = 0
return {
"title": title,
"order_index": order_ix,
"guidance_text": sec.get("guidance_text"),
"phase_kind": pk,
"phase_order_index": p_oi,
"parallel_stream_order_index": p_so,
}
def _insert_training_plan_template_sections(cur, template_id: int, sections_in: List[Any]) -> None:
for si, sec in enumerate(sections_in):
row = _normalize_training_plan_template_section_payload(sec, si)
cur.execute( cur.execute(
""" """
SELECT id, title, guidance_text INSERT INTO training_plan_template_sections (
template_id, order_index, title, guidance_text,
phase_kind, phase_order_index, parallel_stream_order_index
) VALUES (%s, %s, %s, %s, %s, %s, %s)
""",
(
template_id,
row["order_index"],
row["title"],
row["guidance_text"],
row["phase_kind"],
row["phase_order_index"],
row["parallel_stream_order_index"],
),
)
def _template_rows_to_phases_payload(rows: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
"""Flache Vorlagen-Sektionen → `phases`-Liste wie beim Training-Unit PUT (nur Gliederung, leere items)."""
if not rows:
return []
phases_out: List[Dict[str, Any]] = []
i = 0
n = len(rows)
while i < n:
r0 = rows[i]
pk0 = str(r0.get("phase_kind") or "whole_group").strip().lower()
if pk0 not in ("whole_group", "parallel"):
pk0 = "whole_group"
try:
p_oix0 = int(r0.get("phase_order_index") if r0.get("phase_order_index") is not None else 0)
except (TypeError, ValueError):
p_oix0 = 0
run: List[Dict[str, Any]] = []
while i < n:
r = rows[i]
pk = str(r.get("phase_kind") or "whole_group").strip().lower()
if pk not in ("whole_group", "parallel"):
pk = "whole_group"
try:
p_oix = int(r.get("phase_order_index") if r.get("phase_order_index") is not None else 0)
except (TypeError, ValueError):
p_oix = 0
if pk != pk0 or p_oix != p_oix0:
break
run.append(r)
i += 1
if pk0 == "whole_group":
secs = []
for j, rr in enumerate(run):
tid = rr.get("id")
secs.append(
{
"title": rr.get("title"),
"order_index": j,
"guidance_notes": rr.get("guidance_text"),
"items": [],
**(
{"source_template_section_id": int(tid)}
if tid is not None
else {}
),
}
)
phases_out.append(
{
"phase_kind": "whole_group",
"order_index": p_oix0,
"title": None,
"guidance_notes": None,
"sections": secs,
}
)
else:
by_stream: Dict[int, List[Dict[str, Any]]] = {}
for rr in run:
raw_so = rr.get("parallel_stream_order_index")
try:
so = int(raw_so) if raw_so is not None and raw_so != "" else 0
except (TypeError, ValueError):
so = 0
by_stream.setdefault(so, []).append(rr)
stream_order = sorted(by_stream.keys())
streams = []
for so in stream_order:
bucket = by_stream[so]
st: Dict[str, Any] = {
"order_index": so,
"title": None,
"notes": None,
"sections": [],
}
for j, rr in enumerate(bucket):
tid = rr.get("id")
st["sections"].append(
{
"title": rr.get("title"),
"order_index": j,
"guidance_notes": rr.get("guidance_text"),
"items": [],
**(
{"source_template_section_id": int(tid)}
if tid is not None
else {}
),
}
)
streams.append(st)
phases_out.append(
{
"phase_kind": "parallel",
"order_index": p_oix0,
"title": None,
"guidance_notes": None,
"streams": streams,
}
)
return phases_out
def _instantiate_from_template(
cur,
unit_id: int,
template_id: int,
*,
profile_id: int,
role: str,
unit_created_by: int,
) -> None:
cur.execute(
"""
SELECT id, title, guidance_text, order_index, phase_kind, phase_order_index, parallel_stream_order_index
FROM training_plan_template_sections FROM training_plan_template_sections
WHERE template_id = %s WHERE template_id = %s
ORDER BY order_index ORDER BY order_index
""", """,
(template_id,), (template_id,),
) )
rows = cur.fetchall() rows_raw = cur.fetchall()
for gi, row in enumerate(rows): rows = [r2d(r) for r in rows_raw]
r = r2d(row)
cur.execute(
"""
INSERT INTO training_unit_sections (
training_unit_id, phase_id, parallel_stream_id, order_index, title, guidance_notes, source_template_section_id
) VALUES (%s, %s, NULL, %s, %s, %s, %s)
""",
(unit_id, pid, gi, r["title"], r["guidance_text"], r["id"]),
)
# Fallback: keine Sektionen in Vorlage → ein leerer Block
if not rows: if not rows:
_clear_unit_plan_content(cur, unit_id)
pid = _ensure_default_whole_group_phase(cur, unit_id, order_index=0)
cur.execute( cur.execute(
""" """
INSERT INTO training_unit_sections ( INSERT INTO training_unit_sections (
@ -1557,6 +1712,18 @@ def _instantiate_from_template(cur, unit_id: int, template_id: int):
""", """,
(unit_id, pid), (unit_id, pid),
) )
return
phases_payload = _template_rows_to_phases_payload(rows)
_clear_unit_plan_content(cur, unit_id)
_replace_unit_phases(
cur,
unit_id,
phases_payload,
profile_id,
role,
unit_created_by,
)
def _fetch_training_plan_template_row(cur, tid: int) -> Dict[str, Any]: def _fetch_training_plan_template_row(cur, tid: int) -> Dict[str, Any]:
@ -1674,18 +1841,7 @@ def create_training_plan_template(data: dict, tenant: TenantContext = Depends(ge
(club_id, profile_id, name, data.get("description"), visibility), (club_id, profile_id, name, data.get("description"), visibility),
) )
tid = cur.fetchone()["id"] tid = cur.fetchone()["id"]
for si, sec in enumerate(sections_in): _insert_training_plan_template_sections(cur, tid, sections_in)
title = (sec.get("title") or "").strip() or f"Abschnitt {si + 1}"
order_ix = sec.get("order_index")
if order_ix is None:
order_ix = si
cur.execute(
"""
INSERT INTO training_plan_template_sections (template_id, order_index, title, guidance_text)
VALUES (%s, %s, %s, %s)
""",
(tid, order_ix, title, sec.get("guidance_text")),
)
conn.commit() conn.commit()
return get_training_plan_template(tid, tenant) return get_training_plan_template(tid, tenant)
@ -1743,18 +1899,7 @@ def update_training_plan_template(template_id: int, data: dict, tenant: TenantCo
"DELETE FROM training_plan_template_sections WHERE template_id = %s", (template_id,) "DELETE FROM training_plan_template_sections WHERE template_id = %s", (template_id,)
) )
sections_in = data["sections"] or [] sections_in = data["sections"] or []
for si, sec in enumerate(sections_in): _insert_training_plan_template_sections(cur, template_id, sections_in)
title = (sec.get("title") or "").strip() or f"Abschnitt {si + 1}"
order_ix = sec.get("order_index")
if order_ix is None:
order_ix = si
cur.execute(
"""
INSERT INTO training_plan_template_sections (template_id, order_index, title, guidance_text)
VALUES (%s, %s, %s, %s)
""",
(template_id, order_ix, title, sec.get("guidance_text")),
)
conn.commit() conn.commit()
return get_training_plan_template(template_id, tenant) return get_training_plan_template(template_id, tenant)
@ -2400,7 +2545,14 @@ def create_training_unit(data: dict, tenant: TenantContext = Depends(get_tenant_
elif sections_in is not None: elif sections_in is not None:
_replace_unit_sections(cur, unit_id, sections_in) _replace_unit_sections(cur, unit_id, sections_in)
elif tpl_id_safe: elif tpl_id_safe:
_instantiate_from_template(cur, unit_id, tpl_id_safe) _instantiate_from_template(
cur,
unit_id,
tpl_id_safe,
profile_id=profile_id,
role=role,
unit_created_by=profile_id,
)
elif exercises_in is not None: elif exercises_in is not None:
_insert_sections_from_legacy_exercises(cur, unit_id, exercises_in) _insert_sections_from_legacy_exercises(cur, unit_id, exercises_in)
@ -2576,7 +2728,14 @@ def update_training_unit(unit_id: int, data: dict, tenant: TenantContext = Depen
cur.execute( cur.execute(
"UPDATE training_units SET plan_template_id = %s WHERE id = %s", (tid, unit_id) "UPDATE training_units SET plan_template_id = %s WHERE id = %s", (tid, unit_id)
) )
_instantiate_from_template(cur, unit_id, tid) _instantiate_from_template(
cur,
unit_id,
tid,
profile_id=profile_id,
role=role,
unit_created_by=int(unit_row.get("created_by") or profile_id),
)
content_handled = True content_handled = True
_assert_single_plan_content_key_update(data) _assert_single_plan_content_key_update(data)
@ -2783,7 +2942,14 @@ def quick_create_training_unit(data: dict, tenant: TenantContext = Depends(get_t
unit_id = cur.fetchone()["id"] unit_id = cur.fetchone()["id"]
if tpl_id_safe: if tpl_id_safe:
_instantiate_from_template(cur, unit_id, tpl_id_safe) _instantiate_from_template(
cur,
unit_id,
tpl_id_safe,
profile_id=profile_id,
role=role,
unit_created_by=profile_id,
)
_promote_private_exercises_used_in_unit(cur, unit_id, profile_id, role) _promote_private_exercises_used_in_unit(cur, unit_id, profile_id, role)
conn.commit() conn.commit()

View File

@ -0,0 +1,206 @@
"""
PostgreSQL-Integration: Rahmenprogramm-Slot mit verschachtelten `phases` (Blueprint-Unit).
Aktivierung:
- Lokal: TRAINING_PLANNING_INTEGRATION=1
- CI: .gitea/workflows/test.yml setzt die Variable beim pytest-Lauf.
Prüft `_insert_slots_and_blueprints` `_replace_unit_phases` wie beim API-PUT mit Slot-Payload.
"""
from __future__ import annotations
import os
import uuid
import pytest
from db import get_db, get_cursor
from routers.training_framework_programs import _insert_slots_and_blueprints
from routers.training_planning import _fetch_phases_nested
def _integration_enabled() -> bool:
return os.getenv("TRAINING_PLANNING_INTEGRATION", "").strip().lower() in ("1", "true", "yes")
pytestmark = [
pytest.mark.integration,
pytest.mark.skipif(
not _integration_enabled(),
reason="TRAINING_PLANNING_INTEGRATION=1 und PostgreSQL (DB_*) erforderlich",
),
]
def _db_ping() -> bool:
try:
with get_db() as conn:
cur = get_cursor(conn)
cur.execute("SELECT 1 AS ok")
row = cur.fetchone()
return row is not None and row.get("ok") == 1
except Exception:
return False
@pytest.fixture(scope="module")
def db_ready():
if not _db_ping():
pytest.skip("PostgreSQL nicht erreichbar (DB_HOST/DB_PORT/…)")
def test_framework_blueprint_slot_phases_roundtrip(db_ready):
"""Ein Slot mit `phases` erzeugt eine Blueprint-Unit mit identischer Phasenstruktur."""
suffix = uuid.uuid4().hex[:12]
club_name = f"fw_ph_club_{suffix}"
email = f"fw_ph_{suffix}@test.local"
from auth import hash_pin
with get_db() as conn:
cur = get_cursor(conn)
cur.execute(
"INSERT INTO clubs (name, abbreviation, status) VALUES (%s, %s, %s) RETURNING id",
(club_name, "F", "active"),
)
club_id = int(cur.fetchone()["id"])
cur.execute(
"""
INSERT INTO profiles (email, pin_hash, name, role, active_club_id)
VALUES (%s, %s, %s, %s, %s)
RETURNING id
""",
(email, hash_pin("x"), f"FWPH {suffix}", "trainer", club_id),
)
profile_id = int(cur.fetchone()["id"])
cur.execute(
"""
INSERT INTO exercises (title, goal, execution, visibility, status, created_by)
VALUES (%s, %s, %s, %s, %s, %s)
RETURNING id
""",
(f"Übung FWPH {suffix}", "Ziel", "Ablauf", "private", "draft", profile_id),
)
ex_id = int(cur.fetchone()["id"])
cur.execute(
"""
INSERT INTO training_framework_programs (
title, description,
planned_period_start, planned_period_end,
visibility, club_id, created_by,
focus_area_id, style_direction_id
) VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s)
RETURNING id
""",
(
f"Rahmen FWPH {suffix}",
None,
None,
None,
"private",
club_id,
profile_id,
None,
None,
),
)
fw_id = int(cur.fetchone()["id"])
phases_in = [
{
"phase_kind": "whole_group",
"order_index": 0,
"title": "Aufwärmen",
"sections": [
{
"title": "Gemeinsam",
"order_index": 0,
"items": [
{"item_type": "note", "order_index": 0, "note_body": "Los"},
],
},
],
},
{
"phase_kind": "parallel",
"order_index": 1,
"title": "Breakout",
"streams": [
{
"order_index": 0,
"title": "Matte A",
"sections": [
{
"title": "Technik A",
"order_index": 0,
"items": [
{
"item_type": "exercise",
"order_index": 0,
"exercise_id": ex_id,
"planned_duration_min": 10,
},
],
},
],
},
],
},
]
slots_in = [
{
"sort_order": 0,
"title": "Session 1",
"notes": None,
"phases": phases_in,
},
]
_insert_slots_and_blueprints(cur, fw_id, slots_in, profile_id, "trainer")
cur.execute(
"""
SELECT id FROM training_framework_slots
WHERE framework_program_id = %s
ORDER BY sort_order
LIMIT 1
""",
(fw_id,),
)
slot_row = cur.fetchone()
assert slot_row is not None
slot_id = int(slot_row["id"])
cur.execute(
"SELECT id FROM training_units WHERE framework_slot_id = %s",
(slot_id,),
)
bu_row = cur.fetchone()
assert bu_row is not None
blueprint_unit_id = int(bu_row["id"])
nested = _fetch_phases_nested(cur, blueprint_unit_id)
conn.commit()
try:
assert len(nested) == 2
assert nested[0]["phase_kind"] == "whole_group"
assert len(nested[0].get("sections") or []) == 1
assert nested[1]["phase_kind"] == "parallel"
streams = nested[1].get("streams") or []
assert len(streams) == 1
assert len(streams[0].get("sections") or []) == 1
assert streams[0]["sections"][0]["title"] == "Technik A"
assert int(streams[0]["sections"][0]["items"][0]["exercise_id"]) == ex_id
finally:
with get_db() as conn:
cur = get_cursor(conn)
cur.execute("DELETE FROM training_framework_programs WHERE id = %s", (fw_id,))
cur.execute("DELETE FROM exercises WHERE id = %s", (ex_id,))
cur.execute("DELETE FROM profiles WHERE id = %s", (profile_id,))
cur.execute("DELETE FROM clubs WHERE id = %s", (club_id,))
conn.commit()

View File

@ -1,8 +1,8 @@
# Shinkan Jinkendo Version Information # Shinkan Jinkendo Version Information
APP_VERSION = "0.8.140" APP_VERSION = "0.8.141"
BUILD_DATE = "2026-05-12" BUILD_DATE = "2026-05-14"
DB_SCHEMA_VERSION = "20260515063" DB_SCHEMA_VERSION = "20260515064"
MODULE_VERSIONS = { MODULE_VERSIONS = {
"legal_documents": "1.4.0", # Admin: Live-Vorschau pro Abschnitt + modale Vollvorschau (Editor + Dokumentenliste) "legal_documents": "1.4.0", # Admin: Live-Vorschau pro Abschnitt + modale Vollvorschau (Editor + Dokumentenliste)
@ -24,7 +24,7 @@ MODULE_VERSIONS = {
"exercises": "2.28.0", # GET /api/exercises Keyset cursor_updated_at + cursor_id; Sortierung id als Tie-break "exercises": "2.28.0", # GET /api/exercises Keyset cursor_updated_at + cursor_id; Sortierung id als Tie-break
"training_units": "0.3.0", # GET /api/training-units Keyset cursor_planned_date + cursor_id (+ optional cursor_planned_time); Sort mit id-Tiebreak "training_units": "0.3.0", # GET /api/training-units Keyset cursor_planned_date + cursor_id (+ optional cursor_planned_time); Sort mit id-Tiebreak
"training_programs": "0.1.0", "training_programs": "0.1.0",
"planning": "0.11.0", # PUT/POST training_units: phases (parallel streams); Rahmen→Termin-Kopie _replace_unit_phases; apply-training-module phase_order_index + parallel_stream_order_index "planning": "0.12.0", # Trainingsvorlagen: Phasen/Streams in template_sections (064); Instantiate über _replace_unit_phases
"dashboard": "1.1.0", # GET /api/dashboard/kpis inkl. training_home (ein Client-Roundtrip für KPIs + nächste Termine) "dashboard": "1.1.0", # GET /api/dashboard/kpis inkl. training_home (ein Client-Roundtrip für KPIs + nächste Termine)
"training_modules": "1.0.0", "training_modules": "1.0.0",
"import_wiki": "1.0.0", "import_wiki": "1.0.0",
@ -36,6 +36,13 @@ MODULE_VERSIONS = {
} }
CHANGELOG = [ CHANGELOG = [
{
"version": "0.8.141",
"date": "2026-05-14",
"changes": [
"DB 064: Vorlagen-Sektionen mit phase_kind / phase_order_index / parallel_stream_order_index; Speichern und Anwenden behält Split-Sessions; Server: Vorlage → Einheit über Phasen-Replace.",
],
},
{ {
"version": "0.8.140", "version": "0.8.140",
"date": "2026-05-14", "date": "2026-05-14",

View File

@ -75,10 +75,12 @@ Die sichtbaren Funktionen hängen von **Rolle** und **Kontext** ab (eingeloggter
### 4.4 Trainingsplanung ### 4.4 Trainingsplanung
- **Trainingseinheiten** als planbare Objekte mit **Sektionen** und **Einträgen** (Übungen, ggf. mit **Variante** und Metadaten wie Dauer). - **Trainingseinheiten** als planbare Objekte mit **Sektionen** und **Einträgen** (Übungen, ggf. mit **Variante** und Metadaten wie Dauer).
- **Trainingsvorlagen / Mikrovorlagen** (wo eingerichtet): Struktur wiederverwenden. - **Phasen & parallele Streams (Breakout):** Eine Einheit kann aus abwechselnden **Ganzgruppenphasen** und **Parallelphasen** bestehen; in einer Parallelphase führen **mehrere Streams** (Teilstrecken) je eigene Abschnitte/Übungen. Planung über Breakout-UI; API liefert **`phases`** und flache **`sections`** (Migration **063**, siehe **`docs/HANDOVER.md`**). Technische Details: `.claude/docs/technical/PARALLEL_TRAINING_STREAMS_SPEC.md`.
- **Trainingsvorlagen / Mikrovorlagen** (wo eingerichtet): Struktur wiederverwenden (Phasen in Vorlagen: Ausbau siehe Handover „offen“).
- **Trainingsrahmenprogramm (Bibliothek):** übergeordnete Programme mit **Zielen** und **Slots**; Slot-Inhalt technisch als **Blueprint-Trainingsunit** abgebildet. - **Trainingsrahmenprogramm (Bibliothek):** übergeordnete Programme mit **Zielen** und **Slots**; Slot-Inhalt technisch als **Blueprint-Trainingsunit** abgebildet.
- **Materialisierung:** aus einem Rahmen-Slot kann eine **konkrete Kalender-Einheit** für eine Gruppe erzeugt werden (API vorhanden; UI-Anbindung kann erweitert werden). - **Materialisierung:** aus einem Rahmen-Slot kann eine **konkrete Kalender-Einheit** für eine Gruppe erzeugt werden (API vorhanden; UI-Anbindung kann erweitert werden).
- **Durchführung:** Ansicht zum Abarbeiten einer Einheit; **Coaching-Modus** als separater Erlebnispfad (generischer Zeit-Block pro Platzierung); bei **Kombinationsübungen** zusätzliche **Stations-/Kandidaten-Schicht und Archetyp-Hinweise** siehe Kombination-Fachspez **Anhang A** (implementierter Umfang vs. nächste Stufen). - **Durchführung („Plan & Ablauf“):** Ablauf anhand Phasen/Streams darstellen und abarbeiten (inkl. Split-Logik in der Anzeige).
- **Coaching-Modus:** eigener Ablauf mit Schritt-für-Schritt-Timeline, Stream-Wahl pro Parallelphase, Hinweis **„Parallelphase · Abschluss“** (Gruppen zusammenführen) vor der nächsten Ganzgruppenphase oder vor dem nächsten Split; **Nachbereitung** mit Ist-Minuten und Speichern wie in der Planung (inkl. **`phases`**). Nach erfolgreichem Speichern Wechsel zur **Plan- und Ablaufsicht** derselben Einheit. Bei **Kombinationsübungen** zusätzlich **Stations-/Kandidaten-Schicht und Archetyp-Hinweise** (Fachspez **Anhang A**; Ausbauschritte B/C).
### 4.5 Medienbibliothek und Archiv ### 4.5 Medienbibliothek und Archiv
@ -118,7 +120,8 @@ Nicht als „broken“ gemeint, sondern als **typische nächste Ausbaustellen**
- Kalender-UX: **„Aus Rahmen übernehmen“** flächendeckend und ggf. bulkfähig anbinden. - Kalender-UX: **„Aus Rahmen übernehmen“** flächendeckend und ggf. bulkfähig anbinden.
- **Policies** für geteilte Rahmen (Wer darf Bibliotheks-Rahmen sehen/kopieren?). - **Policies** für geteilte Rahmen (Wer darf Bibliotheks-Rahmen sehen/kopieren?).
- **Skill-Kategorie-Admin-UI**, **Dark Mode/Responsive/PWA-Ausbau**, **KI-Suche** über Volltext hinaus je nach Backlog. - **Skill-Kategorie-Admin-UI**, **Dark Mode/Responsive/PWA-Ausbau**, **KI-Suche** über Volltext hinaus je nach Backlog.
- **Coach / Kombination:** nächste Stufen **Zeitleisten-Splitting** und **Archetyp-Timer** (Fachspez §10.4 Stufe B/C; Umsetzungsplan Phase 4bd); **geführtes Erfassen** von `method_profile` im Übungseditor. - **Coach / Kombination:** Kombi-spezifische **Archetyp-Stufen B/C** (Zeitleisten-Splitting, archetypnahe Timer — Fachspez §10.4); **geführtes Erfassen** von `method_profile` im Übungseditor.
- **Breakout:** vollständige **Server-Spiegelung** neuer Abschnitte in **`phases`**; **Vorlagen** phasengleich mit Kalendereinheit; optional **Stream-Tabs** in der Durchführungsansicht laut technischer Spec — siehe **`docs/HANDOVER.md`** (Arbeitspaket „Coaching & Breakout“).
--- ---
@ -126,5 +129,6 @@ Nicht als „broken“ gemeint, sondern als **typische nächste Ausbaustellen**
| Datum | Änderung | | Datum | Änderung |
|-------|----------| |-------|----------|
| 2026-05-14 | Trainingsplanung: Phasen/parallele Streams, Coaching (Rejoin, Nachbereitung → Planansicht); Lücken §5 ergänzt. Verweis `HANDOVER.md`. |
| 2026-05-12 | Erstfassung für Übergabe an fachliches Design; Abgleich mit Code-Navigation, `version.py`, `HANDOVER.md`, `FEATURES_DELIVERED`, `DOMAIN_MODEL`. | | 2026-05-12 | Erstfassung für Übergabe an fachliches Design; Abgleich mit Code-Navigation, `version.py`, `HANDOVER.md`, `FEATURES_DELIVERED`, `DOMAIN_MODEL`. |
| 2026-05-12 | Kombinationsübungen + Coaching Stufe A; Verweise auf Fachspezifikation (`…Kombinationsuebungen…` V3 Anhang A) und `TRAINING_MODULES_IMPLEMENTATION_PLAN.md`. | | 2026-05-12 | Kombinationsübungen + Coaching Stufe A; Verweise auf Fachspezifikation (`…Kombinationsuebungen…` V3 Anhang A) und `TRAINING_MODULES_IMPLEMENTATION_PLAN.md`. |

View File

@ -1,7 +1,7 @@
# Shinkan Jinkendo Entwicklungsstand & Handover # Shinkan Jinkendo Entwicklungsstand & Handover
**Stand:** 2026-05-13 **Stand:** 2026-05-14
**App-Version / DB-Schema:** App **0.8.120**, DB-Schema **`20260514062`** (`backend/version.py`: `APP_VERSION`, `DB_SCHEMA_VERSION`) **App-Version / DB-Schema:** App **`0.8.140`** (u. a. Planungs-Breakout-UI), DB-Schema **`20260515063`** — maßgeblich **`backend/version.py`**: `APP_VERSION`, `DB_SCHEMA_VERSION`
Diese Datei ist die **Einstiegs-Doku für neue Chat-Sessions**: Anforderungen im Detail stehen in `.claude/docs/` (siehe unten); hier der **implementierte Stand**, **Medien-Meilenstein** und **sinnvolle nächste Schritte**. Diese Datei ist die **Einstiegs-Doku für neue Chat-Sessions**: Anforderungen im Detail stehen in `.claude/docs/` (siehe unten); hier der **implementierte Stand**, **Medien-Meilenstein** und **sinnvolle nächste Schritte**.
@ -76,7 +76,32 @@ Das Schema ist gegenüber dem Code zurück: Migration **`022_skills_schema_compl
- **036 / 037:** Bibliotheks-Rahmen, Slot-Inhalt als **`training_units`** mit **`framework_slot_id`**; **`POST /api/training-units/from-framework-slot`**. - **036 / 037:** Bibliotheks-Rahmen, Slot-Inhalt als **`training_units`** mit **`framework_slot_id`**; **`POST /api/training-units/from-framework-slot`**.
- **Code:** `training_framework_programs.py`, `training_planning.py`; Frontend **`TrainingFrameworkProgramEditPage.jsx`**, **`createTrainingUnitFromFrameworkSlot`** in `api.js`. - **Code:** `training_framework_programs.py`, `training_planning.py`; Frontend **`TrainingFrameworkProgramEditPage.jsx`**, **`createTrainingUnitFromFrameworkSlot`** in `api.js`.
### Trainingsmodule, Kombinationsübungen und Coach (Stand **0.8.120**) ### Trainingsplan: Phasen, parallele Streams und Coaching (Stand **0.8.1370.8.140**)
- **Schema / API:** Migration **063**`training_unit_phases`, `training_unit_parallel_streams`; Sektionen mit `phase_id` bzw. `parallel_stream_id`. **`GET /api/training-units/:id`** liefert **`phases`** (verschachtelt) und weiterhin flache **`sections`**. **`PUT/POST`** mit **`phases`** für Breakout-Einheiten (vgl. `CHANGELOG` **0.8.138**); Legacy: flache `sections` → implizite Ganzgruppen-Phase.
- **Planung (Frontend):** Breakout-Panel — neue Ganzgruppen-/parallele Phase, Streams in der Parallelphase; Speichern sendet `phases` bei phasierten Einheiten (`trainingUnitSectionsForm.js`, `TrainingPlanningPage`).
- **Durchführung „Plan & Ablauf“:** `TrainingUnitRunPage.jsx` nutzt **`sectionsWithPlanLocForDisplay`** / **`buildPlanRunViewModelFromSections`** aus **`frontend/src/utils/trainingPlanUtils.js`**, damit Anzeige mit Phasen/Streams konsistent ist (inkl. Normalisierung fehlender `planLoc` auf flachen Sektionen).
- **Coaching-Modus (`TrainingCoachPage.jsx`):**
- Flache Timeline aus Phasen (`flattenPlanTimeline`): **Split-Punkte** (`branch_gate`) bis Stream-Wahl, eine gewählte Spur pro Parallelphase, **Outline**, Timer, Ist-Minuten pro Item.
- **Rejoin nach Parallelphase:** Beim Übergang **Parallel → Ganzgruppe** (oder **Parallel → nächster Split** / `branch_gate`) erscheint die Karte „Parallelphase · Abschluss“ mit **„Gruppen zusammengeführt — weiter mit dem Plan“**, solange noch Einträge folgen; am Planende weiter **„Zur Nachbereitung“** (`coachShouldPromptSplitRejoinTransition` in `trainingPlanUtils.js`).
- **Nachbereitung / Speichern:** Payload über **`buildCoachSavePlanPayload`** (wie Planungseditor: **`phases`**, keine Zerstörung phasierter Struktur). Nach erfolgreichem Speichern: Coach-Session-Storage bereinigt, **`navigate('/planning/run/:unitId', { replace: true })`** (Plan & Ablauf).
- **Robustheit:** Abschnitte ohne Eintrag im `phases`-Baum, die nach Erben fälschlich **`parallel`** wären, werden für die **Anzeige** als **Ganzgruppenblock** nach der letzten bekannten Phasen-Ordnung ausgewiesen (`sectionsWithPlanLocForDisplay`).
- **Konzept / technische Spec:** `.claude/docs/functional/PARALLEL_TRAINING_STREAMS_CONCEPT.md`, `.claude/docs/technical/PARALLEL_TRAINING_STREAMS_SPEC.md`.
#### Arbeitspaket „Coaching & Breakout“ — noch offen
| # | Thema | Kurzbeschreibung |
|---|--------|------------------|
| 1 | **Backend / Datenkonsistenz** | Neue oder verschobene Sektionen konsistent in **`phases`** persistieren (nicht nur Client-Normalisierung). |
| 2 | **UX nach Speichern** | Optional: **im Coach bleiben** vs. **Planansicht** (aktuell: immer Plan & Ablauf). |
| 3 | **Kantenfälle Coach** | „Fertig“ bei abweichendem **Timer-Owner** vs. **Rejoin**/letzter Schritt prüfen. |
| 4 | **Tests** | Smoke: zwei Splitphasen, Ganzgruppe dazwischen/am Ende, Nachbereitung speichern, Rejoin. |
| 5 | **Run-UI vs. Spec** | Technische Spec §5.2 (Tabs pro Stream): Abgleich mit **`TrainingUnitRunPage`**. |
| 6 | **Trainer pro Stream** | UI und Policy zu **`assigned_trainer_profile_ids`** / Kopf-Co-Trainer offen. |
| 7 | **Vorlagen** | `training_plan_templates` phasen-/stream-kompatibel (Spec §5.3). |
| 8 | **Kombi-Coach B/C** | Fachspez **§10.4 / §10.6**, `TRAINING_MODULES_IMPLEMENTATION_PLAN.md` Phase **4**. |
### Trainingsmodule, Kombinationsübungen und Coach Stufe A
- **Fachspez & Drift-Schutz:** `.claude/docs/functional/Shinkan Trainingsmodule Kombinationsuebungen Spezifikation V2.md` (**§10.2.1** IDs, **§10.4** Coaching-Stufen, **§10.6** Produkt-Backlog, **Anhang A** Abgleich). - **Fachspez & Drift-Schutz:** `.claude/docs/functional/Shinkan Trainingsmodule Kombinationsuebungen Spezifikation V2.md` (**§10.2.1** IDs, **§10.4** Coaching-Stufen, **§10.6** Produkt-Backlog, **Anhang A** Abgleich).
- **Umsetzungsplan:** `.claude/docs/working/TRAINING_MODULES_IMPLEMENTATION_PLAN.md` (Phase **2** / **4** teilweise; Pakete **4ag** — u.a. **4e** Archetyp-Admin, **4f** Massen-Vorbelegung, **4g** Backend-Validierung). - **Umsetzungsplan:** `.claude/docs/working/TRAINING_MODULES_IMPLEMENTATION_PLAN.md` (Phase **2** / **4** teilweise; Pakete **4ag** — u.a. **4e** Archetyp-Admin, **4f** Massen-Vorbelegung, **4g** Backend-Validierung).
@ -155,17 +180,18 @@ Das Schema ist gegenüber dem Code zurück: Migration **`022_skills_schema_compl
## 7. Nächste Session — sinnvolle Arbeitspakete ## 7. Nächste Session — sinnvolle Arbeitspakete
1. **P-13 Frontend-Verifikation:** Melde-Flow in Medienbibliothek, Inbox-Workflow (Status, Archiv, Wiedereröffnen), Club-Admin-Ansicht manuell auf Dev-System durchspielen. E-Mail-Benachrichtigungen verifizieren (SMTP-Log). 1. **Coaching & Breakout (Regression):** Mehrphasen-Einheit mit zwei Splits und Ganzgruppen dazwischen — Rejoin-Karten, Nachbereitung speichern, Anzeige in Plan & Ablauf (`docs/HANDOVER.md` Arbeitspaket-Tabelle).
2. **Inline (Spec Abschnitt 11):** Basis umgesetzt — verbleibend: gezielte UX-Politik; optional Server-Normalisierung/Absicherung prüfen, falls Produkt es verlangt. 2. **P-13 Frontend-Verifikation:** Melde-Flow in Medienbibliothek, Inbox-Workflow (Status, Archiv, Wiedereröffnen), Club-Admin-Ansicht manuell auf Dev-System durchspielen. E-Mail-Benachrichtigungen verifizieren (SMTP-Log).
3. **Tests:** pytest für `media_assets`-Router (Leserechte, Lifecycle, `from-asset`); ggf. Snapshot der Pfad-Umzug-Logik. 3. **Inline (Spec Abschnitt 11):** Basis umgesetzt — verbleibend: gezielte UX-Politik; optional Server-Normalisierung/Absicherung prüfen, falls Produkt es verlangt.
4. **Retention:** Job-Dokumentation + Betrieb (ENV, Intervall); Dry-Run beschreiben. 4. **Tests:** pytest für `media_assets`-Router (Leserechte, Lifecycle, `from-asset`); ggf. Snapshot der Pfad-Umzug-Logik.
5. **S3/Adapter:** Speicher-Abstraktion (Spec Abschnitt 7) — wenn Produkt es verlangt. 5. **Retention:** Job-Dokumentation + Betrieb (ENV, Intervall); Dry-Run beschreiben.
6. **Rahmen/UI:** Kalender „aus Rahmen übernehmen” weiter anbinden (parallel, unabhängig von Medien). 6. **S3/Adapter:** Speicher-Abstraktion (Spec Abschnitt 7) — wenn Produkt es verlangt.
7. **Fachlicher Nutzerüberblick:** bei größeren UX-Änderungen **`docs/FACHLICHE_NUTZERFUNKTIONEN.md`** mitpflegen. 7. **Rahmen/UI:** Kalender „aus Rahmen übernehmen” weiter anbinden (parallel, unabhängig von Medien).
8. **Kombinations-Coach (Archetyp B/C):** Fachspez §10.4 / **§10.6**; nach Implementierung **Anhang A** + `TRAINING_MODULES_IMPLEMENTATION_PLAN.md` aktualisieren (kein Doc-Drift). 8. **Fachlicher Nutzerüberblick:** bei größeren UX-Änderungen **`docs/FACHLICHE_NUTZERFUNKTIONEN.md`** mitpflegen.
9. **Archetyp-Administration:** Konfiguration oder DB statt nur `COMBINATION_ARCHETYPE_IDS` / `combinationArchetypes.js` (Paket **4e**). 9. **Kombinations-Coach (Archetyp B/C):** Fachspez §10.4 / **§10.6**; nach Implementierung **Anhang A** + `TRAINING_MODULES_IMPLEMENTATION_PLAN.md` aktualisieren (kein Doc-Drift).
10. **Kombi-Zeitfelder:** Massen-Vorbelegung aller Slots aus Archetyp/Global + optionales Modal beim Archetypwechsel (Paket **4f**, `COMBINATION_TIMING_PROFILE_PLAN.md`). 10. **Archetyp-Administration:** Konfiguration oder DB statt nur `COMBINATION_ARCHETYPE_IDS` / `combinationArchetypes.js` (Paket **4e**).
11. **Backend-Validierung** `method_profile` / `planning_method_profile` je Archetyp (Paket **4g**). 11. **Kombi-Zeitfelder:** Massen-Vorbelegung aller Slots aus Archetyp/Global + optionales Modal beim Archetypwechsel (Paket **4f**, `COMBINATION_TIMING_PROFILE_PLAN.md`).
12. **Backend-Validierung** `method_profile` / `planning_method_profile` je Archetyp (Paket **4g**).
--- ---
@ -174,7 +200,8 @@ Das Schema ist gegenüber dem Code zurück: Migration **`022_skills_schema_compl
| Bereich | Einstieg | | Bereich | Einstieg |
|---------|----------| |---------|----------|
| Backend API | `backend/main.py`; u.a. **`media_assets.py`**, **`exercises.py`** (`COMBINATION_ARCHETYPE_IDS`, `enrich_exercise_detail`), **`profiles.py`**, **`training_framework_programs.py`**, `tenant_context.py` | | Backend API | `backend/main.py`; u.a. **`media_assets.py`**, **`exercises.py`** (`COMBINATION_ARCHETYPE_IDS`, `enrich_exercise_detail`), **`profiles.py`**, **`training_framework_programs.py`**, `tenant_context.py` |
| Coach-Kombination / Merge-Profil (Frontend) | `TrainingCoachPage.jsx`, `ExerciseFullContent.jsx`, `CombinationCoachSlots.jsx`, `CombinationPlanBracket.jsx`, `utils/comboPlanningMethodProfile.js`, `utils/combinationMethodProfileUi.js`, `constants/combinationArchetypes.js` | | Coach, Plan-Timeline, PUT-Payload phasiert | `TrainingCoachPage.jsx`, **`frontend/src/utils/trainingPlanUtils.js`** (`flattenPlanTimeline`, `buildCoachSavePlanPayload`, `sectionsWithPlanLocForDisplay`, Split-Rejoin-Helfer), `TrainingUnitRunPage.jsx` |
| Coach-Kombination / Merge-Profil (Frontend) | `ExerciseFullContent.jsx`, `CombinationCoachSlots.jsx`, `CombinationPlanBracket.jsx`, `utils/comboPlanningMethodProfile.js`, `utils/combinationMethodProfileUi.js`, `constants/combinationArchetypes.js` |
| Migrationen | `backend/migrations/` (040+ Mitgliedschaft/Governance; **045+** Medien-Stack) | | Migrationen | `backend/migrations/` (040+ Mitgliedschaft/Governance; **045+** Medien-Stack) |
| Frontend API | `frontend/src/utils/api.js` | | Frontend API | `frontend/src/utils/api.js` |
| Aktiver Verein (UI) | `frontend/src/utils/activeClub.js`, `AuthContext.jsx` | | Aktiver Verein (UI) | `frontend/src/utils/activeClub.js`, `AuthContext.jsx` |

View File

@ -9,6 +9,7 @@
- **Offen Phase 1:** Inbox optional **TTL** / nur bei sichtbarem Widget. - **Offen Phase 1:** Inbox optional **TTL** / nur bei sichtbarem Widget.
- **Phase 3 (abgeschlossen 2026-05-14):** Übungsliste modularisiert; Trainingsplanung/Übungsformular: **Page-Dateien unter Soft-Limit** — Implementierung in `TrainingPlanningPageRoot.jsx`, `ExerciseFormPageRoot.jsx`, `ExercisesListPageRoot.jsx`; `pages/*.jsx` nur Re-Export. Playwright **Tests 910**. - **Phase 3 (abgeschlossen 2026-05-14):** Übungsliste modularisiert; Trainingsplanung/Übungsformular: **Page-Dateien unter Soft-Limit** — Implementierung in `TrainingPlanningPageRoot.jsx`, `ExerciseFormPageRoot.jsx`, `ExercisesListPageRoot.jsx`; `pages/*.jsx` nur Re-Export. Playwright **Tests 910**.
- **Phase 4 (fortlaufend 2026-05-14):** API **Welle 1** `client.js`; **Welle 2** `planning.js`; **Welle 3** `exercises.js`; `utils/api.js` bleibt Facade (`export *`, `api`-Objekt `...exercises`, `...planning`). - **Phase 4 (fortlaufend 2026-05-14):** API **Welle 1** `client.js`; **Welle 2** `planning.js`; **Welle 3** `exercises.js`; `utils/api.js` bleibt Facade (`export *`, `api`-Objekt `...exercises`, `...planning`).
- **Trainingsplan Breakout / Coach (2026-05-14):** Phasen + parallele Streams (**063**, Frontend **0.8.1370.8.140**), Coach-Rejoin und Nachbereitung — siehe **`docs/HANDOVER.md`**, **`technical/PARALLEL_TRAINING_STREAMS_SPEC.md`**.
**Ziel:** Nach MVP eine **nachhaltige** Architektur für Wachstum, **Performance** (Server + schwache Clients) und **sichere Feature-Erweiterung**. **Ziel:** Nach MVP eine **nachhaltige** Architektur für Wachstum, **Performance** (Server + schwache Clients) und **sichere Feature-Erweiterung**.
**Leitdokumente:** [ZIELBILD_ARCHITEKTUR.md](./ZIELBILD_ARCHITEKTUR.md), [SCHULDEN_UND_REMEDIATION.md](./SCHULDEN_UND_REMEDIATION.md), [VERBINDLICHE_REGELN_SHINKAN.md](./VERBINDLICHE_REGELN_SHINKAN.md). **Leitdokumente:** [ZIELBILD_ARCHITEKTUR.md](./ZIELBILD_ARCHITEKTUR.md), [SCHULDEN_UND_REMEDIATION.md](./SCHULDEN_UND_REMEDIATION.md), [VERBINDLICHE_REGELN_SHINKAN.md](./VERBINDLICHE_REGELN_SHINKAN.md).

View File

@ -245,7 +245,7 @@ function reorderBlocksImmutable(blocks, fromI, toBeforeIdx) {
/** /**
* @param {(updater: (prev: Array) => Array) => void} props.onSectionsChange wie React setState * @param {(updater: (prev: Array) => Array) => void} props.onSectionsChange wie React setState
* @param {(p: { fromSlot: number, fromSectionIdx: number, toSlot: number, toSectionIdx: number, toParallelStream?: { po: number, so: number } }) => void} [props.onMoveSectionsAcrossSlots] Rahmenprogramm: Abschnitt zwischen Slots verschieben * @param {(p: { fromSlot: number, fromSectionIdx?: number, toSlot: number, toSectionIdx: number, toParallelStream?: { po: number, so: number }, parallelPhaseRunOrderIndex?: number, insertBeforeParallelInTarget?: number, firstInParallelStreamInTarget?: { po: number, so: number } }) => void} [props.onMoveSectionsAcrossSlots] Rahmenprogramm: Abschnitt(e) zwischen Slots verschieben
*/ */
export default function TrainingUnitSectionsEditor({ export default function TrainingUnitSectionsEditor({
sections, sections,
@ -773,7 +773,8 @@ export default function TrainingUnitSectionsEditor({
if ( if (
typeof onMoveSectionsAcrossSlots === 'function' && typeof onMoveSectionsAcrossSlots === 'function' &&
sectionToSlot >= 0 && sectionToSlot >= 0 &&
fromSlot >= 0 fromSlot >= 0 &&
fromSlot !== sectionToSlot
) { ) {
return { kind: 'crossSlot', fromSi, fromSlot } return { kind: 'crossSlot', fromSi, fromSlot }
} }
@ -806,6 +807,22 @@ export default function TrainingUnitSectionsEditor({
if (parsed.kind === 'phaseRun') { if (parsed.kind === 'phaseRun') {
const dragPo = Number(parsed.phaseRunMove.phaseOrderIndex) || 0 const dragPo = Number(parsed.phaseRunMove.phaseOrderIndex) || 0
if (dragPo === targetPo) return if (dragPo === targetPo) return
const fs = typeof parsed.fromSlot === 'number' ? parsed.fromSlot : -1
if (
typeof onMoveSectionsAcrossSlots === 'function' &&
sectionToSlot >= 0 &&
fs >= 0 &&
fs !== sectionToSlot
) {
const ins = indicesOfParallelPhase(list, targetPo)[0] ?? list.length
onMoveSectionsAcrossSlots({
fromSlot: fs,
toSlot: sectionToSlot,
toSectionIdx: ins,
parallelPhaseRunOrderIndex: dragPo,
})
return
}
patch((prev) => { patch((prev) => {
const idxs = indicesOfParallelPhase(prev, targetPo) const idxs = indicesOfParallelPhase(prev, targetPo)
const fg = idxs.length ? idxs[0] : -1 const fg = idxs.length ? idxs[0] : -1
@ -817,7 +834,17 @@ export default function TrainingUnitSectionsEditor({
return return
} }
if (parsed.kind === 'crossSlot') return if (parsed.kind === 'crossSlot') {
if (typeof onMoveSectionsAcrossSlots !== 'function') return
onMoveSectionsAcrossSlots({
fromSlot: parsed.fromSlot,
fromSectionIdx: parsed.fromSi,
toSlot: sectionToSlot,
toSectionIdx: 0,
insertBeforeParallelInTarget: targetPo,
})
return
}
const { fromSi } = parsed const { fromSi } = parsed
patch((prev) => { patch((prev) => {
@ -853,6 +880,23 @@ export default function TrainingUnitSectionsEditor({
if (parsed.kind === 'phaseRun') { if (parsed.kind === 'phaseRun') {
const dragPo = Number(parsed.phaseRunMove.phaseOrderIndex) || 0 const dragPo = Number(parsed.phaseRunMove.phaseOrderIndex) || 0
if (dragPo === targetPo) return if (dragPo === targetPo) return
const fs = typeof parsed.fromSlot === 'number' ? parsed.fromSlot : -1
if (
typeof onMoveSectionsAcrossSlots === 'function' &&
sectionToSlot >= 0 &&
fs >= 0 &&
fs !== sectionToSlot
) {
const si = sectionIndicesForParallelStream(list, targetPo, targetSo)
const ins = si.length ? si[0] : indicesOfParallelPhase(list, targetPo)[0] ?? list.length
onMoveSectionsAcrossSlots({
fromSlot: fs,
toSlot: sectionToSlot,
toSectionIdx: ins,
parallelPhaseRunOrderIndex: dragPo,
})
return
}
patch((prev) => { patch((prev) => {
const idxs = indicesOfParallelPhase(prev, targetPo) const idxs = indicesOfParallelPhase(prev, targetPo)
const fg = idxs.length ? idxs[0] : -1 const fg = idxs.length ? idxs[0] : -1
@ -864,7 +908,17 @@ export default function TrainingUnitSectionsEditor({
return return
} }
if (parsed.kind === 'crossSlot') return if (parsed.kind === 'crossSlot') {
if (typeof onMoveSectionsAcrossSlots !== 'function') return
onMoveSectionsAcrossSlots({
fromSlot: parsed.fromSlot,
fromSectionIdx: parsed.fromSi,
toSlot: sectionToSlot,
toSectionIdx: 0,
firstInParallelStreamInTarget: { po: targetPo, so: targetSo },
})
return
}
const { fromSi } = parsed const { fromSi } = parsed
patch((prev) => { patch((prev) => {
@ -897,9 +951,24 @@ export default function TrainingUnitSectionsEditor({
const fromSlot = typeof data.fromSlot === 'number' ? data.fromSlot : -1 const fromSlot = typeof data.fromSlot === 'number' ? data.fromSlot : -1
if (phaseRunMove != null && phaseRunMove.phaseOrderIndex != null) { if (phaseRunMove != null && phaseRunMove.phaseOrderIndex != null) {
patch((prev) => {
const po = Number(phaseRunMove.phaseOrderIndex) || 0 const po = Number(phaseRunMove.phaseOrderIndex) || 0
let next = moveParallelPhaseRunToInsertBefore(prev, po, insertBeforeIdx) if (
typeof onMoveSectionsAcrossSlots === 'function' &&
sectionToSlot >= 0 &&
fromSlot >= 0 &&
fromSlot !== sectionToSlot
) {
onMoveSectionsAcrossSlots({
fromSlot,
toSlot: sectionToSlot,
toSectionIdx: insertBeforeIdx,
parallelPhaseRunOrderIndex: po,
})
return
}
patch((prev) => {
const poLocal = Number(phaseRunMove.phaseOrderIndex) || 0
let next = moveParallelPhaseRunToInsertBefore(prev, poLocal, insertBeforeIdx)
if (enableParallelPhaseControls) next = afterSectionReorderParallelGuard(prev, next) if (enableParallelPhaseControls) next = afterSectionReorderParallelGuard(prev, next)
return next return next
}) })
@ -927,17 +996,11 @@ export default function TrainingUnitSectionsEditor({
} }
} }
if (
enableParallelPhaseControls &&
(insertBeforeIdx === fromSi || insertBeforeIdx === fromSi + 1)
) {
return
}
if ( if (
typeof onMoveSectionsAcrossSlots === 'function' && typeof onMoveSectionsAcrossSlots === 'function' &&
sectionToSlot >= 0 && sectionToSlot >= 0 &&
fromSlot >= 0 fromSlot >= 0 &&
fromSlot !== sectionToSlot
) { ) {
onMoveSectionsAcrossSlots({ onMoveSectionsAcrossSlots({
fromSlot, fromSlot,
@ -1011,7 +1074,8 @@ export default function TrainingUnitSectionsEditor({
if ( if (
typeof onMoveSectionsAcrossSlots === 'function' && typeof onMoveSectionsAcrossSlots === 'function' &&
sectionToSlot >= 0 && sectionToSlot >= 0 &&
fromSlot >= 0 fromSlot >= 0 &&
fromSlot !== sectionToSlot
) { ) {
onMoveSectionsAcrossSlots({ onMoveSectionsAcrossSlots({
fromSlot, fromSlot,

View File

@ -19,6 +19,8 @@ import {
buildPlanPayloadForSave, buildPlanPayloadForSave,
hydrateExercisePlanningRow, hydrateExercisePlanningRow,
insertTrainingModuleIntoPlanningSections, insertTrainingModuleIntoPlanningSections,
templateSectionsPayloadFromFormSections,
formSectionsFromPlanTemplateRows,
} from '../../utils/trainingUnitSectionsForm' } from '../../utils/trainingUnitSectionsForm'
import { import {
addDaysIsoDate, addDaysIsoDate,
@ -549,12 +551,8 @@ function TrainingPlanningPageRoot() {
setFormData((fd) => ({ setFormData((fd) => ({
...fd, ...fd,
sections: (tpl.sections || []).length sections: (tpl.sections || []).length
? tpl.sections.map((s) => ({ ? formSectionsFromPlanTemplateRows(tpl.sections)
title: s.title, : [defaultSection()],
guidance_notes: s.guidance_text || '',
items: []
}))
: [defaultSection()]
})) }))
} catch (err) { } catch (err) {
toast.error('Vorlage laden: ' + err.message) toast.error('Vorlage laden: ' + err.message)
@ -651,10 +649,7 @@ function TrainingPlanningPageRoot() {
try { try {
await api.createTrainingPlanTemplate({ await api.createTrainingPlanTemplate({
name: name.trim(), name: name.trim(),
sections: formData.sections.map((s) => ({ sections: templateSectionsPayloadFromFormSections(formData.sections),
title: s.title || 'Abschnitt',
guidance_text: s.guidance_notes?.trim() ? s.guidance_notes.trim() : null
}))
}) })
await loadPlanTemplates() await loadPlanTemplates()
toast.success('Vorlage gespeichert.') toast.success('Vorlage gespeichert.')
@ -663,6 +658,29 @@ function TrainingPlanningPageRoot() {
} }
} }
const handleDeletePlanTemplate = useCallback(
async (tpl) => {
if (!tpl?.id) return
const label = (tpl.name || '').trim() || `Vorlage #${tpl.id}`
if (
!window.confirm(
`Trainingsvorlage „${label}“ wirklich löschen? Die Aktion kann nicht rückgängig gemacht werden.`
)
) {
return
}
try {
await api.deleteTrainingPlanTemplate(tpl.id)
setDraftPlanTemplateId((prev) => (String(prev) === String(tpl.id) ? '' : prev))
await loadPlanTemplates()
toast.success('Vorlage gelöscht.')
} catch (err) {
toast.error(err.message || 'Löschen fehlgeschlagen')
}
},
[loadPlanTemplates, toast]
)
const openModuleApplyModal = useCallback(async (placement) => { const openModuleApplyModal = useCallback(async (placement) => {
setModuleApplyErr('') setModuleApplyErr('')
setModuleApplySearchQuery('') setModuleApplySearchQuery('')
@ -1929,6 +1947,7 @@ function TrainingPlanningPageRoot() {
draftPlanTemplateId={draftPlanTemplateId} draftPlanTemplateId={draftPlanTemplateId}
onDraftTemplateSelect={applyTemplateFromSelect} onDraftTemplateSelect={applyTemplateFromSelect}
planTemplates={planTemplates} planTemplates={planTemplates}
onDeletePlanTemplate={handleDeletePlanTemplate}
clubDirectory={clubDirectory} clubDirectory={clubDirectory}
clubDirectoryForCo={clubDirectoryForCo} clubDirectoryForCo={clubDirectoryForCo}
planningModalClubId={planningModalClubId} planningModalClubId={planningModalClubId}

View File

@ -18,6 +18,7 @@ export default function TrainingPlanningUnitFormModal({
draftPlanTemplateId, draftPlanTemplateId,
onDraftTemplateSelect, onDraftTemplateSelect,
planTemplates, planTemplates,
onDeletePlanTemplate,
clubDirectory, clubDirectory,
clubDirectoryForCo, clubDirectoryForCo,
planningModalClubId, planningModalClubId,
@ -129,6 +130,70 @@ export default function TrainingPlanningUnitFormModal({
</div> </div>
)} )}
{planTemplates.length > 0 && typeof onDeletePlanTemplate === 'function' ? (
<details
className="card"
style={{
marginBottom: '1.35rem',
padding: '12px 14px',
background: 'var(--surface2)',
border: '1px solid var(--border)',
}}
>
<summary style={{ cursor: 'pointer', fontWeight: 600, color: 'var(--text1)' }}>
Gespeicherte Vorlagen löschen
</summary>
<p style={{ margin: '0.65rem 0 0.75rem', fontSize: '0.82rem', color: 'var(--text2)', lineHeight: 1.45 }}>
Du kannst eigene Vorlagen entfernen. Plattform-Admins dürfen auch fremde Vorlagen löschen. Einheiten, die
noch auf eine Vorlage verweisen, behalten ihren Ablauf; die Verknüpfung zur Vorlage wird vom Server
entfernt.
</p>
<ul style={{ listStyle: 'none', margin: 0, padding: 0 }}>
{planTemplates.map((t, ti) => {
const roleLc = String(user?.role || '').toLowerCase()
const isPlatformAdmin = roleLc === 'admin' || roleLc === 'superadmin'
const canDel =
user &&
(isPlatformAdmin || Number(t.created_by) === Number(user.id))
return (
<li
key={t.id}
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
gap: '10px',
padding: '8px 0',
borderTop: ti === 0 ? 'none' : '1px solid var(--border)',
}}
>
<span style={{ minWidth: 0, flex: 1, fontSize: '0.9rem' }}>
<strong style={{ color: 'var(--text1)' }}>{t.name}</strong>
{typeof t.sections_count === 'number' ? (
<span style={{ fontSize: '0.82rem', color: 'var(--text2)', marginLeft: '6px' }}>
· {t.sections_count} Abschn.
</span>
) : null}
</span>
{canDel ? (
<button
type="button"
className="btn btn-danger"
style={{ flexShrink: 0, padding: '6px 12px', fontSize: '0.82rem' }}
onClick={() => onDeletePlanTemplate(t)}
>
Löschen
</button>
) : (
<span style={{ fontSize: '0.78rem', color: 'var(--text3)', flexShrink: 0 }}>nur Lesen</span>
)}
</li>
)
})}
</ul>
</details>
) : null}
<form onSubmit={onSubmit}> <form onSubmit={onSubmit}>
<h3 style={{ marginBottom: '1rem' }}>Planung</h3> <h3 style={{ marginBottom: '1rem' }}>Planung</h3>

View File

@ -12,9 +12,12 @@ import {
defaultSection, defaultSection,
normalizeUnitToForm, normalizeUnitToForm,
enrichSectionsWithVariants, enrichSectionsWithVariants,
buildSectionsPayload, buildPlanPayloadForSave,
hydrateExercisePlanningRow, hydrateExercisePlanningRow,
reorderBlockIntoParallelStreamEnd, reorderBlockIntoParallelStreamEnd,
indicesOfParallelPhase,
reorderSectionBeforeParallelRunAsWholeGroup,
reorderSectionAsFirstInParallelStream,
} from '../utils/trainingUnitSectionsForm' } from '../utils/trainingUnitSectionsForm'
const DND_FW_SLOT = 'application/x-shinkan-framework-slot' const DND_FW_SLOT = 'application/x-shinkan-framework-slot'
@ -48,10 +51,10 @@ 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 baseSecs = s.sections && s.sections.length ? s.sections : [defaultSection('Ablauf')]
out.push({ out.push({
...s, ...s,
sections: await enrichSectionsWithVariants(sec), sections: await enrichSectionsWithVariants(baseSecs),
}) })
} }
return out return out
@ -132,7 +135,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 +158,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 =
@ -554,7 +564,17 @@ export default function TrainingFrameworkProgramEditPage() {
} }
const moveSectionsAcrossFrameworkSlots = useCallback( const moveSectionsAcrossFrameworkSlots = useCallback(
({ fromSlot, fromSectionIdx, toSlot, toSectionIdx, toParallelStream }) => { (payload) => {
const {
fromSlot,
fromSectionIdx,
toSlot,
toSectionIdx,
toParallelStream,
parallelPhaseRunOrderIndex,
insertBeforeParallelInTarget,
firstInParallelStreamInTarget,
} = payload
setForm((prev) => { setForm((prev) => {
const slots = prev.slots.map((sl) => ({ const slots = prev.slots.map((sl) => ({
...sl, ...sl,
@ -572,15 +592,7 @@ export default function TrainingFrameworkProgramEditPage() {
} }
const fromSecs = slots[fromSlot].sections const fromSecs = slots[fromSlot].sections
if ( const toSecs = slots[toSlot].sections
typeof fromSectionIdx !== 'number' ||
fromSectionIdx < 0 ||
fromSectionIdx >= fromSecs.length
) {
return prev
}
const [block] = fromSecs.splice(fromSectionIdx, 1)
const applyParallelStreamEnd = const applyParallelStreamEnd =
toParallelStream != null && toParallelStream.po != null && toParallelStream.so != null toParallelStream != null && toParallelStream.po != null && toParallelStream.so != null
@ -591,6 +603,43 @@ export default function TrainingFrameworkProgramEditPage() {
} }
: null : null
/** Gesamten Parallel-Lauf aus fromSlot an toSectionIdx in toSlot legen */
if (parallelPhaseRunOrderIndex != null && parallelPhaseRunOrderIndex !== '') {
const po = Number(parallelPhaseRunOrderIndex) || 0
const idxs = indicesOfParallelPhase(fromSecs, po)
if (!idxs.length) {
return prev
}
const blocks = idxs.map((i) => fromSecs[i])
for (const i of [...idxs].sort((a, b) => b - a)) {
fromSecs.splice(i, 1)
}
if (fromSlot === toSlot) {
let insertAt = Number(toSectionIdx) || 0
for (const i of idxs) {
if (i < insertAt) insertAt -= 1
}
insertAt = Math.max(0, Math.min(insertAt, fromSecs.length))
fromSecs.splice(insertAt, 0, ...blocks)
slots[fromSlot].sections = fromSecs
return { ...prev, slots }
}
const insertAt = Math.max(0, Math.min(Number(toSectionIdx) || 0, toSecs.length))
toSecs.splice(insertAt, 0, ...blocks)
slots[toSlot].sections = toSecs
return { ...prev, slots }
}
if (
typeof fromSectionIdx !== 'number' ||
fromSectionIdx < 0 ||
fromSectionIdx >= fromSecs.length
) {
return prev
}
const [block] = fromSecs.splice(fromSectionIdx, 1)
if (fromSlot === toSlot) { if (fromSlot === toSlot) {
let insertAt = toSectionIdx let insertAt = toSectionIdx
if (fromSectionIdx < toSectionIdx) insertAt = toSectionIdx - 1 if (fromSectionIdx < toSectionIdx) insertAt = toSectionIdx - 1
@ -598,15 +647,45 @@ export default function TrainingFrameworkProgramEditPage() {
fromSecs.splice(insertAt, 0, block) fromSecs.splice(insertAt, 0, block)
if (applyParallelStreamEnd) { if (applyParallelStreamEnd) {
slots[fromSlot].sections = applyParallelStreamEnd(fromSecs, insertAt) slots[fromSlot].sections = applyParallelStreamEnd(fromSecs, insertAt)
} else {
slots[fromSlot].sections = fromSecs
} }
return { ...prev, slots } return { ...prev, slots }
} }
const toSecs = slots[toSlot].sections if (insertBeforeParallelInTarget != null && insertBeforeParallelInTarget !== '') {
const tpo = Number(insertBeforeParallelInTarget) || 0
const pIdxs = indicesOfParallelPhase(toSecs, tpo)
const ins = pIdxs.length ? pIdxs[0] : toSecs.length
toSecs.splice(ins, 0, block)
slots[toSlot].sections = reorderSectionBeforeParallelRunAsWholeGroup(toSecs, ins, tpo)
return { ...prev, slots }
}
if (
firstInParallelStreamInTarget != null &&
firstInParallelStreamInTarget.po != null &&
firstInParallelStreamInTarget.so != null
) {
const fpo = Number(firstInParallelStreamInTarget.po) || 0
const fso = Number(firstInParallelStreamInTarget.so) || 0
const nextSecs = [...toSecs, block]
const movedFromI = nextSecs.length - 1
slots[toSlot].sections = reorderSectionAsFirstInParallelStream(
nextSecs,
movedFromI,
fpo,
fso
)
return { ...prev, slots }
}
const ia = Math.max(0, Math.min(toSectionIdx, toSecs.length)) const ia = Math.max(0, Math.min(toSectionIdx, toSecs.length))
toSecs.splice(ia, 0, block) toSecs.splice(ia, 0, block)
if (applyParallelStreamEnd) { if (applyParallelStreamEnd) {
slots[toSlot].sections = applyParallelStreamEnd(toSecs, ia) slots[toSlot].sections = applyParallelStreamEnd(toSecs, ia)
} else {
slots[toSlot].sections = toSecs
} }
return { ...prev, slots } return { ...prev, slots }
}) })
@ -710,6 +789,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 +854,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>

View File

@ -721,20 +721,47 @@ function stripPlanLoc(sec) {
return rest return rest
} }
export function inheritPlanLocForPhasedSave(sections) { function phaseIndexToInt(v, fallback = 0) {
let prev = { if (v === null || v === undefined || v === '') return fallback
phaseKind: 'whole_group', const n = typeof v === 'number' ? v : Number(v)
phaseOrderIndex: 0, return Number.isFinite(n) ? n : fallback
parallelStreamOrderIndex: null,
phaseTitle: null,
phaseGuidanceNotes: null,
streamTitle: null,
streamNotes: null,
streamAssignedTrainerProfileIds: null,
} }
/**
* planLoc für Phasen-PUT kanonisieren (Großschreibung, numerische Indizes).
* Verhindert u. a. abgebrochene Phasen-Runs bei "0" !== 0 und falsche whole_group-Zweige.
*/
function canonicalPlanLocForPhasedSave(pl) {
if (!pl || typeof pl !== 'object') return null
const rawKind = String(pl.phaseKind || '').toLowerCase().trim()
let kind = rawKind === 'parallel' || rawKind === 'whole_group' ? rawKind : null
if (
!kind &&
pl.parallelStreamOrderIndex != null &&
pl.parallelStreamOrderIndex !== ''
) {
kind = 'parallel'
}
if (!kind) return null
const phaseOrderIndex = phaseIndexToInt(pl.phaseOrderIndex, 0)
let parallelStreamOrderIndex = null
if (kind === 'parallel') {
parallelStreamOrderIndex = phaseIndexToInt(pl.parallelStreamOrderIndex, 0)
}
return {
...pl,
phaseKind: kind,
phaseOrderIndex,
parallelStreamOrderIndex,
}
}
export function inheritPlanLocForPhasedSave(sections) {
let prev = { ...defaultPlanLocWholeGroup(0) }
return (sections || []).map((s) => { return (sections || []).map((s) => {
if (s?.planLoc && s.planLoc.phaseKind) { const canon = canonicalPlanLocForPhasedSave(s?.planLoc)
prev = { ...s.planLoc } if (canon) {
prev = { ...canon }
return { ...s, planLoc: prev } return { ...s, planLoc: prev }
} }
return { ...s, planLoc: { ...prev } } return { ...s, planLoc: { ...prev } }
@ -1127,13 +1154,61 @@ function buildPhasesPayloadFromFlat(sections) {
*/ */
export function buildPlanPayloadForSave(sections) { export function buildPlanPayloadForSave(sections) {
const list = Array.isArray(sections) ? sections : [] const list = Array.isArray(sections) ? sections : []
const anyPhased = list.some((s) => s && s.planLoc && s.planLoc.phaseKind) const anyPhased = list.some((s) => s && canonicalPlanLocForPhasedSave(s.planLoc) != null)
if (!anyPhased) { if (!anyPhased) {
return { sections: buildSectionsPayload(list) } return { sections: buildSectionsPayload(list) }
} }
return buildPhasesPayloadFromFlat(list) return buildPhasesPayloadFromFlat(list)
} }
/** Payload-Zeilen für POST/PUT /api/training-plan-templates (inkl. Split-/Phasen-Metadaten). */
export function templateSectionsPayloadFromFormSections(sections) {
const norm = inheritPlanLocForPhasedSave(Array.isArray(sections) ? sections : [])
return norm.map((s, si) => {
const canon = canonicalPlanLocForPhasedSave(s.planLoc)
const pk = canon?.phaseKind === 'parallel' ? 'parallel' : 'whole_group'
const poi = canon?.phaseOrderIndex ?? 0
const pso = canon?.phaseKind === 'parallel' ? (canon.parallelStreamOrderIndex ?? 0) : null
return {
order_index: si,
title: (s.title || '').trim() || `Abschnitt ${si + 1}`,
guidance_text: s.guidance_notes?.trim() ? s.guidance_notes.trim() : null,
phase_kind: pk,
phase_order_index: poi,
parallel_stream_order_index: pso,
}
})
}
/** GET-Vorlage → Editor-Abschnitte mit planLoc (Split-Sessions). */
export function formSectionsFromPlanTemplateRows(templateSections) {
const rows = Array.isArray(templateSections) ? [...templateSections] : []
rows.sort((a, b) => (a.order_index ?? 0) - (b.order_index ?? 0))
if (!rows.length) return [defaultSection()]
return rows.map((s) => {
const pk = String(s.phase_kind || 'whole_group').toLowerCase().trim()
const poiRaw = s.phase_order_index
const poi = poiRaw == null || poiRaw === '' ? 0 : Number(poiRaw)
const phaseOrderIndex = Number.isFinite(poi) ? poi : 0
const soRaw = s.parallel_stream_order_index
let planLoc
if (pk === 'parallel') {
const so = soRaw == null || soRaw === '' ? 0 : Number(soRaw)
planLoc = {
...defaultPlanLocParallel(phaseOrderIndex, Number.isFinite(so) ? so : 0),
}
} else {
planLoc = { ...defaultPlanLocWholeGroup(phaseOrderIndex) }
}
return {
title: s.title || 'Abschnitt',
guidance_notes: s.guidance_text || '',
items: [],
planLoc,
}
})
}
/** Fügt die Positionen eines Moduls in lokale Abschnitte ein (wie eine Übung, ohne Zwischenspeichern der Einheit). */ /** Fügt die Positionen eines Moduls in lokale Abschnitte ein (wie eine Übung, ohne Zwischenspeichern der Einheit). */
export async function insertTrainingModuleIntoPlanningSections({ export async function insertTrainingModuleIntoPlanningSections({
sections, sections,