Implement Phase 3 Enhancements for Skill Scoring and Profiles
Some checks failed
Deploy Development / deploy (push) Failing after 22s
Test Suite / pytest-backend (push) Successful in 35s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Failing after 3s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m14s
Some checks failed
Deploy Development / deploy (push) Failing after 22s
Test Suite / pytest-backend (push) Successful in 35s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Failing after 3s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m14s
- Added capabilities for weighted skill profiles, allowing trainers to compare training modules, frameworks, and regression paths based on skill contributions. - Updated the skill scoring specification to include peer context separation and list filtering, ensuring accurate comparisons among visible artifacts of the same type. - Enhanced the API to support batch summaries for skill profiles and discovery suggestions, improving data retrieval efficiency. - Refactored frontend components to display skill metrics, including scores and peer percentages, with improved filtering options for better user experience. - Updated documentation to reflect the latest changes and enhancements in the skill scoring system.
This commit is contained in:
parent
a7a428745f
commit
1d698e4b0a
|
|
@ -104,6 +104,7 @@ Die exakten Zahlen hängen von der Umgebung ab (siehe Admin/DB). Die Skills/Übu
|
|||
**Skills-System:**
|
||||
|
||||
- [x] Hierarchisches Schema, Fokusbereich-Zuordnung, Exercise-Skill mit Levels
|
||||
- [x] **Gewichtetes Fähigkeiten-Profil (Phase 3):** Module, Rahmenprogramme, Regressionspfade; Peer-Kontext getrennt; Listen-Filter + Discovery — **`technical/SKILL_SCORING_SPEC.md`**
|
||||
|
||||
**Admin-UI:**
|
||||
|
||||
|
|
|
|||
|
|
@ -407,10 +407,9 @@ skill_level_definitions (
|
|||
- Reaktion (Koordination, target_level: 2, intensity: mittel)
|
||||
|
||||
**Attribute pro Fähigkeitsbezug:**
|
||||
- is_primary (Haupt- oder Nebenfähigkeit)
|
||||
- intensity (niedrig/mittel/hoch)
|
||||
- required_level (Voraussetzung, 1-5)
|
||||
- target_level (Ziel-Level, 1-5)
|
||||
- `intensity` — Nutzeneinschätzung: **niedrig | mittel | hoch** (Standard **mittel**)
|
||||
- `required_level` / `target_level` — Stufen-Spanne (kanonische Slugs basis … optimierung)
|
||||
- `is_primary` — Legacy-Feld; **nicht mehr in der UI**, beim Speichern immer false; Scoring ignoriert es
|
||||
|
||||
**🆕 Fokusbereich-Filterung:**
|
||||
- Bei Übungen mit Fokusbereich "Karate" sollten primär KARATE-Fähigkeiten zugeordnet werden
|
||||
|
|
@ -474,6 +473,34 @@ 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**).
|
||||
|
||||
### Trainingsmodul (Bibliothek)
|
||||
|
||||
**Abgrenzung:** Wiederverwendbare **Übungsfolge** (`training_modules` + `training_module_items`) — kein Kalendertermin, kein Rahmen-Slot. Übernahme in geplante Einheiten über Planung (`apply-training-module`).
|
||||
|
||||
**Governance:** wie andere Bibliotheksartefakte (`visibility`, `club_id`, `library_content_visibility_sql`).
|
||||
|
||||
### Gewichtetes Fähigkeiten-Profil (Planungs-Bausteine, Phase 3)
|
||||
|
||||
**Zweck:** Aus den verknüpften Übungen eines Planungsartefakts wird ein **Fähigkeiten-Profil** berechnet (Trainingsgewicht je Fähigkeit). Trainer vergleichen Bausteine **innerhalb desselben Typs**, um z. B. das passendste Modul für eine Ziel-Fähigkeit zu finden.
|
||||
|
||||
**Artefakttypen (getrennte Peer-Kontexte):**
|
||||
|
||||
| Typ | Vergleich |
|
||||
|-----|-----------|
|
||||
| `training_module` | nur sichtbare **Module** |
|
||||
| `framework_program` | nur sichtbare **Rahmenprogramme** |
|
||||
| `progression_graph` | nur sichtbare **Regressionspfade** |
|
||||
|
||||
**Metriken (Nutzer):**
|
||||
|
||||
- **Score / Gewicht** — absolut (Dauer × Häufigkeit × Intensität × Stufen-Spanne)
|
||||
- **Prozent** — Anteil am stärksten sichtbaren Peer **desselben Typs** für diese Fähigkeit (max. 100 %)
|
||||
- **★** — stärkster Peer in diesem Kontext
|
||||
|
||||
**UI:** Profile in Editoren; KPI-Kacheln und Filter in Listen (`/planning/framework-programs`, `/planning/training-modules`); Discovery auf der Fähigkeiten-Seite.
|
||||
|
||||
**Technik:** `backend/skill_scoring.py`, `routers/skill_profiles.py` — Spec **`technical/SKILL_SCORING_SPEC.md`**.
|
||||
|
||||
### 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 **Rahmenprogramm‑Slot** (Serien‑Session über Wochen): Slots strukturieren **mehrere Einheiten** in einem Programm; **Streams** strukturieren **gleichzeitige** Abläufe **innerhalb einer** Einheit.
|
||||
|
|
@ -648,12 +675,13 @@ skill_level_definitions (
|
|||
- [ ] Level-Definitionen aus Fähigkeitsmatrix extrahieren (optional)
|
||||
- [ ] Skills-Beschreibungen aus Wiki importieren (Migration 024)
|
||||
- [ ] Admin-UI für Fähigkeiten-Kategorien (CRUD)
|
||||
- [ ] Skill-Filter in Übungssuche integrieren
|
||||
- [x] Skill-Filter in Übungssuche (SkillTreeMultiSelect + Stufen)
|
||||
- [x] Gewichtetes Fähigkeiten-Profil für Planungs-Bausteine (Module, Rahmen, Pfade) — siehe `technical/SKILL_SCORING_SPEC.md`
|
||||
- [ ] Reifegradmodelle definieren (Kombination Fokusbereich + Stil + Zielgruppe)
|
||||
- [ ] KI-Unterstützung für Trainingsplanung (basierend auf Fähigkeiten-Level)
|
||||
|
||||
---
|
||||
|
||||
**Letzte Aktualisierung:** 2026-04-27
|
||||
**Letzte Aktualisierung:** 2026-05-20
|
||||
**Verantwortlich:** Claude Code
|
||||
**Review:** Pending
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# Gelieferte Features & technische Basis (Q2 2026)
|
||||
|
||||
**Stand:** 2026-05-12
|
||||
**Stand:** 2026-05-20
|
||||
**Referenz:** `backend/version.py` — aktuelle **APP_VERSION** / **DB_SCHEMA_VERSION** (Stand Code u. a. **0.8.96**)
|
||||
|
||||
Dieses Dokument bündelt die in der Entwicklungsphase erreichten **lieferbaren** Funktionen und die zugehörigen **technischen Artefakte**. Trainingsrahmen‑Bibliothek + Slot‑Blueprint: **`technical/TRAINING_FRAMEWORK_SPEC.md`** §2. **Progressionsgraph zwischen Übungen** (Zwischenstand, Grenzen): **§§3–4**. **Medien-Archiv & Bibliothek:** Abschnitt **12** unten + **`MEDIA_ASSETS_AND_ARCHIVE_SPEC.md`**. Detail-Spezifikationen bleiben in den verlinkten Pfaden unter `.claude/docs/technical/` und `.claude/docs/functional/`.
|
||||
|
|
@ -169,11 +169,35 @@ Einzelnorm: **`technical/MEDIA_ASSETS_AND_ARCHIVE_SPEC.md`**. Kurzüberblick gel
|
|||
|
||||
---
|
||||
|
||||
## 15. Verweise
|
||||
## 15. Gewichtetes Fähigkeiten-Scoring (Phase 3, Stand 2026-05-20)
|
||||
|
||||
Norm: **`technical/SKILL_SCORING_SPEC.md`**.
|
||||
|
||||
### 15.1 Backend
|
||||
|
||||
- **`skill_scoring.py`:** Gewichtung (Dauer × Vorkommen × Intensität × Stufen); `compute_planning_corpus_by_type()` mit getrennten Corpora; `universal_percent` capped auf 100 %
|
||||
- **`routers/skill_profiles.py`:** Profile-GET pro Artefakt; `POST /api/skill-profiles/batch-summaries`; `GET /api/skill-discovery/suggestions`
|
||||
- Sichtbarkeit: **`library_content_visibility_sql`** (Planungs-Bibliothek, nicht „nur Verein club“)
|
||||
|
||||
### 15.2 Frontend
|
||||
|
||||
- **Listen:** Rahmenprogramme + Trainingsmodule — Filter-Modal (wie Übungen), Chips, `SkillTreeMultiSelect` (Portal-Dropdown)
|
||||
- **KPI:** `SkillProfileCompact` — Top je Unterkategorie, Score + Peer-%
|
||||
- **Editoren + Modal:** `SkillProfilePanel`, `SkillProfileFullModal`
|
||||
- **Discovery:** `SkillDiscoveryPanel` auf Fähigkeiten-Seite
|
||||
|
||||
### 15.3 Offen
|
||||
|
||||
- Corpus-Caching; pytest für Typ-Trennung; Filter-Persistenz; Skill-Filter Import-Dialog „Rahmen übernehmen“
|
||||
|
||||
---
|
||||
|
||||
## 16. Verweise
|
||||
|
||||
| Thema | Dokument |
|
||||
|--------|----------|
|
||||
| Rahmenprogramm / Progressionsgraph | `technical/TRAINING_FRAMEWORK_SPEC.md` |
|
||||
| Fähigkeiten-Scoring Planung | `technical/SKILL_SCORING_SPEC.md` |
|
||||
| API Übungen | `technical/EXERCISES_API_SPEC.md` |
|
||||
| Domänenmodell | `functional/DOMAIN_MODEL.md` |
|
||||
| Datenbank Überblick | `technical/DATABASE_SCHEMA.md` |
|
||||
|
|
|
|||
|
|
@ -1,12 +1,33 @@
|
|||
# Gewichtetes Fähigkeiten-Scoring (Phase 3)
|
||||
|
||||
**Stand:** 2026-05-20
|
||||
**Status:** Variante A (regelbasiert) umgesetzt — **v1.2** (Kategorien-Gruppierung + universelle Skala)
|
||||
**Modul:** `backend/skill_scoring.py`, Router `skill_profiles`
|
||||
**Status:** Variante A (regelbasiert) umgesetzt — **v1.3** (Peer-Kontext getrennt + Listen-Filter)
|
||||
**Modul:** `backend/skill_scoring.py`, Router `backend/routers/skill_profiles.py`
|
||||
|
||||
## Ziel
|
||||
|
||||
Trainer wählen **Schwerpunkt-Fähigkeiten** und erhalten Vorschläge für **Rahmenprogramme**, **Trainingsmodule** und **Regressionspfade** (Progressionsgraphen), deren Übungen diese Fähigkeiten stark abdecken.
|
||||
Trainer wählen **Schwerpunkt-Fähigkeiten** und finden passende **Bausteine** für die Trainingsplanung:
|
||||
|
||||
- **Trainingsmodule** — wiederverwendbare Übungsfolgen
|
||||
- **Rahmenprogramme** — Programme mit Zielen und Session-Slots
|
||||
- **Regressionspfade** (Progressionsgraphen) — Übungsketten
|
||||
|
||||
Das Scoring beantwortet: *Wie stark trainiert dieser Baustein eine Fähigkeit?* und *Wie stark ist er im Vergleich zu anderen **sichtbaren** Bausteinen **desselben Typs**?*
|
||||
|
||||
## Fachliche Kernregel: Peer-Kontext (nicht vermischen)
|
||||
|
||||
| Planungs-Artefakt | Vergleichsgruppe (`universal_percent`) |
|
||||
|-------------------|----------------------------------------|
|
||||
| Trainingsmodul | nur andere **sichtbare Module** |
|
||||
| Rahmenprogramm | nur andere **sichtbare Rahmenprogramme** |
|
||||
| Regressionspfad | nur andere **sichtbare Pfade** |
|
||||
|
||||
**Nicht** verglichen werden:
|
||||
|
||||
- Module vs. Rahmenprogramme vs. Pfade (kein Mix)
|
||||
- Artefakte anderer Vereine, auf die der Nutzer keinen Planungszugriff hat
|
||||
|
||||
**Sichtbarkeit:** `library_content_visibility_sql` — private, vereinsinterne und offizielle Inhalte gemäß Mandant/Rolle, analog zu anderen Bibliothekslisten.
|
||||
|
||||
## Datenquellen
|
||||
|
||||
|
|
@ -19,7 +40,7 @@ Trainer wählen **Schwerpunkt-Fähigkeiten** und erhalten Vorschläge für **Rah
|
|||
|
||||
Fähigkeiten je Übung: `exercise_skills` → `skills` (nur `status = active`).
|
||||
|
||||
## Gewichtungsformel (v1.1)
|
||||
## Gewichtungsformel (v1.1 / v1.2)
|
||||
|
||||
Pro **Übungsvorkommen** (eine Zeile im Ablauf / Modul / Kanten-Endpunkt):
|
||||
|
||||
|
|
@ -44,8 +65,6 @@ Kanonische Slugs: basis … optimierung (1–5). Fehlen beide: Faktor 1,0.
|
|||
- **Mittelpunkt** = durchschnittliche Stufe
|
||||
- Faktor ≈ `(0,92 + 0,04 × Spanne) × (0,95 + 0,025 × Mittelpunkt)` → typisch 0,96–1,20
|
||||
|
||||
Beispiel: von Grundlagen bis Aufbau (Spanne 2) > nur Basis (Spanne 1).
|
||||
|
||||
### Bewusst nicht im Scoring
|
||||
|
||||
| Feld | Grund |
|
||||
|
|
@ -53,44 +72,113 @@ Beispiel: von Grundlagen bis Aufbau (Spanne 2) > nur Basis (Spanne 1).
|
|||
| `is_primary` | Perspektivabhängig; bleibt in Übungs-UI, fließt nicht ins Profil ein |
|
||||
| `development_contribution` | Legacy-DB-Feld, in UI nicht gepflegt |
|
||||
|
||||
Aggregation:
|
||||
## Aggregierte Metriken
|
||||
|
||||
- Summe pro `skill_id` → `weight` / `score` (Trainingsgewicht in gewichteten Minuten — **absolut, über Programme vergleichbar**)
|
||||
- `artifact_share_percent` / `share_percent` = Anteil an `total_weight` **innerhalb dieses Artefakts** (summiert 100 % — nur noch sekundär)
|
||||
- `by_main_category[]` → je Unterkategorie `top_skill` (stärkste Fähigkeit nach absolutem Gewicht)
|
||||
- `universal_percent` = `weight / max_weight_in_corpus(skill_id) × 100` — Referenz aus **Vereins-Artefakten** (`visibility=club`, aktiver Verein): Rahmenprogramme, Module, Regressionspfade
|
||||
- `club_best` / `club_best_by_skill`: stärkstes Vereins-Element je Fähigkeit (Titel, Typ, Gewicht)
|
||||
- Listen: `POST /api/skill-profiles/batch-summaries` — ein Corpus-Durchlauf, kompakte Profile für viele IDs
|
||||
| Feld | Bedeutung |
|
||||
|------|-----------|
|
||||
| `weight` / `score` | Absolutes **Trainingsgewicht** (gewichtete Minuten) — über alle Fähigkeiten eines Artefakts vergleichbar |
|
||||
| `share_percent` | Anteil am `total_weight` **innerhalb dieses Artefakts** (summiert 100 %) — sekundär |
|
||||
| `by_main_category[]` | Je Unterkategorie `top_skill` (stärkste Fähigkeit nach Gewicht) |
|
||||
| `universal_percent` | Anteil am **Maximum derselben Fähigkeit im Peer-Kontext** (max. 100 %) |
|
||||
| `is_club_best_for_skill` | Stärkster sichtbarer Peer für diese Fähigkeit (★ in UI) |
|
||||
| `club_best` | Referenz-Peer (Titel, Typ, Gewicht) — **Legacy-Name**, fachlich Peer-Best |
|
||||
|
||||
Discovery-Sortierung und Vorschläge nutzen **`match_score`** (= Summe absoluter Gewichte der gewählten Fähigkeiten), nicht den Plan-internen Anteil.
|
||||
### Berechnung `universal_percent`
|
||||
|
||||
```
|
||||
effective_ref = max(max_weight_in_peer_corpus(skill_id), eigenes_gewicht)
|
||||
universal_percent = min(100, weight / effective_ref × 100)
|
||||
```
|
||||
|
||||
Corpus je Typ: `compute_planning_corpus_by_type()` scannt sichtbare Artefakte getrennt nach `framework_program`, `training_module`, `progression_graph`.
|
||||
|
||||
Discovery-Sortierung nutzt **`match_score`** (= Summe absoluter Gewichte der gewählten Fähigkeiten), nicht den Plan-internen Anteil. Discovery verwendet typ-getrennte Referenz (`fw_ref`, `mod_ref`, `graph_ref`).
|
||||
|
||||
## API
|
||||
|
||||
| Methode | Pfad | Beschreibung |
|
||||
|---------|------|--------------|
|
||||
| GET | `/api/training-framework-programs/{id}/skill-profile` | `overall` + `slots[]` mit je `profile` |
|
||||
| GET | `/api/training-modules/{id}/skill-profile` | `overall` |
|
||||
| GET | `/api/exercise-progression-graphs/{id}/skill-profile` | `overall` |
|
||||
| GET | `/api/training-framework-programs/{id}/skill-profile` | `overall` + `slots[]` mit je `profile`; `reference_scale` (Peer-Kontext Rahmenprogramme) |
|
||||
| GET | `/api/training-modules/{id}/skill-profile` | `overall`; `reference_scale` (Peer-Kontext Module) |
|
||||
| GET | `/api/exercise-progression-graphs/{id}/skill-profile` | `overall`; `reference_scale` (Peer-Kontext Pfade) |
|
||||
| POST | `/api/skill-profiles/batch-summaries` | Kompakte Profile für Listen; Body: `frameworkProgramIds`, `trainingModuleIds`, …; Response: `summaries`, `reference_scale_by_type`, `club_best_by_skill` |
|
||||
| GET | `/api/skill-discovery/suggestions?skill_ids=1,2,3` | Ranking sichtbarer Artefakte; Query `types`, `limit` |
|
||||
|
||||
Zugriff: `get_tenant_context` + gleiche Sichtbarkeit wie Parent-Artefakt (`library_content_visibility_sql`).
|
||||
Zugriff: `get_tenant_context` + `library_content_visibility_sql` wie Parent-Artefakt.
|
||||
|
||||
### `reference_scale` / `reference_scale_by_type`
|
||||
|
||||
```json
|
||||
{
|
||||
"scope": "planning_peer",
|
||||
"artifact_type": "training_module",
|
||||
"artifacts_scanned": 12,
|
||||
"skills_in_corpus": 34,
|
||||
"description": "Prozent = Anteil am stärksten sichtbaren Eintrag unter Trainingsmodulen …"
|
||||
}
|
||||
```
|
||||
|
||||
## UI
|
||||
|
||||
- **Rahmenprogramm bearbeiten:** Panel „Fähigkeiten-Schwerpunkte“, inkl. Aufklapp pro Session
|
||||
- **Trainingsmodul bearbeiten:** Panel „Fähigkeiten im Modul“
|
||||
- **Progressionsgraph:** Panel „Fähigkeiten entlang des Pfads“
|
||||
- **Fähigkeiten-Seite → Planungs-Vorschläge:** Multi-Select + Bibliothekssuche
|
||||
### Bearbeitung (Vollprofil)
|
||||
|
||||
Profil wird nach **Speichern** neu geladen (`skillProfileTick`).
|
||||
| Ort | Panel | `artifactType` |
|
||||
|-----|-------|----------------|
|
||||
| Rahmenprogramm bearbeiten | Fähigkeiten-Schwerpunkte (+ Sessions) | `framework_program` |
|
||||
| Trainingsmodul bearbeiten | Fähigkeiten im Modul | `training_module` |
|
||||
| Progressionsgraph | Fähigkeiten entlang des Pfads | `progression_graph` |
|
||||
|
||||
Anzeige: Top je Kategorie (Editor) oder alle Fähigkeiten (Modal). Hinweise nennen Peer-Kontext explizit (z. B. „72 % Rahmenpr.“).
|
||||
|
||||
### Listen & Filter (UX wie Übungsliste)
|
||||
|
||||
| Liste | Filter |
|
||||
|-------|--------|
|
||||
| Rahmenprogramme (`/planning/framework-programs`) | Suche, Katalog (Fokus/Trainingsart/Zielgruppe), Session-Dauer, **Fähigkeiten** (`SkillTreeMultiSelect`), Mindest-% im Peer-Kontext, Sortierung nach Stärke |
|
||||
| Trainingsmodule (`/planning/training-modules`) | Suche, **Fähigkeiten** (+ Min-%, Sortierung) |
|
||||
|
||||
- **Filter-Button** mit Badge, entfernbare **Chips**, Einstellungen im **Modal** (`PlanningArtifactFilterModal`)
|
||||
- KPI-Kacheln: Top-Fähigkeit **je Unterkategorie** mit Score + Peer-%
|
||||
- Vollprofil-Modal: `SkillProfileFullModal` mit `displayMode=full`
|
||||
|
||||
Profil wird nach Speichern neu geladen (`skillProfileTick` in Editoren).
|
||||
|
||||
### Discovery
|
||||
|
||||
**Fähigkeiten-Seite → Planungs-Vorschläge:** Multi-Select + API `/api/skill-discovery/suggestions` (optional Filter `types`).
|
||||
|
||||
## Frontend-Module (Auswahl)
|
||||
|
||||
| Pfad | Rolle |
|
||||
|------|--------|
|
||||
| `frontend/src/components/planning/PlanningArtifactFilterModal.jsx` | Filter-Modal |
|
||||
| `frontend/src/components/planning/PlanningSkillFilterSection.jsx` | Fähigkeiten-Block im Modal |
|
||||
| `frontend/src/utils/planningArtifactFilterChips.js` | Chip-Labels + Entfernen |
|
||||
| `frontend/src/utils/frameworkProgramListHelpers.js` | Client-Filter Rahmenprogramme |
|
||||
| `frontend/src/utils/trainingModuleListHelpers.js` | Client-Filter Module |
|
||||
| `frontend/src/components/skills/SkillProfileCompact.jsx` | KPI-Kacheln in Listen |
|
||||
| `frontend/src/components/SkillTreeMultiSelect.jsx` | Baumauswahl (Portal-Dropdown in Modals) |
|
||||
|
||||
## Grenzen / später
|
||||
|
||||
- Kein Cache in DB (`skill_profile_json`) — on-the-fly; bei Performance >50 Artefakte serverseitiger Index
|
||||
- Kein DB-Cache (`skill_profile_json`) — on-the-fly; bei >50 Artefakten pro Typ serverseitiger Index/Caching
|
||||
- Entwicklungsziele am Rahmenkopf bleiben Freitext (kein Scoring)
|
||||
- KI-Zusammenfassung (Variante B Roadmap) nicht Teil von v1.0
|
||||
- KI-Zusammenfassung (Variante B) nicht Teil von v1.0
|
||||
- Trainings**einheiten** (Kalender) optional als nächste Erweiterung
|
||||
- Filter-Persistenz („Als Standard speichern“) wie bei Übungen — noch nicht für Planungslisten
|
||||
- Fähigkeiten-Filter im Dialog **Planung → Rahmen übernehmen** — Katalog ja, Skill-Filter optional nachziehen
|
||||
- API-Feldnamen `club_*` / `skillMinClubPercent` — technische Altlast, semantisch Peer-Kontext
|
||||
|
||||
## Tests
|
||||
|
||||
- `backend/tests/test_skill_scoring.py` — Multiplikator, Aggregation, Match-Score
|
||||
- `backend/tests/test_skill_scoring.py` — Multiplikator, Aggregation, Match-Score, Cap 100 %
|
||||
- **Offen:** dedizierte Tests für `compute_planning_corpus_by_type` (Typ-Trennung)
|
||||
|
||||
## Verweise
|
||||
|
||||
| Dokument | Inhalt |
|
||||
|----------|--------|
|
||||
| `functional/DOMAIN_MODEL.md` | Domänenabschnitt Planungs-Fähigkeiten-Profil |
|
||||
| `docs/FACHLICHE_NUTZERFUNKTIONEN.md` | Nutzerüberblick Listen/Filter |
|
||||
| `docs/HANDOVER.md` | Handover-Abschnitt Phase 3 |
|
||||
| `technical/ACCESS_LAYER_AND_GOVERNANCE_PLAN.md` | Sichtbarkeit / Mandant |
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@
|
|||
| `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). |
|
||||
| `technical/SKILL_SCORING_SPEC.md` | **Fähigkeiten-Profil** der Rahmen‑Slots / Module / Pfade; Listen-Filter und Peer‑Vergleich (nur gleicher Artefakttyp). |
|
||||
|
||||
**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).
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
**Bezug:** `functional/Shinkan Trainingsmodule Kombinationsuebungen Spezifikation V2.md` (Kopf „V3“, inkl. **§ 10.2.1**, **§ 10.4 Coaching-Stufen**, **Anhang A** Implementierungsabgleich — Drift-Schutz)
|
||||
**Technische Entwurfsspezifikation:** `technical/TRAINING_MODULES_AND_COMBINATION_EXERCISES_SPEC.md`
|
||||
**Stand dieses Dokuments:** 2026-05-12 (Abgleich mit Code App **0.8.110**, siehe `backend/version.py`)
|
||||
**Stand dieses Dokuments:** 2026-05-20 (Abgleich mit Code, siehe `backend/version.py`)
|
||||
|
||||
## Ziele
|
||||
|
||||
|
|
@ -12,7 +12,7 @@ Umsetzung der MVP-Punkte aus der Fachspezifikation ohne die bestehende Planung z
|
|||
|
||||
| Phase | Inhalt | Status |
|
||||
|-------|--------|--------|
|
||||
| **1** | **Trainingsmodule (Bibliothek):** Tabellen `training_modules`, `training_module_items`; REST CRUD mit Governance wie andere Bibliotheken; Übernahme in eine bestehende Einheit per `POST /api/training-units/{id}/apply-training-module` (Anfügen ans Ende eines Abschnitts via `section_order_index`); optionale Lineage-Spalte `source_training_module_id` auf Planungsitems; UI: Liste/Editor unter `/planning/training-modules`, Link von der Planung, Modal „Modul übernehmen“ | **umgesetzt (MVP Schritt 1)** |
|
||||
| **1** | **Trainingsmodule (Bibliothek):** Tabellen `training_modules`, `training_module_items`; REST CRUD mit Governance wie andere Bibliotheken; Übernahme in eine bestehende Einheit per `POST /api/training-units/{id}/apply-training-module` (Anfügen ans Ende eines Abschnitts via `section_order_index`); optionale Lineage-Spalte `source_training_module_id` auf Planungsitems; UI: Liste/Editor unter `/planning/training-modules`, Link von der Planung, Modal „Modul übernehmen“; **Ergänzung 2026-05-20:** Fähigkeiten-Profil + Listen-Filter (Peer-Vergleich nur unter Modulen) — `technical/SKILL_SCORING_SPEC.md` | **umgesetzt (MVP Schritt 1)** |
|
||||
| **2** | Kombinationsübungen: `exercise_kind`/`combination_*`, Slots, Pools, `method_archetype`, `method_profile` (JSON) | **teilweise** — wie links; zusätzlich **057** `planning_method_profile`; Planungs-Merge Client (`effectiveComboMethodProfile`); Archetypen weiterhin **nur Code-Konstanten** (kein Admin) | **Offen:** Archetyp-Admin-UI; Profil↔Archetyp-Validierung Backend; „alle Slots vorbelegen“ / Presets (siehe Fachspez **§ 10.6**); Haupt-/Nebenmethoden an Kombi wo Spec es verlangt |
|
||||
| **3** | Planungsblöcke: Gruppierung, Auflösen, „als Modul speichern“, erweiterter Übernahmemodus (Zwischenposition) | geplant |
|
||||
| **4** | Coaching: Archetyp-Support | **teilweise:** **Stufe A** — Merge Katalog+Planung; `CombinationPlanBracket` in Peek/Run; globale Profilzahlen mit Labels (`describeGlobalComboProfile`); Stations-/Timing-Zusammenfassung inkl. Wdh.-Hinweise. **Stufe B/C** — **offen** (§ 10.6, Anhang A) |
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@
|
|||
> | Setup-Dokument | `.claude/docs/working/SHINKAN_PROJECT_SETUP.md` |
|
||||
> | Anforderungen | `.claude/docs/functional/SHINKAN_REQUIREMENTS.md` |
|
||||
> | Medien-Archiv, Lifecycle, Inline (Plan §11) | `.claude/docs/technical/MEDIA_ASSETS_AND_ARCHIVE_SPEC.md` |
|
||||
> | Fähigkeiten-Scoring (Planungs-Bausteine) | `.claude/docs/technical/SKILL_SCORING_SPEC.md` |
|
||||
> | Handover / nächste Session | **`docs/HANDOVER.md`** |
|
||||
> | Fachlicher Nutzerüberblick (Design/Product) | **`docs/FACHLICHE_NUTZERFUNKTIONEN.md`** |
|
||||
> | Architektur-Zielbild, Refaktor-Roadmap, verbindliche Shinkan-Regeln | **`docs/architecture/README.md`** |
|
||||
|
|
@ -90,6 +91,7 @@ Kurz (Stand 2026-05-14): App- und DB-Version siehe **`backend/version.py`**; Ker
|
|||
|
||||
### Log (Auszug)
|
||||
|
||||
- 2026-05-20: **Fähigkeiten-Scoring Phase 3** — gewichtete Profile für Module/Rahmen/Pfade; Peer-Vergleich getrennt nach Artefakttyp; Listen-Filter + Discovery — siehe `SKILL_SCORING_SPEC.md`, `docs/HANDOVER.md` §2.6, `FEATURES_DELIVERED_2026-Q2.md` §15.
|
||||
- 2026-05-07: **Medien** — zentrales Archiv (`media_assets`), Bibliothek-UI, Lifecycle/Papierkorb, `from-asset`, Speicherpfade `library/…`, Governance `official`/Copyright; **0.8.59** aktiver Verein UI/API-Sync — siehe `.claude/docs/library/FEATURES_DELIVERED_2026-Q2.md` §12, `docs/HANDOVER.md`.
|
||||
- 2026-05-05: Rahmen nur Bibliothek (**036**), Slot‑Ablauf = `training_units` + Sektionen (**037**), `POST /api/training-units/from-framework-slot`, keine `training_framework_slot_exercises` mehr — siehe `DATABASE_SCHEMA.md` / `FEATURES_DELIVERED_2026-Q2.md`.
|
||||
- 2026-04-27: Übungsvarianten API/UI, Migration 030, Listen-UX-Suche, Admin-Upload-Limits — siehe `PROJECT_STATUS.md` und `docs/library/FEATURES_DELIVERED_2026-Q2.md`.
|
||||
|
|
|
|||
|
|
@ -113,6 +113,25 @@ def normalize_exercise_skill_level(value) -> Optional[str]:
|
|||
return s
|
||||
return _LEGACY_SKILL_LEVEL_SLUG.get(s)
|
||||
|
||||
|
||||
_ALLOWED_SKILL_INTENSITY = frozenset({"niedrig", "mittel", "hoch"})
|
||||
|
||||
|
||||
def normalize_exercise_skill_intensity(value) -> str:
|
||||
"""Kanonische Nutzeneinschätzung; leer/ungültig → mittel (kein leerer Wert)."""
|
||||
if value is None:
|
||||
return "mittel"
|
||||
key = str(value).strip().lower()
|
||||
if key in ("low",):
|
||||
return "niedrig"
|
||||
if key in ("medium",):
|
||||
return "mittel"
|
||||
if key in ("high",):
|
||||
return "hoch"
|
||||
if key in _ALLOWED_SKILL_INTENSITY:
|
||||
return key
|
||||
return "mittel"
|
||||
|
||||
MEDIA_ROOT = Path(os.getenv("MEDIA_ROOT", str(Path(__file__).resolve().parent.parent / "media")))
|
||||
MAX_EXERCISE_MEDIA = 10
|
||||
# Upload-Limits (Übungs-Medien): Trainer wie bisher kleiner; Admin/Superadmin höheres Limit für große Videos
|
||||
|
|
@ -1102,13 +1121,15 @@ def enrich_exercise_detail(exercise_id: int, cur) -> dict:
|
|||
FROM exercise_skills es
|
||||
JOIN skills s ON es.skill_id = s.id
|
||||
WHERE es.exercise_id = %s
|
||||
ORDER BY es.is_primary DESC, s.name""",
|
||||
ORDER BY s.name""",
|
||||
(exercise_id,)
|
||||
)
|
||||
exercise["skills"] = [r2d(r) for r in cur.fetchall()]
|
||||
for sk in exercise["skills"]:
|
||||
sk["required_level"] = normalize_exercise_skill_level(sk.get("required_level"))
|
||||
sk["target_level"] = normalize_exercise_skill_level(sk.get("target_level"))
|
||||
sk["intensity"] = normalize_exercise_skill_intensity(sk.get("intensity"))
|
||||
sk["is_primary"] = False
|
||||
|
||||
# Variants (1:N) - mit Progression (Reihenfolge: sequence_order, dann progression_level)
|
||||
cur.execute(
|
||||
|
|
@ -1223,8 +1244,8 @@ def assign_exercise_relations(
|
|||
(
|
||||
exercise_id,
|
||||
skill["skill_id"],
|
||||
skill.get("is_primary", False),
|
||||
skill.get("intensity"),
|
||||
False,
|
||||
normalize_exercise_skill_intensity(skill.get("intensity")),
|
||||
normalize_exercise_skill_level(skill.get("required_level")),
|
||||
normalize_exercise_skill_level(skill.get("target_level")),
|
||||
skill.get("ai_suggested", False),
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ from db import get_db, get_cursor, r2d
|
|||
from auth import require_auth
|
||||
from smw_client import SmwClient, SmwClientError
|
||||
from smw_mapper import map_wiki_to_exercise, map_wiki_to_skill, map_wiki_to_method, build_skill_assignments
|
||||
from routers.exercises import normalize_exercise_skill_intensity
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
|
@ -615,14 +616,16 @@ def _assign_exercise_skills(cur, conn, exercise_id: int, skill_assignments: list
|
|||
VALUES (%s, %s, %s, %s, %s, %s)
|
||||
ON CONFLICT (exercise_id, skill_id) DO UPDATE SET
|
||||
target_level = EXCLUDED.target_level,
|
||||
is_primary = EXCLUDED.is_primary""",
|
||||
required_level = EXCLUDED.required_level,
|
||||
intensity = EXCLUDED.intensity,
|
||||
is_primary = false""",
|
||||
(
|
||||
exercise_id,
|
||||
sid,
|
||||
assignment.get("target_level"),
|
||||
assignment.get("required_level"),
|
||||
assignment.get("intensity"),
|
||||
assignment.get("is_primary", False),
|
||||
normalize_exercise_skill_intensity(assignment.get("intensity")),
|
||||
False,
|
||||
)
|
||||
)
|
||||
conn.commit()
|
||||
|
|
|
|||
|
|
@ -377,8 +377,8 @@ def build_skill_assignments(mapped: dict) -> list[dict]:
|
|||
"skill_name": skill_name,
|
||||
"target_level": target_slug,
|
||||
"required_level": None,
|
||||
"intensity": None,
|
||||
"is_primary": idx == 0,
|
||||
"intensity": "mittel",
|
||||
"is_primary": False,
|
||||
})
|
||||
return assignments
|
||||
|
||||
|
|
|
|||
20
backend/tests/test_exercise_skill_intensity.py
Normal file
20
backend/tests/test_exercise_skill_intensity.py
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
"""Normalisierung Übung ↔ Fähigkeit Intensität."""
|
||||
from routers.exercises import normalize_exercise_skill_intensity
|
||||
|
||||
|
||||
def test_normalize_exercise_skill_intensity_defaults_to_mittel():
|
||||
assert normalize_exercise_skill_intensity(None) == "mittel"
|
||||
assert normalize_exercise_skill_intensity("") == "mittel"
|
||||
assert normalize_exercise_skill_intensity("—") == "mittel"
|
||||
|
||||
|
||||
def test_normalize_exercise_skill_intensity_canonical():
|
||||
assert normalize_exercise_skill_intensity("niedrig") == "niedrig"
|
||||
assert normalize_exercise_skill_intensity("mittel") == "mittel"
|
||||
assert normalize_exercise_skill_intensity("hoch") == "hoch"
|
||||
|
||||
|
||||
def test_normalize_exercise_skill_intensity_legacy_aliases():
|
||||
assert normalize_exercise_skill_intensity("low") == "niedrig"
|
||||
assert normalize_exercise_skill_intensity("medium") == "mittel"
|
||||
assert normalize_exercise_skill_intensity("high") == "hoch"
|
||||
|
|
@ -66,6 +66,8 @@ Die sichtbaren Funktionen hängen von **Rolle** und **Kontext** ab (eingeloggter
|
|||
- **Globaler Fähigkeitskatalog** mit hierarchischer Struktur (Kategorien, Stufen); Zuordnung zu Übungen.
|
||||
- **Trainingsmethoden-Katalog** (bestehende Domäne).
|
||||
- **Admin/Katalog-Pflege** für Fokusbereiche, Stile, Zielgruppen und Zusammenhänge (Plattform-Admin-Bereich).
|
||||
- **Planungs-Vorschläge (Phase 3):** Auf der Fähigkeiten-Seite können Schwerpunkte gewählt werden; Shinkan schlägt passende **Rahmenprogramme**, **Trainingsmodule** und **Regressionspfade** vor (Sortierung nach Trainingsgewicht).
|
||||
- **Fähigkeiten-Profile an Planungs-Bausteinen:** Listen zeigen pro Modul/Rahmenprogramm KPI-Kacheln (Top je Kategorie) mit **Score** und **Peer-Prozent** — Vergleich nur unter sichtbaren Bausteinen **desselben Typs** (Modul vs. Modul, nicht Modul vs. Plan). Filter analog zur Übungsliste (Modal, Chips, Baumauswahl).
|
||||
|
||||
### 4.3 Reifegradmodelle (Fähigkeitsmatrix)
|
||||
|
||||
|
|
@ -78,6 +80,8 @@ Die sichtbaren Funktionen hängen von **Rolle** und **Kontext** ab (eingeloggter
|
|||
- **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.
|
||||
- **Trainingsmodule (Bibliothek):** wiederverwendbare Übungsfolgen; Übernahme in geplante Einheiten; **Fähigkeiten-Profil** und **Filter** in der Modul-Liste (Peer-Vergleich nur unter Modulen).
|
||||
- **Rahmenprogramm-Liste:** Suche und Filter (Katalog, Dauer, Fähigkeiten) — Peer-Vergleich nur unter Rahmenprogrammen.
|
||||
- **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 („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).
|
||||
|
|
@ -129,6 +133,7 @@ Nicht als „broken“ gemeint, sondern als **typische nächste Ausbaustellen**
|
|||
|
||||
| Datum | Änderung |
|
||||
|-------|----------|
|
||||
| 2026-05-20 | Fähigkeiten-Scoring Phase 3: Peer-Kontext, Listen-Filter Module/Rahmen, Planungs-Vorschläge. |
|
||||
| 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 | Kombinationsübungen + Coaching Stufe A; Verweise auf Fachspezifikation (`…Kombinationsuebungen…` V3 Anhang A) und `TRAINING_MODULES_IMPLEMENTATION_PLAN.md`. |
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# Shinkan Jinkendo – Entwicklungsstand & Handover
|
||||
|
||||
**Stand:** 2026-05-19
|
||||
**Stand:** 2026-05-20
|
||||
**App-Version / DB-Schema:** App **`0.8.149`** (Einheiten-Editor Vollseite), 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**.
|
||||
|
|
@ -35,6 +35,7 @@ Das Schema ist gegenüber dem Code zurück: Migration **`022_skills_schema_compl
|
|||
| **Umsetzungsplan** (Module/Kombination/Coach) | `.claude/docs/working/TRAINING_MODULES_IMPLEMENTATION_PLAN.md` |
|
||||
| Überblick DB | `.claude/docs/technical/DATABASE_SCHEMA.md` |
|
||||
| Domäne | `.claude/docs/functional/DOMAIN_MODEL.md` |
|
||||
| **Gewichtetes Fähigkeiten-Scoring (Phase 3)** | `.claude/docs/technical/SKILL_SCORING_SPEC.md` |
|
||||
| **Lieferliste inkl. Medien** | `.claude/docs/library/FEATURES_DELIVERED_2026-Q2.md` §12 |
|
||||
| **Fachlicher Nutzerüberblick (Design/Product)** | **`docs/FACHLICHE_NUTZERFUNKTIONEN.md`** |
|
||||
|
||||
|
|
@ -69,6 +70,15 @@ Das Schema ist gegenüber dem Code zurück: Migration **`022_skills_schema_compl
|
|||
|
||||
- **`AdminMaturityModelsPage.jsx`**, **`MaturityModelBindingsAdmin.jsx`**, **`MaturityMatrixToolsAdmin.jsx`**; APIs in `api.js`.
|
||||
|
||||
### 2.6 Gewichtetes Fähigkeiten-Scoring (Phase 3, Stand 2026-05-20)
|
||||
|
||||
- **Spec:** `.claude/docs/technical/SKILL_SCORING_SPEC.md`
|
||||
- **Backend:** `skill_scoring.py`, `routers/skill_profiles.py` — Profile on-the-fly aus Übungsvorkommen + `exercise_skills`; **Peer-Kontext getrennt** (`framework_program` | `training_module` | `progression_graph`) über `library_content_visibility_sql`
|
||||
- **Metriken:** Trainingsgewicht (`weight`); **Peer-%** (`universal_percent`, max. 100 %) nur unter sichtbaren Bausteinen **desselben Typs**; ★ = stärkster Peer je Fähigkeit
|
||||
- **API:** Skill-Profile pro Artefakt; `POST /api/skill-profiles/batch-summaries` für Listen; `GET /api/skill-discovery/suggestions`
|
||||
- **Frontend:** KPI-Kacheln + Filter-Modal (UX wie Übungsliste) auf **`/planning/framework-programs`** und **`/planning/training-modules`**; Panels in Editoren; Discovery auf Fähigkeiten-Seite; `SkillTreeMultiSelect` mit Portal-Dropdown in Modals
|
||||
- **Offen (Backlog):** Corpus-Caching bei großen Bibliotheken; Tests für Typ-Trennung; Filter-Persistenz; Skill-Filter im Dialog „Rahmen übernehmen“; API-Umbenennung `club_*` → Peer-Namen
|
||||
|
||||
---
|
||||
|
||||
## 3. Trainingsrahmenprogramm & Planungs‑Blueprint (kurz)
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
import { stripHtmlToText } from '../utils/htmlUtils'
|
||||
import { normalizeAdvanceMode, parseComboRepSeriesCountUi } from '../utils/combinationMethodProfileUi'
|
||||
import { normalizeExerciseSkillIntensity } from '../constants/exerciseSkillIntensity'
|
||||
import { request, API_URL, ACTIVE_CLUB_STORAGE_KEY } from './client.js'
|
||||
|
||||
/** Wie `mergeActiveClubHeader` in client.js — lokal, damit Raw-`fetch`-Pfade nicht von einem Namensimport abhängen. */
|
||||
|
|
@ -89,8 +90,7 @@ export function buildExerciseApiPayload(formData, extras = {}) {
|
|||
age_groups: [],
|
||||
skills: (formData.skills || []).map((s) => ({
|
||||
skill_id: s.skill_id,
|
||||
is_primary: !!s.is_primary,
|
||||
intensity: s.intensity || null,
|
||||
intensity: normalizeExerciseSkillIntensity(s.intensity),
|
||||
required_level: s.required_level || null,
|
||||
target_level: s.target_level || null,
|
||||
})),
|
||||
|
|
|
|||
|
|
@ -39,14 +39,11 @@ import {
|
|||
} from '../../utils/navReturnContext'
|
||||
import { useBeforeUnloadWhen, useUnsavedChangesBlocker } from '../../hooks/useUnsavedChangesBlocker'
|
||||
|
||||
const INTENSITY_OPTIONS = [
|
||||
{ value: '', label: '—' },
|
||||
{ value: 'niedrig', label: 'niedrig' },
|
||||
{ value: 'mittel', label: 'mittel' },
|
||||
{ value: 'hoch', label: 'hoch' },
|
||||
]
|
||||
|
||||
const VARIANT_DIFFICULTY = [
|
||||
import {
|
||||
EXERCISE_SKILL_INTENSITY_DEFAULT,
|
||||
EXERCISE_SKILL_INTENSITY_OPTIONS,
|
||||
normalizeExerciseSkillIntensity,
|
||||
} from '../../constants/exerciseSkillIntensity' = [
|
||||
{ value: '', label: '—' },
|
||||
{ value: 'easier', label: 'Einfacher' },
|
||||
{ value: 'same', label: 'Gleich' },
|
||||
|
|
@ -391,8 +388,7 @@ function detailToForm(exercise) {
|
|||
skills:
|
||||
exercise.skills?.map((s) => ({
|
||||
skill_id: s.skill_id,
|
||||
is_primary: s.is_primary || false,
|
||||
intensity: s.intensity || '',
|
||||
intensity: normalizeExerciseSkillIntensity(s.intensity),
|
||||
required_level: normalizeSkillLevelSlug(s.required_level),
|
||||
target_level: normalizeSkillLevelSlug(s.target_level),
|
||||
})) || [],
|
||||
|
|
@ -820,8 +816,7 @@ function ExerciseFormPageRoot() {
|
|||
...formData.skills,
|
||||
{
|
||||
skill_id: id,
|
||||
is_primary: formData.skills.length === 0,
|
||||
intensity: '',
|
||||
intensity: EXERCISE_SKILL_INTENSITY_DEFAULT,
|
||||
required_level: '',
|
||||
target_level: '',
|
||||
},
|
||||
|
|
@ -829,10 +824,10 @@ function ExerciseFormPageRoot() {
|
|||
setSkillPick('')
|
||||
}
|
||||
|
||||
const setSkillPrimary = (idx) => {
|
||||
const removeSkillRow = (idx) => {
|
||||
updateFormField(
|
||||
'skills',
|
||||
formData.skills.map((s, i) => ({ ...s, is_primary: i === idx })),
|
||||
formData.skills.filter((_, i) => i !== idx),
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -843,12 +838,6 @@ function ExerciseFormPageRoot() {
|
|||
)
|
||||
}
|
||||
|
||||
const removeSkillRow = (idx) => {
|
||||
const next = formData.skills.filter((_, i) => i !== idx)
|
||||
if (next.length && !next.some((s) => s.is_primary)) next[0].is_primary = true
|
||||
updateFormField('skills', next)
|
||||
}
|
||||
|
||||
const performSaveAttempt = useCallback(
|
||||
async ({ fromUnsavedDialog = false, closeAfter = false } = {}) => {
|
||||
if (!formData.title || formData.title.trim().length < 3) {
|
||||
|
|
@ -1880,26 +1869,20 @@ function ExerciseFormPageRoot() {
|
|||
</span>
|
||||
) : null}
|
||||
</div>
|
||||
<label style={{ fontSize: '12px', display: 'flex', alignItems: 'center', gap: '4px' }}>
|
||||
<input
|
||||
type="radio"
|
||||
name="skill-primary"
|
||||
checked={row.is_primary}
|
||||
onChange={() => setSkillPrimary(idx)}
|
||||
/>
|
||||
primär
|
||||
</label>
|
||||
<label className="exercise-filter-skill-level-field">
|
||||
<span className="exercise-filter-skill-level-caption">Intensität</span>
|
||||
<select
|
||||
className="form-input"
|
||||
value={row.intensity || ''}
|
||||
value={normalizeExerciseSkillIntensity(row.intensity)}
|
||||
onChange={(e) => updateSkillField(idx, 'intensity', e.target.value)}
|
||||
>
|
||||
{INTENSITY_OPTIONS.map((o) => (
|
||||
<option key={o.value || 'i'} value={o.value}>
|
||||
{EXERCISE_SKILL_INTENSITY_OPTIONS.map((o) => (
|
||||
<option key={o.value} value={o.value}>
|
||||
{o.label}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</label>
|
||||
<select
|
||||
className="form-input"
|
||||
value={row.required_level || ''}
|
||||
|
|
|
|||
21
frontend/src/constants/exerciseSkillIntensity.js
Normal file
21
frontend/src/constants/exerciseSkillIntensity.js
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
/** Kanonische Intensität Übung ↔ Fähigkeit (Scoring + Formular). */
|
||||
|
||||
export const EXERCISE_SKILL_INTENSITY_DEFAULT = 'mittel'
|
||||
|
||||
export const EXERCISE_SKILL_INTENSITY_OPTIONS = [
|
||||
{ value: 'niedrig', label: 'niedrig' },
|
||||
{ value: 'mittel', label: 'mittel' },
|
||||
{ value: 'hoch', label: 'hoch' },
|
||||
]
|
||||
|
||||
export function normalizeExerciseSkillIntensity(value) {
|
||||
const key = String(value ?? '').trim().toLowerCase()
|
||||
if (key === 'niedrig' || key === 'low') return 'niedrig'
|
||||
if (key === 'hoch' || key === 'high') return 'hoch'
|
||||
return 'mittel'
|
||||
}
|
||||
|
||||
export function formatExerciseSkillIntensityLabel(value) {
|
||||
const n = normalizeExerciseSkillIntensity(value)
|
||||
return EXERCISE_SKILL_INTENSITY_OPTIONS.find((o) => o.value === n)?.label || n
|
||||
}
|
||||
|
|
@ -12,6 +12,7 @@ import ExerciseAttachmentMediaStrip from '../components/ExerciseAttachmentMediaS
|
|||
import CombinationPlanBracket from '../components/CombinationPlanBracket'
|
||||
import ExercisePeekModal from '../components/ExercisePeekModal'
|
||||
import { formatSkillLevelSlug } from '../constants/skillLevels'
|
||||
import { formatExerciseSkillIntensityLabel } from '../constants/exerciseSkillIntensity'
|
||||
|
||||
function TagRow({ exercise }) {
|
||||
const tags = []
|
||||
|
|
@ -244,9 +245,9 @@ function ExerciseDetailPage() {
|
|||
const lvl =
|
||||
rl || tl ? ` (${[rl, tl].filter(Boolean).join(' → ')})` : ''
|
||||
return (
|
||||
<span key={s.id} className={`exercise-tag${s.is_primary ? ' exercise-tag--accent' : ''}`}>
|
||||
<span key={s.id} className="exercise-tag">
|
||||
{s.skill_name}
|
||||
{s.intensity ? ` · ${s.intensity}` : ''}
|
||||
{` · ${formatExerciseSkillIntensityLabel(s.intensity)}`}
|
||||
{lvl}
|
||||
</span>
|
||||
)
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user