Verbesserung Darstellung Rahmenprogramme #42
|
|
@ -5372,6 +5372,14 @@ html.modal-scroll-locked .app-main {
|
||||||
border-color: var(--border2);
|
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) */
|
/* Liste Rahmenprogramme: Abstand nur über gap (kein .card+.card zwischen li) */
|
||||||
.framework-programs-list {
|
.framework-programs-list {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
|
|
|
||||||
|
|
@ -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 (
|
||||||
|
<div className={'fw-prog-filter-block' + (className ? ` ${className}` : '')}>
|
||||||
|
<div className="fw-import-results-bar">
|
||||||
|
<div className="fw-import-results-bar__count">
|
||||||
|
<strong className="fw-import-results-bar__num">{matchCount}</strong>
|
||||||
|
<span>
|
||||||
|
{' '}
|
||||||
|
von {totalCount} Rahmenprogramm{totalCount === 1 ? '' : 'en'}
|
||||||
|
</span>
|
||||||
|
{matchCount === 0 && totalCount > 0 ? (
|
||||||
|
<span className="fw-import-results-bar__warn"> — kein Treffer</span>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
<div className="fw-import-results-bar__actions">
|
||||||
|
{filterActive ? (
|
||||||
|
<span className="fw-import-filter-badge" title={filterSummaryParts.join(' · ')}>
|
||||||
|
Filter aktiv
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
{filterActive ? (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-secondary framework-ctrl framework-ctrl--xs"
|
||||||
|
disabled={disabled}
|
||||||
|
onClick={clearFilters}
|
||||||
|
>
|
||||||
|
Filter zurücksetzen
|
||||||
|
</button>
|
||||||
|
) : null}
|
||||||
|
{onPanelOpenChange ? (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-secondary framework-ctrl framework-ctrl--xs"
|
||||||
|
disabled={disabled}
|
||||||
|
onClick={togglePanel}
|
||||||
|
aria-expanded={panelOpen}
|
||||||
|
>
|
||||||
|
{panelOpen ? 'Filter einklappen' : 'Filter anzeigen'}
|
||||||
|
</button>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{!panelOpen && filterActive && filterSummaryParts.length > 0 ? (
|
||||||
|
<ul className="fw-import-filter-chips" aria-label="Aktive Filter">
|
||||||
|
{filterSummaryParts.map((part) => (
|
||||||
|
<li key={part} className="fw-import-filter-chip">
|
||||||
|
{part}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{panelOpen ? (
|
||||||
|
<div className="fw-import-filter-panel">
|
||||||
|
<div className="fw-import-filter-panel__grid">
|
||||||
|
<div className="form-row fw-import-filter-panel__search">
|
||||||
|
<label className="form-label">Suche (Titel, Ziele, Katalog)</label>
|
||||||
|
<input
|
||||||
|
className="form-input"
|
||||||
|
value={filters.query}
|
||||||
|
onChange={(e) => updateFilter({ query: e.target.value })}
|
||||||
|
placeholder="z. B. Gürtel, Koordination …"
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<fieldset className="fw-import-duration-fieldset">
|
||||||
|
<legend className="form-label">Ziel-Session-Dauer</legend>
|
||||||
|
<div className="fw-import-duration-mode" role="radiogroup" aria-label="Dauer-Filtermodus">
|
||||||
|
{[
|
||||||
|
{ id: 'any', label: 'Alle' },
|
||||||
|
{ id: 'range', label: 'Zeitspanne' },
|
||||||
|
{ id: 'preset', label: 'Vorhandene Zeiten' },
|
||||||
|
].map((opt) => (
|
||||||
|
<label key={opt.id} className="fw-import-duration-mode__opt">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name={durationRadioName}
|
||||||
|
checked={filters.durationMode === opt.id}
|
||||||
|
disabled={disabled || (opt.id === 'preset' && distinctDurations.length === 0)}
|
||||||
|
onChange={() =>
|
||||||
|
updateFilter({
|
||||||
|
durationMode: opt.id,
|
||||||
|
...(opt.id === 'any'
|
||||||
|
? { durationRangeFrom: '', durationRangeTo: '', durationPresetMin: null }
|
||||||
|
: {}),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<span>{opt.label}</span>
|
||||||
|
</label>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{filters.durationMode === 'range' ? (
|
||||||
|
<div className="responsive-grid-2 fw-import-duration-range">
|
||||||
|
<div className="form-row" style={{ marginBottom: 0 }}>
|
||||||
|
<label className="form-label">Von (Minuten)</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min={0}
|
||||||
|
className="form-input"
|
||||||
|
value={filters.durationRangeFrom}
|
||||||
|
onChange={(e) =>
|
||||||
|
updateFilter({ durationMode: 'range', durationRangeFrom: e.target.value })
|
||||||
|
}
|
||||||
|
placeholder="z. B. 60"
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="form-row" style={{ marginBottom: 0 }}>
|
||||||
|
<label className="form-label">Bis (Minuten)</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min={0}
|
||||||
|
className="form-input"
|
||||||
|
value={filters.durationRangeTo}
|
||||||
|
onChange={(e) =>
|
||||||
|
updateFilter({ durationMode: 'range', durationRangeTo: e.target.value })
|
||||||
|
}
|
||||||
|
placeholder="z. B. 90"
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{filters.durationMode === 'preset' ? (
|
||||||
|
distinctDurations.length === 0 ? (
|
||||||
|
<p className="form-sub" style={{ margin: '8px 0 0' }}>
|
||||||
|
In der Bibliothek sind noch keine Session-Dauern hinterlegt. Nutze „Zeitspanne“ oder lege
|
||||||
|
Dauer pro Session im Rahmenprogramm fest.
|
||||||
|
</p>
|
||||||
|
) : (
|
||||||
|
<div className="fw-import-duration-presets">
|
||||||
|
{distinctDurations.map((min) => {
|
||||||
|
const on = filters.durationPresetMin === min
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={min}
|
||||||
|
type="button"
|
||||||
|
className={
|
||||||
|
'btn framework-ctrl framework-ctrl--xs' +
|
||||||
|
(on ? ' fw-import-duration-preset--on' : ' btn-secondary')
|
||||||
|
}
|
||||||
|
disabled={disabled}
|
||||||
|
onClick={() =>
|
||||||
|
updateFilter({
|
||||||
|
durationMode: 'preset',
|
||||||
|
durationPresetMin: on ? null : min,
|
||||||
|
durationRangeFrom: '',
|
||||||
|
durationRangeTo: '',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{formatDurationDisplay(min)}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
) : null}
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
|
{catalogFocusAreas.length > 0 ? (
|
||||||
|
<div className="fw-import-catalog-block">
|
||||||
|
<span className="form-label">Fokusbereich</span>
|
||||||
|
<div className="framework-catalog-checkgrid">
|
||||||
|
{catalogFocusAreas.map((fa) => (
|
||||||
|
<label key={fa.id} className="framework-catalog-check">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={(filters.focusAreaIds || []).includes(String(fa.id))}
|
||||||
|
onChange={() => toggleId('focusAreaIds', fa.id)}
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
<span>{fa.name}</span>
|
||||||
|
</label>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{catalogTrainingTypes.length > 0 ? (
|
||||||
|
<div className="fw-import-catalog-block">
|
||||||
|
<span className="form-label">Trainingsart</span>
|
||||||
|
<div className="framework-catalog-checkgrid">
|
||||||
|
{catalogTrainingTypes.map((t) => (
|
||||||
|
<label key={t.id} className="framework-catalog-check">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={(filters.trainingTypeIds || []).includes(String(t.id))}
|
||||||
|
onChange={() => toggleId('trainingTypeIds', t.id)}
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
<span>{t.name}</span>
|
||||||
|
</label>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
{catalogTargetGroups.length > 0 ? (
|
||||||
|
<div className="fw-import-catalog-block">
|
||||||
|
<span className="form-label">Zielgruppe</span>
|
||||||
|
<div className="framework-catalog-checkgrid">
|
||||||
|
{catalogTargetGroups.map((tg) => (
|
||||||
|
<label key={tg.id} className="framework-catalog-check">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={(filters.targetGroupIds || []).includes(String(tg.id))}
|
||||||
|
onChange={() => toggleId('targetGroupIds', tg.id)}
|
||||||
|
disabled={disabled}
|
||||||
|
/>
|
||||||
|
<span>{tg.name}</span>
|
||||||
|
</label>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
{showHint ? (
|
||||||
|
<p className="form-sub fw-import-filter-panel__hint">
|
||||||
|
Entwicklungsziele sind freie Texte — die Suche durchsucht auch Ziel-Titel. Bei der Dauer werden nur
|
||||||
|
Programme mit hinterlegter Session-Dauer berücksichtigt.
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -1,12 +1,10 @@
|
||||||
import React, { useEffect, useMemo, useState } from 'react'
|
import React, { useEffect, useMemo, useState } from 'react'
|
||||||
|
import FrameworkProgramsFilterBlock from './FrameworkProgramsFilterBlock'
|
||||||
import {
|
import {
|
||||||
collectDistinctSessionDurationsMinutes,
|
|
||||||
EMPTY_FRAMEWORK_IMPORT_FILTERS,
|
EMPTY_FRAMEWORK_IMPORT_FILTERS,
|
||||||
filterFrameworkPrograms,
|
filterFrameworkPrograms,
|
||||||
frameworkProgramOptionLabel,
|
frameworkProgramOptionLabel,
|
||||||
frameworkSessionDurationLabel,
|
frameworkSessionDurationLabel,
|
||||||
hasActiveFrameworkImportFilters,
|
|
||||||
summarizeFrameworkImportFilters,
|
|
||||||
} from '../../utils/frameworkProgramListHelpers'
|
} from '../../utils/frameworkProgramListHelpers'
|
||||||
import { formatDurationDisplay } from '../../utils/trainingDurationUtils'
|
import { formatDurationDisplay } from '../../utils/trainingDurationUtils'
|
||||||
|
|
||||||
|
|
@ -46,28 +44,11 @@ export default function TrainingPlanningFrameworkImportModal({
|
||||||
}
|
}
|
||||||
}, [open])
|
}, [open])
|
||||||
|
|
||||||
const distinctDurations = useMemo(
|
|
||||||
() => collectDistinctSessionDurationsMinutes(frameworkProgramsList),
|
|
||||||
[frameworkProgramsList]
|
|
||||||
)
|
|
||||||
|
|
||||||
const filteredPrograms = useMemo(
|
const filteredPrograms = useMemo(
|
||||||
() => filterFrameworkPrograms(frameworkProgramsList, filters),
|
() => filterFrameworkPrograms(frameworkProgramsList, filters),
|
||||||
[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
|
const matchCount = filteredPrograms.length
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -81,19 +62,6 @@ export default function TrainingPlanningFrameworkImportModal({
|
||||||
return (frameworkProgramsList || []).find((p) => String(p.id) === String(fwImportProgramId))
|
return (frameworkProgramsList || []).find((p) => String(p.id) === String(fwImportProgramId))
|
||||||
}, [frameworkProgramsList, 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
|
if (!open) return null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -108,230 +76,18 @@ export default function TrainingPlanningFrameworkImportModal({
|
||||||
<strong>eigene geplante Einheit</strong> in der aktuellen Gruppe (Kopie des Ablaufs).
|
<strong>eigene geplante Einheit</strong> in der aktuellen Gruppe (Kopie des Ablaufs).
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div className="fw-import-results-bar">
|
<FrameworkProgramsFilterBlock
|
||||||
<div className="fw-import-results-bar__count">
|
programs={frameworkProgramsList}
|
||||||
<strong className="fw-import-results-bar__num">{matchCount}</strong>
|
filters={filters}
|
||||||
<span>
|
onFiltersChange={setFilters}
|
||||||
{' '}
|
panelOpen={filterPanelOpen}
|
||||||
von {totalCount} Rahmenprogramm{totalCount === 1 ? '' : 'en'}
|
onPanelOpenChange={setFilterPanelOpen}
|
||||||
</span>
|
catalogFocusAreas={catalogFocusAreas}
|
||||||
{matchCount === 0 && totalCount > 0 ? (
|
catalogTrainingTypes={catalogTrainingTypes}
|
||||||
<span className="fw-import-results-bar__warn"> — kein Treffer</span>
|
catalogTargetGroups={catalogTargetGroups}
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
<div className="fw-import-results-bar__actions">
|
|
||||||
{filterActive ? (
|
|
||||||
<span className="fw-import-filter-badge" title={filterSummaryParts.join(' · ')}>
|
|
||||||
Filter aktiv
|
|
||||||
</span>
|
|
||||||
) : null}
|
|
||||||
{filterActive ? (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="btn btn-secondary framework-ctrl framework-ctrl--xs"
|
|
||||||
disabled={fwImportSubmitting}
|
|
||||||
onClick={clearFilters}
|
|
||||||
>
|
|
||||||
Filter zurücksetzen
|
|
||||||
</button>
|
|
||||||
) : null}
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="btn btn-secondary framework-ctrl framework-ctrl--xs"
|
|
||||||
disabled={fwImportSubmitting}
|
|
||||||
onClick={() => setFilterPanelOpen((v) => !v)}
|
|
||||||
aria-expanded={filterPanelOpen}
|
|
||||||
>
|
|
||||||
{filterPanelOpen ? 'Filter einklappen' : 'Filter anzeigen'}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{!filterPanelOpen && filterActive && filterSummaryParts.length > 0 ? (
|
|
||||||
<ul className="fw-import-filter-chips" aria-label="Aktive Filter">
|
|
||||||
{filterSummaryParts.map((part) => (
|
|
||||||
<li key={part} className="fw-import-filter-chip">
|
|
||||||
{part}
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
{filterPanelOpen ? (
|
|
||||||
<div className="fw-import-filter-panel">
|
|
||||||
<div className="fw-import-filter-panel__grid">
|
|
||||||
<div className="form-row fw-import-filter-panel__search">
|
|
||||||
<label className="form-label">Suche (Titel, Ziele, Katalog)</label>
|
|
||||||
<input
|
|
||||||
className="form-input"
|
|
||||||
value={filters.query}
|
|
||||||
onChange={(e) => updateFilter({ query: e.target.value })}
|
|
||||||
placeholder="z. B. Gürtel, Koordination …"
|
|
||||||
disabled={fwImportSubmitting}
|
disabled={fwImportSubmitting}
|
||||||
|
durationRadioName="fw-duration-mode"
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
|
|
||||||
<fieldset className="fw-import-duration-fieldset">
|
|
||||||
<legend className="form-label">Ziel-Session-Dauer</legend>
|
|
||||||
<div className="fw-import-duration-mode" role="radiogroup" aria-label="Dauer-Filtermodus">
|
|
||||||
{[
|
|
||||||
{ id: 'any', label: 'Alle' },
|
|
||||||
{ id: 'range', label: 'Zeitspanne' },
|
|
||||||
{ id: 'preset', label: 'Vorhandene Zeiten' },
|
|
||||||
].map((opt) => (
|
|
||||||
<label key={opt.id} className="fw-import-duration-mode__opt">
|
|
||||||
<input
|
|
||||||
type="radio"
|
|
||||||
name="fw-duration-mode"
|
|
||||||
checked={filters.durationMode === opt.id}
|
|
||||||
disabled={fwImportSubmitting || (opt.id === 'preset' && distinctDurations.length === 0)}
|
|
||||||
onChange={() =>
|
|
||||||
updateFilter({
|
|
||||||
durationMode: opt.id,
|
|
||||||
...(opt.id === 'any'
|
|
||||||
? { durationRangeFrom: '', durationRangeTo: '', durationPresetMin: null }
|
|
||||||
: {}),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<span>{opt.label}</span>
|
|
||||||
</label>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{filters.durationMode === 'range' ? (
|
|
||||||
<div className="responsive-grid-2 fw-import-duration-range">
|
|
||||||
<div className="form-row" style={{ marginBottom: 0 }}>
|
|
||||||
<label className="form-label">Von (Minuten)</label>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
min={0}
|
|
||||||
className="form-input"
|
|
||||||
value={filters.durationRangeFrom}
|
|
||||||
onChange={(e) =>
|
|
||||||
updateFilter({ durationMode: 'range', durationRangeFrom: e.target.value })
|
|
||||||
}
|
|
||||||
placeholder="z. B. 60"
|
|
||||||
disabled={fwImportSubmitting}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="form-row" style={{ marginBottom: 0 }}>
|
|
||||||
<label className="form-label">Bis (Minuten)</label>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
min={0}
|
|
||||||
className="form-input"
|
|
||||||
value={filters.durationRangeTo}
|
|
||||||
onChange={(e) =>
|
|
||||||
updateFilter({ durationMode: 'range', durationRangeTo: e.target.value })
|
|
||||||
}
|
|
||||||
placeholder="z. B. 90"
|
|
||||||
disabled={fwImportSubmitting}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
{filters.durationMode === 'preset' ? (
|
|
||||||
distinctDurations.length === 0 ? (
|
|
||||||
<p className="form-sub" style={{ margin: '8px 0 0' }}>
|
|
||||||
In der Bibliothek sind noch keine Session-Dauern hinterlegt. Nutze „Zeitspanne“ oder lege
|
|
||||||
Dauer pro Session im Rahmenprogramm fest.
|
|
||||||
</p>
|
|
||||||
) : (
|
|
||||||
<div className="fw-import-duration-presets">
|
|
||||||
{distinctDurations.map((min) => {
|
|
||||||
const on = filters.durationPresetMin === min
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
key={min}
|
|
||||||
type="button"
|
|
||||||
className={
|
|
||||||
'btn framework-ctrl framework-ctrl--xs' +
|
|
||||||
(on ? ' fw-import-duration-preset--on' : ' btn-secondary')
|
|
||||||
}
|
|
||||||
disabled={fwImportSubmitting}
|
|
||||||
onClick={() =>
|
|
||||||
updateFilter({
|
|
||||||
durationMode: 'preset',
|
|
||||||
durationPresetMin: on ? null : min,
|
|
||||||
durationRangeFrom: '',
|
|
||||||
durationRangeTo: '',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{formatDurationDisplay(min)}
|
|
||||||
</button>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
) : null}
|
|
||||||
</fieldset>
|
|
||||||
|
|
||||||
{catalogFocusAreas.length > 0 ? (
|
|
||||||
<div className="fw-import-catalog-block">
|
|
||||||
<span className="form-label">Fokusbereich</span>
|
|
||||||
<div className="framework-catalog-checkgrid">
|
|
||||||
{catalogFocusAreas.map((fa) => (
|
|
||||||
<label key={fa.id} className="framework-catalog-check">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={(filters.focusAreaIds || []).includes(String(fa.id))}
|
|
||||||
onChange={() => toggleId('focusAreaIds', fa.id)}
|
|
||||||
disabled={fwImportSubmitting}
|
|
||||||
/>
|
|
||||||
<span>{fa.name}</span>
|
|
||||||
</label>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
{catalogTrainingTypes.length > 0 ? (
|
|
||||||
<div className="fw-import-catalog-block">
|
|
||||||
<span className="form-label">Trainingsart</span>
|
|
||||||
<div className="framework-catalog-checkgrid">
|
|
||||||
{catalogTrainingTypes.map((t) => (
|
|
||||||
<label key={t.id} className="framework-catalog-check">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={(filters.trainingTypeIds || []).includes(String(t.id))}
|
|
||||||
onChange={() => toggleId('trainingTypeIds', t.id)}
|
|
||||||
disabled={fwImportSubmitting}
|
|
||||||
/>
|
|
||||||
<span>{t.name}</span>
|
|
||||||
</label>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
{catalogTargetGroups.length > 0 ? (
|
|
||||||
<div className="fw-import-catalog-block">
|
|
||||||
<span className="form-label">Zielgruppe</span>
|
|
||||||
<div className="framework-catalog-checkgrid">
|
|
||||||
{catalogTargetGroups.map((tg) => (
|
|
||||||
<label key={tg.id} className="framework-catalog-check">
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={(filters.targetGroupIds || []).includes(String(tg.id))}
|
|
||||||
onChange={() => toggleId('targetGroupIds', tg.id)}
|
|
||||||
disabled={fwImportSubmitting}
|
|
||||||
/>
|
|
||||||
<span>{tg.name}</span>
|
|
||||||
</label>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
<p className="form-sub fw-import-filter-panel__hint">
|
|
||||||
Entwicklungsziele sind freie Texte — die Suche durchsucht auch Ziel-Titel. Bei der Dauer werden nur
|
|
||||||
Programme mit hinterlegter Session-Dauer berücksichtigt.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
<div className="form-row fw-import-program-select">
|
<div className="form-row fw-import-program-select">
|
||||||
<label className="form-label">
|
<label className="form-label">
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,16 @@
|
||||||
import React, { useCallback, useEffect, useMemo, useState } from 'react'
|
import React, { useCallback, useEffect, useMemo, useState } from 'react'
|
||||||
import api from '../utils/api'
|
import api from '../utils/api'
|
||||||
import NavStateLink from '../components/NavStateLink'
|
import NavStateLink from '../components/NavStateLink'
|
||||||
|
import FrameworkProgramsFilterBlock from '../components/planning/FrameworkProgramsFilterBlock'
|
||||||
import { useAuth } from '../context/AuthContext'
|
import { useAuth } from '../context/AuthContext'
|
||||||
import { getTenantClubDependencyKey } from '../utils/activeClub'
|
import { getTenantClubDependencyKey } from '../utils/activeClub'
|
||||||
import { buildFrameworkProgramsListReturnContext } from '../utils/navReturnContext'
|
import { buildFrameworkProgramsListReturnContext } from '../utils/navReturnContext'
|
||||||
import { frameworkSessionDurationLabel } from '../utils/frameworkProgramListHelpers'
|
import {
|
||||||
|
EMPTY_FRAMEWORK_IMPORT_FILTERS,
|
||||||
|
filterFrameworkPrograms,
|
||||||
|
frameworkSessionDurationLabel,
|
||||||
|
hasActiveFrameworkImportFilters,
|
||||||
|
} from '../utils/frameworkProgramListHelpers'
|
||||||
|
|
||||||
function dashIfEmpty(val) {
|
function dashIfEmpty(val) {
|
||||||
const s = (val ?? '').toString().trim()
|
const s = (val ?? '').toString().trim()
|
||||||
|
|
@ -87,16 +93,38 @@ export default function TrainingFrameworkProgramsListPage() {
|
||||||
const [rows, setRows] = useState([])
|
const [rows, setRows] = useState([])
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
const [error, setError] = useState('')
|
const [error, setError] = useState('')
|
||||||
|
const [catalogFocusAreas, setCatalogFocusAreas] = useState([])
|
||||||
|
const [catalogTrainingTypes, setCatalogTrainingTypes] = useState([])
|
||||||
|
const [catalogTargetGroups, setCatalogTargetGroups] = useState([])
|
||||||
|
const [filters, setFilters] = useState(() => ({ ...EMPTY_FRAMEWORK_IMPORT_FILTERS }))
|
||||||
|
const [filterPanelOpen, setFilterPanelOpen] = useState(true)
|
||||||
|
|
||||||
|
const filteredRows = useMemo(
|
||||||
|
() => filterFrameworkPrograms(rows, filters),
|
||||||
|
[rows, filters]
|
||||||
|
)
|
||||||
|
const filterActive = hasActiveFrameworkImportFilters(filters)
|
||||||
|
|
||||||
const load = useCallback(async () => {
|
const load = useCallback(async () => {
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
setError('')
|
setError('')
|
||||||
try {
|
try {
|
||||||
const list = await api.listTrainingFrameworkPrograms()
|
const [list, fa, tt, tg] = await Promise.all([
|
||||||
|
api.listTrainingFrameworkPrograms(),
|
||||||
|
api.listFocusAreas({ status: 'active' }),
|
||||||
|
api.listTrainingTypes({ status: 'active' }),
|
||||||
|
api.listTargetGroups({ status: 'active' }),
|
||||||
|
])
|
||||||
setRows(Array.isArray(list) ? list : [])
|
setRows(Array.isArray(list) ? list : [])
|
||||||
|
setCatalogFocusAreas(Array.isArray(fa) ? fa : [])
|
||||||
|
setCatalogTrainingTypes(Array.isArray(tt) ? tt : [])
|
||||||
|
setCatalogTargetGroups(Array.isArray(tg) ? tg : [])
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setError(e.message || 'Laden fehlgeschlagen')
|
setError(e.message || 'Laden fehlgeschlagen')
|
||||||
setRows([])
|
setRows([])
|
||||||
|
setCatalogFocusAreas([])
|
||||||
|
setCatalogTrainingTypes([])
|
||||||
|
setCatalogTargetGroups([])
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
}
|
}
|
||||||
|
|
@ -181,8 +209,31 @@ export default function TrainingFrameworkProgramsListPage() {
|
||||||
</NavStateLink>
|
</NavStateLink>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
|
<>
|
||||||
|
<FrameworkProgramsFilterBlock
|
||||||
|
programs={rows}
|
||||||
|
filters={filters}
|
||||||
|
onFiltersChange={setFilters}
|
||||||
|
panelOpen={filterPanelOpen}
|
||||||
|
onPanelOpenChange={setFilterPanelOpen}
|
||||||
|
catalogFocusAreas={catalogFocusAreas}
|
||||||
|
catalogTrainingTypes={catalogTrainingTypes}
|
||||||
|
catalogTargetGroups={catalogTargetGroups}
|
||||||
|
durationRadioName="fw-list-duration-mode"
|
||||||
|
className="fw-prog-filter-block--list"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{filteredRows.length === 0 ? (
|
||||||
|
<div className="card" style={{ marginTop: '1rem' }}>
|
||||||
|
<p style={{ color: 'var(--text2)', margin: 0 }}>
|
||||||
|
{filterActive
|
||||||
|
? 'Kein Rahmenprogramm passt zu den gewählten Filtern. Passe die Kriterien an oder setze den Filter zurück.'
|
||||||
|
: 'Keine Einträge.'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
<ul className="framework-programs-list">
|
<ul className="framework-programs-list">
|
||||||
{rows.map((r) => (
|
{filteredRows.map((r) => (
|
||||||
<li key={r.id} className="card">
|
<li key={r.id} className="card">
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
|
|
@ -241,5 +292,7 @@ export default function TrainingFrameworkProgramsListPage() {
|
||||||
</ul>
|
</ul>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user