All checks were successful
Deploy Development / deploy (push) Successful in 43s
Test Suite / pytest-backend (push) Successful in 38s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m14s
- Updated the FrameworkProgramsFilterBlock to include a search input and filter modal, improving user interaction and accessibility. - Refactored CSS styles for filter components to ensure consistent layout and spacing. - Removed deprecated panel open state management, streamlining the component logic. - Integrated new filtering capabilities in the TrainingPlanningFrameworkImportModal and TrainingModulesListPage, enhancing the overall filtering experience. - Improved the display of active filters and results count, providing clearer feedback to users.
219 lines
8.2 KiB
JavaScript
219 lines
8.2 KiB
JavaScript
import React, { useCallback, useEffect, useMemo, useState } from 'react'
|
|
import api from '../utils/api'
|
|
import NavStateLink from '../components/NavStateLink'
|
|
import FrameworkProgramsFilterBlock from '../components/planning/FrameworkProgramsFilterBlock'
|
|
import FrameworkProgramListCard from '../components/planning/FrameworkProgramListCard'
|
|
import SkillProfileFullModal from '../components/skills/SkillProfileFullModal'
|
|
import { useAuth } from '../context/AuthContext'
|
|
import { getTenantClubDependencyKey } from '../utils/activeClub'
|
|
import { buildFrameworkProgramsListReturnContext } from '../utils/navReturnContext'
|
|
import {
|
|
EMPTY_FRAMEWORK_IMPORT_FILTERS,
|
|
filterFrameworkPrograms,
|
|
hasActiveFrameworkImportFilters,
|
|
} from '../utils/frameworkProgramListHelpers'
|
|
|
|
export default function TrainingFrameworkProgramsListPage() {
|
|
const { user } = useAuth()
|
|
const tenantClubDepKey = useMemo(() => getTenantClubDependencyKey(user), [user])
|
|
const frameworkListReturn = useMemo(() => buildFrameworkProgramsListReturnContext(), [])
|
|
const [rows, setRows] = useState([])
|
|
const [loading, setLoading] = useState(true)
|
|
const [error, setError] = useState('')
|
|
const [catalogFocusAreas, setCatalogFocusAreas] = useState([])
|
|
const [catalogTrainingTypes, setCatalogTrainingTypes] = useState([])
|
|
const [catalogTargetGroups, setCatalogTargetGroups] = useState([])
|
|
const [catalogSkills, setCatalogSkills] = useState([])
|
|
const [filters, setFilters] = useState(() => ({ ...EMPTY_FRAMEWORK_IMPORT_FILTERS }))
|
|
const [skillSummaries, setSkillSummaries] = useState({})
|
|
const [summariesLoading, setSummariesLoading] = useState(false)
|
|
const [profileModal, setProfileModal] = useState(null)
|
|
|
|
const filteredRows = useMemo(
|
|
() => filterFrameworkPrograms(rows, filters, skillSummaries),
|
|
[rows, filters, skillSummaries]
|
|
)
|
|
const filterActive = hasActiveFrameworkImportFilters(filters)
|
|
|
|
const load = useCallback(async () => {
|
|
setLoading(true)
|
|
setError('')
|
|
try {
|
|
const [list, fa, tt, tg, skills] = await Promise.all([
|
|
api.listTrainingFrameworkPrograms(),
|
|
api.listFocusAreas({ status: 'active' }),
|
|
api.listTrainingTypes({ status: 'active' }),
|
|
api.listTargetGroups({ status: 'active' }),
|
|
api.listSkillsCatalog({ status: 'active' }),
|
|
])
|
|
setRows(Array.isArray(list) ? list : [])
|
|
setCatalogFocusAreas(Array.isArray(fa) ? fa : [])
|
|
setCatalogTrainingTypes(Array.isArray(tt) ? tt : [])
|
|
setCatalogTargetGroups(Array.isArray(tg) ? tg : [])
|
|
setCatalogSkills(Array.isArray(skills) ? skills : [])
|
|
} catch (e) {
|
|
setError(e.message || 'Laden fehlgeschlagen')
|
|
setRows([])
|
|
setCatalogFocusAreas([])
|
|
setCatalogTrainingTypes([])
|
|
setCatalogTargetGroups([])
|
|
setCatalogSkills([])
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}, [])
|
|
|
|
useEffect(() => {
|
|
load()
|
|
}, [load, tenantClubDepKey])
|
|
|
|
useEffect(() => {
|
|
if (!rows.length) {
|
|
setSkillSummaries({})
|
|
return undefined
|
|
}
|
|
let cancelled = false
|
|
setSummariesLoading(true)
|
|
api
|
|
.batchSkillProfileSummaries({ frameworkProgramIds: rows.map((r) => r.id) })
|
|
.then((data) => {
|
|
if (!cancelled) setSkillSummaries(data?.summaries || {})
|
|
})
|
|
.catch(() => {
|
|
if (!cancelled) setSkillSummaries({})
|
|
})
|
|
.finally(() => {
|
|
if (!cancelled) setSummariesLoading(false)
|
|
})
|
|
return () => {
|
|
cancelled = true
|
|
}
|
|
}, [rows, tenantClubDepKey])
|
|
|
|
async function handleDelete(id, title) {
|
|
if (!confirm(`Rahmenprogramm „${title || id}“ wirklich löschen?`)) return
|
|
try {
|
|
await api.deleteTrainingFrameworkProgram(id)
|
|
await load()
|
|
} catch (e) {
|
|
alert(e.message || 'Löschen fehlgeschlagen')
|
|
}
|
|
}
|
|
|
|
return (
|
|
<div className="fw-prog-page">
|
|
<header className="fw-prog-page__header">
|
|
<div className="fw-prog-page__intro">
|
|
<h1 className="page-title fw-prog-page__title">Trainingsrahmenprogramme</h1>
|
|
<p className="fw-prog-page__lead">
|
|
Vorlagen für Entwicklungsziele und Sessions — die Übernahme in Gruppentermine erfolgt in der
|
|
Trainingsplanung.
|
|
</p>
|
|
<details className="planning-filter-help fw-prog-page__help">
|
|
<summary className="planning-filter-help__summary">Mehr zur Übernahme in die Planung</summary>
|
|
<div className="planning-filter-help__body">
|
|
Unter <strong>Planung</strong> wählst du eine Gruppe und übernimmst Slots aus einem Rahmenprogramm in
|
|
echte Termine. So bleibt die Bibliothek wiederverwendbar, ohne dass Einzelgruppen fest verdrahtet sind.
|
|
</div>
|
|
</details>
|
|
</div>
|
|
<NavStateLink
|
|
to="/planning/framework-programs/new"
|
|
returnContext={frameworkListReturn}
|
|
className="btn btn-primary fw-prog-page__cta"
|
|
>
|
|
Rahmenprogramm anlegen
|
|
</NavStateLink>
|
|
</header>
|
|
|
|
{error ? (
|
|
<div className="card fw-prog-page__error" role="alert">
|
|
{error}
|
|
</div>
|
|
) : null}
|
|
|
|
{loading ? (
|
|
<div className="fw-prog-page__loading card">
|
|
<div className="spinner" aria-hidden="true" />
|
|
<p>Rahmenprogramme werden geladen…</p>
|
|
</div>
|
|
) : rows.length === 0 ? (
|
|
<div className="card fw-prog-page__empty">
|
|
<div className="fw-prog-page__empty-icon" aria-hidden="true">
|
|
📋
|
|
</div>
|
|
<h2 className="fw-prog-page__empty-title">Noch keine Rahmenprogramme</h2>
|
|
<p className="fw-prog-page__empty-text">
|
|
Lege ein neues Programm an — mit Titel, mindestens einem Entwicklungsziel und optional Sessions samt
|
|
Übungsablauf.
|
|
</p>
|
|
<NavStateLink
|
|
to="/planning/framework-programs/new"
|
|
returnContext={frameworkListReturn}
|
|
className="btn btn-primary btn-full"
|
|
>
|
|
Erstes Rahmenprogramm anlegen
|
|
</NavStateLink>
|
|
</div>
|
|
) : (
|
|
<>
|
|
<FrameworkProgramsFilterBlock
|
|
programs={rows}
|
|
filters={filters}
|
|
onFiltersChange={setFilters}
|
|
catalogFocusAreas={catalogFocusAreas}
|
|
catalogTrainingTypes={catalogTrainingTypes}
|
|
catalogTargetGroups={catalogTargetGroups}
|
|
catalogSkills={catalogSkills}
|
|
skillSummaries={skillSummaries}
|
|
durationRadioName="fw-list-duration-mode"
|
|
className="fw-prog-filter-block--list"
|
|
/>
|
|
|
|
{filteredRows.length === 0 ? (
|
|
<div className="card fw-prog-page__empty fw-prog-page__empty--filter">
|
|
<h2 className="fw-prog-page__empty-title">Kein Treffer</h2>
|
|
<p className="fw-prog-page__empty-text">
|
|
{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="fw-prog-list" aria-label="Rahmenprogramme">
|
|
{filteredRows.map((r) => (
|
|
<li key={r.id}>
|
|
<FrameworkProgramListCard
|
|
row={r}
|
|
returnContext={frameworkListReturn}
|
|
onDelete={handleDelete}
|
|
skillSummary={skillSummaries[`framework_program:${r.id}`]}
|
|
skillSummaryLoading={summariesLoading}
|
|
skillFilterIds={filters.skillIds || []}
|
|
skillDisplayLimit={filters.skillDisplayLimit || 10}
|
|
onShowSkillProfile={(row) =>
|
|
setProfileModal({
|
|
artifactType: 'framework_program',
|
|
artifactId: row.id,
|
|
title: (row.title || '').trim() || `Rahmen #${row.id}`,
|
|
})
|
|
}
|
|
/>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
)}
|
|
</>
|
|
)}
|
|
|
|
<SkillProfileFullModal
|
|
open={Boolean(profileModal)}
|
|
onClose={() => setProfileModal(null)}
|
|
artifactType={profileModal?.artifactType}
|
|
artifactId={profileModal?.artifactId}
|
|
title={profileModal?.title}
|
|
/>
|
|
</div>
|
|
)
|
|
}
|