From 9d122d4808fe925c33995e47e38049f6906df903 Mon Sep 17 00:00:00 2001 From: Lars Date: Wed, 20 May 2026 16:05:18 +0200 Subject: [PATCH] Enhance Training Framework Programs List with Filtering and Styling Improvements - Introduced a new filter block for training framework programs, allowing users to refine their selections based on focus areas, training types, and target groups. - Updated the TrainingFrameworkProgramsListPage to integrate the filter block and manage filter states effectively. - Enhanced CSS styles for the filter block and program list, improving layout and spacing for better user experience. - Removed unused filter-related logic from the TrainingPlanningFrameworkImportModal, streamlining the component's functionality. --- frontend/src/app.css | 8 + .../planning/FrameworkProgramsFilterBlock.jsx | 299 ++++++++++++++++++ .../TrainingPlanningFrameworkImportModal.jsx | 270 +--------------- .../TrainingFrameworkProgramsListPage.jsx | 59 +++- 4 files changed, 376 insertions(+), 260 deletions(-) create mode 100644 frontend/src/components/planning/FrameworkProgramsFilterBlock.jsx diff --git a/frontend/src/app.css b/frontend/src/app.css index 783627b..13e2801 100644 --- a/frontend/src/app.css +++ b/frontend/src/app.css @@ -5372,6 +5372,14 @@ html.modal-scroll-locked .app-main { border-color: var(--border2); } +/* Rahmenprogramm-Filter auf Übersichtsseite */ +.fw-prog-filter-block--list { + margin-bottom: 1rem; +} +.fw-prog-filter-block--list .fw-import-filter-panel { + margin-top: 0.75rem; +} + /* Liste Rahmenprogramme: Abstand nur über gap (kein .card+.card zwischen li) */ .framework-programs-list { list-style: none; diff --git a/frontend/src/components/planning/FrameworkProgramsFilterBlock.jsx b/frontend/src/components/planning/FrameworkProgramsFilterBlock.jsx new file mode 100644 index 0000000..0f318c0 --- /dev/null +++ b/frontend/src/components/planning/FrameworkProgramsFilterBlock.jsx @@ -0,0 +1,299 @@ +import React, { useMemo } from 'react' +import { + collectDistinctSessionDurationsMinutes, + EMPTY_FRAMEWORK_IMPORT_FILTERS, + filterFrameworkPrograms, + hasActiveFrameworkImportFilters, + summarizeFrameworkImportFilters, +} from '../../utils/frameworkProgramListHelpers' +import { formatDurationDisplay } from '../../utils/trainingDurationUtils' + +/** + * Gemeinsamer Filter für Rahmenprogramm-Liste und Import-Dialog. + */ +export default function FrameworkProgramsFilterBlock({ + programs = [], + filters, + onFiltersChange, + panelOpen = true, + onPanelOpenChange, + catalogFocusAreas = [], + catalogTrainingTypes = [], + catalogTargetGroups = [], + disabled = false, + durationRadioName = 'fw-duration-mode', + showHint = true, + className = '', +}) { + const distinctDurations = useMemo( + () => collectDistinctSessionDurationsMinutes(programs), + [programs] + ) + + const matchCount = useMemo( + () => filterFrameworkPrograms(programs, filters).length, + [programs, filters] + ) + + const totalCount = (programs || []).length + const filterActive = hasActiveFrameworkImportFilters(filters) + const filterSummaryParts = useMemo( + () => + summarizeFrameworkImportFilters(filters, { + focusAreas: catalogFocusAreas, + trainingTypes: catalogTrainingTypes, + targetGroups: catalogTargetGroups, + }), + [filters, catalogFocusAreas, catalogTrainingTypes, catalogTargetGroups] + ) + + const updateFilter = (patch) => onFiltersChange((prev) => ({ ...prev, ...patch })) + + 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 } + }) + } + + const togglePanel = () => { + if (onPanelOpenChange) onPanelOpenChange(!panelOpen) + } + + return ( +
+
+
+ {matchCount} + + {' '} + von {totalCount} Rahmenprogramm{totalCount === 1 ? '' : 'en'} + + {matchCount === 0 && totalCount > 0 ? ( + — kein Treffer + ) : null} +
+
+ {filterActive ? ( + + Filter aktiv + + ) : null} + {filterActive ? ( + + ) : null} + {onPanelOpenChange ? ( + + ) : null} +
+
+ + {!panelOpen && filterActive && filterSummaryParts.length > 0 ? ( + + ) : null} + + {panelOpen ? ( +
+
+
+ + 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) => ( + + ))} +
+ + {filters.durationMode === 'range' ? ( +
+
+ + + updateFilter({ durationMode: 'range', durationRangeFrom: e.target.value }) + } + placeholder="z. B. 60" + disabled={disabled} + /> +
+
+ + + updateFilter({ durationMode: 'range', durationRangeTo: e.target.value }) + } + placeholder="z. B. 90" + disabled={disabled} + /> +
+
+ ) : 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 ( + + ) + })} +
+ ) + ) : null} +
+ + {catalogFocusAreas.length > 0 ? ( +
+ Fokusbereich +
+ {catalogFocusAreas.map((fa) => ( + + ))} +
+
+ ) : null} + + {catalogTrainingTypes.length > 0 ? ( +
+ Trainingsart +
+ {catalogTrainingTypes.map((t) => ( + + ))} +
+
+ ) : null} + + {catalogTargetGroups.length > 0 ? ( +
+ Zielgruppe +
+ {catalogTargetGroups.map((tg) => ( + + ))} +
+
+ ) : 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} +
+ ) +} diff --git a/frontend/src/components/planning/TrainingPlanningFrameworkImportModal.jsx b/frontend/src/components/planning/TrainingPlanningFrameworkImportModal.jsx index 8743478..cbadef2 100644 --- a/frontend/src/components/planning/TrainingPlanningFrameworkImportModal.jsx +++ b/frontend/src/components/planning/TrainingPlanningFrameworkImportModal.jsx @@ -1,12 +1,10 @@ import React, { useEffect, useMemo, useState } from 'react' +import FrameworkProgramsFilterBlock from './FrameworkProgramsFilterBlock' import { - collectDistinctSessionDurationsMinutes, EMPTY_FRAMEWORK_IMPORT_FILTERS, filterFrameworkPrograms, frameworkProgramOptionLabel, frameworkSessionDurationLabel, - hasActiveFrameworkImportFilters, - summarizeFrameworkImportFilters, } from '../../utils/frameworkProgramListHelpers' import { formatDurationDisplay } from '../../utils/trainingDurationUtils' @@ -46,28 +44,11 @@ export default function TrainingPlanningFrameworkImportModal({ } }, [open]) - const distinctDurations = useMemo( - () => collectDistinctSessionDurationsMinutes(frameworkProgramsList), - [frameworkProgramsList] - ) - const filteredPrograms = useMemo( () => filterFrameworkPrograms(frameworkProgramsList, filters), [frameworkProgramsList, filters] ) - const filterActive = hasActiveFrameworkImportFilters(filters) - const filterSummaryParts = useMemo( - () => - summarizeFrameworkImportFilters(filters, { - focusAreas: catalogFocusAreas, - trainingTypes: catalogTrainingTypes, - targetGroups: catalogTargetGroups, - }), - [filters, catalogFocusAreas, catalogTrainingTypes, catalogTargetGroups] - ) - - const totalCount = (frameworkProgramsList || []).length const matchCount = filteredPrograms.length useEffect(() => { @@ -81,19 +62,6 @@ export default function TrainingPlanningFrameworkImportModal({ return (frameworkProgramsList || []).find((p) => String(p.id) === String(fwImportProgramId)) }, [frameworkProgramsList, fwImportProgramId]) - const updateFilter = (patch) => setFilters((prev) => ({ ...prev, ...patch })) - - const clearFilters = () => setFilters({ ...EMPTY_FRAMEWORK_IMPORT_FILTERS }) - - const toggleId = (key, id) => { - const s = String(id) - setFilters((prev) => { - const cur = prev[key] || [] - const next = cur.includes(s) ? cur.filter((x) => x !== s) : [...cur, s] - return { ...prev, [key]: next } - }) - } - if (!open) return null return ( @@ -108,230 +76,18 @@ export default function TrainingPlanningFrameworkImportModal({ eigene geplante Einheit in der aktuellen Gruppe (Kopie des Ablaufs).

-
-
- {matchCount} - - {' '} - von {totalCount} Rahmenprogramm{totalCount === 1 ? '' : 'en'} - - {matchCount === 0 && totalCount > 0 ? ( - — kein Treffer - ) : null} -
-
- {filterActive ? ( - - Filter aktiv - - ) : null} - {filterActive ? ( - - ) : null} - -
-
- - {!filterPanelOpen && filterActive && filterSummaryParts.length > 0 ? ( - - ) : null} - - {filterPanelOpen ? ( -
-
-
- - updateFilter({ query: e.target.value })} - placeholder="z. B. Gürtel, Koordination …" - disabled={fwImportSubmitting} - /> -
- -
- Ziel-Session-Dauer -
- {[ - { id: 'any', label: 'Alle' }, - { id: 'range', label: 'Zeitspanne' }, - { id: 'preset', label: 'Vorhandene Zeiten' }, - ].map((opt) => ( - - ))} -
- - {filters.durationMode === 'range' ? ( -
-
- - - updateFilter({ durationMode: 'range', durationRangeFrom: e.target.value }) - } - placeholder="z. B. 60" - disabled={fwImportSubmitting} - /> -
-
- - - updateFilter({ durationMode: 'range', durationRangeTo: e.target.value }) - } - placeholder="z. B. 90" - disabled={fwImportSubmitting} - /> -
-
- ) : 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 ( - - ) - })} -
- ) - ) : null} -
- - {catalogFocusAreas.length > 0 ? ( -
- Fokusbereich -
- {catalogFocusAreas.map((fa) => ( - - ))} -
-
- ) : null} - - {catalogTrainingTypes.length > 0 ? ( -
- Trainingsart -
- {catalogTrainingTypes.map((t) => ( - - ))} -
-
- ) : null} - - {catalogTargetGroups.length > 0 ? ( -
- Zielgruppe -
- {catalogTargetGroups.map((tg) => ( - - ))} -
-
- ) : null} -
-

- Entwicklungsziele sind freie Texte — die Suche durchsucht auch Ziel-Titel. Bei der Dauer werden nur - Programme mit hinterlegter Session-Dauer berücksichtigt. -

-
- ) : null} +
) : ( + <> + + + {filteredRows.length === 0 ? ( +
+

+ {filterActive + ? 'Kein Rahmenprogramm passt zu den gewählten Filtern. Passe die Kriterien an oder setze den Filter zurück.' + : 'Keine Einträge.'} +

+
+ ) : (
    - {rows.map((r) => ( + {filteredRows.map((r) => (
  • ))}
+ )} + )} )