From 4235246cd715aaf87b278b93bf52589214b6a998 Mon Sep 17 00:00:00 2001 From: Lars Date: Thu, 14 May 2026 11:21:09 +0200 Subject: [PATCH] chore(version): update version and changelog for release 0.8.122 - Bumped APP_VERSION to 0.8.122 and updated the changelog to reflect new features. - Integrated useExerciseListCatalogsAndQuery hook in ExercisesListPage for improved exercise list management and data fetching. - Enhanced documentation to include new concepts for parallel training streams and their technical specifications. - Updated DOMAIN_MODEL and related technical specs to clarify the structure and functionality of training streams within units. --- .claude/docs/functional/DOMAIN_MODEL.md | 12 +- .../PARALLEL_TRAINING_STREAMS_CONCEPT.md | 106 ++++++++++++++ .../PARALLEL_TRAINING_STREAMS_SPEC.md | 130 ++++++++++++++++++ .../docs/technical/TRAINING_FRAMEWORK_SPEC.md | 1 + ..._MODULES_AND_COMBINATION_EXERCISES_SPEC.md | 1 + backend/version.py | 9 +- .../hooks/useExerciseListCatalogsAndQuery.js | 120 ++++++++++++++++ frontend/src/pages/ExercisesListPage.jsx | 126 +++-------------- 8 files changed, 397 insertions(+), 108 deletions(-) create mode 100644 .claude/docs/functional/PARALLEL_TRAINING_STREAMS_CONCEPT.md create mode 100644 .claude/docs/technical/PARALLEL_TRAINING_STREAMS_SPEC.md create mode 100644 frontend/src/hooks/useExerciseListCatalogsAndQuery.js diff --git a/.claude/docs/functional/DOMAIN_MODEL.md b/.claude/docs/functional/DOMAIN_MODEL.md index 8de4ca0..f951050 100644 --- a/.claude/docs/functional/DOMAIN_MODEL.md +++ b/.claude/docs/functional/DOMAIN_MODEL.md @@ -1,7 +1,7 @@ # Shinkan Jinkendo - Fachliches Domänenmodell -**Version:** 0.4.5 -**Stand:** 2026-05-12 (Fachlicher Nutzerüberblick: `docs/FACHLICHE_NUTZERFUNKTIONEN.md`) +**Version:** 0.4.6 +**Stand:** 2026-05-14 (Fachlicher Nutzerüberblick: `docs/FACHLICHE_NUTZERFUNKTIONEN.md`) **Basis:** `shinkan_anforderungsdokument_entwurf.md` + Fähigkeitsmatrix --- @@ -474,6 +474,14 @@ skill_level_definitions ( **Konkretisierung (037/API):** `POST /api/training-units/from-framework-slot` legt eine geplante Einheit aus dem Slot‑Blueprint an; **`origin_framework_slot_id`** dient als Herkunftsreferenz (**Lineage light**; weiteres Feedback/Lineage‑Konzept: Konzeptpapier Schritt **E**). +### Parallele Trainingsstreams (Breakout, Entwurf) + +**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 **Rahmenprogramm‑Slot** (Serien‑Session über Wochen): Slots strukturieren **mehrere Einheiten** in einem Programm; **Streams** strukturieren **gleichzeitige** Abläufe **innerhalb einer** Einheit. + +**Sonderfall Stationen:** Rotation kann **innerhalb** einer Stream‑Planung über **Kombinationsübungen** (Methodenprofil/Archetyp) abgebildet werden; hallenweit **synchron** getaktete Rotation ist eine **erweiterte** Ausbaustufe (siehe Fachkonzept). + +**Dokumentation:** `functional/PARALLEL_TRAINING_STREAMS_CONCEPT.md`, Umsetzung `technical/PARALLEL_TRAINING_STREAMS_SPEC.md`. + --- ## Medien-Archiv & Übungs-Anhänge (Stand 2026-05-07) diff --git a/.claude/docs/functional/PARALLEL_TRAINING_STREAMS_CONCEPT.md b/.claude/docs/functional/PARALLEL_TRAINING_STREAMS_CONCEPT.md new file mode 100644 index 0000000..0023cf8 --- /dev/null +++ b/.claude/docs/functional/PARALLEL_TRAINING_STREAMS_CONCEPT.md @@ -0,0 +1,106 @@ +# Parallele Trainingsstreams (Breakout) — Fachkonzept + +**Status:** Entwurf zur Abstimmung · **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**. + +**Technische Ausarbeitung:** `.claude/docs/technical/PARALLEL_TRAINING_STREAMS_SPEC.md` +**Domänenbegriffe (Überblick):** `.claude/docs/functional/DOMAIN_MODEL.md` (Abschnitt Parallele Streams) + +--- + +## 1. Ausgangslage und Problem + +In Kinder- und Breitensport-Training ist ein typischer Ablauf: + +1. **Gemeinsam:** Aufwärmen, Koordination, Ansagen. +2. **Getrennt:** Kinder in mehrere Gruppen teilen; **Co-Trainer** leiten jeweils eigene Inhalte **gleichzeitig**. +3. **Gemeinsam:** Abschluss, gemeinsame Übungen, Verabschiedung. + +Die aktuelle Shinkan-Planung modelliert pro Termin **eine lineare Folge von Abschnitten und Übungen** pro Einheit. Das genügt nicht, wenn **mehrere gleichzeitige „Unter-Sessions“** mit unterschiedlichen Plänen dokumentiert und auf der Matte geführt werden sollen. + +--- + +## 2. Ziele (fachlich) + +| ID | Ziel | +|----|------| +| PT‑01 | Eine **Kalender-Einheit** bleibt **ein** Termin (eine Halle, eine Gruppe, ein Datum) — kein Splitten in künstlich mehrere Kalendereinträge nur für Parallelität. | +| PT‑02 | **Unbegrenzte** Anzahl paralleler **Streams** (Teilstrecken) in einer oder mehreren **Parallelphasen**. | +| PT‑03 | **Phasenmodell:** klar erkennbar **Gemeinsam** vs. **Parallel** vs. wieder **Gemeinsam** (auch mehrfach hintereinander möglich). | +| PT‑04 | **Rollen:** Leitung (Haupttrainer) und Co-Trainer; Zuordnung der Co-Trainer **soll** an konkrete Streams anschließbar sein (heute: nur flache Liste pro Einheit — siehe technische Spec). | +| PT‑05 | **Sonderfall Stationen:** rotierender Ablauf (z. B. Wechsel alle 20 Min.) **inhaltlich** unterscheiden zwischen (a) Rotation **innerhalb** einer Teilstrecke und (b) **synchron** getakteter Hallen-Rotation — siehe §5. | +| PT‑06 | **Durchführung:** Trainer können „ihre“ Spur auf dem Gerät abarbeiten; Fortschritt pro Spur nachvollziehbar. | + +**Nicht-Ziel (frühe Stufen):** Echtzeit-Synchronisation mehrerer Geräte; individuelles Athleten-Tracking; automatische Raumbelegung. + +--- + +## 3. Begriffe + +| Begriff | Definition | +|---------|------------| +| **Einheit / Termin** | Geplante `training_unit` für Gruppe und Datum — übergeordneter Rahmen des Abends. | +| **Phase** | Organisatorischer Block innerhalb der Einheit: entweder **ganze Gruppe** oder **parallel**. | +| **Stream / Teilstrecke** | Innerhalb einer Parallelphase: eine von N **gleichzeitig** stattfindenden Unter-Abläufen mit **eigenem** Miniplan (Abschnitte, Übungen, Notizen — analog heutiger Planung). | +| **Synchronisationspunkt** | Fachlich: alle treffen sich wieder (Beginn einer **Gemeinschaftsphase** nach Parallelität). | +| **Station (Rotation)** | Inhaltlicher Fokus oder Platz, den Teilnehmer **wechselnd** anlaufen; kann als Kombinations-/Zirkellogik oder als koordinierter Hallenrhythmus modelliert werden (§5). | + +**Abgrenzung „Rahmenprogramm-Slot“:** Ein Slot im **Rahmenprogramm** ist eine **Session in einer Serie** (z. B. Woche 1 vs. Woche 2), **nicht** „Teilgruppe A gleichzeitig mit Teilgruppe B in derselben Stunde“. Parallele Streams sind **innerhalb einer Einheit**, orthogonal zum Rahmen-Slot. + +**Abgrenzung **Kombinationsübung**:** Eine Kombi-Übung bündelt **mehrere Einzelübungen** mit Methodenprofil (Archetyp, ggf. Rotation) **in einem Plan-Item**. Sie ersetzt **nicht** mehrere Trainer mit **jeweils eigenem Gesamtablauf**, kann aber **pro Stream** für Stationslogik genutzt werden. + +--- + +## 4. Szenarien + +### 4.1 Klassischer Breakout + +30 Min. gemeinsam → 25 Min. drei parallele Streams (Gruppe an Matte / an Schlagsack / Fußarbeit) → 15 Min. gemeinsam. + +### 4.2 Viele Kinder, mehrere Co-Trainer + +Haupttrainer plant die Gesamtstruktur; jeder Co-Trainer sieht in der Durchführung primär die zugewiesene Teilstrecke. + +### 4.3 Rollierendes Stationssystem + +Alle Gruppen arbeiten an **verschiedenen Schwerpunkten** und **wechseln** nach festem Intervall die Station — entweder **nur innerhalb einer Spur** oder **hallenweit synchron** (offene fachliche Präzisierung in MVP vs. später, §5). + +--- + +## 5. Sonderfall: Stationen und Kombinationsübungen + +### 5.1 Variante A — Rotation innerhalb einer Teilstrecke + +Eine Teilgruppe rotiert durch mehrere Übungen (Zeit oder Runden). Das liegt nah an einer **Kombinationsübung** mit Archetyp z. B. „Zirkel / zeitgesteuerte Rotation“ und Parametern (Wechselintervall). **Empfehlung:** Diese Variante über **bestehendes** Kombinationsübungs-Konzept in der jeweiligen **Stream-Planung** abbilden (`planning_method_profile`). + +### 5.2 Variante B — Synchron getaktete Hallen-Rotation + +Alle Streams (oder alle Kinder insgesamt) **wechseln gleichzeitig** zur nächsten Station; Startstation kann pro Teilgruppe **versetzt** sein. Das ist **organisatorisch** schwerer: es braucht entweder **Phasen-Metadaten** (globaler Takt) oder eine explizite **Rot/Matrix**. **Empfehlung:** In einer **zweiten Ausbaustufe** abbilden; MVP kann bei Variante A starten, sofern fachlich ausreichend. + +--- + +## 6. Rollen und Verantwortlichkeiten + +- **Leitungstrainer:** Hält den Faden, startet Gemeinschaftsphasen, koordiniert Parallelbeginn/-ende (fachlich; ggf. später UI-Hinweise). +- **Co-Trainer:** Verantwortlich für **zugeteilte** Streams; Zuordnung soll **pro Stream** möglich werden (Erweiterung gegenüber reiner Einheits-Co-Trainer-Liste). + +--- + +## 7. Offene fachliche Entscheidungen + +1. **MVP Umfang:** Reicht **freie Parallelität** ohne **synchronen** Hallenwechsel (Variante B)? +2. **Dauer:** Sollen Phasen oder Streams **Soll-Minuten** tragen (nur Anzeige vs. später Timer)? +3. **Vorlagen:** Müssen `training_plan_templates` parallel-fähig werden **vor** oder **mit** der ersten Implementierung? +4. **Sichtbarkeit:** Dürfen alle Co-Trainer alle Streams sehen, oder „nur meine Spur“? + +--- + +## 8. Verwandte Dokumente + +| Dokument | Bezug | +|----------|--------| +| `technical/TRAINING_FRAMEWORK_SPEC.md` | Rahmen-Slots = Serien-Sessions, **nicht** Intra-Einheit-Parallelität | +| `technical/TRAINING_MODULES_AND_COMBINATION_EXERCISES_SPEC.md` | Kombinationsübungen, Archetypen, Stationslogik **im Item** | +| `functional/Shinkan Trainingsmodule Kombinationsuebungen Spezifikation V2.md` | Fachliche Tiefe Kombi | +| `docs/FACHLICHE_NUTZERFUNKTIONEN.md` | Nutzerüberblick | +| `technical/DATABASE_SCHEMA.md` | Aktueller Stand Tabellen | diff --git a/.claude/docs/technical/PARALLEL_TRAINING_STREAMS_SPEC.md b/.claude/docs/technical/PARALLEL_TRAINING_STREAMS_SPEC.md new file mode 100644 index 0000000..5d894e3 --- /dev/null +++ b/.claude/docs/technical/PARALLEL_TRAINING_STREAMS_SPEC.md @@ -0,0 +1,130 @@ +# Parallele Trainingsstreams — Technische Spezifikation (Umsetzung) + +**Status:** Entwurf · **Stand:** 2026-05-14 +**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). + +--- + +## 1. Ist-Stand (relevant) + +| Bereich | Aktuell | +|---------|---------| +| Planstruktur | **Eine** lineare Liste `training_unit_sections` je `training_unit_id`; Items in `training_unit_section_items`. | +| Rahmenprogramm | `training_framework_slots` verweisen auf **Blueprint**-`training_units` — Slots = **Serien-Spalten**, nicht simultane Breakouts in **einer** Halle. | +| Kombinationsübung | Ein **Item** kann Kombi sein; `planning_method_profile` = Snapshot; Coaching-UI teilweise (`CombinationPlanBracket` in Run/Peek). | +| Trainer-Zuweisung | `lead_trainer_profile_id`, `assistant_trainer_profile_ids` am **`training_units`**-Kopf; **keine** Zuordnung zu „welcher parallelen Spur“. | +| Run-Modus | `TrainingUnitRunPage`: sortierte Sektionen/Items, Checkliste, Fortschritt in `sessionStorage` pro Einheit. | + +**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). + +--- + +## 2. Zielarchitektur (logisch) + +``` +training_unit (Kalender-Einheit) +├── phase (order, kind: whole_group | parallel, optional Metadaten) +│ ├── [whole_group] → sections[] → items[] (wie heute) +│ └── [parallel] → stream (order, label, optional trainer_ids[]) +│ └── sections[] → items[] +``` + +**Abwärtskompatibilität:** Einheiten **ohne** explizite Phasen/Streams verhalten sich wie heute: **implizit** eine einzige „Gemeinschaftsphase“ mit den vorhandenen Sektionen (Migration: alle bestehenden Sektionen an diese Default-Hülle hängen). + +--- + +## 3. Datenmodell — Optionen + +### 3.1 Empfohlen: explizite Phasen + Streams (normalisiert) + +Neue Tabellen (Namen bei Implementierung final festlegen): + +| Tabelle | Zweck | +|---------|--------| +| `training_unit_phases` | `training_unit_id`, `order_index`, `phase_kind` (`whole_group` \| `parallel`), optional `title`, `guidance_notes`, optional `planned_duration_min` | +| `training_unit_parallel_streams` | `phase_id` (FK, nur wenn parent parallel), `order_index`, `title`/`label`, optional `notes`, optional `assigned_trainer_profile_ids` JSONB (oder 1:n-Hilfstabelle) | + +**Anpassung `training_unit_sections`:** Zusätzliche FK-Spalte(n), z. B.: + +- `phase_id` **NULL** und `parallel_stream_id` **NULL** → **Legacy / Default-Einheitsphase** (Migration setzt Default-Phase); oder +- genau einer von `phase_id` (whole group) oder `parallel_stream_id` gesetzt. + +**Constraints:** CHECK: nicht beide gesetzt; bei `phase_kind = parallel` Sektionen nur unter `parallel_stream_id`; bei `whole_group` nur unter `phase_id`. + +**Vorteil:** Klare Semantik, Reporting, API-Shape konsistent. + +### 3.2 Minimalvariante (nicht ideal fachlich) + +Nur **`training_unit_parallel_streams`** + `parallel_stream_id` auf Sektionen; Phasen implizit durch „Marker“-Sektionen oder Konvention. **Nicht empfohlen**, erschwert UI und Erklärbarkeit. + +--- + +## 4. API + +- **`GET /api/training-units/:id`** (und Listen-Payloads wo vollständiger Plan nötig): verschachtelte Struktur **Phasen → Streams → sections → items** oder flache `sections` mit ausgefüllten `phase_id` / `parallel_stream_id` (Frontend kann normalisieren). +- **`PUT/PATCH`:** Atomares Ersetzen der Phasen/Streams/Sektionen analog zu bestehendem `_replace_unit_sections`-Muster; **Validierung** der CHECK-Regeln serverseitig. +- **Blueprint / Rahmen:** Blueprint-`training_units` dürfen dieselbe Struktur tragen; `GET` Kalenderliste blendet Blueprints weiter aus (`framework_slot_id IS NOT NULL`). + +**Governance / Mandant:** Unverändert über Einheit → `group_id`; keine neuen Mandanten-Entitäten. + +--- + +## 5. Frontend + +### 5.1 Planung (`TrainingPlanningPage`) + +- Darstellung als **vertikale Phasen**: Gemeinschaftsblöcke + Parallelphase mit **N Spalten** (Streams). +- **Wiederverwendung:** `TrainingUnitSectionsEditor` **pro Stream** und pro Gemeinschaftsphase — analog zur Wiederverwendung **pro Rahmen-Slot** in `TrainingFrameworkProgramEditPage`. +- **Co-Trainer:** UI pro Stream (`assigned_trainer_profile_ids`); Regel zur **Kopfliste** `assistant_trainer_profile_ids` festlegen (z. B. Union aller Stream-Zuweisungen für „Wer ist heute dabei“ + Rückwärtskompatibilität wenn Stream-Felder leer). + +### 5.2 Durchführung (`TrainingUnitRunPage`) + +- Gemeinschaftsphasen: heutiges **lineares** Verhalten. +- Parallelphase: **Tabs, Akkordeon oder Swipe** zwischen Streams; Fortschritt **pro Stream** (Storage-Key z. B. `${unitId}:${streamId}`). +- Kombi-Items: unverändert `CombinationPlanBracket` / `effectiveComboMethodProfile`. +- Optional später: Filter „nur meine Spur“ anhand Session-Profil vs. Stream-Zuweisung. + +### 5.3 Vorlagen (`training_plan_templates`) + +- Erweiterung um **dieselbe** Phasen/Streams-Semantik (Kindtabellen oder serialisiertes JSON — Abgleich mit Kopierlogik aus Vorlage in Einheit). +- **Kein** Live-Spiegel: weiterhin Materialisierung beim Anwenden. + +--- + +## 6. Bezug Kombinationsübungen + +- **Variante A** (Rotation innerhalb einer Teilstrecke): ein oder mehrere **Items** vom Typ Kombi im jeweiligen Stream; Archetyp und Parameter wie in `TRAINING_MODULES_AND_COMBINATION_EXERCISES_SPEC.md`. +- **Variante B** (synchron Hallenweit): erweiterte **Phasen-** oder **Stream-übergreifende** Metadaten — **nicht** in MVP-Zwang; eigenes Teilpaket nach fachlicher Freigabe (`PARALLEL_TRAINING_STREAMS_CONCEPT.md` §5.2). + +--- + +## 7. Migration und Risiken + +1. **Datenmigration:** Alle existierenden `training_unit_sections` einer Einheit einer **Default-Phase** `whole_group` zuordnen. +2. **API-Versionierung:** Clients, die nur flache `sections` erwarten, müssen angepasst werden (oder Server liefert **beides** kurzzeitig — nur wenn nötig). +3. **Performance:** Tiefe Kopien (Rahmen-Slot, Duplikat Einheit) müssen rekursiv Phasen/Streams mitsamt Sektionen/Items kopieren. +4. **Tests:** pytest für PUT/GET mit gemischten Phasen; ggf. Playwright-Smoke für Planung/Run. + +--- + +## 8. Implementierungsphasen (Vorschlag) + +| Phase | Inhalt | +|-------|--------| +| **P1** | Schema Phasen + Streams; Migration; GET/PATCH Einheit verschachtelt; Planungs-UI; Run-UI mit Stream-Tabs | +| **P2** | Trainer-Zuordnung pro Stream + effektive Anzeige; Vorlagen erweitert | +| **P3** | Synchroner Hallen-Takt / Rotationsmatrix (falls fachlich freigegeben) | + +--- + +## 9. Verwandte Dokumente + +| Dokument | Bezug | +|----------|--------| +| `functional/PARALLEL_TRAINING_STREAMS_CONCEPT.md` | Fachziele, Begriffe, Entscheidungsfragen | +| `technical/TRAINING_FRAMEWORK_SPEC.md` | Rahmen-Slot vs. Parallelität | +| `technical/TRAINING_MODULES_AND_COMBINATION_EXERCISES_SPEC.md` | Kombi, `planning_method_profile` | +| `technical/DATABASE_SCHEMA.md`, `backend/migrations/` | DDL-Historie | +| `frontend/src/pages/TrainingPlanningPage.jsx`, `TrainingUnitRunPage.jsx`, `TrainingFrameworkProgramEditPage.jsx` | Ist-UI | diff --git a/.claude/docs/technical/TRAINING_FRAMEWORK_SPEC.md b/.claude/docs/technical/TRAINING_FRAMEWORK_SPEC.md index 2062851..485b574 100644 --- a/.claude/docs/technical/TRAINING_FRAMEWORK_SPEC.md +++ b/.claude/docs/technical/TRAINING_FRAMEWORK_SPEC.md @@ -15,6 +15,7 @@ | `DATABASE_SCHEMA.md` | **Nachgeordnete** Übersicht: Migrationshistorie und Tabellenliste; Detail-DDL primär **hier §2–§3** + SQL unter `backend/migrations/`. | | `functional/DOMAIN_MODEL.md` | Fachliche Begriffe; Kurzverweis auf Progressionsgraph ergänzt. | | `TRAINING_CURRICULUM_AND_GOVERNANCE_CONCEPT.md` | **Was** und **warum** (Bibliothek vs. Instanz, Governance, CURR‑Tabelle). | +| `functional/PARALLEL_TRAINING_STREAMS_CONCEPT.md`, `technical/PARALLEL_TRAINING_STREAMS_SPEC.md` | **Parallele Streams / Breakout innerhalb einer Einheit** — orthogonale Domäne zu **Rahmen‑Slots** (Serien‑Sessions). | **Konsequenz:** Diese Datei bleibt der **technische Arbeitspool** für Rahmenprogramm Stufe 1–2. Abschnitt **§4** beschreibt explizit den **aktuellen Produktfreigabe-Umfang** und **bekannte Lücken** (damit Trainingsplanung weiter gebaut werden kann ohne falscher Erwartung an „Alternative‑Pakete“ in der UI). diff --git a/.claude/docs/technical/TRAINING_MODULES_AND_COMBINATION_EXERCISES_SPEC.md b/.claude/docs/technical/TRAINING_MODULES_AND_COMBINATION_EXERCISES_SPEC.md index d9b0952..6a00d68 100644 --- a/.claude/docs/technical/TRAINING_MODULES_AND_COMBINATION_EXERCISES_SPEC.md +++ b/.claude/docs/technical/TRAINING_MODULES_AND_COMBINATION_EXERCISES_SPEC.md @@ -15,6 +15,7 @@ | Dokument | Bezug | |----------|--------| | `TRAINING_FRAMEWORK_SPEC.md` | Rahmen-Bibliothek, Slot-Blueprint, Kopiersemantik (`from-framework-slot`) | +| `functional/PARALLEL_TRAINING_STREAMS_CONCEPT.md`, `technical/PARALLEL_TRAINING_STREAMS_SPEC.md` | Parallele Teilstrecken **innerhalb einer Einheit**; Kombi-Übungen weiter nutzbar **pro Stream** für Stationsrotation | | `DATABASE_SCHEMA.md` | Aktueller Stand `training_units`, Sektionen, Items | | `functional/DOMAIN_MODEL.md` | Domänenbegriffe (bei Bedarf zu erweitern) | | `EXERCISES_*` (Katalog) | Einzelübungen, Varianten | diff --git a/backend/version.py b/backend/version.py index 5cda9fe..e798998 100644 --- a/backend/version.py +++ b/backend/version.py @@ -1,6 +1,6 @@ # Shinkan Jinkendo Version Information -APP_VERSION = "0.8.121" +APP_VERSION = "0.8.122" BUILD_DATE = "2026-05-12" DB_SCHEMA_VERSION = "20260514062" @@ -36,6 +36,13 @@ MODULE_VERSIONS = { } CHANGELOG = [ + { + "version": "0.8.122", + "date": "2026-05-13", + "changes": [ + "Frontend Phase 3 (Teil): useExerciseListCatalogsAndQuery — Katalog-Fetch und Übungslisten-Laden/Keyset-Pagination aus ExercisesListPage in Hook ausgelagert.", + ], + }, { "version": "0.8.121", "date": "2026-05-13", diff --git a/frontend/src/hooks/useExerciseListCatalogsAndQuery.js b/frontend/src/hooks/useExerciseListCatalogsAndQuery.js new file mode 100644 index 0000000..64bbe00 --- /dev/null +++ b/frontend/src/hooks/useExerciseListCatalogsAndQuery.js @@ -0,0 +1,120 @@ +import { useState, useEffect, useCallback } from 'react' +import api from '../utils/api' + +export const EXERCISE_LIST_PAGE_SIZE = 100 + +/** + * Lädt Kataloge für Filter/Bulk einmalig und hält die Übungsliste (Offset + Keyset „Mehr laden“) synchron zu queryBase. + */ +export function useExerciseListCatalogsAndQuery({ queryBase, pageTab, tenantClubDepKey }) { + const [catalogs, setCatalogs] = useState({ + focusAreas: [], + styleDirections: [], + trainingTypes: [], + targetGroups: [], + skills: [], + }) + const [catalogsReady, setCatalogsReady] = useState(false) + const [exercises, setExercises] = useState([]) + const [listFetching, setListFetching] = useState(false) + const [loadingMore, setLoadingMore] = useState(false) + const [hasMore, setHasMore] = useState(false) + + useEffect(() => { + let cancelled = false + ;(async () => { + try { + const [fa, sd, tt, tg, sk] = await Promise.all([ + api.listFocusAreas(), + api.listStyleDirections(), + api.listTrainingTypes(), + api.listTargetGroups(), + api.listSkills(), + ]) + if (!cancelled) { + setCatalogs({ + focusAreas: fa, + styleDirections: sd, + trainingTypes: tt, + targetGroups: tg, + skills: sk, + }) + setCatalogsReady(true) + } + } catch (err) { + if (!cancelled) { + console.error(err) + alert('Kataloge konnten nicht geladen werden: ' + err.message) + setCatalogsReady(true) + } + } + })() + return () => { + cancelled = true + } + }, []) + + useEffect(() => { + if (!catalogsReady || pageTab !== 'list') return + let cancelled = false + const run = async () => { + setListFetching(true) + try { + const batch = await api.listExercises({ + ...queryBase, + limit: EXERCISE_LIST_PAGE_SIZE, + offset: 0, + }) + if (cancelled) return + setExercises(batch) + setHasMore(batch.length === EXERCISE_LIST_PAGE_SIZE) + } catch (err) { + if (!cancelled) { + console.error('Failed to load data:', err) + alert('Fehler beim Laden: ' + err.message) + } + } finally { + if (!cancelled) setListFetching(false) + } + } + run() + return () => { + cancelled = true + } + }, [queryBase, catalogsReady, pageTab, tenantClubDepKey]) + + const loadMore = useCallback(async () => { + if (loadingMore || !hasMore) return + const last = exercises[exercises.length - 1] + if (!last?.id || last.updated_at == null) return + setLoadingMore(true) + try { + const batch = await api.listExercises({ + ...queryBase, + limit: EXERCISE_LIST_PAGE_SIZE, + cursor_updated_at: + typeof last.updated_at === 'string' + ? last.updated_at + : new Date(last.updated_at).toISOString(), + cursor_id: last.id, + }) + setExercises((prev) => [...prev, ...batch]) + setHasMore(batch.length === EXERCISE_LIST_PAGE_SIZE) + } catch (err) { + alert('Fehler: ' + err.message) + } finally { + setLoadingMore(false) + } + }, [loadingMore, hasMore, exercises, queryBase]) + + return { + catalogs, + catalogsReady, + exercises, + setExercises, + listFetching, + loadingMore, + hasMore, + loadMore, + } +} diff --git a/frontend/src/pages/ExercisesListPage.jsx b/frontend/src/pages/ExercisesListPage.jsx index f58ae55..5e2473b 100644 --- a/frontend/src/pages/ExercisesListPage.jsx +++ b/frontend/src/pages/ExercisesListPage.jsx @@ -10,6 +10,7 @@ import ExerciseListBulkModal from '../components/exercises/ExerciseListBulkModal import ExerciseListSearchBar from '../components/exercises/ExerciseListSearchBar' import { buildExerciseListFilterChips } from '../utils/exerciseListFilterChips' import { applyDashboardExerciseListUrl, buildExerciseListQueryBase } from '../utils/exerciseListQuery' +import { useExerciseListCatalogsAndQuery } from '../hooks/useExerciseListCatalogsAndQuery' import { INITIAL_EXERCISE_LIST_FILTERS, mergeExerciseListPrefsFromApi, @@ -18,7 +19,6 @@ import { const ExerciseProgressionGraphPanel = lazy(() => import('../components/ExerciseProgressionGraphPanel')) -const PAGE_SIZE = 100 const BULK_MAX_IDS = 500 const EXERCISES_PAGE_TABS = [ { id: 'list', label: 'Liste' }, @@ -40,18 +40,6 @@ function ExercisesListPage() { } }) - const [exercises, setExercises] = useState([]) - const [catalogs, setCatalogs] = useState({ - focusAreas: [], - styleDirections: [], - trainingTypes: [], - targetGroups: [], - skills: [], - }) - const [catalogsReady, setCatalogsReady] = useState(false) - const [listFetching, setListFetching] = useState(false) - const [loadingMore, setLoadingMore] = useState(false) - const [hasMore, setHasMore] = useState(false) const [searchInput, setSearchInput] = useState('') const [aiSearchInput, setAiSearchInput] = useState('') const [debouncedSearch, setDebouncedSearch] = useState('') @@ -115,6 +103,26 @@ function ExercisesListPage() { return () => window.removeEventListener('keydown', onKey) }, [filterModalOpen]) + const queryBase = useMemo( + () => buildExerciseListQueryBase(filters, debouncedSearch, debouncedAiSearch, mineOnly), + [filters, debouncedSearch, debouncedAiSearch, mineOnly] + ) + + const { + catalogs, + catalogsReady, + exercises, + setExercises, + listFetching, + loadingMore, + hasMore, + loadMore, + } = useExerciseListCatalogsAndQuery({ queryBase, pageTab, tenantClubDepKey }) + + useEffect(() => { + setSelectedIds(new Set()) + }, [queryBase]) + const focusOptions = useMemo( () => catalogs.focusAreas.map((fa) => ({ @@ -191,15 +199,6 @@ function ExercisesListPage() { return [...new Set(titles)].slice(0, 80) }, [exercises]) - const queryBase = useMemo( - () => buildExerciseListQueryBase(filters, debouncedSearch, debouncedAiSearch, mineOnly), - [filters, debouncedSearch, debouncedAiSearch, mineOnly] - ) - - useEffect(() => { - setSelectedIds(new Set()) - }, [queryBase]) - const clubNameById = useMemo(() => { const m = {} for (const c of activeClubMemberships(user?.clubs)) { @@ -257,89 +256,6 @@ function ExercisesListPage() { return base }, [isSuperadmin]) - useEffect(() => { - let cancelled = false - ;(async () => { - try { - const [fa, sd, tt, tg, sk] = await Promise.all([ - api.listFocusAreas(), - api.listStyleDirections(), - api.listTrainingTypes(), - api.listTargetGroups(), - api.listSkills(), - ]) - if (!cancelled) { - setCatalogs({ - focusAreas: fa, - styleDirections: sd, - trainingTypes: tt, - targetGroups: tg, - skills: sk, - }) - setCatalogsReady(true) - } - } catch (err) { - if (!cancelled) { - console.error(err) - alert('Kataloge konnten nicht geladen werden: ' + err.message) - setCatalogsReady(true) - } - } - })() - return () => { - cancelled = true - } - }, []) - - useEffect(() => { - if (!catalogsReady || pageTab !== 'list') return - let cancelled = false - const run = async () => { - setListFetching(true) - try { - const batch = await api.listExercises({ ...queryBase, limit: PAGE_SIZE, offset: 0 }) - if (cancelled) return - setExercises(batch) - setHasMore(batch.length === PAGE_SIZE) - } catch (err) { - if (!cancelled) { - console.error('Failed to load data:', err) - alert('Fehler beim Laden: ' + err.message) - } - } finally { - if (!cancelled) setListFetching(false) - } - } - run() - return () => { - cancelled = true - } - }, [queryBase, catalogsReady, pageTab, tenantClubDepKey]) - - const loadMore = async () => { - if (loadingMore || !hasMore) return - const last = exercises[exercises.length - 1] - if (!last?.id || last.updated_at == null) return - setLoadingMore(true) - try { - const batch = await api.listExercises({ - ...queryBase, - limit: PAGE_SIZE, - cursor_updated_at: - typeof last.updated_at === 'string' - ? last.updated_at - : new Date(last.updated_at).toISOString(), - cursor_id: last.id, - }) - setExercises((prev) => [...prev, ...batch]) - setHasMore(batch.length === PAGE_SIZE) - } catch (err) { - alert('Fehler: ' + err.message) - } finally { - setLoadingMore(false) - } - } - const handleDelete = async (exercise) => { if (!confirm(`Übung "${exercise.title}" wirklich löschen?`)) return try {