diff --git a/.claude/docs/working/FRAMEWORK_PROGRAM_FILTER_AND_SKILLS_ROADMAP.md b/.claude/docs/working/FRAMEWORK_PROGRAM_FILTER_AND_SKILLS_ROADMAP.md
new file mode 100644
index 0000000..d9d8663
--- /dev/null
+++ b/.claude/docs/working/FRAMEWORK_PROGRAM_FILTER_AND_SKILLS_ROADMAP.md
@@ -0,0 +1,65 @@
+# Rahmenprogramm: Filter, Dauer, Fähigkeiten-Schwerpunkte (Roadmap)
+
+**Stand:** 2026-05-20
+**Status:** Phase 1 umgesetzt (Listen + Import-Filter); Phase 2–3 offen
+
+## Phase 1 (umgesetzt)
+
+### Listen-Anzeige Session-Dauer
+
+- **GET `/api/training-framework-programs`:** `session_duration_min`, `session_duration_max` (aus Blueprint-`training_units.planned_duration_min`), `goal_titles_agg`, ID-Arrays für Katalog-M:N.
+- **UI:** Rahmenprogramm-Liste, Trainingsplanung (Einheiten-Liste/Kalender), Import-Dialog (Programm + pro Slot).
+
+### Import-Filter (clientseitig)
+
+- Textsuche (Titel, Beschreibung, Ziele, Katalog-Namen)
+- Fokusbereich, Trainingsart, Zielgruppe (Checkboxen, Katalog-API)
+- Ziel-Session-Dauer in Minuten (±10 Min Toleranz gegen Min/Max der Slots)
+
+**Grenze:** Entwicklungsziele sind **freie Texte** pro Rahmen (`training_framework_goals.title`), keine kontrollierte Taxonomie → Filter nur Volltext, keine homogene „Ziel-Tags“-Liste.
+
+## Phase 2 (empfohlen, ohne KI)
+
+| Kriterium | Datenquelle heute | Verbesserung |
+|-----------|-------------------|--------------|
+| Fokusbereich / Stil / Trainingsart / Zielgruppe | M:N am Rahmenkopf | bereits filterbar |
+| Entwicklungsziele | Freitext-Ziele | Optional: Ziel-Vorlagen-Katalog oder Tags (Migration) |
+| Session-Dauer | `planned_duration_min` pro Slot | erledigt |
+| Fähigkeiten-Schwerpunkt | noch nicht | siehe Phase 3 |
+
+**API-Erweiterung (optional):** `GET /api/training-framework-programs?focus_area_id=&training_type_id=&duration_min=` serverseitig — sinnvoll ab >50 Rahmen in der Bibliothek.
+
+## Phase 3 — Fähigkeiten aus Übungen (Schwerpunkte dynamisch)
+
+### Ziel
+
+Aus allen Übungen in allen Slots eines Rahmenprogramms die verknüpften **Fähigkeiten** (`exercise_skills` → `skills`, ggf. Fokusbereich der Fähigkeit) aggregieren, gewichten und als **Vorschlags-Schwerpunkte** oder Metadaten am Rahmen anzeigen (nicht zwingend automatisch in den Kopf schreiben).
+
+### Variante A — Regelbasiert (ohne KI)
+
+1. Pro Blueprint-Unit alle `exercise_id` aus `training_unit_section_items` sammeln.
+2. Join `exercise_skills` (optional Gewicht: `planned_duration_min` der Zeile, Anzahl Vorkommen, Primär-Fähigkeit).
+3. Top-N Fähigkeiten / Fokusbereiche nach Summe oder Anteil an Gesamtminuten.
+4. Ergebnis cachen in `training_framework_programs.skill_profile_json` (Migration) oder nur on-the-fly bei GET Detail.
+
+**Vorteil:** reproduzierbar, offline, Governance-konform.
+**Aufwand:** ca. 1–2 Tage Backend + kleine UI-Karte „Fähigkeiten-Profil (aus Übungen)“.
+
+### Variante B — KI-Zusammenfassung (OpenRouter, optional)
+
+1. Input: Titel Rahmen, Ziele (Text), Liste Übungstitel + Dauer + vorhandene Skill-Namen.
+2. Prompt: strukturiertes JSON (`suggested_focus_areas[]`, `skill_emphasis[]`, `rationale_de`).
+3. Speichern als `ai_context_summary` (Version, Modell, Timestamp) — **nur Vorschlag**, manuelle Bestätigung vor Übernahme in Stammdaten.
+
+**Vorteil:** natürliche Schwerpunkte auch bei unvollständigen Skill-Links.
+**Risiko:** Halluzination, Kosten, Datenschutz (Vereinsdaten in Prompt).
+
+### Empfehlung
+
+Zuerst **Variante A** für Listen/Filter und Abgleich mit manuell gesetzten Fokusbereichen; KI nur als **„Vorschlag generieren“-Button** im Rahmen-Editor, wenn Regelwerk und Katalog-Zuordnung zu dünn sind.
+
+## Offene Produktfragen
+
+1. Soll Filter **UND** (alle Kriterien) oder **ODER** (mindestens eines) sein? — Import aktuell **UND**.
+2. Rahmen mit **unterschiedlichen** Slot-Dauern: Liste zeigt Min–Max; Filter „90 Min“ trifft Range.
+3. Sollen homogenisierte **Entwicklungsziel-Tags** ein eigener Katalog werden (Admin), analog `target_groups`?
diff --git a/backend/routers/training_framework_programs.py b/backend/routers/training_framework_programs.py
index b2d2eaa..f817e15 100644
--- a/backend/routers/training_framework_programs.py
+++ b/backend/routers/training_framework_programs.py
@@ -444,7 +444,46 @@ def list_training_framework_programs(tenant: TenantContext = Depends(get_tenant_
FROM training_framework_program_target_groups j
JOIN target_groups tg ON tg.id = j.target_group_id
WHERE j.framework_program_id = fp.id
- ) AS target_group_names_agg
+ ) AS target_group_names_agg,
+ (
+ SELECT STRING_AGG(g.title::text, ' | ' ORDER BY g.sort_order)
+ FROM training_framework_goals g
+ WHERE g.framework_program_id = fp.id
+ ) AS goal_titles_agg,
+ (
+ SELECT MIN(tu.planned_duration_min)::int
+ FROM training_framework_slots fs
+ INNER JOIN training_units tu ON tu.framework_slot_id = fs.id
+ WHERE fs.framework_program_id = fp.id
+ AND tu.planned_duration_min IS NOT NULL
+ ) AS session_duration_min,
+ (
+ SELECT MAX(tu.planned_duration_min)::int
+ FROM training_framework_slots fs
+ INNER JOIN training_units tu ON tu.framework_slot_id = fs.id
+ WHERE fs.framework_program_id = fp.id
+ AND tu.planned_duration_min IS NOT NULL
+ ) AS session_duration_max,
+ (
+ SELECT COALESCE(json_agg(j.focus_area_id ORDER BY j.focus_area_id), '[]'::json)
+ FROM training_framework_program_focus_areas j
+ WHERE j.framework_program_id = fp.id
+ ) AS focus_area_ids,
+ (
+ SELECT COALESCE(json_agg(j.style_direction_id ORDER BY j.style_direction_id), '[]'::json)
+ FROM training_framework_program_style_directions j
+ WHERE j.framework_program_id = fp.id
+ ) AS style_direction_ids,
+ (
+ SELECT COALESCE(json_agg(j.training_type_id ORDER BY j.training_type_id), '[]'::json)
+ FROM training_framework_program_training_types j
+ WHERE j.framework_program_id = fp.id
+ ) AS training_type_ids,
+ (
+ SELECT COALESCE(json_agg(j.target_group_id ORDER BY j.target_group_id), '[]'::json)
+ FROM training_framework_program_target_groups j
+ WHERE j.framework_program_id = fp.id
+ ) AS target_group_ids
FROM training_framework_programs fp
"""
vis_clause, vis_params = library_content_visibility_sql(
diff --git a/frontend/src/components/planning/TrainingPlanningFrameworkImportModal.jsx b/frontend/src/components/planning/TrainingPlanningFrameworkImportModal.jsx
index 3a499e9..248832b 100644
--- a/frontend/src/components/planning/TrainingPlanningFrameworkImportModal.jsx
+++ b/frontend/src/components/planning/TrainingPlanningFrameworkImportModal.jsx
@@ -1,4 +1,10 @@
-import React from 'react'
+import React, { useMemo, useState } from 'react'
+import {
+ filterFrameworkPrograms,
+ frameworkProgramOptionLabel,
+ frameworkSessionDurationLabel,
+} from '../../utils/frameworkProgramListHelpers'
+import { formatDurationDisplay } from '../../utils/trainingDurationUtils'
/**
* Modal: geplante Einheiten aus einem Trainingsrahmenprogramm (Blueprint-Slots) erzeugen.
@@ -6,6 +12,9 @@ import React from 'react'
export default function TrainingPlanningFrameworkImportModal({
open,
frameworkProgramsList,
+ catalogFocusAreas = [],
+ catalogTrainingTypes = [],
+ catalogTargetGroups = [],
fwImportProgramId,
onProgramChange,
fwImportLoading,
@@ -23,6 +32,41 @@ export default function TrainingPlanningFrameworkImportModal({
onSubmit,
onClose,
}) {
+ const [filterQuery, setFilterQuery] = useState('')
+ const [filterFocusIds, setFilterFocusIds] = useState([])
+ const [filterTypeIds, setFilterTypeIds] = useState([])
+ const [filterTargetGroupIds, setFilterTargetGroupIds] = useState([])
+ const [filterDurationMin, setFilterDurationMin] = useState('')
+
+ const filteredPrograms = useMemo(
+ () =>
+ filterFrameworkPrograms(frameworkProgramsList, {
+ query: filterQuery,
+ focusAreaIds: filterFocusIds,
+ trainingTypeIds: filterTypeIds,
+ targetGroupIds: filterTargetGroupIds,
+ durationTargetMin: filterDurationMin === '' ? null : parseInt(filterDurationMin, 10),
+ }),
+ [
+ frameworkProgramsList,
+ filterQuery,
+ filterFocusIds,
+ filterTypeIds,
+ filterTargetGroupIds,
+ filterDurationMin,
+ ]
+ )
+
+ const selectedProgramSummary = useMemo(() => {
+ if (!fwImportProgramId) return null
+ return (frameworkProgramsList || []).find((p) => String(p.id) === String(fwImportProgramId))
+ }, [frameworkProgramsList, fwImportProgramId])
+
+ const toggleId = (list, setList, id) => {
+ const s = String(id)
+ setList((prev) => (prev.includes(s) ? prev.filter((x) => x !== s) : [...prev, s]))
+ }
+
if (!open) return null
return (
@@ -48,7 +92,7 @@ export default function TrainingPlanningFrameworkImportModal({
background: 'var(--surface)',
borderRadius: '12px',
padding: 'clamp(14px, 3vw, 1.75rem)',
- maxWidth: 'min(620px, 100%)',
+ maxWidth: 'min(680px, 100%)',
width: '100%',
maxHeight: '90vh',
overflowY: 'auto',
@@ -60,9 +104,104 @@ export default function TrainingPlanningFrameworkImportModal({
Wähle ein Trainingsrahmenprogramm und eine oder mehrere Sessions. Pro Session entsteht eine{' '}
eigene geplante Einheit in der aktuellen Gruppe (Kopie des Ablaufs). Die{' '}
- Verknüpfung zum Rahmen-Slot wird gespeichert, damit die Herkunft sichtbar bleibt.
+ Verknüpfung zum Rahmen-Slot bleibt sichtbar.
+ {filteredPrograms.length} von {frameworkProgramsList.length} Rahmenprogramm(en) sichtbar.
+ Entwicklungsziele sind freie Texte — die Suche durchsucht auch Ziel-Titel.
+