All checks were successful
Deploy Development / deploy (push) Successful in 39s
Test Suite / pytest-backend (push) Successful in 42s
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 1m15s
- Updated the skill scoring specification to include club-specific metrics and improved aggregation methods for skill profiles. - Introduced new API endpoints for batch skill profile summaries, allowing for efficient retrieval of compact skill data. - Enhanced frontend components to display skill profiles with club comparisons, improving user interaction and visibility of skill strengths. - Added filtering options for skills in the framework programs, enabling users to refine selections based on training weight relative to club maximums. - Improved CSS styles for skill profile displays, ensuring a cohesive and user-friendly interface across the application.
194 lines
6.8 KiB
JavaScript
194 lines
6.8 KiB
JavaScript
import React, { useCallback, useEffect, useMemo, useState } from 'react'
|
|
import api from '../utils/api'
|
|
import NavStateLink from '../components/NavStateLink'
|
|
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'
|
|
|
|
export default function TrainingModulesListPage() {
|
|
const { user } = useAuth()
|
|
const tenantClubDepKey = useMemo(() => getTenantClubDependencyKey(user), [user])
|
|
const modulesListReturn = useMemo(() => buildTrainingModulesListReturnContext(), [])
|
|
const [rows, setRows] = useState([])
|
|
const [loading, setLoading] = useState(true)
|
|
const [error, setError] = useState('')
|
|
const [skillSummaries, setSkillSummaries] = useState({})
|
|
const [summariesLoading, setSummariesLoading] = useState(false)
|
|
const [profileModal, setProfileModal] = useState(null)
|
|
|
|
const load = useCallback(async () => {
|
|
setLoading(true)
|
|
setError('')
|
|
try {
|
|
const list = await api.listTrainingModules()
|
|
setRows(Array.isArray(list) ? list : [])
|
|
} catch (e) {
|
|
setError(e.message || 'Laden fehlgeschlagen')
|
|
setRows([])
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}, [])
|
|
|
|
useEffect(() => {
|
|
load()
|
|
}, [load, tenantClubDepKey])
|
|
|
|
useEffect(() => {
|
|
if (!rows.length) {
|
|
setSkillSummaries({})
|
|
return undefined
|
|
}
|
|
let cancelled = false
|
|
setSummariesLoading(true)
|
|
api
|
|
.batchSkillProfileSummaries({ trainingModuleIds: 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(`Trainingsmodul „${title || id}“ wirklich löschen?`)) return
|
|
try {
|
|
await api.deleteTrainingModule(id)
|
|
await load()
|
|
} catch (e) {
|
|
alert(e.message || 'Löschen fehlgeschlagen')
|
|
}
|
|
}
|
|
|
|
return (
|
|
<>
|
|
<div
|
|
style={{
|
|
display: 'flex',
|
|
flexWrap: 'wrap',
|
|
alignItems: 'flex-start',
|
|
justifyContent: 'space-between',
|
|
gap: '1rem',
|
|
marginBottom: '1.25rem',
|
|
}}
|
|
>
|
|
<div>
|
|
<h1 className="page-title" style={{ marginBottom: '0.35rem' }}>
|
|
Trainingsmodule
|
|
</h1>
|
|
<p style={{ color: 'var(--text2)', fontSize: '0.95rem', maxWidth: '38rem', margin: 0 }}>
|
|
Wiederverwendbare Übungsfolgen für die Trainingsplanung. Fähigkeiten werden im Vereinskontext verglichen.
|
|
</p>
|
|
</div>
|
|
<NavStateLink
|
|
to="/planning/training-modules/new"
|
|
returnContext={modulesListReturn}
|
|
className="btn btn-primary"
|
|
style={{ textDecoration: 'none' }}
|
|
>
|
|
Neues Modul
|
|
</NavStateLink>
|
|
</div>
|
|
|
|
{error ? (
|
|
<p style={{ color: 'var(--danger)', marginBottom: '1rem' }}>{error}</p>
|
|
) : null}
|
|
{loading ? (
|
|
<p style={{ color: 'var(--text2)' }}>Laden …</p>
|
|
) : rows.length === 0 ? (
|
|
<div className="card" style={{ padding: '1.25rem' }}>
|
|
<p style={{ margin: 0, color: 'var(--text2)' }}>Noch keine Module angelegt.</p>
|
|
</div>
|
|
) : (
|
|
<ul style={{ listStyle: 'none', padding: 0, margin: 0, display: 'flex', flexDirection: 'column', gap: '10px' }}>
|
|
{rows.map((r) => (
|
|
<li key={r.id} className="card" style={{ padding: '1rem 1.15rem' }}>
|
|
<div
|
|
style={{
|
|
display: 'flex',
|
|
flexWrap: 'wrap',
|
|
justifyContent: 'space-between',
|
|
gap: '10px',
|
|
alignItems: 'flex-start',
|
|
}}
|
|
>
|
|
<div style={{ flex: '1 1 220px', minWidth: 0 }}>
|
|
<NavStateLink
|
|
to={`/planning/training-modules/${r.id}`}
|
|
returnContext={modulesListReturn}
|
|
style={{
|
|
fontWeight: 700,
|
|
fontSize: '1.05rem',
|
|
color: 'var(--accent-dark)',
|
|
textDecoration: 'none',
|
|
wordBreak: 'break-word',
|
|
}}
|
|
>
|
|
{(r.title || '').trim() || `Modul #${r.id}`}
|
|
</NavStateLink>
|
|
<p style={{ margin: '0.35rem 0 0', fontSize: '0.88rem', color: 'var(--text2)', lineHeight: 1.45 }}>
|
|
{(r.summary || '').trim() || '—'}{' '}
|
|
<span style={{ color: 'var(--text3)' }}>
|
|
({Number(r.items_count) || 0} Position{r.items_count === 1 ? '' : 'en'})
|
|
</span>
|
|
</p>
|
|
<div style={{ marginTop: '0.65rem' }}>
|
|
<SkillProfileCompact
|
|
summary={skillSummaries[`training_module:${r.id}`]}
|
|
loading={summariesLoading}
|
|
displayLimit={4}
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '8px' }}>
|
|
<button
|
|
type="button"
|
|
className="btn btn-secondary btn-small"
|
|
onClick={() =>
|
|
setProfileModal({
|
|
artifactType: 'training_module',
|
|
artifactId: r.id,
|
|
title: (r.title || '').trim() || `Modul #${r.id}`,
|
|
})
|
|
}
|
|
>
|
|
Fähigkeiten-Profil
|
|
</button>
|
|
<NavStateLink
|
|
to={`/planning/training-modules/${r.id}`}
|
|
returnContext={modulesListReturn}
|
|
className="btn btn-secondary"
|
|
style={{ textDecoration: 'none' }}
|
|
>
|
|
Bearbeiten
|
|
</NavStateLink>
|
|
<button type="button" className="btn btn-secondary" onClick={() => handleDelete(r.id, r.title)}>
|
|
Löschen
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</li>
|
|
))}
|
|
</ul>
|
|
)}
|
|
|
|
<SkillProfileFullModal
|
|
open={Boolean(profileModal)}
|
|
onClose={() => setProfileModal(null)}
|
|
artifactType={profileModal?.artifactType}
|
|
artifactId={profileModal?.artifactId}
|
|
title={profileModal?.title}
|
|
/>
|
|
</>
|
|
)
|
|
}
|