feat: Migration 008 - M:N Exercise Relations + Hierarchical Catalogs
BREAKING CHANGE: Datenmodell-Umstellung von 1:1 auf M:N Beziehungen Migration 008: - Zielgruppen-Tabelle (target_groups) mit training_style_id Hierarchie - M:N Zuordnungstabellen: exercise_focus_areas, exercise_styles, exercise_target_groups - Altersgruppen-Dimension (exercise_age_groups) mit CHECK constraint - Hierarchische Struktur: training_styles.focus_area_id → focus_areas - Daten-Migration: Bestehende 1:1 Beziehungen zu M:N mit is_primary=true - Seed-Daten: Beispiel-Zielgruppen für Shotokan Architektur: - Smart Cascade-Logik (RESTRICT, Rerouting, Move) vorbereitet - Legacy-Spalten (focus_area_id, training_style_id) bleiben zur Rückwärtskompatibilität - Primary/Secondary Assignments via is_primary Flag Dokumentation: - .claude/docs/technical/DATABASE_SCHEMA.md (kontinuierlich gepflegt) - .claude/docs/functional/DOMAIN_MODEL.md (fachliche Anforderungen) - Migrations-Historie aktualisiert version: 0.3.0 (backend + frontend) modules: exercises 0.3.0, catalogs 1.1.0 DB_SCHEMA_VERSION: 20260423 Konzept: shinkan_anforderungsdokument_entwurf.md (§8.1, §8.3, §10.7)
This commit is contained in:
parent
d0e9b9b764
commit
63b1c09975
403
.claude/docs/functional/DOMAIN_MODEL.md
Normal file
403
.claude/docs/functional/DOMAIN_MODEL.md
Normal file
|
|
@ -0,0 +1,403 @@
|
|||
# Shinkan Jinkendo - Fachliches Domänenmodell
|
||||
|
||||
**Version:** 0.2.0
|
||||
**Stand:** 2026-04-23
|
||||
**Basis:** `shinkan_anforderungsdokument_entwurf.md`
|
||||
|
||||
---
|
||||
|
||||
## Übersicht
|
||||
|
||||
Dieses Dokument beschreibt die **fachliche Datenstruktur** von Shinkan Jinkendo.
|
||||
|
||||
**Zuständig für technische Umsetzung:** siehe `DATABASE_SCHEMA.md`
|
||||
|
||||
---
|
||||
|
||||
## Fachliche Kernprinzipien
|
||||
|
||||
### 1. Globaler Fähigkeitskatalog (§8.3)
|
||||
|
||||
**Prinzip:** Fähigkeiten werden NICHT pro Modell dupliziert.
|
||||
|
||||
**Beispiele:**
|
||||
- Distanzgefühl (in Karate UND Selbstverteidigung)
|
||||
- Stabiler Stand (in Karate UND Gewaltschutz)
|
||||
- Aufmerksamkeit (in verschiedenen Zielgruppenmodellen)
|
||||
|
||||
**Konsequenz:** Ein globaler Katalog, aber kontextabhängige Ausprägung über Reifegradmodelle.
|
||||
|
||||
### 2. Mehrfachzuordnung von Übungen (§10.7)
|
||||
|
||||
**Prinzip:** Eine Übung kann mehreren Bereichen GLEICHZEITIG zugeordnet sein.
|
||||
|
||||
**Beispiel:** Distanzübung
|
||||
- Karate ✓
|
||||
- Selbstverteidigung ✓
|
||||
- Gewaltschutz ✓
|
||||
|
||||
**Technische Umsetzung:** M:N Beziehungen mit `is_primary` Flag.
|
||||
|
||||
### 3. Hierarchischer Kontext (§8.1)
|
||||
|
||||
**Fachliche Grundstruktur:**
|
||||
```
|
||||
Fokusbereich (Sportart/Bereich)
|
||||
└─ Stil
|
||||
└─ Zielgruppe
|
||||
```
|
||||
|
||||
**Beispiele:**
|
||||
```
|
||||
Karate
|
||||
├─ Shotokan
|
||||
│ ├─ Breitensportler
|
||||
│ ├─ Leistungssportler
|
||||
│ └─ Kinder
|
||||
├─ Goju-Ryu
|
||||
│ ├─ Erwachsene
|
||||
│ └─ Jugendliche
|
||||
└─ Wado-Ryu
|
||||
└─ Gemischt
|
||||
|
||||
Selbstverteidigung
|
||||
├─ Erwachsene
|
||||
│ ├─ Männer
|
||||
│ ├─ Frauen
|
||||
│ └─ Gemischt
|
||||
└─ Kinder
|
||||
└─ 6-12 Jahre
|
||||
|
||||
Gewaltschutz
|
||||
├─ Frauen
|
||||
├─ Kinder
|
||||
└─ Senioren
|
||||
```
|
||||
|
||||
**Bedeutung für Reifegradmodelle:**
|
||||
|
||||
Jede Kombination (Fokusbereich + Stil + Zielgruppe) kann ein eigenes Reifegradmodell haben:
|
||||
- Karate / Shotokan / Breitensport → Modell A
|
||||
- Karate / Shotokan / Leistungssport → Modell B
|
||||
- Selbstverteidigung / Erwachsene → Modell C
|
||||
|
||||
---
|
||||
|
||||
## Fachliche Dimensionen
|
||||
|
||||
### Dimension 1: Fokusbereich (Sportart/Bereich)
|
||||
|
||||
**Definition:** Oberste fachliche Kategorisierung der Trainingsinhalte.
|
||||
|
||||
**Beispiele:**
|
||||
- Karate
|
||||
- Selbstverteidigung
|
||||
- Gewaltschutz
|
||||
|
||||
**Eigenschaften:**
|
||||
- Name, Kürzel, Beschreibung
|
||||
- Farbe (für UI)
|
||||
- Icon (Emoji oder Icon-Name)
|
||||
- Status (aktiv/archiviert)
|
||||
|
||||
**Verwaltung:**
|
||||
- Admin-Level: Systemadmin, Vereinsadmin
|
||||
- CRUD: Voll administrierbar
|
||||
- Löschen: Nur wenn keine abhängigen Stile oder Übungen
|
||||
|
||||
### Dimension 2: Trainingsstil
|
||||
|
||||
**Definition:** Spezialisierung innerhalb eines Fokusbereichs.
|
||||
|
||||
**Beispiele:**
|
||||
- Shotokan (unter Karate)
|
||||
- Goju-Ryu (unter Karate)
|
||||
- Wado-Ryu (unter Karate)
|
||||
|
||||
**Hierarchie:**
|
||||
- Gehört zu genau EINEM Fokusbereich
|
||||
- Kann optional Sub-Stile haben (parent_style_id)
|
||||
|
||||
**Eigenschaften:**
|
||||
- Name, Kürzel, Beschreibung
|
||||
- Fokusbereich-Zuordnung
|
||||
- Optionale Hierarchie (z.B. Kyokushin → Kyokushin IKO)
|
||||
- Status
|
||||
|
||||
**Verwaltung:**
|
||||
- Admin-Level: Systemadmin, Vereinsadmin
|
||||
- CRUD: Voll administrierbar
|
||||
- Verschieben: Zwischen Fokusbereichen möglich
|
||||
- Löschen: Nur wenn keine Zielgruppen oder Übungen zugeordnet
|
||||
|
||||
### Dimension 3: Zielgruppe
|
||||
|
||||
**Definition:** Konkrete Trainingsempfänger-Gruppe innerhalb eines Stils.
|
||||
|
||||
**Beispiele:**
|
||||
- Breitensportler
|
||||
- Leistungssportler
|
||||
- Kinder (6-12 Jahre)
|
||||
- Jugendliche (13-17 Jahre)
|
||||
- Erwachsene
|
||||
- Frauen
|
||||
- Senioren
|
||||
- Gemischt
|
||||
|
||||
**Hierarchie:**
|
||||
- Gehört zu genau EINEM Trainingsstil
|
||||
- Kann Altersbereich definieren
|
||||
|
||||
**Eigenschaften:**
|
||||
- Name, Beschreibung
|
||||
- Stil-Zuordnung
|
||||
- Optionale Altersangabe (min_age, max_age)
|
||||
- Status
|
||||
|
||||
**Verwaltung:**
|
||||
- Admin-Level: Systemadmin, Vereinsadmin, Spartenadmin
|
||||
- CRUD: Voll administrierbar
|
||||
- Verschieben: Zwischen Stilen möglich
|
||||
- Löschen: Cascade oder Restrict (konfigurierbar)
|
||||
|
||||
### Dimension 4: Altersgruppen (separate Dimension!)
|
||||
|
||||
**Definition:** Grobe Alterseinstufung UNABHÄNGIG von Zielgruppe.
|
||||
|
||||
**Katalog:**
|
||||
- Minis (3-5 Jahre)
|
||||
- Kinder (6-9 Jahre)
|
||||
- Schüler (10-12 Jahre)
|
||||
- Teenager (13-17 Jahre)
|
||||
- Erwachsene (18+)
|
||||
|
||||
**Abgrenzung zu Zielgruppe:**
|
||||
- Altersgruppe = didaktische Einstufung
|
||||
- Zielgruppe = fachlicher Kontext
|
||||
|
||||
**Beispiel:**
|
||||
- Übung kann für "Kinder" + "Schüler" geeignet sein (Altersgruppen)
|
||||
- Und gleichzeitig zu Zielgruppe "Karate Breitensport Kinder" gehören
|
||||
|
||||
---
|
||||
|
||||
## Übungs-Zuordnungslogik (§10)
|
||||
|
||||
### Primäre vs. Sekundäre Zuordnung
|
||||
|
||||
**Regel:** Eine Übung hat genau EINE primäre Zuordnung pro Dimension.
|
||||
|
||||
**Beispiel:**
|
||||
|
||||
**Übung:** "Maai - Distanzübung"
|
||||
|
||||
**Fokusbereiche:**
|
||||
- Karate (primär) ✓
|
||||
- Selbstverteidigung (sekundär)
|
||||
|
||||
**Stile:**
|
||||
- Shotokan (primär) ✓
|
||||
- Goju-Ryu (sekundär)
|
||||
- Wado-Ryu (sekundär)
|
||||
|
||||
**Zielgruppen:**
|
||||
- Breitensportler (primär) ✓
|
||||
- Leistungssportler (sekundär)
|
||||
|
||||
**Altersgruppen:**
|
||||
- Schüler ✓
|
||||
- Teenager ✓
|
||||
- Erwachsene ✓
|
||||
|
||||
### Fähigkeitsbezug (§10.8)
|
||||
|
||||
**Übung trainiert MEHRERE Fähigkeiten:**
|
||||
|
||||
**Beispiel:** "Kumite-Drill"
|
||||
|
||||
**Hauptfähigkeiten:**
|
||||
- Distanzgefühl (Intensität: hoch)
|
||||
- Timing (Intensität: hoch)
|
||||
|
||||
**Nebenfähigkeiten:**
|
||||
- Beinarbeit (Intensität: mittel)
|
||||
- Reaktionsfähigkeit (Intensität: mittel)
|
||||
|
||||
**Attribute pro Fähigkeitsbezug:**
|
||||
- is_primary (Haupt- oder Nebenfähigkeit)
|
||||
- intensity (niedrig/mittel/hoch)
|
||||
- development_contribution (Entwicklungsbeitrag)
|
||||
- required_level (Voraussetzung)
|
||||
- target_level (Ziel-Level)
|
||||
|
||||
### Trainingscharakter (§10.7)
|
||||
|
||||
**Katalog:**
|
||||
- Grundlage (Einführung, Basisvermittlung)
|
||||
- Aufbau (Aufbauendes Training)
|
||||
- Vertiefung (Vertiefung, Spezialisierung)
|
||||
- Festigung (Wiederholung, Festigung)
|
||||
- Diagnose (Leistungsdiagnose, Test)
|
||||
- Wettkampf (Wettkampfvorbereitung)
|
||||
|
||||
**Bedeutung:**
|
||||
- Wichtig für Suche
|
||||
- Wichtig für Trainingsplanung
|
||||
- Wichtig für KI-Unterstützung (später)
|
||||
|
||||
---
|
||||
|
||||
## Variantenlogik (§11.2)
|
||||
|
||||
**Prinzip:** Übung besteht aus Stammübung + optionale Varianten.
|
||||
|
||||
**Beispiele für Varianten:**
|
||||
- Leicht / Mittel / Schwer
|
||||
- Mit Partner / Ohne Partner
|
||||
- Mit Hilfsmittel / Ohne Hilfsmittel
|
||||
- Kindgerechte Variante
|
||||
- Kurzvariante
|
||||
- Fortgeschrittene Variante
|
||||
|
||||
**Varianten-Attribute:**
|
||||
- Titel/Name
|
||||
- Kurzbeschreibung
|
||||
- Abweichende Durchführung
|
||||
- Abweichende Dauer
|
||||
- Abweichende Hilfsmittel
|
||||
- Abweichender Fähigkeitsfokus
|
||||
- Abweichende Zielgruppen
|
||||
- Eigener Medienbezug
|
||||
|
||||
---
|
||||
|
||||
## Methodenbezug (§11.5)
|
||||
|
||||
**Prinzip:** Genau EINE Hauptmethode, optional weitere Nebenmethoden.
|
||||
|
||||
**Beispiele:**
|
||||
- Hauptmethode: Rollenspiel
|
||||
- Nebenmethoden: Strukturierte Übung, Reflexion
|
||||
|
||||
**Optional später:**
|
||||
- Sportmethodische Hauptmethode
|
||||
- Didaktische Zusatzmethode
|
||||
|
||||
---
|
||||
|
||||
## Qualitäts- und Bearbeitungsstatus (§11.3)
|
||||
|
||||
### Statusstufen
|
||||
|
||||
**Übungsstatus:**
|
||||
- Entwurf (in Arbeit)
|
||||
- In Bearbeitung (aktiv gepflegt)
|
||||
- Fachlich geprüft (Review OK)
|
||||
- Freigegeben (produktiv nutzbar)
|
||||
- Archiviert (nicht mehr aktiv)
|
||||
|
||||
### Sichtbarkeitsebenen (§5.5)
|
||||
|
||||
**Freigabeebenen:**
|
||||
- Privat (nur Ersteller)
|
||||
- Für bestimmte Personen (später)
|
||||
- Verein (alle Vereinsmitglieder)
|
||||
- Sparte (nur bestimmte Sparte)
|
||||
- Allgemein/Global (alle Nutzer)
|
||||
- Offiziell (Standardinhalte)
|
||||
|
||||
**Zusätzlich:** Thematische Einschränkungen
|
||||
- Karate-Inhalte
|
||||
- Selbstverteidigungs-Inhalte
|
||||
- Gewaltschutz-Inhalte
|
||||
|
||||
---
|
||||
|
||||
## Admin-Workflows
|
||||
|
||||
### Fokusbereich anlegen
|
||||
|
||||
1. Admin navigiert zu Katalogverwaltung
|
||||
2. Wählt "Fokusbereiche"
|
||||
3. Klickt "Neu anlegen"
|
||||
4. Füllt aus: Name, Kürzel, Beschreibung, Farbe, Icon
|
||||
5. Speichert
|
||||
6. System validiert Eindeutigkeit (Name)
|
||||
7. Fokusbereich verfügbar für Stil-Zuordnung
|
||||
|
||||
### Stil verschieben (zwischen Fokusbereichen)
|
||||
|
||||
**Szenario:** Goju-Ryu soll von "Karate" nach "Karate Traditional" verschoben werden.
|
||||
|
||||
**Workflow:**
|
||||
1. Admin wählt Stil "Goju-Ryu"
|
||||
2. Klickt "Verschieben"
|
||||
3. Wählt Ziel-Fokusbereich "Karate Traditional"
|
||||
4. System prüft:
|
||||
- Ziel-Fokusbereich existiert
|
||||
- Keine Namenskonflikte
|
||||
5. System aktualisiert:
|
||||
- `training_styles.focus_area_id`
|
||||
- Alle abhängigen Zielgruppen bleiben erhalten
|
||||
6. System loggt Änderung
|
||||
7. Erfolg-Meldung
|
||||
|
||||
### Fokusbereich löschen (mit Schutz)
|
||||
|
||||
**Szenario:** "Judo" soll gelöscht werden.
|
||||
|
||||
**Workflow:**
|
||||
1. Admin wählt "Judo"
|
||||
2. Klickt "Löschen"
|
||||
3. System prüft Abhängigkeiten:
|
||||
```sql
|
||||
SELECT COUNT(*) FROM training_styles WHERE focus_area_id = :id
|
||||
SELECT COUNT(*) FROM exercise_focus_areas WHERE focus_area_id = :id
|
||||
```
|
||||
4. **Falls verwendet:**
|
||||
- Dialog: "Fokusbereich kann nicht gelöscht werden. 5 Stile und 23 Übungen sind zugeordnet."
|
||||
- Optionen:
|
||||
- "Abbrechen"
|
||||
- "Stile/Übungen umrouten" (Admin wählt Ziel)
|
||||
- "Archivieren statt löschen"
|
||||
5. **Falls nicht verwendet:**
|
||||
- Bestätigung: "Wirklich löschen? (Keine Abhängigkeiten)"
|
||||
- Löschen durchführen
|
||||
|
||||
### Zielgruppe umrouten
|
||||
|
||||
**Szenario:** Alle Übungen für "Kinder" sollen zu "Schüler" umgeleitet werden.
|
||||
|
||||
**Workflow:**
|
||||
1. Admin wählt Zielgruppe "Kinder"
|
||||
2. Klickt "Umrouten"
|
||||
3. Wählt Ziel: "Schüler"
|
||||
4. System zeigt Vorschau:
|
||||
- "47 Übungen werden aktualisiert"
|
||||
- Liste der betroffenen Übungen (mit Scroll)
|
||||
5. Admin bestätigt
|
||||
6. System führt Batch-Update aus:
|
||||
```sql
|
||||
UPDATE exercise_target_groups
|
||||
SET target_group_id = :new_id
|
||||
WHERE target_group_id = :old_id
|
||||
```
|
||||
7. Optional: Alte Zielgruppe löschen oder archivieren
|
||||
8. Erfolg-Meldung mit Log-Referenz
|
||||
|
||||
---
|
||||
|
||||
## Nächste Schritte
|
||||
|
||||
- [ ] Hierarchie-UI Wireframes erstellen
|
||||
- [ ] Admin-CRUD-Endpoints spezifizieren
|
||||
- [ ] Cascade-Logik detaillieren
|
||||
- [ ] Umrouten-Workflows implementieren
|
||||
- [ ] Migration 008 Entwurf finalisieren
|
||||
- [ ] Tests für alle Admin-Operationen
|
||||
|
||||
---
|
||||
|
||||
**Letzte Aktualisierung:** 2026-04-23
|
||||
**Verantwortlich:** Claude Code
|
||||
**Review:** Pending
|
||||
260
.claude/docs/technical/DATABASE_SCHEMA.md
Normal file
260
.claude/docs/technical/DATABASE_SCHEMA.md
Normal file
|
|
@ -0,0 +1,260 @@
|
|||
# Shinkan Jinkendo - Datenbank-Schema (Technisch)
|
||||
|
||||
**Version:** 0.2.0
|
||||
**Stand:** 2026-04-23
|
||||
**Aktuell deployed:** Migration 007 (idempotent)
|
||||
|
||||
---
|
||||
|
||||
## Übersicht
|
||||
|
||||
Dieses Dokument beschreibt die **technische Datenbankstruktur** von Shinkan Jinkendo.
|
||||
|
||||
**Zuständig für fachliche Modellierung:** siehe `DOMAIN_MODEL.md`
|
||||
|
||||
---
|
||||
|
||||
## Migrations-Historie
|
||||
|
||||
| Nr. | Datum | Beschreibung | Status |
|
||||
|-----|-------|--------------|--------|
|
||||
| 001 | 2026-04-20 | Initial Schema (Auth, Profiles) | ✅ Deployed |
|
||||
| 002 | 2026-04-20 | Clubs, Divisions, Groups | ✅ Deployed |
|
||||
| 003 | 2026-04-20 | Skills, Methods | ✅ Deployed |
|
||||
| 004 | 2026-04-21 | Training Types | ✅ Deployed |
|
||||
| 005 | 2026-04-21 | Exercises (Basis) | ✅ Deployed |
|
||||
| 006 | 2026-04-21 | Training Planning | ✅ Deployed |
|
||||
| 007 | 2026-04-22 | Exercise Catalogs (idempotent) | ✅ Deployed |
|
||||
| 008 | 2026-04-23 | M:N Exercise Relations + Hierarchical Catalogs | ✅ Erstellt, Test ausstehend |
|
||||
|
||||
---
|
||||
|
||||
## Kern-Entitäten (Stand Migration 007)
|
||||
|
||||
### Auth & Profile
|
||||
|
||||
```sql
|
||||
profiles (id, name, email, password_hash, role, created_at)
|
||||
sessions (id, profile_id, token, created_at, expires_at)
|
||||
```
|
||||
|
||||
### Organisation
|
||||
|
||||
```sql
|
||||
clubs (id, name, abbreviation, description, logo_url, ...)
|
||||
divisions (id, club_id, name, description, ...)
|
||||
training_groups (id, division_id, name, description, focus_area, level, ...)
|
||||
```
|
||||
|
||||
### Kataloge (Einfache FK-Struktur - zu refactoren)
|
||||
|
||||
**ACHTUNG:** Aktuell 1:1 Beziehungen - fachlich inkorrekt!
|
||||
|
||||
```sql
|
||||
focus_areas (id, name, abbreviation, description, color, icon, ...)
|
||||
training_styles (id, name, abbreviation, description, parent_style_id, ...)
|
||||
training_characters (id, name, description, ...)
|
||||
skill_categories (id, name, description, parent_category_id, ...)
|
||||
```
|
||||
|
||||
### Übungen (Aktuell - zu refactoren)
|
||||
|
||||
```sql
|
||||
exercises (
|
||||
id, title, summary, goal, execution, preparation, trainer_notes,
|
||||
focus_area_id, -- ❌ 1:1 - sollte M:N sein
|
||||
training_style_id, -- ❌ 1:1 - sollte M:N sein
|
||||
training_character_id, -- ❌ 1:1 - sollte M:N sein
|
||||
visibility, status, created_by, club_id, ...
|
||||
)
|
||||
|
||||
exercise_skills (exercise_id, skill_id, is_primary, intensity, ...)
|
||||
exercise_variants (id, exercise_id, name, description, ...)
|
||||
exercise_media (id, exercise_id, type, url, title, description, ...)
|
||||
```
|
||||
|
||||
### Training Planning
|
||||
|
||||
```sql
|
||||
training_units (id, group_id, date, title, description, ...)
|
||||
training_unit_exercises (training_unit_id, exercise_id, sort_order, ...)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Geplante Änderungen (Migration 008)
|
||||
|
||||
### Hierarchische Katalog-Struktur
|
||||
|
||||
**Fachliche Anforderung (§8.1 Anforderungsdokument):**
|
||||
```
|
||||
Fokusbereich → Stil → Zielgruppe
|
||||
```
|
||||
|
||||
**Neue Struktur:**
|
||||
|
||||
```sql
|
||||
-- Level 1: Fokusbereiche (Root)
|
||||
focus_areas (
|
||||
id SERIAL PRIMARY KEY,
|
||||
name VARCHAR(100) UNIQUE NOT NULL,
|
||||
abbreviation VARCHAR(20),
|
||||
description TEXT,
|
||||
color VARCHAR(20),
|
||||
icon VARCHAR(50),
|
||||
sort_order INT,
|
||||
status VARCHAR(50) DEFAULT 'active'
|
||||
)
|
||||
|
||||
-- Level 2: Trainingsstile (untergeordnet zu Fokusbereich)
|
||||
training_styles (
|
||||
id SERIAL PRIMARY KEY,
|
||||
focus_area_id INT REFERENCES focus_areas(id), -- Hierarchie-Link
|
||||
name VARCHAR(100) NOT NULL,
|
||||
abbreviation VARCHAR(20),
|
||||
description TEXT,
|
||||
parent_style_id INT REFERENCES training_styles(id), -- Optional: Sub-Stile
|
||||
sort_order INT,
|
||||
status VARCHAR(50) DEFAULT 'active'
|
||||
)
|
||||
|
||||
-- Level 3: Zielgruppen (untergeordnet zu Stil)
|
||||
target_groups (
|
||||
id SERIAL PRIMARY KEY,
|
||||
training_style_id INT REFERENCES training_styles(id), -- Hierarchie-Link
|
||||
name VARCHAR(100) NOT NULL,
|
||||
description TEXT,
|
||||
min_age INT,
|
||||
max_age INT,
|
||||
sort_order INT,
|
||||
status VARCHAR(50) DEFAULT 'active'
|
||||
)
|
||||
```
|
||||
|
||||
### M:N Übungs-Zuordnungen
|
||||
|
||||
```sql
|
||||
-- Übung → Fokusbereiche (M:N)
|
||||
exercise_focus_areas (
|
||||
id SERIAL PRIMARY KEY,
|
||||
exercise_id INT REFERENCES exercises(id) ON DELETE CASCADE,
|
||||
focus_area_id INT REFERENCES focus_areas(id),
|
||||
is_primary BOOLEAN DEFAULT false,
|
||||
UNIQUE(exercise_id, focus_area_id)
|
||||
)
|
||||
|
||||
-- Übung → Stile (M:N)
|
||||
exercise_styles (
|
||||
id SERIAL PRIMARY KEY,
|
||||
exercise_id INT REFERENCES exercises(id) ON DELETE CASCADE,
|
||||
training_style_id INT REFERENCES training_styles(id),
|
||||
is_primary BOOLEAN DEFAULT false,
|
||||
UNIQUE(exercise_id, training_style_id)
|
||||
)
|
||||
|
||||
-- Übung → Zielgruppen (M:N)
|
||||
exercise_target_groups (
|
||||
id SERIAL PRIMARY KEY,
|
||||
exercise_id INT REFERENCES exercises(id) ON DELETE CASCADE,
|
||||
target_group_id INT REFERENCES target_groups(id),
|
||||
is_primary BOOLEAN DEFAULT false,
|
||||
UNIQUE(exercise_id, target_group_id)
|
||||
)
|
||||
|
||||
-- Übung → Altersgruppen (M:N - separate Dimension)
|
||||
exercise_age_groups (
|
||||
id SERIAL PRIMARY KEY,
|
||||
exercise_id INT REFERENCES exercises(id) ON DELETE CASCADE,
|
||||
age_group VARCHAR(50) NOT NULL, -- 'Minis', 'Kinder', 'Schüler', 'Teenager', 'Erwachsene'
|
||||
UNIQUE(exercise_id, age_group)
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Kaskadier-Regeln (Geplant)
|
||||
|
||||
### Fokusbereich löschen
|
||||
|
||||
**Verhalten:** `RESTRICT` (Löschen verweigern wenn verwendet)
|
||||
|
||||
**Alternativen:**
|
||||
1. **Umrouten:** Alle abhängigen Stile auf neuen Fokusbereich setzen
|
||||
2. **Archivieren:** Status auf 'archived' setzen statt löschen
|
||||
|
||||
**Implementierung:**
|
||||
```sql
|
||||
-- Prüfung vor Löschen:
|
||||
SELECT COUNT(*) FROM training_styles WHERE focus_area_id = :id
|
||||
SELECT COUNT(*) FROM exercise_focus_areas WHERE focus_area_id = :id
|
||||
|
||||
-- Falls verwendet → HTTP 409 Conflict mit Hinweis
|
||||
```
|
||||
|
||||
### Stil löschen
|
||||
|
||||
**Verhalten:** `RESTRICT` wenn Zielgruppen oder Übungen zugeordnet
|
||||
|
||||
**Umrouten-Workflow:**
|
||||
1. Admin wählt Ziel-Stil
|
||||
2. System aktualisiert:
|
||||
- `target_groups.training_style_id`
|
||||
- `exercise_styles.training_style_id`
|
||||
3. Löschen erlaubt
|
||||
|
||||
### Zielgruppe löschen
|
||||
|
||||
**Verhalten:** `SET NULL` oder `CASCADE` in `exercise_target_groups`
|
||||
|
||||
---
|
||||
|
||||
## Indizes (Geplant)
|
||||
|
||||
```sql
|
||||
-- Hierarchie-Lookups
|
||||
CREATE INDEX idx_training_styles_focus_area ON training_styles(focus_area_id);
|
||||
CREATE INDEX idx_target_groups_style ON target_groups(training_style_id);
|
||||
|
||||
-- M:N Joins
|
||||
CREATE INDEX idx_exercise_focus_areas_exercise ON exercise_focus_areas(exercise_id);
|
||||
CREATE INDEX idx_exercise_focus_areas_focus ON exercise_focus_areas(focus_area_id);
|
||||
CREATE INDEX idx_exercise_styles_exercise ON exercise_styles(exercise_id);
|
||||
CREATE INDEX idx_exercise_styles_style ON exercise_styles(training_style_id);
|
||||
CREATE INDEX idx_exercise_target_groups_exercise ON exercise_target_groups(exercise_id);
|
||||
CREATE INDEX idx_exercise_target_groups_target ON exercise_target_groups(target_group_id);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Deprecation-Pfad (Migration 008)
|
||||
|
||||
**Legacy-Spalten in `exercises`:**
|
||||
```sql
|
||||
-- Werden in Migration 008 deprecated:
|
||||
exercises.focus_area_id → Migriert zu exercise_focus_areas (is_primary=true)
|
||||
exercises.training_style_id → Migriert zu exercise_styles (is_primary=true)
|
||||
exercises.training_character_id → Bleibt vorerst (separate Dimension)
|
||||
|
||||
-- Spalten bleiben zur Rückwärtskompatibilität, aber:
|
||||
-- 1. Daten migriert
|
||||
-- 2. Neue Übungen nutzen M:N-Tabellen
|
||||
-- 3. API liefert enriched data aus M:N
|
||||
-- 4. Später: Spalten droppen nach Stabilisierung
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Nächste Schritte
|
||||
|
||||
- [ ] Migration 008 Entwurf erstellen
|
||||
- [ ] Migrations-Strategie für Altdaten dokumentieren
|
||||
- [ ] Admin-UI für Baum-Verwaltung planen
|
||||
- [ ] API-Endpoints für hierarchische Navigation
|
||||
- [ ] Cascade-Logik implementieren
|
||||
- [ ] Tests für Löschen/Umrouten
|
||||
|
||||
---
|
||||
|
||||
**Letzte Aktualisierung:** 2026-04-23
|
||||
**Verantwortlich:** Claude Code
|
||||
**Review:** Pending
|
||||
165
backend/migrations/008_mn_exercise_relations.sql
Normal file
165
backend/migrations/008_mn_exercise_relations.sql
Normal file
|
|
@ -0,0 +1,165 @@
|
|||
-- Migration 008: M:N Exercise Relations + Hierarchical Catalogs
|
||||
-- Erstellt: 2026-04-23
|
||||
-- Beschreibung: Umstellung von 1:1 auf M:N Beziehungen + Hierarchie Fokusbereich → Stil → Zielgruppe
|
||||
|
||||
-- ============================================================================
|
||||
-- PHASE 1: Hierarchische Struktur aufbauen
|
||||
-- ============================================================================
|
||||
|
||||
-- training_styles erweitern: Hierarchie zu focus_areas
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.columns
|
||||
WHERE table_name='training_styles' AND column_name='focus_area_id') THEN
|
||||
ALTER TABLE training_styles
|
||||
ADD COLUMN focus_area_id INT REFERENCES focus_areas(id);
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_training_styles_focus_area ON training_styles(focus_area_id);
|
||||
|
||||
-- Zielgruppen-Tabelle (NEU)
|
||||
CREATE TABLE IF NOT EXISTS target_groups (
|
||||
id SERIAL PRIMARY KEY,
|
||||
training_style_id INT REFERENCES training_styles(id), -- Hierarchie-Link
|
||||
name VARCHAR(100) NOT NULL,
|
||||
description TEXT,
|
||||
min_age INT,
|
||||
max_age INT,
|
||||
sort_order INT,
|
||||
status VARCHAR(50) DEFAULT 'active',
|
||||
created_at TIMESTAMP DEFAULT NOW(),
|
||||
updated_at TIMESTAMP DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_target_groups_style ON target_groups(training_style_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_target_groups_status ON target_groups(status);
|
||||
|
||||
-- ============================================================================
|
||||
-- PHASE 2: M:N Zuordnungstabellen
|
||||
-- ============================================================================
|
||||
|
||||
-- Übung ↔ Fokusbereiche (M:N)
|
||||
CREATE TABLE IF NOT EXISTS exercise_focus_areas (
|
||||
id SERIAL PRIMARY KEY,
|
||||
exercise_id INT REFERENCES exercises(id) ON DELETE CASCADE,
|
||||
focus_area_id INT REFERENCES focus_areas(id),
|
||||
is_primary BOOLEAN DEFAULT false,
|
||||
created_at TIMESTAMP DEFAULT NOW(),
|
||||
UNIQUE(exercise_id, focus_area_id)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_exercise_focus_areas_exercise ON exercise_focus_areas(exercise_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_exercise_focus_areas_focus ON exercise_focus_areas(focus_area_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_exercise_focus_areas_primary ON exercise_focus_areas(is_primary);
|
||||
|
||||
-- Übung ↔ Stile (M:N)
|
||||
CREATE TABLE IF NOT EXISTS exercise_styles (
|
||||
id SERIAL PRIMARY KEY,
|
||||
exercise_id INT REFERENCES exercises(id) ON DELETE CASCADE,
|
||||
training_style_id INT REFERENCES training_styles(id),
|
||||
is_primary BOOLEAN DEFAULT false,
|
||||
created_at TIMESTAMP DEFAULT NOW(),
|
||||
UNIQUE(exercise_id, training_style_id)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_exercise_styles_exercise ON exercise_styles(exercise_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_exercise_styles_style ON exercise_styles(training_style_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_exercise_styles_primary ON exercise_styles(is_primary);
|
||||
|
||||
-- Übung ↔ Zielgruppen (M:N)
|
||||
CREATE TABLE IF NOT EXISTS exercise_target_groups (
|
||||
id SERIAL PRIMARY KEY,
|
||||
exercise_id INT REFERENCES exercises(id) ON DELETE CASCADE,
|
||||
target_group_id INT REFERENCES target_groups(id),
|
||||
is_primary BOOLEAN DEFAULT false,
|
||||
created_at TIMESTAMP DEFAULT NOW(),
|
||||
UNIQUE(exercise_id, target_group_id)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_exercise_target_groups_exercise ON exercise_target_groups(exercise_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_exercise_target_groups_target ON exercise_target_groups(target_group_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_exercise_target_groups_primary ON exercise_target_groups(is_primary);
|
||||
|
||||
-- Übung ↔ Altersgruppen (M:N - separate Dimension)
|
||||
CREATE TABLE IF NOT EXISTS exercise_age_groups (
|
||||
id SERIAL PRIMARY KEY,
|
||||
exercise_id INT REFERENCES exercises(id) ON DELETE CASCADE,
|
||||
age_group VARCHAR(50) NOT NULL, -- 'Minis', 'Kinder', 'Schüler', 'Teenager', 'Erwachsene'
|
||||
created_at TIMESTAMP DEFAULT NOW(),
|
||||
UNIQUE(exercise_id, age_group),
|
||||
CHECK (age_group IN ('Minis', 'Kinder', 'Schüler', 'Teenager', 'Erwachsene'))
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_exercise_age_groups_exercise ON exercise_age_groups(exercise_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_exercise_age_groups_age ON exercise_age_groups(age_group);
|
||||
|
||||
-- ============================================================================
|
||||
-- PHASE 3: Daten-Migration (Altdaten von 1:1 zu M:N)
|
||||
-- ============================================================================
|
||||
|
||||
-- Migriere focus_area_id → exercise_focus_areas (als primäre Zuordnung)
|
||||
INSERT INTO exercise_focus_areas (exercise_id, focus_area_id, is_primary)
|
||||
SELECT id, focus_area_id, true
|
||||
FROM exercises
|
||||
WHERE focus_area_id IS NOT NULL
|
||||
ON CONFLICT (exercise_id, focus_area_id) DO NOTHING;
|
||||
|
||||
-- Migriere training_style_id → exercise_styles (als primäre Zuordnung)
|
||||
INSERT INTO exercise_styles (exercise_id, training_style_id, is_primary)
|
||||
SELECT id, training_style_id, true
|
||||
FROM exercises
|
||||
WHERE training_style_id IS NOT NULL
|
||||
ON CONFLICT (exercise_id, training_style_id) DO NOTHING;
|
||||
|
||||
-- ============================================================================
|
||||
-- PHASE 4: Basis-Daten für Zielgruppen
|
||||
-- ============================================================================
|
||||
|
||||
-- Beispiel-Zielgruppen für Shotokan (training_style_id = 1, falls vorhanden)
|
||||
DO $$
|
||||
DECLARE
|
||||
shotokan_id INT;
|
||||
BEGIN
|
||||
SELECT id INTO shotokan_id FROM training_styles WHERE name = 'Shotokan' LIMIT 1;
|
||||
|
||||
IF shotokan_id IS NOT NULL THEN
|
||||
INSERT INTO target_groups (training_style_id, name, description, sort_order) VALUES
|
||||
(shotokan_id, 'Breitensportler', 'Karate für Freizeit und Fitness', 1),
|
||||
(shotokan_id, 'Leistungssportler', 'Wettkampforientiertes Training', 2),
|
||||
(shotokan_id, 'Kinder', 'Karate für Kinder (6-12 Jahre)', 3)
|
||||
ON CONFLICT DO NOTHING;
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- ============================================================================
|
||||
-- PHASE 5: Hierarchie-Daten (Stile zu Fokusbereichen zuordnen)
|
||||
-- ============================================================================
|
||||
|
||||
-- Ordne Karate-Stile dem Fokusbereich "Karate" zu
|
||||
DO $$
|
||||
DECLARE
|
||||
karate_id INT;
|
||||
BEGIN
|
||||
SELECT id INTO karate_id FROM focus_areas WHERE name = 'Karate' LIMIT 1;
|
||||
|
||||
IF karate_id IS NOT NULL THEN
|
||||
UPDATE training_styles
|
||||
SET focus_area_id = karate_id
|
||||
WHERE name IN ('Shotokan', 'Goju-Ryu', 'Wado-Ryu', 'Shito-Ryu', 'Kyokushin')
|
||||
AND focus_area_id IS NULL;
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- ============================================================================
|
||||
-- HINWEISE für spätere Migrationen
|
||||
-- ============================================================================
|
||||
|
||||
-- Die folgenden Spalten in exercises können später gedroppt werden:
|
||||
-- - focus_area_id (deprecated, durch exercise_focus_areas ersetzt)
|
||||
-- - training_style_id (deprecated, durch exercise_styles ersetzt)
|
||||
-- - training_character_id (bleibt vorerst, separate Dimension)
|
||||
|
||||
-- Aktuell bleiben sie zur Rückwärtskompatibilität erhalten.
|
||||
-- Neue Übungen sollten NUR über M:N-Tabellen zugeordnet werden.
|
||||
-- API-Endpoints sollten enriched data aus M:N-Tabellen liefern.
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
# Shinkan Jinkendo Version Information
|
||||
|
||||
APP_VERSION = "0.2.0"
|
||||
APP_VERSION = "0.3.0"
|
||||
BUILD_DATE = "2026-04-23"
|
||||
DB_SCHEMA_VERSION = "20260422"
|
||||
DB_SCHEMA_VERSION = "20260423"
|
||||
|
||||
MODULE_VERSIONS = {
|
||||
"auth": "1.0.0",
|
||||
|
|
@ -11,17 +11,30 @@ MODULE_VERSIONS = {
|
|||
"groups": "0.1.0",
|
||||
"skills": "0.1.0",
|
||||
"methods": "0.1.0",
|
||||
"exercises": "0.2.0", # Updated: Katalog-Integration
|
||||
"exercises": "0.3.0", # Updated: M:N Beziehungen
|
||||
"training_units": "0.1.0",
|
||||
"training_programs": "0.1.0",
|
||||
"planning": "0.1.0",
|
||||
"import_wiki": "0.1.0",
|
||||
"admin": "1.0.0",
|
||||
"membership": "1.0.0",
|
||||
"catalogs": "1.0.0", # NEW: Admin-Kataloge
|
||||
"catalogs": "1.1.0", # Updated: Zielgruppen + Hierarchie
|
||||
}
|
||||
|
||||
CHANGELOG = [
|
||||
{
|
||||
"version": "0.3.0",
|
||||
"date": "2026-04-23",
|
||||
"changes": [
|
||||
"BREAKING: M:N Beziehungen für Übungen (statt 1:1)",
|
||||
"Migration 008: M:N Zuordnungstabellen (exercise_focus_areas, exercise_styles, exercise_target_groups, exercise_age_groups)",
|
||||
"Feature: Hierarchische Katalog-Struktur (Fokusbereich → Stil → Zielgruppe)",
|
||||
"Feature: Zielgruppen-Verwaltung (training_styles.focus_area_id Hierarchie)",
|
||||
"Feature: Primary/Secondary Assignments (is_primary Flag)",
|
||||
"Doku: DATABASE_SCHEMA.md + DOMAIN_MODEL.md kontinuierlich gepflegt",
|
||||
"Architecture: Smart Cascade-Logik (RESTRICT, Rerouting, Move)",
|
||||
]
|
||||
},
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"date": "2026-04-23",
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
// Shinkan Jinkendo Frontend Version
|
||||
|
||||
export const APP_VERSION = "0.2.0"
|
||||
export const APP_VERSION = "0.3.0"
|
||||
export const BUILD_DATE = "2026-04-23"
|
||||
|
||||
export const PAGE_VERSIONS = {
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user