feat: enhance TrainingPlanningPage with new training unit creation UI
All checks were successful
Deploy Development / deploy (push) Successful in 34s
Test Suite / pytest-backend (push) Successful in 6s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 6s
Test Suite / playwright-tests (push) Successful in 22s

- Introduced a new layout for creating training units within a card format, improving visual organization and user experience.
- Added CSS styles for various elements related to training unit creation, including titles, hints, and action buttons.
- Removed the quick template ID state and related functionality to streamline the creation process.
- Updated user prompts and hints to guide users more effectively in selecting training groups and creating new training units.
This commit is contained in:
Lars 2026-05-06 08:44:15 +02:00
parent d4b9db9520
commit 2007f3f659
2 changed files with 130 additions and 92 deletions

View File

@ -3824,6 +3824,98 @@ a.analysis-split__nav-item {
} }
} }
/* ── Trainingsplanung: Abschnitt „Neue Trainingseinheit“ + Vorlage im Modal ───────── */
.training-planning-create--in-card {
margin-top: 1.25rem;
padding-top: 1.25rem;
border-top: 1px solid var(--border, rgba(0, 0, 0, 0.08));
}
.training-planning-create__intro {
margin-bottom: 1rem;
}
.training-planning-create__title {
margin: 0 0 0.45rem;
font-size: 1.06rem;
font-weight: 700;
color: var(--text1);
letter-spacing: -0.01em;
}
.training-planning-create__lede {
margin: 0;
font-size: 0.92rem;
line-height: 1.55;
color: var(--text2);
max-width: 52rem;
}
.training-planning-create__actions {
display: flex;
flex-wrap: wrap;
gap: 12px;
align-items: center;
}
.training-planning-create__cta {
min-height: 44px;
padding-left: 1.35rem;
padding-right: 1.35rem;
font-weight: 600;
}
.training-planning-create__secondary {
min-height: 44px;
padding-left: 1rem;
padding-right: 1rem;
}
.training-planning-create__hint {
margin: 0.85rem 0 0;
font-size: 0.82rem;
line-height: 1.45;
color: var(--text3);
max-width: 48rem;
}
.training-planning-create__hint--warn {
color: var(--text2);
margin-top: 0.65rem;
padding: 0.55rem 0.7rem;
border-radius: 8px;
background: var(--surface2);
border: 1px solid var(--border2);
}
.training-planning-template-panel {
padding: 1rem 1.1rem;
border-radius: 12px;
border: 1px solid var(--border2);
background: linear-gradient(165deg, var(--surface2) 0%, var(--surface) 100%);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.04);
}
.training-planning-template-panel__label {
font-weight: 600;
margin-bottom: 0.4rem;
display: block;
}
.training-planning-template-panel__select {
font-size: 0.94rem;
padding: 0.55rem 0.65rem;
width: 100%;
max-width: 100%;
}
.training-planning-template-panel__help {
margin: 0.65rem 0 0;
font-size: 0.82rem;
color: var(--text2);
line-height: 1.48;
}
@media print { @media print {
.desktop-sidebar, .desktop-sidebar,
.bottom-nav, .bottom-nav,

View File

@ -121,7 +121,6 @@ function TrainingPlanningPage() {
/** Abschnitts-Editor bei Bearbeitung: Planung vs. Nachbereitung (Ist & Abweichungen) */ /** Abschnitts-Editor bei Bearbeitung: Planung vs. Nachbereitung (Ist & Abweichungen) */
const [sectionsEditMode, setSectionsEditMode] = useState('planning') const [sectionsEditMode, setSectionsEditMode] = useState('planning')
const [draftPlanTemplateId, setDraftPlanTemplateId] = useState('') const [draftPlanTemplateId, setDraftPlanTemplateId] = useState('')
const [quickTemplateId, setQuickTemplateId] = useState('')
const [exercisePickerOpen, setExercisePickerOpen] = useState(false) const [exercisePickerOpen, setExercisePickerOpen] = useState(false)
const [exercisePickerTarget, setExercisePickerTarget] = useState(null) const [exercisePickerTarget, setExercisePickerTarget] = useState(null)
const [planningPeekCtx, setPlanningPeekCtx] = useState(null) const [planningPeekCtx, setPlanningPeekCtx] = useState(null)
@ -461,28 +460,6 @@ function TrainingPlanningPage() {
return { fpTitle, slotBit, fpId: unit.origin_framework_program_id } return { fpTitle, slotBit, fpId: unit.origin_framework_program_id }
} }
const handleQuickCreate = async () => {
if (!selectedGroupId) {
alert('Bitte wähle zuerst eine Trainingsgruppe')
return
}
const date = prompt('Datum für neue Trainingseinheit (YYYY-MM-DD):', today)
if (!date) return
try {
const body = {
group_id: parseInt(selectedGroupId, 10),
planned_date: date
}
if (quickTemplateId) {
body.plan_template_id = parseInt(quickTemplateId, 10)
}
await api.quickCreateTrainingUnit(body)
await loadUnits()
} catch (err) {
alert('Fehler beim Erstellen: ' + err.message)
}
}
const handleCreate = () => { const handleCreate = () => {
if (!selectedGroupId) { if (!selectedGroupId) {
alert('Bitte wähle zuerst eine Trainingsgruppe') alert('Bitte wähle zuerst eine Trainingsgruppe')
@ -1191,80 +1168,39 @@ function TrainingPlanningPage() {
</div> </div>
)} )}
<div <div className="training-planning-create training-planning-create--in-card">
style={{ <div className="training-planning-create__intro">
marginTop: '1.25rem', <h3 className="training-planning-create__title">Neue Trainingseinheit</h3>
paddingTop: '1rem', <p className="training-planning-create__lede">
borderTop: '1px solid var(--border, rgba(0,0,0,0.08))' Termin mit Datum, Zeiten und Ablauf (Abschnitte &amp; Übungen) festlegen optional eine{' '}
}} <strong>Trainingsvorlage</strong> für die Gliederung wählen oder Inhalte aus einem{' '}
> <strong>Rahmenprogramm</strong> übernehmen.
<p style={{ fontSize: '0.88rem', color: 'var(--text2)', marginBottom: '0.75rem' }}> </p>
<strong>Plan anlegen:</strong> neue Trainingseinheit mit Datum, Zeit und Ablauf oder schnell nur mit Datum (Zeiten aus der Gruppe).
{!selectedGroupId && ( {!selectedGroupId && (
<span style={{ display: 'block', marginTop: '0.35rem' }}> <p className="training-planning-create__hint training-planning-create__hint--warn">
Wähle oben eine Trainingsgruppe, um die Schaltflächen zu aktivieren. Wähle oben eine Trainingsgruppe, um fortzufahren.
</span> </p>
)} )}
{groups.length === 0 && ( {groups.length === 0 && (
<span style={{ display: 'block', marginTop: '0.35rem' }}> <p className="training-planning-create__hint training-planning-create__hint--warn">
Es gibt noch keine aktive Trainingsgruppe unter{' '} Es gibt noch keine aktive Trainingsgruppe unter{' '}
<Link to="/clubs"> <Link to="/clubs">Vereine</Link> anlegen oder aktivieren.
Vereine </p>
</Link>{' '}
anlegen oder aktivieren.
</span>
)} )}
</p> </div>
<div <div className="training-planning-create__actions">
style={{
display: 'flex',
flexWrap: 'wrap',
gap: '0.5rem',
alignItems: 'center'
}}
>
<button <button
type="button" type="button"
className="btn btn-primary" className="btn btn-primary training-planning-create__cta"
disabled={!selectedGroupId} disabled={!selectedGroupId}
title={!selectedGroupId ? 'Zuerst eine Trainingsgruppe wählen' : undefined} title={!selectedGroupId ? 'Zuerst eine Trainingsgruppe wählen' : undefined}
onClick={handleCreate} onClick={handleCreate}
> >
+ Neue Trainingseinheit planen Trainingseinheit planen
</button> </button>
<div style={{ display: 'flex', alignItems: 'center', gap: '0.5rem', flexWrap: 'wrap' }}>
<label className="form-label" style={{ marginBottom: 0 }}>
Schnell (+ optional Vorlage):
</label>
<select
className="form-input"
style={{ minWidth: '180px', marginBottom: 0 }}
value={quickTemplateId}
onChange={(e) => setQuickTemplateId(e.target.value)}
disabled={!selectedGroupId}
title={!selectedGroupId ? 'Zuerst Trainingsgruppe wählen' : undefined}
>
<option value="">Standard (leer)</option>
{planTemplates.map((t) => (
<option key={t.id} value={String(t.id)}>
{t.name}
{typeof t.sections_count === 'number' ? ` (${t.sections_count} Abschn.)` : ''}
</option>
))}
</select>
<button
type="button"
className="btn btn-secondary"
disabled={!selectedGroupId}
title={!selectedGroupId ? 'Zuerst eine Trainingsgruppe wählen' : undefined}
onClick={handleQuickCreate}
>
Schnell erstellen
</button>
</div>
<button <button
type="button" type="button"
className="btn btn-secondary" className="btn btn-secondary training-planning-create__secondary"
disabled={!selectedGroupId} disabled={!selectedGroupId}
title={!selectedGroupId ? 'Zuerst eine Trainingsgruppe wählen' : undefined} title={!selectedGroupId ? 'Zuerst eine Trainingsgruppe wählen' : undefined}
onClick={openFrameworkImportModal} onClick={openFrameworkImportModal}
@ -1272,13 +1208,18 @@ function TrainingPlanningPage() {
Aus Rahmen übernehmen Aus Rahmen übernehmen
</button> </button>
</div> </div>
<p className="training-planning-create__hint">
Vorlage (Ohne Vorlage oder gespeicherte Gliederung) stellst du im sich öffnenden Dialog ein; dort auch
Kalenderdatum und Zeiten.
</p>
</div> </div>
</div> </div>
{!selectedGroupId ? ( {!selectedGroupId ? (
<div className="card"> <div className="card">
<p style={{ color: 'var(--text2)', textAlign: 'center' }}> <p style={{ color: 'var(--text2)', textAlign: 'center' }}>
Wähle oben eine Trainingsgruppe danach kannst du mit <strong>Neue Trainingseinheit planen</strong> starten. Wähle oben eine Trainingsgruppe danach kannst du unter{' '}
<strong>Trainingseinheit planen</strong> einen Termin anlegen.
</p> </p>
</div> </div>
) : planView === 'calendar' ? ( ) : planView === 'calendar' ? (
@ -1460,8 +1401,8 @@ function TrainingPlanningPage() {
) : units.length === 0 ? ( ) : units.length === 0 ? (
<div className="card"> <div className="card">
<p style={{ color: 'var(--text2)', textAlign: 'center' }}> <p style={{ color: 'var(--text2)', textAlign: 'center' }}>
Keine Trainingseinheiten in diesem Zeitraum. Nutze oben <strong>Neue Trainingseinheit planen</strong> oder{' '} Keine Trainingseinheiten in diesem Zeitraum. Unten unter <strong>Neue Trainingseinheit</strong> einen
<strong>Schnell erstellen</strong>, um den ersten Termin anzulegen. Termin anlegen optional mit Vorlage im Dialog.
</p> </p>
</div> </div>
) : ( ) : (
@ -2099,22 +2040,27 @@ function TrainingPlanningPage() {
})() : null} })() : null}
{!editingUnit && ( {!editingUnit && (
<div className="form-row" style={{ marginBottom: '1.25rem' }}> <div className="training-planning-template-panel" style={{ marginBottom: '1.35rem' }}>
<label className="form-label">Gliederungsvorlage (optional)</label> <label className="form-label training-planning-template-panel__label" htmlFor="planning-draft-template">
Vorlage für den Ablauf
</label>
<select <select
className="form-input" id="planning-draft-template"
className="form-input training-planning-template-panel__select"
value={draftPlanTemplateId} value={draftPlanTemplateId}
onChange={(e) => applyTemplateFromSelect(e.target.value)} onChange={(e) => applyTemplateFromSelect(e.target.value)}
> >
<option value="">Keine Vorlage</option> <option value="">Ohne Vorlage leere Gliederung (ein Abschnitt)</option>
{planTemplates.map((t) => ( {planTemplates.map((t) => (
<option key={t.id} value={String(t.id)}> <option key={t.id} value={String(t.id)}>
{t.name} {t.name}
{typeof t.sections_count === 'number' ? ` · ${t.sections_count} Abschn.` : ''}
</option> </option>
))} ))}
</select> </select>
<p style={{ fontSize: '0.8rem', color: 'var(--text2)', marginTop: '0.35rem' }}> <p className="training-planning-template-panel__help">
Lädt die Abschnitte und Hinweise aus der Vorlage; Übungen fügst du hier ein. Übernimmt nur die <strong>Sektionsstruktur</strong> aus der Bibliothek; Übungen trägst du unten bei
den Abschnitten ein. Gespeicherte Vorlagen kannst du unter Planung später erweitern.
</p> </p>
</div> </div>
)} )}