feat: enhance UI and functionality in Training Framework pages
Some checks failed
Deploy Development / deploy (push) Successful in 35s
Test Suite / pytest-backend (push) Successful in 7s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 6s
Test Suite / playwright-tests (push) Failing after 53s
Some checks failed
Deploy Development / deploy (push) Successful in 35s
Test Suite / pytest-backend (push) Successful in 7s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 6s
Test Suite / playwright-tests (push) Failing after 53s
- Added new CSS styles for segment buttons and admin assignment matrix, improving layout and responsiveness. - Refactored AssignmentsTab component to utilize new styles and improve accessibility with aria-labels. - Introduced collapsible details for framework edit introduction, enhancing user guidance. - Updated TrainingPlanningPage to streamline button styling and improve visual consistency across components.
This commit is contained in:
parent
14884e6e55
commit
18a58cb5a5
|
|
@ -1213,6 +1213,22 @@ button.capture-shell__nav-item {
|
|||
border-left: 1.5px solid var(--border2);
|
||||
}
|
||||
|
||||
/* Gleich breite Segment-Buttons (z. B. mobile Rahmenprogramm-Tabs) */
|
||||
.planning-segment-group--equal {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
.planning-segment-group--equal .planning-segment-group__btn {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* Etwas größere Segmente (Planung: Liste / Kalender) */
|
||||
.planning-segment-group--comfort .planning-segment-group__btn {
|
||||
padding: 10px 18px;
|
||||
font-size: 0.92rem;
|
||||
}
|
||||
|
||||
/* Ausklappbare Kontext-Hilfe (Filterzeile Planung) */
|
||||
.planning-filter-help {
|
||||
flex: 1 1 100%;
|
||||
|
|
@ -1246,6 +1262,109 @@ button.capture-shell__nav-item {
|
|||
}
|
||||
}
|
||||
|
||||
/* Rahmenprogramm-Editor: Kurz-Einstieg ausklappbar */
|
||||
.framework-edit-intro {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.framework-edit-intro__summary {
|
||||
cursor: pointer;
|
||||
font-size: 0.88rem;
|
||||
font-weight: 600;
|
||||
color: var(--accent-dark);
|
||||
list-style: none;
|
||||
user-select: none;
|
||||
padding: 10px 12px;
|
||||
border-radius: 10px;
|
||||
border: 1px dashed var(--border2);
|
||||
background: var(--surface2);
|
||||
}
|
||||
.framework-edit-intro__summary::-webkit-details-marker {
|
||||
display: none;
|
||||
}
|
||||
.framework-edit-intro__body {
|
||||
margin-top: 10px;
|
||||
padding: 12px 14px;
|
||||
font-size: 0.88rem;
|
||||
line-height: 1.55;
|
||||
color: var(--text2);
|
||||
border-radius: 10px;
|
||||
border: 1px solid var(--border);
|
||||
background: var(--surface);
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.framework-edit-intro__summary {
|
||||
color: var(--accent);
|
||||
}
|
||||
}
|
||||
|
||||
/* Admin: Zuordnungsmatrix (Stilrichtungen ↔ Zielgruppen) */
|
||||
.admin-assignments-wrap {
|
||||
background: var(--surface);
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
}
|
||||
.admin-assignments-wrap__title {
|
||||
margin-top: 0;
|
||||
font-size: 1.15rem;
|
||||
font-weight: 700;
|
||||
}
|
||||
.admin-assignments-matrix-container {
|
||||
overflow-x: auto;
|
||||
margin-top: 20px;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
.admin-assignments-matrix {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
min-width: 600px;
|
||||
}
|
||||
.admin-assignments-matrix th,
|
||||
.admin-assignments-matrix td {
|
||||
border: 1px solid var(--border);
|
||||
padding: 12px;
|
||||
}
|
||||
.admin-assignments-matrix th {
|
||||
background: var(--surface2);
|
||||
font-weight: 600;
|
||||
color: var(--text1);
|
||||
}
|
||||
.admin-assignments-matrix__corner {
|
||||
position: sticky;
|
||||
left: 0;
|
||||
background: var(--surface);
|
||||
z-index: 2;
|
||||
}
|
||||
.admin-assignments-matrix__row-label {
|
||||
position: sticky;
|
||||
left: 0;
|
||||
background: var(--surface);
|
||||
z-index: 1;
|
||||
padding: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
.admin-assignments-matrix tbody tr:hover {
|
||||
background: var(--surface2);
|
||||
}
|
||||
.admin-assignments-matrix__focus-header td {
|
||||
background: var(--surface2);
|
||||
padding: 8px 12px;
|
||||
font-weight: 600;
|
||||
color: var(--text2);
|
||||
}
|
||||
.admin-assignments-matrix__th-narrow {
|
||||
text-align: center;
|
||||
padding: 12px;
|
||||
}
|
||||
@media (max-width: 768px) {
|
||||
.admin-assignments-matrix {
|
||||
font-size: 14px;
|
||||
}
|
||||
.admin-assignments-matrix th,
|
||||
.admin-assignments-matrix td {
|
||||
padding: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Admin: Split-Layout wie .analysis-split (nur Gruppen in der Nav) */
|
||||
.admin-shell {
|
||||
width: 100%;
|
||||
|
|
@ -2962,36 +3081,26 @@ button.capture-shell__nav-item {
|
|||
}
|
||||
.framework-edit__tabbar {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
align-items: stretch;
|
||||
gap: 8px;
|
||||
margin-bottom: 14px;
|
||||
padding: 2px 0 12px;
|
||||
padding: 6px 0 12px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
flex-wrap: nowrap;
|
||||
overflow-x: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
scrollbar-width: none;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 6;
|
||||
background: var(--bg);
|
||||
}
|
||||
.framework-edit__tabbar::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
.framework-edit__tab {
|
||||
flex: 1 1 0;
|
||||
.framework-edit__tabbar .planning-segment-group {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
padding: 10px 8px;
|
||||
border: 1px solid var(--border2);
|
||||
border-radius: 10px;
|
||||
background: var(--surface2);
|
||||
color: var(--text2);
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
font-family: var(--font);
|
||||
}
|
||||
.framework-edit__tab--active {
|
||||
background: var(--accent-light);
|
||||
color: var(--accent-dark);
|
||||
border-color: var(--accent);
|
||||
}
|
||||
.framework-edit__plan-stack {
|
||||
display: flex;
|
||||
|
|
|
|||
|
|
@ -5,22 +5,24 @@ function AssignmentsTab({ styleDirections, targetGroups, assignments, loading, e
|
|||
const [saving, setSaving] = useState(false)
|
||||
|
||||
if (loading) {
|
||||
return <div style={{ textAlign: 'center', padding: '40px' }}><div className="spinner"></div></div>
|
||||
return (
|
||||
<div className="empty-state" style={{ padding: '2.5rem' }}>
|
||||
<div className="spinner" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
async function toggleAssignment(styleDirectionId, targetGroupId, currentlyAssigned) {
|
||||
setSaving(true)
|
||||
try {
|
||||
if (currentlyAssigned) {
|
||||
// Find and delete the assignment
|
||||
const assignment = assignments.find(
|
||||
a => a.style_direction_id === styleDirectionId && a.target_group_id === targetGroupId
|
||||
(a) => a.style_direction_id === styleDirectionId && a.target_group_id === targetGroupId
|
||||
)
|
||||
if (assignment) {
|
||||
await api.deleteStyleDirectionTargetGroup(assignment.id)
|
||||
}
|
||||
} else {
|
||||
// Create new assignment
|
||||
await api.createStyleDirectionTargetGroup({
|
||||
style_direction_id: styleDirectionId,
|
||||
target_group_id: targetGroupId,
|
||||
|
|
@ -37,11 +39,10 @@ function AssignmentsTab({ styleDirections, targetGroups, assignments, loading, e
|
|||
|
||||
function isAssigned(styleDirectionId, targetGroupId) {
|
||||
return assignments.some(
|
||||
a => a.style_direction_id === styleDirectionId && a.target_group_id === targetGroupId
|
||||
(a) => a.style_direction_id === styleDirectionId && a.target_group_id === targetGroupId
|
||||
)
|
||||
}
|
||||
|
||||
// Group style directions by focus area
|
||||
const groupedStyles = styleDirections.reduce((acc, sd) => {
|
||||
const key = sd.focus_area_name || 'Ohne Fokusbereich'
|
||||
if (!acc[key]) acc[key] = []
|
||||
|
|
@ -50,30 +51,30 @@ function AssignmentsTab({ styleDirections, targetGroups, assignments, loading, e
|
|||
}, {})
|
||||
|
||||
return (
|
||||
<div style={{ background: 'var(--surface)', borderRadius: '12px', padding: '20px' }}>
|
||||
<h2 style={{ marginTop: 0 }}>Zuordnungen: Stilrichtungen ↔ Zielgruppen</h2>
|
||||
{error && <div style={{ color: 'var(--danger)', padding: '16px', background: 'var(--surface2)', borderRadius: '8px', marginBottom: '16px' }}>{error}</div>}
|
||||
<div className="admin-assignments-wrap">
|
||||
<h2 className="admin-assignments-wrap__title">Zuordnungen: Stilrichtungen ↔ Zielgruppen</h2>
|
||||
{error && <div className="admin-matrix-alert">{error}</div>}
|
||||
|
||||
{targetGroups.length === 0 && (
|
||||
<div style={{ textAlign: 'center', color: 'var(--text3)', padding: '40px' }}>
|
||||
Keine Zielgruppen vorhanden. Bitte erst im Tab "Kataloge" anlegen.
|
||||
<div className="empty-state" style={{ padding: '2rem 1rem' }}>
|
||||
Keine Zielgruppen vorhanden. Bitte zuerst unter <strong>Kataloge</strong> anlegen.
|
||||
</div>
|
||||
)}
|
||||
|
||||
{styleDirections.length === 0 && (
|
||||
<div style={{ textAlign: 'center', color: 'var(--text3)', padding: '40px' }}>
|
||||
Keine Stilrichtungen vorhanden. Bitte erst im Tab "Hierarchie" anlegen.
|
||||
<div className="empty-state" style={{ padding: '2rem 1rem' }}>
|
||||
Keine Stilrichtungen vorhanden. Bitte zuerst unter <strong>Hierarchie</strong> anlegen.
|
||||
</div>
|
||||
)}
|
||||
|
||||
{targetGroups.length > 0 && styleDirections.length > 0 && (
|
||||
<div className="assignment-matrix-container">
|
||||
<table className="assignment-matrix">
|
||||
<div className="admin-assignments-matrix-container">
|
||||
<table className="admin-assignments-matrix">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style={{ position: 'sticky', left: 0, background: 'var(--surface)', zIndex: 2 }}>Stilrichtung</th>
|
||||
{targetGroups.map(tg => (
|
||||
<th key={tg.id} style={{ textAlign: 'center', padding: '12px' }}>
|
||||
<th className="admin-assignments-matrix__corner">Stilrichtung</th>
|
||||
{targetGroups.map((tg) => (
|
||||
<th key={tg.id} className="admin-assignments-matrix__th-narrow">
|
||||
{tg.name}
|
||||
</th>
|
||||
))}
|
||||
|
|
@ -82,17 +83,18 @@ function AssignmentsTab({ styleDirections, targetGroups, assignments, loading, e
|
|||
<tbody>
|
||||
{Object.entries(groupedStyles).map(([focusAreaName, styles]) => (
|
||||
<React.Fragment key={focusAreaName}>
|
||||
<tr className="focus-area-header">
|
||||
<td colSpan={targetGroups.length + 1} style={{ background: 'var(--surface2)', padding: '8px 12px', fontWeight: 600, color: 'var(--text2)' }}>
|
||||
<tr>
|
||||
<td
|
||||
className="admin-assignments-matrix__focus-header"
|
||||
colSpan={targetGroups.length + 1}
|
||||
>
|
||||
{focusAreaName}
|
||||
</td>
|
||||
</tr>
|
||||
{styles.map(sd => (
|
||||
{styles.map((sd) => (
|
||||
<tr key={sd.id}>
|
||||
<td style={{ position: 'sticky', left: 0, background: 'var(--surface)', zIndex: 1, padding: '12px', fontWeight: 500 }}>
|
||||
{sd.name}
|
||||
</td>
|
||||
{targetGroups.map(tg => {
|
||||
<td className="admin-assignments-matrix__row-label">{sd.name}</td>
|
||||
{targetGroups.map((tg) => {
|
||||
const assigned = isAssigned(sd.id, tg.id)
|
||||
return (
|
||||
<td key={tg.id} style={{ textAlign: 'center', padding: '12px' }}>
|
||||
|
|
@ -101,7 +103,8 @@ function AssignmentsTab({ styleDirections, targetGroups, assignments, loading, e
|
|||
checked={assigned}
|
||||
onChange={() => toggleAssignment(sd.id, tg.id, assigned)}
|
||||
disabled={saving}
|
||||
style={{ width: '20px', height: '20px', cursor: 'pointer' }}
|
||||
aria-label={`${sd.name} — ${tg.name}`}
|
||||
style={{ width: '20px', height: '20px', cursor: 'pointer', accentColor: 'var(--accent)' }}
|
||||
/>
|
||||
</td>
|
||||
)
|
||||
|
|
@ -114,45 +117,6 @@ function AssignmentsTab({ styleDirections, targetGroups, assignments, loading, e
|
|||
</table>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<style>{`
|
||||
.assignment-matrix-container {
|
||||
overflow-x: auto;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.assignment-matrix {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
min-width: 600px;
|
||||
}
|
||||
|
||||
.assignment-matrix th,
|
||||
.assignment-matrix td {
|
||||
border: 1px solid var(--border);
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.assignment-matrix th {
|
||||
background: var(--surface2);
|
||||
font-weight: 600;
|
||||
color: var(--text1);
|
||||
}
|
||||
|
||||
.assignment-matrix tbody tr:hover {
|
||||
background: var(--surface2);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.assignment-matrix {
|
||||
font-size: 14px;
|
||||
}
|
||||
.assignment-matrix th,
|
||||
.assignment-matrix td {
|
||||
padding: 8px;
|
||||
}
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -663,52 +663,39 @@ export default function TrainingFrameworkProgramEditPage() {
|
|||
|
||||
<h1 style={{ marginBottom: '0.75rem' }}>{isNew ? 'Neues Rahmenprogramm' : 'Rahmenprogramm bearbeiten'}</h1>
|
||||
|
||||
<div className="card" style={{ marginBottom: '1rem', background: 'var(--surface2)', borderStyle: 'dashed' }}>
|
||||
<p style={{ fontSize: '0.88rem', color: 'var(--text2)', lineHeight: 1.55, margin: 0 }}>
|
||||
<details className="framework-edit-intro">
|
||||
<summary className="framework-edit-intro__summary">
|
||||
Kurz erklärt: Was ist ein Rahmenprogramm?
|
||||
</summary>
|
||||
<div className="framework-edit-intro__body">
|
||||
<strong style={{ color: 'var(--text1)' }}>Rahmenprogramm (Bibliothek):</strong> Wiederverwendbare Vorlage mit
|
||||
Zielen und Session‑Slots. <strong>Zuordnung zu Gruppe oder Kalendertermin</strong> erfolgt aus der{' '}
|
||||
Zielen und Session‑Slots. Die <strong>Zuordnung zu Gruppe oder Kalendertermin</strong> erfolgt aus der{' '}
|
||||
<strong>Gruppen‑Planung</strong> („Übernahme“). Pro Slot planst du den Ablauf wie bei einer Trainingsseinheit:{' '}
|
||||
<strong>Abschnitte</strong>, Übungen mit Varianten und Dauer, <strong>Zwischen‑Anmerkungen</strong>.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
|
||||
<div
|
||||
className="framework-edit__tabbar"
|
||||
role="tablist"
|
||||
aria-label="Bereiche"
|
||||
style={
|
||||
desktopLayout
|
||||
? { display: 'none' }
|
||||
: {
|
||||
display: 'flex',
|
||||
gap: 6,
|
||||
marginBottom: 14,
|
||||
padding: '6px 0 12px',
|
||||
borderBottom: '2px solid var(--accent)',
|
||||
flexWrap: 'nowrap',
|
||||
overflowX: 'auto',
|
||||
position: 'sticky',
|
||||
top: 0,
|
||||
zIndex: 6,
|
||||
background: 'var(--bg)',
|
||||
<div className="framework-edit__tabbar" role="tablist" aria-label="Bereiche">
|
||||
<div className="planning-segment-group planning-segment-group--equal">
|
||||
{[
|
||||
{ id: 'meta', label: 'Stammdaten' },
|
||||
{ id: 'plan', label: 'Plan (Ziele & Sessions)' },
|
||||
].map((t) => (
|
||||
<button
|
||||
key={t.id}
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-selected={frameworkTab === t.id}
|
||||
className={
|
||||
'planning-segment-group__btn' +
|
||||
(frameworkTab === t.id ? ' planning-segment-group__btn--active' : '')
|
||||
}
|
||||
}
|
||||
>
|
||||
{[
|
||||
{ id: 'meta', label: 'Stammdaten' },
|
||||
{ id: 'plan', label: 'Plan (Ziele & Sessions)' },
|
||||
].map((t) => (
|
||||
<button
|
||||
key={t.id}
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-selected={frameworkTab === t.id}
|
||||
className={'framework-edit__tab' + (frameworkTab === t.id ? ' framework-edit__tab--active' : '')}
|
||||
onClick={() => setFrameworkTab(t.id)}
|
||||
>
|
||||
{t.label}
|
||||
</button>
|
||||
))}
|
||||
onClick={() => setFrameworkTab(t.id)}
|
||||
>
|
||||
{t.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -100,12 +100,23 @@ export default function TrainingFrameworkProgramsListPage() {
|
|||
}}
|
||||
>
|
||||
<div>
|
||||
<h1 style={{ marginBottom: '0.35rem' }}>Trainingsrahmenprogramme</h1>
|
||||
<p style={{ color: 'var(--text2)', fontSize: '0.95rem', maxWidth: '36rem' }}>
|
||||
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).
|
||||
<h1 className="page-title" style={{ marginBottom: '0.35rem' }}>
|
||||
Trainingsrahmenprogramme
|
||||
</h1>
|
||||
<p style={{ color: 'var(--text2)', fontSize: '0.95rem', maxWidth: '36rem', margin: 0 }}>
|
||||
Vorlagen für Ziele und Sessions — die Verknüpfung mit Gruppenterminen erfolgt in der{' '}
|
||||
<Link to="/planning" style={{ fontWeight: 600, color: 'var(--accent-dark)' }}>
|
||||
Trainingsplanung
|
||||
</Link>
|
||||
.
|
||||
</p>
|
||||
<details className="planning-filter-help" style={{ marginTop: '10px', maxWidth: '36rem' }}>
|
||||
<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>
|
||||
<Link
|
||||
to="/planning/framework-programs/new"
|
||||
|
|
|
|||
|
|
@ -865,31 +865,18 @@ function TrainingPlanningPage() {
|
|||
Ansicht
|
||||
</span>
|
||||
<div
|
||||
className="planning-segment-group planning-segment-group--comfort"
|
||||
role="group"
|
||||
aria-label="Darstellung Liste oder Kalender"
|
||||
style={{
|
||||
display: 'inline-flex',
|
||||
borderRadius: '10px',
|
||||
border: '1.5px solid var(--border2)',
|
||||
overflow: 'hidden',
|
||||
background: 'var(--surface2)',
|
||||
boxSizing: 'border-box',
|
||||
}}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
aria-pressed={planView === 'list'}
|
||||
onClick={() => setPlanView('list')}
|
||||
style={{
|
||||
border: 'none',
|
||||
padding: '10px 20px',
|
||||
fontWeight: 600,
|
||||
fontSize: '0.92rem',
|
||||
cursor: 'pointer',
|
||||
background: planView === 'list' ? 'var(--accent-dark)' : 'transparent',
|
||||
color: planView === 'list' ? '#fff' : 'var(--text1)',
|
||||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
className={
|
||||
'planning-segment-group__btn' +
|
||||
(planView === 'list' ? ' planning-segment-group__btn--active' : '')
|
||||
}
|
||||
>
|
||||
Liste
|
||||
</button>
|
||||
|
|
@ -904,17 +891,10 @@ function TrainingPlanningPage() {
|
|||
return prev || new Date().toISOString().slice(0, 7)
|
||||
})
|
||||
}}
|
||||
style={{
|
||||
border: 'none',
|
||||
borderLeft: '1.5px solid var(--border2)',
|
||||
padding: '10px 20px',
|
||||
fontWeight: 600,
|
||||
fontSize: '0.92rem',
|
||||
cursor: 'pointer',
|
||||
background: planView === 'calendar' ? 'var(--accent-dark)' : 'transparent',
|
||||
color: planView === 'calendar' ? '#fff' : 'var(--text1)',
|
||||
whiteSpace: 'nowrap',
|
||||
}}
|
||||
className={
|
||||
'planning-segment-group__btn' +
|
||||
(planView === 'calendar' ? ' planning-segment-group__btn--active' : '')
|
||||
}
|
||||
>
|
||||
Kalender
|
||||
</button>
|
||||
|
|
@ -927,8 +907,8 @@ function TrainingPlanningPage() {
|
|||
</div>
|
||||
|
||||
<p style={{ color: 'var(--text2)', fontSize: '0.95rem', marginBottom: '1.25rem' }}>
|
||||
Wähle eine Trainingsgruppe, lege dann Termine mit Inhalt (Abschnitte und Übungen) an — ein Plan entsteht aus einer oder mehreren{' '}
|
||||
<strong>Trainingseinheiten</strong> im gewählten Zeitraum.
|
||||
Wähle eine Trainingsgruppe und lege <strong>Trainingseinheiten</strong> für den Zeitraum an (Inhalt: Abschnitte
|
||||
und Übungen).
|
||||
</p>
|
||||
|
||||
<div className="card" style={{ marginBottom: '1.25rem', padding: '12px 14px' }}>
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user