shinkan-jinkendo/frontend/src/pages/TrainingModulesListPage.jsx
Lars 78c6c51520
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
Enhance Skill Scoring and Profile Features
- 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.
2026-05-21 09:05:13 +02:00

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}
/>
</>
)
}