2026-05-05 09:02:13 +02:00
|
|
|
import React, { useCallback, useEffect, useState } from 'react'
|
|
|
|
|
import { Link } from 'react-router-dom'
|
|
|
|
|
import api from '../utils/api'
|
|
|
|
|
|
2026-05-05 13:11:17 +02:00
|
|
|
const TYPE_COUNT = (r) =>
|
|
|
|
|
typeof r.training_types_count === 'number' ? r.training_types_count : null
|
|
|
|
|
const TG_COUNT = (r) =>
|
|
|
|
|
typeof r.target_groups_count === 'number' ? r.target_groups_count : null
|
2026-05-05 09:02:13 +02:00
|
|
|
|
2026-05-05 13:11:17 +02:00
|
|
|
function contextTeaser(r) {
|
|
|
|
|
const bits = []
|
|
|
|
|
if (r.focus_area_name) bits.push(r.focus_area_name)
|
|
|
|
|
if (r.style_direction_name) bits.push(r.style_direction_name)
|
|
|
|
|
const tn = TYPE_COUNT(r)
|
|
|
|
|
const gn = TG_COUNT(r)
|
|
|
|
|
if (tn != null && tn > 0) bits.push(`${tn} Trainingsart${tn === 1 ? '' : 'en'}`)
|
|
|
|
|
if (gn != null && gn > 0) bits.push(`${gn} Zielgruppe${gn === 1 ? '' : 'n'}`)
|
|
|
|
|
return bits.length ? bits.join(' · ') : null
|
|
|
|
|
}
|
2026-05-05 09:02:13 +02:00
|
|
|
export default function TrainingFrameworkProgramsListPage() {
|
|
|
|
|
const [rows, setRows] = useState([])
|
|
|
|
|
const [loading, setLoading] = useState(true)
|
|
|
|
|
const [error, setError] = useState('')
|
|
|
|
|
|
|
|
|
|
const load = useCallback(async () => {
|
|
|
|
|
setLoading(true)
|
|
|
|
|
setError('')
|
|
|
|
|
try {
|
|
|
|
|
const list = await api.listTrainingFrameworkPrograms()
|
|
|
|
|
setRows(Array.isArray(list) ? list : [])
|
|
|
|
|
} catch (e) {
|
|
|
|
|
setError(e.message || 'Laden fehlgeschlagen')
|
|
|
|
|
setRows([])
|
|
|
|
|
} finally {
|
|
|
|
|
setLoading(false)
|
|
|
|
|
}
|
|
|
|
|
}, [])
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
load()
|
|
|
|
|
}, [load])
|
|
|
|
|
|
|
|
|
|
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 (
|
2026-05-05 12:41:59 +02:00
|
|
|
<div className="app-page">
|
2026-05-05 09:02:13 +02:00
|
|
|
<div
|
|
|
|
|
style={{
|
|
|
|
|
display: 'flex',
|
|
|
|
|
flexWrap: 'wrap',
|
|
|
|
|
alignItems: 'flex-start',
|
|
|
|
|
justifyContent: 'space-between',
|
|
|
|
|
gap: '1rem',
|
|
|
|
|
marginBottom: '1.25rem',
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<div>
|
|
|
|
|
<h1 style={{ marginBottom: '0.35rem' }}>Trainingsrahmenprogramme</h1>
|
|
|
|
|
<p style={{ color: 'var(--text2)', fontSize: '0.95rem', maxWidth: '36rem' }}>
|
2026-05-05 13:11:17 +02:00
|
|
|
Wiederverwendbare Vorlagen für Ziele und Sessions. Die Verknüpfung mit{' '}
|
|
|
|
|
<strong>konkreten Gruppeneinheiten</strong> erfolgt aus der <strong>Planung der Gruppe</strong> (Übernahme
|
|
|
|
|
mit Bezug zum Rahmen).
|
2026-05-05 09:02:13 +02:00
|
|
|
</p>
|
|
|
|
|
</div>
|
2026-05-05 11:11:52 +02:00
|
|
|
<Link
|
|
|
|
|
to="/planning/framework-programs/new"
|
|
|
|
|
className="btn btn-primary"
|
|
|
|
|
style={{ textDecoration: 'none', whiteSpace: 'nowrap' }}
|
|
|
|
|
>
|
|
|
|
|
Rahmenprogramm anlegen
|
2026-05-05 09:02:13 +02:00
|
|
|
</Link>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<p style={{ marginBottom: '1rem' }}>
|
|
|
|
|
<Link to="/planning" style={{ color: 'var(--accent-dark)' }}>
|
|
|
|
|
← Zurück zur Trainingsplanung
|
|
|
|
|
</Link>
|
|
|
|
|
</p>
|
|
|
|
|
|
|
|
|
|
{error && (
|
|
|
|
|
<div className="card" style={{ borderLeft: '4px solid var(--danger)', marginBottom: '1rem' }}>
|
|
|
|
|
{error}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{loading ? (
|
|
|
|
|
<div style={{ textAlign: 'center', padding: '2rem' }}>
|
|
|
|
|
<div className="spinner" />
|
|
|
|
|
<p>Laden…</p>
|
|
|
|
|
</div>
|
|
|
|
|
) : rows.length === 0 ? (
|
|
|
|
|
<div className="card">
|
2026-05-05 11:11:52 +02:00
|
|
|
<p style={{ color: 'var(--text2)', marginBottom: '1rem' }}>
|
|
|
|
|
Noch kein Rahmenprogramm gespeichert. Lege ein neues an — mit Titel, mindestens einem Ziel und optional
|
|
|
|
|
Slots samt Übungen.
|
2026-05-05 09:02:13 +02:00
|
|
|
</p>
|
2026-05-05 11:11:52 +02:00
|
|
|
<Link
|
|
|
|
|
to="/planning/framework-programs/new"
|
|
|
|
|
className="btn btn-primary btn-full"
|
|
|
|
|
style={{ textDecoration: 'none' }}
|
|
|
|
|
>
|
|
|
|
|
Rahmenprogramm anlegen
|
|
|
|
|
</Link>
|
2026-05-05 09:02:13 +02:00
|
|
|
</div>
|
|
|
|
|
) : (
|
|
|
|
|
<ul style={{ listStyle: 'none' }}>
|
|
|
|
|
{rows.map((r) => (
|
|
|
|
|
<li key={r.id} className="card" style={{ marginBottom: '12px' }}>
|
|
|
|
|
<div
|
|
|
|
|
style={{
|
|
|
|
|
display: 'flex',
|
|
|
|
|
flexWrap: 'wrap',
|
|
|
|
|
alignItems: 'flex-start',
|
|
|
|
|
justifyContent: 'space-between',
|
|
|
|
|
gap: '0.75rem',
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
<div>
|
|
|
|
|
<Link
|
|
|
|
|
to={`/planning/framework-programs/${r.id}`}
|
|
|
|
|
style={{ fontWeight: 600, fontSize: '1.05rem', color: 'var(--text1)' }}
|
|
|
|
|
>
|
|
|
|
|
{r.title || `Rahmen #${r.id}`}
|
|
|
|
|
</Link>
|
|
|
|
|
<div style={{ fontSize: '0.85rem', color: 'var(--text2)', marginTop: '0.35rem' }}>
|
2026-05-05 13:11:17 +02:00
|
|
|
<span>
|
|
|
|
|
{r.goals_count ?? '—'} Ziele · {r.slots_count ?? '—'} Slots
|
|
|
|
|
</span>
|
|
|
|
|
{contextTeaser(r) ? (
|
|
|
|
|
<span style={{ display: 'block', marginTop: '0.25rem', color: 'var(--text3)' }}>
|
|
|
|
|
{contextTeaser(r)}
|
2026-05-05 09:02:13 +02:00
|
|
|
</span>
|
|
|
|
|
) : null}
|
|
|
|
|
</div>
|
|
|
|
|
{r.description ? (
|
|
|
|
|
<p style={{ marginTop: '0.5rem', fontSize: '0.9rem', color: 'var(--text2)' }}>{r.description}</p>
|
|
|
|
|
) : null}
|
|
|
|
|
</div>
|
|
|
|
|
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '0.5rem' }}>
|
|
|
|
|
<Link
|
|
|
|
|
to={`/planning/framework-programs/${r.id}`}
|
|
|
|
|
className="btn btn-secondary"
|
|
|
|
|
style={{ textDecoration: 'none' }}
|
|
|
|
|
>
|
|
|
|
|
Bearbeiten
|
|
|
|
|
</Link>
|
|
|
|
|
<button type="button" className="btn btn-secondary" onClick={() => handleDelete(r.id, r.title)}>
|
|
|
|
|
Löschen
|
|
|
|
|
</button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</li>
|
|
|
|
|
))}
|
|
|
|
|
</ul>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
}
|