diff --git a/.claude/docs/PROJECT_STATUS.md b/.claude/docs/PROJECT_STATUS.md index 58e388d..e4a793f 100644 --- a/.claude/docs/PROJECT_STATUS.md +++ b/.claude/docs/PROJECT_STATUS.md @@ -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:** diff --git a/.claude/docs/functional/DOMAIN_MODEL.md b/.claude/docs/functional/DOMAIN_MODEL.md index 2c4ae68..d512d08 100644 --- a/.claude/docs/functional/DOMAIN_MODEL.md +++ b/.claude/docs/functional/DOMAIN_MODEL.md @@ -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 diff --git a/.claude/docs/library/FEATURES_DELIVERED_2026-Q2.md b/.claude/docs/library/FEATURES_DELIVERED_2026-Q2.md index 995588b..e6f1af9 100644 --- a/.claude/docs/library/FEATURES_DELIVERED_2026-Q2.md +++ b/.claude/docs/library/FEATURES_DELIVERED_2026-Q2.md @@ -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` | diff --git a/.claude/docs/technical/SKILL_SCORING_SPEC.md b/.claude/docs/technical/SKILL_SCORING_SPEC.md index 17ef945..bc7b208 100644 --- a/.claude/docs/technical/SKILL_SCORING_SPEC.md +++ b/.claude/docs/technical/SKILL_SCORING_SPEC.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 | diff --git a/.claude/docs/technical/TRAINING_FRAMEWORK_SPEC.md b/.claude/docs/technical/TRAINING_FRAMEWORK_SPEC.md index 485b574..c32f96c 100644 --- a/.claude/docs/technical/TRAINING_FRAMEWORK_SPEC.md +++ b/.claude/docs/technical/TRAINING_FRAMEWORK_SPEC.md @@ -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). diff --git a/.claude/docs/working/TRAINING_MODULES_IMPLEMENTATION_PLAN.md b/.claude/docs/working/TRAINING_MODULES_IMPLEMENTATION_PLAN.md index 89468f8..e4a3a45 100644 --- a/.claude/docs/working/TRAINING_MODULES_IMPLEMENTATION_PLAN.md +++ b/.claude/docs/working/TRAINING_MODULES_IMPLEMENTATION_PLAN.md @@ -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) | diff --git a/CLAUDE.md b/CLAUDE.md index 0a4901d..ec43ca3 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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`. diff --git a/backend/routers/exercises.py b/backend/routers/exercises.py index 8fd392e..6057637 100644 --- a/backend/routers/exercises.py +++ b/backend/routers/exercises.py @@ -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), diff --git a/backend/routers/import_wiki.py b/backend/routers/import_wiki.py index 4dbb918..3cc18eb 100644 --- a/backend/routers/import_wiki.py +++ b/backend/routers/import_wiki.py @@ -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() diff --git a/backend/smw_mapper.py b/backend/smw_mapper.py index 970713e..4d56a9f 100644 --- a/backend/smw_mapper.py +++ b/backend/smw_mapper.py @@ -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 diff --git a/backend/tests/test_exercise_skill_intensity.py b/backend/tests/test_exercise_skill_intensity.py new file mode 100644 index 0000000..d098a13 --- /dev/null +++ b/backend/tests/test_exercise_skill_intensity.py @@ -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" diff --git a/docs/FACHLICHE_NUTZERFUNKTIONEN.md b/docs/FACHLICHE_NUTZERFUNKTIONEN.md index 96d247f..123a9be 100644 --- a/docs/FACHLICHE_NUTZERFUNKTIONEN.md +++ b/docs/FACHLICHE_NUTZERFUNKTIONEN.md @@ -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`. | diff --git a/docs/HANDOVER.md b/docs/HANDOVER.md index 7b05e7f..9ff22c1 100644 --- a/docs/HANDOVER.md +++ b/docs/HANDOVER.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) diff --git a/frontend/src/api/exercises.js b/frontend/src/api/exercises.js index 3c3f387..ec16dfd 100644 --- a/frontend/src/api/exercises.js +++ b/frontend/src/api/exercises.js @@ -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, })), diff --git a/frontend/src/app.css b/frontend/src/app.css index 9feb71f..757bb70 100644 --- a/frontend/src/app.css +++ b/frontend/src/app.css @@ -6548,6 +6548,367 @@ html.modal-scroll-locked .app-main { min-width: 0; } +/* Übungsformular — Register-Tabs & farbige Bereiche */ +.exercise-form-edit { + padding-top: 4px; +} +.exercise-form-edit__tabbar { + display: flex; + align-items: stretch; + margin: 0 0 16px; + padding: 0 0 12px; + border-bottom: 1px solid var(--border); + position: sticky; + top: 0; + z-index: 6; + background: var(--surface); +} +.exercise-form-edit__tabbar .admin-page-subtabs { + flex: 1; + min-width: 0; +} +.exercise-form-panel { + padding: 4px 0 8px 14px; + margin-bottom: 4px; + border-left: 3px solid var(--border); +} +.exercise-form-panel--basics { + border-left-color: var(--accent); +} +.exercise-form-panel--guide { + border-left-color: color-mix(in srgb, #2563eb 70%, var(--accent)); +} +.exercise-form-panel--classify { + border-left-color: color-mix(in srgb, #7c3aed 65%, var(--accent)); +} +.exercise-form-panel--combo { + border-left-color: color-mix(in srgb, #d97706 70%, var(--accent-dark)); +} +.exercise-form-panel--variants { + border-left-color: color-mix(in srgb, #0891b2 70%, var(--accent)); +} +.exercise-form-panel--media { + border-left-color: color-mix(in srgb, var(--text3) 55%, var(--border)); +} +.exercise-form-panel__title { + margin: 0 0 4px; + font-size: 1.05rem; + font-weight: 700; +} +.exercise-form-panel__hint { + margin: 0 0 14px; + font-size: 12px; + color: var(--text3); + line-height: 1.45; +} +.exercise-form-panel__body { + min-width: 0; +} +.exercise-form-type-box { + padding: 12px; + border-radius: 10px; + border: 1px solid var(--border); + background: var(--surface2); + margin-bottom: 12px; +} +.exercise-form-type-box__hint { + margin: 8px 0 0; + font-size: 12px; + color: var(--text2); + line-height: 1.45; +} +.exercise-form-inline-tab-link { + padding: 0; + border: none; + background: none; + color: var(--accent-dark); + font: inherit; + font-weight: 700; + text-decoration: underline; + cursor: pointer; +} +.exercise-form-subsection { + padding: 12px 0; + border-top: 1px dashed var(--border); + margin-top: 8px; +} +.exercise-form-subsection:first-child { + border-top: none; + margin-top: 0; + padding-top: 0; +} +.exercise-form-subsection__title { + margin: 0 0 6px; + font-size: 0.95rem; + font-weight: 700; +} +.exercise-form-subsection__hint { + margin: 0 0 10px; + font-size: 12px; + color: var(--text3); + line-height: 1.4; +} + +/* Übungsformular — Klassifikation & Meta-Chips */ +.exercise-form-meta-panel { + margin: 0; + padding: 14px; + border: 1px solid var(--border); + border-radius: 12px; + background: var(--surface2); +} +.exercise-form-meta-panel__title { + margin: 0 0 12px; + font-size: 1rem; + font-weight: 700; +} +.exercise-form-meta-panel__grid { + display: grid; + grid-template-columns: 1fr; + gap: 10px; +} +@media (min-width: 720px) { + .exercise-form-meta-panel__grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } +} +.exercise-meta-block { + padding: 10px 12px; + border: 1px solid var(--border); + border-radius: 10px; + background: var(--surface); +} +.exercise-meta-block--skills { + margin-top: 10px; +} +.exercise-meta-block__head { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + margin-bottom: 6px; +} +.exercise-meta-block__title { + margin: 0; + font-size: 13px; + font-weight: 700; + color: var(--text1); +} +.exercise-meta-block__add { + font-size: 11px; + padding: 3px 8px; + flex-shrink: 0; +} +.exercise-meta-block__hint, +.exercise-meta-block__empty { + margin: 0 0 8px; + font-size: 12px; + color: var(--text3); + line-height: 1.35; +} +.exercise-meta-block__empty { + margin-bottom: 0; +} +.exercise-meta-block__chips { + display: flex; + flex-wrap: wrap; + gap: 6px; +} +.exercise-catalog-chip { + display: inline-flex; + align-items: center; + gap: 2px; + max-width: 100%; + padding: 2px 4px 2px 2px; + border-radius: 999px; + border: 1px solid var(--border); + background: var(--surface2); +} +.exercise-catalog-chip__select { + border: none; + background: transparent; + padding: 4px 8px; + font-size: 12px; + font-family: inherit; + color: var(--text1); + min-width: 0; + max-width: min(220px, 72vw); + cursor: pointer; +} +.exercise-catalog-chip__select:focus { + outline: none; +} +.exercise-catalog-chip__primary, +.exercise-catalog-chip__remove { + display: inline-flex; + align-items: center; + justify-content: center; + width: 24px; + height: 24px; + padding: 0; + border: none; + border-radius: 999px; + background: transparent; + color: var(--text3); + font-size: 12px; + line-height: 1; + cursor: pointer; + flex-shrink: 0; +} +.exercise-catalog-chip__primary--on { + color: var(--accent-dark); +} +.exercise-catalog-chip__remove:hover, +.exercise-catalog-chip__primary:hover { + background: color-mix(in srgb, var(--border) 40%, transparent); + color: var(--text1); +} +.exercise-skills-add { + display: flex; + flex-wrap: wrap; + gap: 8px; + align-items: stretch; + margin-bottom: 10px; +} +.exercise-skills-add .skill-tree-select { + flex: 1 1 180px; + min-width: 0; +} +.exercise-skills-list { + list-style: none; + margin: 0; + padding: 0; + display: flex; + flex-direction: column; + gap: 8px; +} +.exercise-skill-chip { + display: flex; + flex-direction: column; + gap: 8px; + padding: 10px 12px; + border-radius: 10px; + border: 1px solid var(--border); + background: var(--surface2); +} +@media (min-width: 640px) { + .exercise-skill-chip { + flex-direction: row; + align-items: center; + justify-content: flex-start; + gap: 12px; + } +} +.exercise-skill-chip__identity { + flex: 0 1 auto; + min-width: 0; + max-width: min(240px, 100%); +} +.exercise-skill-chip__name { + display: block; + font-size: 13px; + font-weight: 700; + color: var(--text1); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.exercise-skill-chip__path { + display: block; + margin-top: 2px; + font-size: 11px; + color: var(--text3); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} +.exercise-skill-chip__controls { + display: flex; + flex-wrap: wrap; + align-items: flex-end; + gap: 8px 10px; + flex: 1 1 auto; + min-width: 0; +} +.exercise-skill-chip__field { + display: flex; + flex-direction: column; + gap: 3px; + min-width: 0; +} +.exercise-skill-chip__caption { + font-size: 10px; + font-weight: 600; + color: var(--text3); + text-transform: uppercase; + letter-spacing: 0.04em; +} +.exercise-skill-chip__levels { + display: flex; + flex-wrap: nowrap; + align-items: flex-end; + gap: 6px; +} +.exercise-skill-level-select { + width: 52px; + min-width: 52px; + padding: 5px 6px; + font-size: 13px; + text-align: center; +} +.exercise-skill-chip__dash { + padding-bottom: 7px; + color: var(--text3); + font-size: 12px; + font-weight: 600; +} +.exercise-intensity-segment { + display: inline-flex; + border: 1px solid var(--border); + border-radius: 8px; + overflow: hidden; + background: var(--surface); +} +.exercise-intensity-segment__btn { + padding: 5px 8px; + border: none; + border-right: 1px solid var(--border); + background: transparent; + font-size: 11px; + font-family: inherit; + color: var(--text2); + cursor: pointer; + white-space: nowrap; +} +.exercise-intensity-segment__btn:last-child { + border-right: none; +} +.exercise-intensity-segment__btn--active { + background: color-mix(in srgb, var(--accent) 16%, var(--surface)); + color: var(--accent-dark); + font-weight: 700; +} +.exercise-skill-chip__remove { + display: inline-flex; + align-items: center; + justify-content: center; + width: 28px; + height: 28px; + margin-left: auto; + padding: 0; + border: 1px solid var(--border); + border-radius: 999px; + background: var(--surface); + color: var(--text3); + font-size: 12px; + cursor: pointer; + flex-shrink: 0; +} +.exercise-skill-chip__remove:hover { + color: var(--danger); + border-color: color-mix(in srgb, var(--danger) 35%, var(--border)); +} + .skills-editor-row { display: grid; grid-template-columns: 1fr auto; diff --git a/frontend/src/components/exercises/ExerciseCatalogAssocEditor.jsx b/frontend/src/components/exercises/ExerciseCatalogAssocEditor.jsx new file mode 100644 index 0000000..c536181 --- /dev/null +++ b/frontend/src/components/exercises/ExerciseCatalogAssocEditor.jsx @@ -0,0 +1,97 @@ +import React from 'react' + +/** + * Kompakte Katalog-Zuordnung (Fokus, Stile, Zielgruppen …) als Chip-Zeilen. + */ +export default function ExerciseCatalogAssocEditor({ + title, + rows, + setRows, + options, + idKey, + emptyLabel, + showPrimary = true, +}) { + const setPrimary = (idx) => { + setRows(rows.map((r, i) => ({ ...r, is_primary: i === idx }))) + } + const updateRow = (idx, patch) => { + const next = rows.map((r, i) => (i === idx ? { ...r, ...patch } : r)) + if (patch.is_primary === true) { + next.forEach((r, i) => { + if (i !== idx) r.is_primary = false + }) + } + setRows(next) + } + const addRow = () => setRows([...rows, { [idKey]: '', is_primary: rows.length === 0 }]) + const removeRow = (idx) => { + const next = rows.filter((_, i) => i !== idx) + if (next.length && showPrimary && !next.some((r) => r.is_primary)) next[0].is_primary = true + setRows(next) + } + + const optionLabel = (o) => { + const parts = [] + if (o.icon) parts.push(o.icon) + parts.push(o.name) + if (o.abbreviation) parts.push(`(${o.abbreviation})`) + return parts.join(' ') + } + + return ( +
{emptyLabel}
+ ) : ( +{hint}
: null} +{emptyLabel}
- )} - {rows.map((row, idx) => ( -- Pro Durchgang nur eine Variante bearbeiten – weniger Scrollen. Reihenfolge entspricht Planung und Auswahl im - Training; „Voraussetzung“ nutzt ihr später für Progressions-Serien. -
+ + + {isEdit && formData.exercise_kind !== 'combination' ? ( +Neue Uploads oder Embeds über die Textfeld-Symbolleiste („Medien im Text“ / „Embed im Text“). Hier verwaltest du Verknüpfungen — Kachel in ein Textfeld ziehen, um sie an der Cursorposition einzufügen @@ -2302,6 +2333,15 @@ function ExerciseFormPageRoot() { Verknüpfungen bleiben nötig (u. a. Zugriff, Orphan-Hinweise): Im Fließtext verweist du gezielt über Platzhalter. Ohne Verknüpfung gäbe es keine exercise_media-ID zum Einbetten.
+Übergänge zu anderen Übungen für Progressions-Serien.
+Je Übung mehrere Fähigkeiten mit Intensität und Niveau (von–bis).
+ +Noch keine Fähigkeit zugeordnet.
+ ) : ( +