diff --git a/frontend/src/app.css b/frontend/src/app.css
index 343fbee..bd4581c 100644
--- a/frontend/src/app.css
+++ b/frontend/src/app.css
@@ -5854,12 +5854,16 @@ html.modal-scroll-locked .app-main {
border-color: var(--border2);
}
-/* Rahmenprogramm-Filter auf Übersichtsseite */
-.fw-prog-filter-block--list {
+/* Rahmenprogramm-/Modul-Filter auf Übersichtsseite */
+.fw-prog-filter-block--list,
+.planning-list-filter-bar {
margin-bottom: 1rem;
}
-.fw-prog-filter-block--list .fw-import-filter-panel {
- margin-top: 0.75rem;
+.planning-list-filter-bar__search {
+ margin-bottom: 0.75rem;
+}
+.planning-list-filter-bar .fw-import-results-bar {
+ margin-top: 0;
}
/* —— Rahmenprogramm-Bibliothek (Liste) —— */
diff --git a/frontend/src/components/planning/FrameworkProgramsFilterBlock.jsx b/frontend/src/components/planning/FrameworkProgramsFilterBlock.jsx
index 983abab..4e77016 100644
--- a/frontend/src/components/planning/FrameworkProgramsFilterBlock.jsx
+++ b/frontend/src/components/planning/FrameworkProgramsFilterBlock.jsx
@@ -1,22 +1,20 @@
-import React, { useMemo } from 'react'
+import React, { useEffect, useMemo, useState } from 'react'
import {
collectDistinctSessionDurationsMinutes,
EMPTY_FRAMEWORK_IMPORT_FILTERS,
filterFrameworkPrograms,
hasActiveFrameworkImportFilters,
- summarizeFrameworkImportFilters,
} from '../../utils/frameworkProgramListHelpers'
-import { formatDurationDisplay } from '../../utils/trainingDurationUtils'
+import { buildPlanningArtifactFilterChips } from '../../utils/planningArtifactFilterChips'
+import PlanningArtifactFilterModal from './PlanningArtifactFilterModal'
/**
- * Gemeinsamer Filter für Rahmenprogramm-Liste und Import-Dialog.
+ * Filter-Leiste für Rahmenprogramm-Liste und Import-Dialog (UX wie Übungsliste).
*/
export default function FrameworkProgramsFilterBlock({
programs = [],
filters,
onFiltersChange,
- panelOpen = true,
- onPanelOpenChange,
catalogFocusAreas = [],
catalogTrainingTypes = [],
catalogTargetGroups = [],
@@ -26,11 +24,11 @@ export default function FrameworkProgramsFilterBlock({
durationRadioName = 'fw-duration-mode',
showHint = true,
className = '',
+ searchPlaceholder = 'Suche (Titel, Ziele, Katalog) …',
+ filterModalTitle = 'Rahmenprogramme filtern',
+ resultLabel = 'Rahmenprogramm',
}) {
- const distinctDurations = useMemo(
- () => collectDistinctSessionDurationsMinutes(programs),
- [programs]
- )
+ const [filterModalOpen, setFilterModalOpen] = useState(false)
const matchCount = useMemo(
() => filterFrameworkPrograms(programs, filters, skillSummaries).length,
@@ -39,319 +37,143 @@ export default function FrameworkProgramsFilterBlock({
const totalCount = (programs || []).length
const filterActive = hasActiveFrameworkImportFilters(filters)
- const filterSummaryParts = useMemo(
- () =>
- summarizeFrameworkImportFilters(filters, {
- focusAreas: catalogFocusAreas,
- trainingTypes: catalogTrainingTypes,
- targetGroups: catalogTargetGroups,
- skills: catalogSkills,
- }),
- [filters, catalogFocusAreas, catalogTrainingTypes, catalogTargetGroups, catalogSkills]
- )
- const updateFilter = (patch) => onFiltersChange((prev) => ({ ...prev, ...patch }))
+ const filterChips = useMemo(
+ () =>
+ buildPlanningArtifactFilterChips({
+ filters,
+ setFilters: onFiltersChange,
+ catalogs: {
+ focusAreas: catalogFocusAreas,
+ trainingTypes: catalogTrainingTypes,
+ targetGroups: catalogTargetGroups,
+ skills: catalogSkills,
+ },
+ artifactType: 'framework_program',
+ emptyFilters: EMPTY_FRAMEWORK_IMPORT_FILTERS,
+ }),
+ [
+ filters,
+ onFiltersChange,
+ catalogFocusAreas,
+ catalogTrainingTypes,
+ catalogTargetGroups,
+ catalogSkills,
+ ]
+ )
const clearFilters = () => onFiltersChange({ ...EMPTY_FRAMEWORK_IMPORT_FILTERS })
- const toggleId = (key, id) => {
- const s = String(id)
- onFiltersChange((prev) => {
- const cur = prev[key] || []
- const next = cur.includes(s) ? cur.filter((x) => x !== s) : [...cur, s]
- return { ...prev, [key]: next }
- })
- }
+ useEffect(() => {
+ if (!filterModalOpen) return undefined
+ const onKey = (e) => {
+ if (e.key === 'Escape') setFilterModalOpen(false)
+ }
+ document.addEventListener('keydown', onKey)
+ return () => document.removeEventListener('keydown', onKey)
+ }, [filterModalOpen])
- const togglePanel = () => {
- if (onPanelOpenChange) onPanelOpenChange(!panelOpen)
- }
+ const resultPlural = totalCount === 1 ? resultLabel : `${resultLabel}e`
return (
-
+
+
+
Suche
+
onFiltersChange((prev) => ({ ...prev, query: e.target.value }))}
+ placeholder={searchPlaceholder}
+ disabled={disabled}
+ enterKeyHint="search"
+ />
+
+
+ setFilterModalOpen(true)}
+ >
+ Filter
+ {filterChips.length > 0 ? (
+
+ {filterChips.length}
+
+ ) : null}
+
+ {filterChips.length > 0 ? (
+
+ Alle entfernen
+
+ ) : null}
+
+
+ {filterChips.length > 0 ? (
+
+ {filterChips.map((c) => (
+ c.onRemove()}
+ >
+ {c.label}
+
+ ×
+
+
+ ))}
+
+ ) : null}
+ {showHint ? (
+
+ Fachliche Filter über „Filter“ — zwischen Feldern UND. Fähigkeiten vergleichen nur unter
+ Rahmenprogrammen.
+
+ ) : null}
+
+
{matchCount}
{' '}
- von {totalCount} Rahmenprogramm{totalCount === 1 ? '' : 'en'}
+ von {totalCount} {resultPlural}
{matchCount === 0 && totalCount > 0 ? (
— kein Treffer
) : null}
-
- {filterActive ? (
-
- Filter aktiv
-
- ) : null}
- {filterActive ? (
-
- Filter zurücksetzen
-
- ) : null}
- {onPanelOpenChange ? (
-
- {panelOpen ? 'Filter einklappen' : 'Filter anzeigen'}
-
- ) : null}
-
+ {filterActive ? (
+
+ Filter aktiv
+
+ ) : null}
- {!panelOpen && filterActive && filterSummaryParts.length > 0 ? (
-
- {filterSummaryParts.map((part) => (
-
- {part}
-
- ))}
-
- ) : null}
-
- {panelOpen ? (
-
-
-
- Suche (Titel, Ziele, Katalog)
- updateFilter({ query: e.target.value })}
- placeholder="z. B. Gürtel, Koordination …"
- disabled={disabled}
- />
-
-
-
- Ziel-Session-Dauer
-
- {[
- { id: 'any', label: 'Alle' },
- { id: 'range', label: 'Zeitspanne' },
- { id: 'preset', label: 'Vorhandene Zeiten' },
- ].map((opt) => (
-
-
- updateFilter({
- durationMode: opt.id,
- ...(opt.id === 'any'
- ? { durationRangeFrom: '', durationRangeTo: '', durationPresetMin: null }
- : {}),
- })
- }
- />
- {opt.label}
-
- ))}
-
-
- {filters.durationMode === 'range' ? (
-
- ) : null}
-
- {filters.durationMode === 'preset' ? (
- distinctDurations.length === 0 ? (
-
- In der Bibliothek sind noch keine Session-Dauern hinterlegt. Nutze „Zeitspanne“ oder lege
- Dauer pro Session im Rahmenprogramm fest.
-
- ) : (
-
- {distinctDurations.map((min) => {
- const on = filters.durationPresetMin === min
- return (
-
- updateFilter({
- durationMode: 'preset',
- durationPresetMin: on ? null : min,
- durationRangeFrom: '',
- durationRangeTo: '',
- })
- }
- >
- {formatDurationDisplay(min)}
-
- )
- })}
-
- )
- ) : null}
-
-
- {catalogFocusAreas.length > 0 ? (
-
-
Fokusbereich
-
- {catalogFocusAreas.map((fa) => (
-
- toggleId('focusAreaIds', fa.id)}
- disabled={disabled}
- />
- {fa.name}
-
- ))}
-
-
- ) : null}
-
- {catalogTrainingTypes.length > 0 ? (
-
-
Trainingsart
-
- {catalogTrainingTypes.map((t) => (
-
- toggleId('trainingTypeIds', t.id)}
- disabled={disabled}
- />
- {t.name}
-
- ))}
-
-
- ) : null}
-
- {catalogTargetGroups.length > 0 ? (
-
-
Zielgruppe
-
- {catalogTargetGroups.map((tg) => (
-
- toggleId('targetGroupIds', tg.id)}
- disabled={disabled}
- />
- {tg.name}
-
- ))}
-
-
- ) : null}
-
- {catalogSkills.length > 0 ? (
-
-
Fähigkeiten (Rahmenprogramme)
-
- Filtert nach Trainingsgewicht relativ zum stärksten sichtbaren Rahmenprogramm je Fähigkeit — nur
- unter Rahmenprogrammen, nicht gegen Module.
-
-
- {catalogSkills.map((sk) => (
-
- toggleId('skillIds', sk.id)}
- disabled={disabled}
- />
- {sk.name}
-
- ))}
-
- {(filters.skillIds || []).length > 0 ? (
-
-
- Mindest-Anteil am Rahmenprogramm-Maximum
-
- updateFilter({ skillMinClubPercent: Number(e.target.value) || 0 })
- }
- >
- Kein Minimum (nur markieren)
- mind. 25%
- mind. 50%
- mind. 75%
-
-
-
- Sortierung
- updateFilter({ skillSort: e.target.value })}
- >
- Bibliotheks-Reihenfolge
- Stärkste gewählte Fähigkeit zuerst
-
-
-
- ) : null}
-
- ) : null}
-
- {showHint ? (
-
- Entwicklungsziele sind freie Texte — die Suche durchsucht auch Ziel-Titel. Bei der Dauer werden nur
- Programme mit hinterlegter Session-Dauer berücksichtigt.
-
- ) : null}
-
- ) : null}
+
setFilterModalOpen(false)}
+ filters={filters}
+ onFiltersChange={onFiltersChange}
+ artifactType="framework_program"
+ programs={programs}
+ catalogFocusAreas={catalogFocusAreas}
+ catalogTrainingTypes={catalogTrainingTypes}
+ catalogTargetGroups={catalogTargetGroups}
+ catalogSkills={catalogSkills}
+ durationRadioName={durationRadioName}
+ onResetAll={clearFilters}
+ disabled={disabled}
+ title={filterModalTitle}
+ showCatalogFilters
+ showDurationFilters
+ />
)
}
diff --git a/frontend/src/components/planning/PlanningArtifactFilterModal.jsx b/frontend/src/components/planning/PlanningArtifactFilterModal.jsx
new file mode 100644
index 0000000..d2eadc5
--- /dev/null
+++ b/frontend/src/components/planning/PlanningArtifactFilterModal.jsx
@@ -0,0 +1,275 @@
+import React, { useMemo } from 'react'
+import { collectDistinctSessionDurationsMinutes } from '../../utils/frameworkProgramListHelpers'
+import { formatDurationDisplay } from '../../utils/trainingDurationUtils'
+import PlanningSkillFilterSection from './PlanningSkillFilterSection'
+
+/**
+ * Filter-Modal für Rahmenprogramme / Trainingsmodule (UX wie ExerciseListFilterModal).
+ */
+export default function PlanningArtifactFilterModal({
+ open,
+ onClose,
+ filters,
+ onFiltersChange,
+ artifactType = 'framework_program',
+ programs = [],
+ catalogFocusAreas = [],
+ catalogTrainingTypes = [],
+ catalogTargetGroups = [],
+ catalogSkills = [],
+ durationRadioName = 'planning-duration-mode',
+ onResetAll,
+ disabled = false,
+ showCatalogFilters = true,
+ showDurationFilters = true,
+ title = 'Filtern',
+}) {
+ const distinctDurations = useMemo(
+ () => (showDurationFilters ? collectDistinctSessionDurationsMinutes(programs) : []),
+ [programs, showDurationFilters]
+ )
+
+ if (!open) return null
+
+ const updateFilter = (patch) => onFiltersChange((prev) => ({ ...prev, ...patch }))
+
+ const toggleId = (key, id) => {
+ const s = String(id)
+ onFiltersChange((prev) => {
+ const cur = prev[key] || []
+ const next = cur.includes(s) ? cur.filter((x) => x !== s) : [...cur, s]
+ return { ...prev, [key]: next }
+ })
+ }
+
+ const artifactLabel =
+ artifactType === 'training_module'
+ ? 'Trainingsmodule'
+ : artifactType === 'framework_program'
+ ? 'Rahmenprogramme'
+ : 'Einträge'
+
+ return (
+
{
+ if (e.target === e.currentTarget) onClose()
+ }}
+ >
+
e.stopPropagation()}
+ >
+
+
+ {title}
+
+
+ Schließen
+
+
+
+
+ Zwischen den Bereichen gilt UND . Mehrere Katalog-Werte innerhalb eines Feldes
+ bedeuten ODER . Fähigkeiten filtern nach Trainingsgewicht — nur unter sichtbaren{' '}
+ {artifactLabel}.
+
+
+ {showCatalogFilters ? (
+
+ Katalog
+
+ {catalogFocusAreas.length > 0 ? (
+
+
Fokusbereich
+
+ {catalogFocusAreas.map((fa) => (
+
+ toggleId('focusAreaIds', fa.id)}
+ disabled={disabled}
+ />
+ {fa.name}
+
+ ))}
+
+
+ ) : null}
+
+ {catalogTrainingTypes.length > 0 ? (
+
+
Trainingsart
+
+ {catalogTrainingTypes.map((t) => (
+
+ toggleId('trainingTypeIds', t.id)}
+ disabled={disabled}
+ />
+ {t.name}
+
+ ))}
+
+
+ ) : null}
+
+ {catalogTargetGroups.length > 0 ? (
+
+
Zielgruppe
+
+ {catalogTargetGroups.map((tg) => (
+
+ toggleId('targetGroupIds', tg.id)}
+ disabled={disabled}
+ />
+ {tg.name}
+
+ ))}
+
+
+ ) : null}
+
+
+ ) : null}
+
+ {showDurationFilters ? (
+
+ Session-Dauer
+
+
+ {[
+ { id: 'any', label: 'Alle' },
+ { id: 'range', label: 'Zeitspanne' },
+ { id: 'preset', label: 'Vorhandene Zeiten' },
+ ].map((opt) => (
+
+
+ updateFilter({
+ durationMode: opt.id,
+ ...(opt.id === 'any'
+ ? { durationRangeFrom: '', durationRangeTo: '', durationPresetMin: null }
+ : {}),
+ })
+ }
+ />
+ {opt.label}
+
+ ))}
+
+
+ {filters.durationMode === 'range' ? (
+
+ ) : null}
+
+ {filters.durationMode === 'preset' ? (
+ distinctDurations.length === 0 ? (
+
+ Noch keine Session-Dauern hinterlegt.
+
+ ) : (
+
+ {distinctDurations.map((min) => {
+ const on = filters.durationPresetMin === min
+ return (
+
+ updateFilter({
+ durationMode: 'preset',
+ durationPresetMin: on ? null : min,
+ durationRangeFrom: '',
+ durationRangeTo: '',
+ })
+ }
+ >
+ {formatDurationDisplay(min)}
+
+ )
+ })}
+
+ )
+ ) : null}
+
+
+ ) : null}
+
+ {catalogSkills.length > 0 ? (
+
+ Fähigkeit und Trainingsgewicht
+ updateFilter({ skillIds: v })}
+ skillMinClubPercent={filters.skillMinClubPercent ?? 0}
+ onSkillMinClubPercentChange={(v) => updateFilter({ skillMinClubPercent: v })}
+ skillSort={filters.skillSort || 'title'}
+ onSkillSortChange={(v) => updateFilter({ skillSort: v })}
+ skillsCatalog={catalogSkills}
+ disabled={disabled}
+ />
+
+ ) : null}
+
+
+
+ Alle Filter zurücksetzen
+
+
+ Fertig
+
+
+
+
+ )
+}
diff --git a/frontend/src/components/planning/PlanningSkillFilterSection.jsx b/frontend/src/components/planning/PlanningSkillFilterSection.jsx
new file mode 100644
index 0000000..16fb796
--- /dev/null
+++ b/frontend/src/components/planning/PlanningSkillFilterSection.jsx
@@ -0,0 +1,69 @@
+import React from 'react'
+import SkillTreeMultiSelect from '../SkillTreeMultiSelect'
+import { peerCorpusCountLabel } from '../../utils/skillProfileListHelpers'
+
+/**
+ * Fähigkeiten-Filter für Planungsartefakte (Rahmenprogramme, Module).
+ * Semantik wie Übungsliste (SkillTreeMultiSelect), aber mit Peer-Prozent statt Stufen.
+ */
+export default function PlanningSkillFilterSection({
+ artifactType = 'framework_program',
+ skillIds = [],
+ onSkillIdsChange,
+ skillMinClubPercent = 0,
+ onSkillMinClubPercentChange,
+ skillSort = 'title',
+ onSkillSortChange,
+ skillsCatalog = [],
+ disabled = false,
+}) {
+ const peerLabel = peerCorpusCountLabel(artifactType)
+ const peerMaxLabel =
+ artifactType === 'framework_program' ? 'Rahmenprogramm-Maximum' : `${peerLabel}-Maximum`
+
+ return (
+
+
Fähigkeit
+
+
+ Trainingsgewicht relativ zum stärksten sichtbaren Eintrag unter {peerLabel} — nicht gemischt mit
+ anderen Planungs-Artefakttypen.
+
+ {(skillIds || []).length > 0 ? (
+
+
+ Mindest-Anteil am {peerMaxLabel}
+ onSkillMinClubPercentChange(Number(e.target.value) || 0)}
+ >
+ Kein Minimum (nur markieren)
+ mind. 25%
+ mind. 50%
+ mind. 75%
+
+
+
+ Sortierung
+ onSkillSortChange(e.target.value)}
+ >
+ Bibliotheks-Reihenfolge
+ Stärkste gewählte Fähigkeit zuerst
+
+
+
+ ) : null}
+
+ )
+}
diff --git a/frontend/src/components/planning/TrainingModulesFilterBlock.jsx b/frontend/src/components/planning/TrainingModulesFilterBlock.jsx
new file mode 100644
index 0000000..d5a9daa
--- /dev/null
+++ b/frontend/src/components/planning/TrainingModulesFilterBlock.jsx
@@ -0,0 +1,148 @@
+import React, { useEffect, useMemo, useState } from 'react'
+import {
+ EMPTY_TRAINING_MODULE_FILTERS,
+ filterTrainingModules,
+ hasActiveTrainingModuleFilters,
+} from '../../utils/trainingModuleListHelpers'
+import { buildPlanningArtifactFilterChips } from '../../utils/planningArtifactFilterChips'
+import PlanningArtifactFilterModal from './PlanningArtifactFilterModal'
+
+/**
+ * Filter-Leiste für Trainingsmodule (UX wie Übungsliste / Rahmenprogramme).
+ */
+export default function TrainingModulesFilterBlock({
+ modules = [],
+ filters,
+ onFiltersChange,
+ catalogSkills = [],
+ skillSummaries = null,
+ disabled = false,
+ className = '',
+}) {
+ const [filterModalOpen, setFilterModalOpen] = useState(false)
+
+ const matchCount = useMemo(
+ () => filterTrainingModules(modules, filters, skillSummaries).length,
+ [modules, filters, skillSummaries]
+ )
+
+ const totalCount = (modules || []).length
+ const filterActive = hasActiveTrainingModuleFilters(filters)
+
+ const filterChips = useMemo(
+ () =>
+ buildPlanningArtifactFilterChips({
+ filters,
+ setFilters: onFiltersChange,
+ catalogs: { skills: catalogSkills },
+ artifactType: 'training_module',
+ emptyFilters: EMPTY_TRAINING_MODULE_FILTERS,
+ }),
+ [filters, onFiltersChange, catalogSkills]
+ )
+
+ const clearFilters = () => onFiltersChange({ ...EMPTY_TRAINING_MODULE_FILTERS })
+
+ useEffect(() => {
+ if (!filterModalOpen) return undefined
+ const onKey = (e) => {
+ if (e.key === 'Escape') setFilterModalOpen(false)
+ }
+ document.addEventListener('keydown', onKey)
+ return () => document.removeEventListener('keydown', onKey)
+ }, [filterModalOpen])
+
+ return (
+
+
+
Suche
+
onFiltersChange((prev) => ({ ...prev, query: e.target.value }))}
+ placeholder="Titel oder Kurzbeschreibung …"
+ disabled={disabled}
+ enterKeyHint="search"
+ />
+
+
+ setFilterModalOpen(true)}
+ >
+ Filter
+ {filterChips.length > 0 ? (
+
+ {filterChips.length}
+
+ ) : null}
+
+ {filterChips.length > 0 ? (
+
+ Alle entfernen
+
+ ) : null}
+
+
+ {filterChips.length > 0 ? (
+
+ {filterChips.map((c) => (
+ c.onRemove()}
+ >
+ {c.label}
+
+ ×
+
+
+ ))}
+
+ ) : null}
+
+ Fachliche Filter über „Filter“. Fähigkeiten vergleichen nur unter sichtbaren Modulen.
+
+
+
+
+
+ {matchCount}
+
+ {' '}
+ von {totalCount} Modul{totalCount === 1 ? '' : 'en'}
+
+ {matchCount === 0 && totalCount > 0 ? (
+ — kein Treffer
+ ) : null}
+
+ {filterActive ? (
+
+ Filter aktiv
+
+ ) : null}
+
+
+
setFilterModalOpen(false)}
+ filters={filters}
+ onFiltersChange={onFiltersChange}
+ artifactType="training_module"
+ catalogSkills={catalogSkills}
+ onResetAll={clearFilters}
+ disabled={disabled}
+ title="Trainingsmodule filtern"
+ showCatalogFilters={false}
+ showDurationFilters={false}
+ />
+
+ )
+}
diff --git a/frontend/src/components/planning/TrainingPlanningFrameworkImportModal.jsx b/frontend/src/components/planning/TrainingPlanningFrameworkImportModal.jsx
index cbadef2..1eaa060 100644
--- a/frontend/src/components/planning/TrainingPlanningFrameworkImportModal.jsx
+++ b/frontend/src/components/planning/TrainingPlanningFrameworkImportModal.jsx
@@ -35,12 +35,10 @@ export default function TrainingPlanningFrameworkImportModal({
onClose,
}) {
const [filters, setFilters] = useState(() => ({ ...EMPTY_FRAMEWORK_IMPORT_FILTERS }))
- const [filterPanelOpen, setFilterPanelOpen] = useState(true)
useEffect(() => {
if (!open) {
setFilters({ ...EMPTY_FRAMEWORK_IMPORT_FILTERS })
- setFilterPanelOpen(true)
}
}, [open])
@@ -80,13 +78,14 @@ export default function TrainingPlanningFrameworkImportModal({
programs={frameworkProgramsList}
filters={filters}
onFiltersChange={setFilters}
- panelOpen={filterPanelOpen}
- onPanelOpenChange={setFilterPanelOpen}
catalogFocusAreas={catalogFocusAreas}
catalogTrainingTypes={catalogTrainingTypes}
catalogTargetGroups={catalogTargetGroups}
disabled={fwImportSubmitting}
durationRadioName="fw-duration-mode"
+ showHint={false}
+ searchPlaceholder="Rahmenprogramm suchen …"
+ filterModalTitle="Rahmenprogramme filtern"
/>
diff --git a/frontend/src/pages/TrainingFrameworkProgramsListPage.jsx b/frontend/src/pages/TrainingFrameworkProgramsListPage.jsx
index 048e2a6..34e5e04 100644
--- a/frontend/src/pages/TrainingFrameworkProgramsListPage.jsx
+++ b/frontend/src/pages/TrainingFrameworkProgramsListPage.jsx
@@ -25,7 +25,6 @@ export default function TrainingFrameworkProgramsListPage() {
const [catalogTargetGroups, setCatalogTargetGroups] = useState([])
const [catalogSkills, setCatalogSkills] = useState([])
const [filters, setFilters] = useState(() => ({ ...EMPTY_FRAMEWORK_IMPORT_FILTERS }))
- const [filterPanelOpen, setFilterPanelOpen] = useState(true)
const [skillSummaries, setSkillSummaries] = useState({})
const [summariesLoading, setSummariesLoading] = useState(false)
const [profileModal, setProfileModal] = useState(null)
@@ -45,7 +44,7 @@ export default function TrainingFrameworkProgramsListPage() {
api.listFocusAreas({ status: 'active' }),
api.listTrainingTypes({ status: 'active' }),
api.listTargetGroups({ status: 'active' }),
- api.listSkills({ status: 'active' }),
+ api.listSkillsCatalog({ status: 'active' }),
])
setRows(Array.isArray(list) ? list : [])
setCatalogFocusAreas(Array.isArray(fa) ? fa : [])
@@ -162,8 +161,6 @@ export default function TrainingFrameworkProgramsListPage() {
programs={rows}
filters={filters}
onFiltersChange={setFilters}
- panelOpen={filterPanelOpen}
- onPanelOpenChange={setFilterPanelOpen}
catalogFocusAreas={catalogFocusAreas}
catalogTrainingTypes={catalogTrainingTypes}
catalogTargetGroups={catalogTargetGroups}
diff --git a/frontend/src/pages/TrainingModulesListPage.jsx b/frontend/src/pages/TrainingModulesListPage.jsx
index a5b5e60..4f0492e 100644
--- a/frontend/src/pages/TrainingModulesListPage.jsx
+++ b/frontend/src/pages/TrainingModulesListPage.jsx
@@ -1,11 +1,17 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react'
import api from '../utils/api'
import NavStateLink from '../components/NavStateLink'
+import TrainingModulesFilterBlock from '../components/planning/TrainingModulesFilterBlock'
import SkillProfileCompact from '../components/skills/SkillProfileCompact'
import SkillProfileFullModal from '../components/skills/SkillProfileFullModal'
import { useAuth } from '../context/AuthContext'
import { getTenantClubDependencyKey } from '../utils/activeClub'
import { buildTrainingModulesListReturnContext } from '../utils/navReturnContext'
+import {
+ EMPTY_TRAINING_MODULE_FILTERS,
+ filterTrainingModules,
+ hasActiveTrainingModuleFilters,
+} from '../utils/trainingModuleListHelpers'
export default function TrainingModulesListPage() {
const { user } = useAuth()
@@ -14,19 +20,32 @@ export default function TrainingModulesListPage() {
const [rows, setRows] = useState([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState('')
+ const [catalogSkills, setCatalogSkills] = useState([])
+ const [filters, setFilters] = useState(() => ({ ...EMPTY_TRAINING_MODULE_FILTERS }))
const [skillSummaries, setSkillSummaries] = useState({})
const [summariesLoading, setSummariesLoading] = useState(false)
const [profileModal, setProfileModal] = useState(null)
+ const filteredRows = useMemo(
+ () => filterTrainingModules(rows, filters, skillSummaries),
+ [rows, filters, skillSummaries]
+ )
+ const filterActive = hasActiveTrainingModuleFilters(filters)
+
const load = useCallback(async () => {
setLoading(true)
setError('')
try {
- const list = await api.listTrainingModules()
+ const [list, skills] = await Promise.all([
+ api.listTrainingModules(),
+ api.listSkillsCatalog({ status: 'active' }),
+ ])
setRows(Array.isArray(list) ? list : [])
+ setCatalogSkills(Array.isArray(skills) ? skills : [])
} catch (e) {
setError(e.message || 'Laden fehlgeschlagen')
setRows([])
+ setCatalogSkills([])
} finally {
setLoading(false)
}
@@ -86,7 +105,8 @@ export default function TrainingModulesListPage() {
Trainingsmodule
- Wiederverwendbare Übungsfolgen für die Trainingsplanung. Prozentwerte vergleichen Module nur unter sichtbaren Modulen.
+ Wiederverwendbare Übungsfolgen für die Trainingsplanung. Prozentwerte vergleichen Module nur unter
+ sichtbaren Modulen.
Noch keine Module angelegt.
) : (
-