chore(version): update version and changelog for release 0.8.122
All checks were successful
Deploy Development / deploy (push) Successful in 37s
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 1m11s

- 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.
This commit is contained in:
Lars 2026-05-14 11:21:09 +02:00
parent 57a8957c93
commit 4235246cd7
8 changed files with 397 additions and 108 deletions

View File

@ -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 SlotBlueprint an; **`origin_framework_slot_id`** dient als Herkunftsreferenz (**Lineage light**; weiteres Feedback/LineageKonzept: 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 **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).
**Dokumentation:** `functional/PARALLEL_TRAINING_STREAMS_CONCEPT.md`, Umsetzung `technical/PARALLEL_TRAINING_STREAMS_SPEC.md`.
---
## Medien-Archiv & Übungs-Anhänge (Stand 2026-05-07)

View File

@ -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 |
|----|------|
| PT01 | 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. |
| PT02 | **Unbegrenzte** Anzahl paralleler **Streams** (Teilstrecken) in einer oder mehreren **Parallelphasen**. |
| PT03 | **Phasenmodell:** klar erkennbar **Gemeinsam** vs. **Parallel** vs. wieder **Gemeinsam** (auch mehrfach hintereinander möglich). |
| PT04 | **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). |
| PT05 | **Sonderfall Stationen:** rotierender Ablauf (z.B. Wechsel alle 20Min.) **inhaltlich** unterscheiden zwischen (a) Rotation **innerhalb** einer Teilstrecke und (b) **synchron** getakteter Hallen-Rotation — siehe §5. |
| PT06 | **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
30Min. gemeinsam → 25Min. drei parallele Streams (Gruppe an Matte / an Schlagsack / Fußarbeit) → 15Min. 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 |

View File

@ -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 |

View File

@ -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, CURRTabelle). |
| `functional/PARALLEL_TRAINING_STREAMS_CONCEPT.md`, `technical/PARALLEL_TRAINING_STREAMS_SPEC.md` | **Parallele Streams / Breakout innerhalb einer Einheit** — orthogonale Domäne zu **RahmenSlots** (SerienSessions). |
**Konsequenz:** Diese Datei bleibt der **technische Arbeitspool** für Rahmenprogramm Stufe 12. Abschnitt **§4** beschreibt explizit den **aktuellen Produktfreigabe-Umfang** und **bekannte Lücken** (damit Trainingsplanung weiter gebaut werden kann ohne falscher Erwartung an „AlternativePakete“ in der UI).

View File

@ -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 |

View File

@ -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",

View File

@ -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,
}
}

View File

@ -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 {