Some checks failed
Deploy Development / deploy (push) Successful in 40s
Test Suite / pytest-backend (push) Successful in 41s
Test Suite / lint-backend (push) Successful in 1s
Test Suite / build-frontend (push) Successful in 11s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Has been cancelled
- Introduced new functions for managing edit, delete, and governance transition permissions for library content, aligning with role-based access control (RBAC) principles. - Updated existing routers to utilize these new functions, ensuring consistent permission checks across training frameworks, modules, and progression graphs. - Enhanced visibility and governance handling for training plan templates and library content, improving overall content management and user experience. - Incremented app version to 0.8.142 and updated changelog to reflect these changes.
641 lines
26 KiB
JavaScript
641 lines
26 KiB
JavaScript
import React, { useEffect, useMemo, useState } from 'react'
|
||
import { Link } from 'react-router-dom'
|
||
import TrainingPlanExerciseVisibilityPanel from '../TrainingPlanExerciseVisibilityPanel'
|
||
import TrainingUnitSectionsEditor from '../TrainingUnitSectionsEditor'
|
||
import { activeClubMemberships } from '../../utils/activeClub'
|
||
import { canDeleteLibraryContent } from '../../utils/libraryContentPermissions'
|
||
import { frameworkLineageText } from '../../utils/trainingPlanningPageHelpers'
|
||
|
||
/**
|
||
* Großes Modal: Neue Trainingseinheit / Einheit bearbeiten (Planung, Trainer, Abschnitte, Durchführung, Notizen).
|
||
*/
|
||
export default function TrainingPlanningUnitFormModal({
|
||
open,
|
||
editingUnit,
|
||
formData,
|
||
updateFormField,
|
||
setFormData,
|
||
onSubmit,
|
||
onCancel,
|
||
draftPlanTemplateId,
|
||
onDraftTemplateSelect,
|
||
planTemplates,
|
||
onDeletePlanTemplate,
|
||
clubDirectory,
|
||
clubDirectoryForCo,
|
||
planningModalClubId,
|
||
user,
|
||
onMetaRefresh,
|
||
sectionsEditMode,
|
||
setSectionsEditMode,
|
||
onSaveAsTemplate,
|
||
onRequestTrainingModulePick,
|
||
onRequestExercisePick,
|
||
onPeekExercise,
|
||
}) {
|
||
const [newTplVisibility, setNewTplVisibility] = useState('private')
|
||
const [newTplClubId, setNewTplClubId] = useState('')
|
||
|
||
const memberClubs = useMemo(() => activeClubMemberships(user?.clubs), [user?.clubs])
|
||
const roleLc = String(user?.role || '').toLowerCase()
|
||
const isSuperadmin = roleLc === 'superadmin'
|
||
|
||
useEffect(() => {
|
||
if (!open) return
|
||
if (planningModalClubId != null && planningModalClubId !== '') {
|
||
setNewTplClubId(String(planningModalClubId))
|
||
} else if (memberClubs.length === 1) {
|
||
setNewTplClubId(String(memberClubs[0].id))
|
||
}
|
||
}, [open, planningModalClubId, memberClubs])
|
||
|
||
if (!open) return null
|
||
|
||
return (
|
||
<div
|
||
data-testid="planning-unit-form-modal"
|
||
style={{
|
||
position: 'fixed',
|
||
top: 0,
|
||
left: 0,
|
||
right: 0,
|
||
bottom: 0,
|
||
background: 'rgba(0,0,0,0.5)',
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
zIndex: 1000,
|
||
padding: '1rem',
|
||
overflowY: 'auto',
|
||
}}
|
||
>
|
||
<div
|
||
style={{
|
||
background: 'var(--surface)',
|
||
borderRadius: '12px',
|
||
padding: 'clamp(12px, 3vw, 2rem)',
|
||
maxWidth: 'min(1100px, 100%)',
|
||
width: '100%',
|
||
maxHeight: '92vh',
|
||
overflowY: 'auto',
|
||
margin: 'max(0px, env(safe-area-inset-top, 0px)) auto',
|
||
boxSizing: 'border-box',
|
||
minWidth: 0,
|
||
}}
|
||
>
|
||
<h2 style={{ marginBottom: '1rem' }}>
|
||
{editingUnit ? 'Trainingseinheit bearbeiten' : 'Neue Trainingseinheit'}
|
||
</h2>
|
||
|
||
{editingUnit?.origin_framework_slot_id
|
||
? (() => {
|
||
const L = frameworkLineageText(editingUnit)
|
||
return (
|
||
<div
|
||
className="card"
|
||
style={{
|
||
marginBottom: '1.1rem',
|
||
padding: '12px 14px',
|
||
background: 'var(--surface2)',
|
||
fontSize: '0.9rem',
|
||
lineHeight: 1.5,
|
||
}}
|
||
>
|
||
<strong style={{ color: 'var(--text1)' }}>Herkunft:</strong>{' '}
|
||
{editingUnit.origin_framework_program_id ? (
|
||
<Link
|
||
to={`/planning/framework-programs/${editingUnit.origin_framework_program_id}`}
|
||
style={{ color: 'var(--accent-dark)' }}
|
||
>
|
||
{L.fpTitle}
|
||
</Link>
|
||
) : (
|
||
L.fpTitle
|
||
)}
|
||
<span style={{ color: 'var(--text2)' }}> · {L.slotBit}</span>
|
||
<p style={{ margin: '0.5rem 0 0', fontSize: '0.82rem', color: 'var(--text2)' }}>
|
||
Inhalt stammt aus dem Session-Blueprint des Rahmenprogramms. Änderungen gelten nur für diese
|
||
geplante Einheit; die Zuordnung zum Rahmen bleibt zur Nachverfolgung erhalten.
|
||
</p>
|
||
</div>
|
||
)
|
||
})()
|
||
: null}
|
||
|
||
{!editingUnit && (
|
||
<div className="training-planning-template-panel" style={{ marginBottom: '1.35rem' }}>
|
||
<label className="form-label training-planning-template-panel__label" htmlFor="planning-draft-template">
|
||
Vorlage für den Ablauf
|
||
</label>
|
||
<select
|
||
id="planning-draft-template"
|
||
className="form-input training-planning-template-panel__select"
|
||
value={draftPlanTemplateId}
|
||
onChange={(e) => onDraftTemplateSelect(e.target.value)}
|
||
>
|
||
<option value="">Ohne Vorlage — leere Gliederung (ein Abschnitt)</option>
|
||
{planTemplates.map((t) => {
|
||
const v = String(t.visibility || 'club').toLowerCase()
|
||
const vLabel = v === 'private' ? 'Privat' : v === 'official' ? 'Offiziell' : 'Verein'
|
||
return (
|
||
<option key={t.id} value={String(t.id)}>
|
||
{t.name}
|
||
{typeof t.sections_count === 'number' ? ` · ${t.sections_count} Abschn.` : ''}{' '}
|
||
· {vLabel}
|
||
</option>
|
||
)
|
||
})}
|
||
</select>
|
||
<p className="training-planning-template-panel__help">
|
||
Ü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>
|
||
</div>
|
||
)}
|
||
|
||
{planTemplates.length > 0 && typeof onDeletePlanTemplate === 'function' ? (
|
||
<details
|
||
className="card"
|
||
style={{
|
||
marginBottom: '1.35rem',
|
||
padding: '12px 14px',
|
||
background: 'var(--surface2)',
|
||
border: '1px solid var(--border)',
|
||
}}
|
||
>
|
||
<summary style={{ cursor: 'pointer', fontWeight: 600, color: 'var(--text1)' }}>
|
||
Gespeicherte Vorlagen löschen
|
||
</summary>
|
||
<p style={{ margin: '0.65rem 0 0.75rem', fontSize: '0.82rem', color: 'var(--text2)', lineHeight: 1.45 }}>
|
||
Entfernen nach Rolle: eigene private Vorlagen; Vereinsinhalte als Vereinsadmin; offizielle nur als
|
||
Plattform‑Admin. Einheiten mit Verweis behalten den Ablauf; die Vorlage wird entkoppelt.
|
||
</p>
|
||
<ul style={{ listStyle: 'none', margin: 0, padding: 0 }}>
|
||
{planTemplates.map((t, ti) => {
|
||
const canDel = user && canDeleteLibraryContent(user, t)
|
||
return (
|
||
<li
|
||
key={t.id}
|
||
style={{
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
justifyContent: 'space-between',
|
||
gap: '10px',
|
||
padding: '8px 0',
|
||
borderTop: ti === 0 ? 'none' : '1px solid var(--border)',
|
||
}}
|
||
>
|
||
<span style={{ minWidth: 0, flex: 1, fontSize: '0.9rem' }}>
|
||
<strong style={{ color: 'var(--text1)' }}>{t.name}</strong>
|
||
<span style={{ fontSize: '0.78rem', color: 'var(--text3)', marginLeft: '6px' }}>
|
||
(
|
||
{String(t.visibility || 'club').toLowerCase() === 'private'
|
||
? 'Privat'
|
||
: String(t.visibility || 'club').toLowerCase() === 'official'
|
||
? 'Offiziell'
|
||
: 'Verein'}
|
||
)
|
||
</span>
|
||
{typeof t.sections_count === 'number' ? (
|
||
<span style={{ fontSize: '0.82rem', color: 'var(--text2)', marginLeft: '6px' }}>
|
||
· {t.sections_count} Abschn.
|
||
</span>
|
||
) : null}
|
||
</span>
|
||
{canDel ? (
|
||
<button
|
||
type="button"
|
||
className="btn btn-danger"
|
||
style={{ flexShrink: 0, padding: '6px 12px', fontSize: '0.82rem' }}
|
||
onClick={() => onDeletePlanTemplate(t)}
|
||
>
|
||
Löschen
|
||
</button>
|
||
) : (
|
||
<span style={{ fontSize: '0.78rem', color: 'var(--text3)', flexShrink: 0 }}>nur Lesen</span>
|
||
)}
|
||
</li>
|
||
)
|
||
})}
|
||
</ul>
|
||
</details>
|
||
) : null}
|
||
|
||
<form onSubmit={onSubmit}>
|
||
<h3 style={{ marginBottom: '1rem' }}>Planung</h3>
|
||
|
||
<div className="responsive-grid-3" style={{ marginBottom: '1rem' }}>
|
||
<div className="form-row">
|
||
<label className="form-label">Datum *</label>
|
||
<input
|
||
type="date"
|
||
className="form-input"
|
||
value={formData.planned_date}
|
||
onChange={(e) => updateFormField('planned_date', e.target.value)}
|
||
required
|
||
/>
|
||
</div>
|
||
|
||
<div className="form-row">
|
||
<label className="form-label">Von</label>
|
||
<input
|
||
type="time"
|
||
className="form-input"
|
||
value={formData.planned_time_start}
|
||
onChange={(e) => updateFormField('planned_time_start', e.target.value)}
|
||
/>
|
||
</div>
|
||
|
||
<div className="form-row">
|
||
<label className="form-label">Bis</label>
|
||
<input
|
||
type="time"
|
||
className="form-input"
|
||
value={formData.planned_time_end}
|
||
onChange={(e) => updateFormField('planned_time_end', e.target.value)}
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="form-row">
|
||
<label className="form-label">Trainingsfokus</label>
|
||
<input
|
||
type="text"
|
||
className="form-input"
|
||
value={formData.planned_focus}
|
||
onChange={(e) => updateFormField('planned_focus', e.target.value)}
|
||
placeholder="z.B. Grundlagen, Kinder altersgerecht"
|
||
/>
|
||
</div>
|
||
|
||
<div
|
||
className="card"
|
||
style={{
|
||
marginTop: '1.25rem',
|
||
marginBottom: '0.25rem',
|
||
padding: '12px 14px',
|
||
background: 'var(--surface2)',
|
||
}}
|
||
>
|
||
<h3 style={{ margin: '0 0 10px', fontSize: '1rem' }}>Trainerzuordnung (diese Einheit)</h3>
|
||
<div className="form-row">
|
||
<label className="form-label">Leitung</label>
|
||
<select
|
||
className="form-input"
|
||
value={formData.lead_trainer_profile_id}
|
||
onChange={(e) => updateFormField('lead_trainer_profile_id', e.target.value)}
|
||
disabled={!editingUnit && !formData.group_id}
|
||
>
|
||
<option value="">Standard (Haupttrainer der Gruppe)</option>
|
||
{clubDirectory.map((m) => {
|
||
const idStr = String(m.id)
|
||
return (
|
||
<option key={idStr} value={idStr}>
|
||
{(m.name || '').trim() || m.email || `Profil ${m.id}`}
|
||
</option>
|
||
)
|
||
})}
|
||
</select>
|
||
<p style={{ fontSize: '0.8rem', color: 'var(--text2)', marginTop: '0.35rem', lineHeight: 1.45 }}>
|
||
Für Vertretungen genügt in der Regel die Vereinsmitgliedschaft; Zuweisen dürfen u. a. Haupt-/Co‑Trainer
|
||
dieser Gruppe, der/die Ersteller:in der Einheit oder Vereinsadmins.
|
||
</p>
|
||
</div>
|
||
<div className="form-row" style={{ marginTop: '0.75rem' }}>
|
||
<label style={{ display: 'flex', alignItems: 'center', gap: '8px', cursor: 'pointer' }}>
|
||
<input
|
||
type="checkbox"
|
||
checked={formData.session_assistants_inherit}
|
||
onChange={(e) => updateFormField('session_assistants_inherit', e.target.checked)}
|
||
/>
|
||
<span style={{ fontSize: '0.9rem', color: 'var(--text1)' }}>
|
||
Co-Trainer wie in der Trainingsgruppe (Standard)
|
||
</span>
|
||
</label>
|
||
</div>
|
||
{!formData.session_assistants_inherit ? (
|
||
<div style={{ marginTop: '10px', maxHeight: '200px', overflowY: 'auto' }}>
|
||
{clubDirectoryForCo.map((m) => {
|
||
const mid = typeof m.id === 'number' ? m.id : parseInt(String(m.id), 10)
|
||
const labelText = `${(m.name || '').trim() || m.email || `Profil ${mid}`}`
|
||
const isOn = Number.isFinite(mid) && formData.session_assistant_profile_ids.includes(mid)
|
||
return (
|
||
<label
|
||
key={`co-${mid}`}
|
||
style={{
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
gap: '8px',
|
||
fontSize: '0.875rem',
|
||
marginBottom: '6px',
|
||
cursor: 'pointer',
|
||
color: 'var(--text1)',
|
||
}}
|
||
>
|
||
<input
|
||
type="checkbox"
|
||
checked={isOn}
|
||
onChange={() => {
|
||
setFormData((prev) => {
|
||
const was = prev.session_assistant_profile_ids.includes(mid)
|
||
const nextIds = was
|
||
? prev.session_assistant_profile_ids.filter((x) => x !== mid)
|
||
: [...prev.session_assistant_profile_ids, mid].sort((a, b) => a - b)
|
||
return { ...prev, session_assistant_profile_ids: nextIds }
|
||
})
|
||
}}
|
||
/>
|
||
<span>{labelText}</span>
|
||
</label>
|
||
)
|
||
})}
|
||
</div>
|
||
) : null}
|
||
{!clubDirectory.length ? (
|
||
<p style={{ margin: '10px 0 0', fontSize: '0.82rem', color: 'var(--text3)', lineHeight: 1.4 }}>
|
||
Keine Einträge im Vereins-Mitgliederverzeichnis oder noch nicht geladen (nur für Vereinsinterne).
|
||
</p>
|
||
) : null}
|
||
</div>
|
||
|
||
<TrainingPlanExerciseVisibilityPanel
|
||
sections={formData.sections}
|
||
targetClubId={planningModalClubId}
|
||
user={user}
|
||
onMetaRefresh={onMetaRefresh}
|
||
/>
|
||
|
||
<div style={{ marginTop: '2rem' }}>
|
||
{editingUnit ? (
|
||
<div style={{ marginBottom: '1rem' }}>
|
||
<div
|
||
role="radiogroup"
|
||
aria-label="Modus für Abschnitte und Übungen"
|
||
style={{
|
||
display: 'flex',
|
||
flexWrap: 'wrap',
|
||
alignItems: 'center',
|
||
gap: '10px',
|
||
}}
|
||
>
|
||
<span className="form-label" style={{ marginBottom: 0, fontSize: '0.82rem' }}>
|
||
Ablauf bearbeiten als
|
||
</span>
|
||
<div
|
||
style={{
|
||
display: 'inline-flex',
|
||
borderRadius: '10px',
|
||
border: '1.5px solid var(--border2)',
|
||
overflow: 'hidden',
|
||
background: 'var(--surface2)',
|
||
}}
|
||
>
|
||
{[
|
||
{ id: 'planning', label: 'Planung' },
|
||
{ id: 'debrief', label: 'Nachbereitung' },
|
||
].map((opt, i) => (
|
||
<button
|
||
key={opt.id}
|
||
type="button"
|
||
role="radio"
|
||
aria-checked={sectionsEditMode === opt.id}
|
||
onClick={() => setSectionsEditMode(opt.id)}
|
||
style={{
|
||
border: 'none',
|
||
padding: '8px 14px',
|
||
fontWeight: 600,
|
||
fontSize: '0.85rem',
|
||
cursor: 'pointer',
|
||
background: sectionsEditMode === opt.id ? 'var(--accent-dark)' : 'transparent',
|
||
color: sectionsEditMode === opt.id ? '#fff' : 'var(--text1)',
|
||
whiteSpace: 'nowrap',
|
||
...(i > 0 ? { borderLeft: '1.5px solid var(--border2)' } : {}),
|
||
}}
|
||
>
|
||
{opt.label}
|
||
</button>
|
||
))}
|
||
</div>
|
||
</div>
|
||
<p style={{ margin: '0.35rem 0 0', fontSize: '0.82rem', color: 'var(--text3)', lineHeight: 1.45 }}>
|
||
{sectionsEditMode === 'debrief'
|
||
? 'Ist‑Minuten rechts in derselben Spaltenbreite wie „Min“ (Plan); Abweichungen als Text über die volle Breite.'
|
||
: 'Ablauf, Übungen und geplante Minuten. Ist‑Werte und Abweichungen unter „Nachbereitung“.'}
|
||
</p>
|
||
</div>
|
||
) : null}
|
||
<TrainingUnitSectionsEditor
|
||
heading="Abschnitte & Übungen"
|
||
headingAccessory={
|
||
<>
|
||
<div
|
||
style={{
|
||
display: 'flex',
|
||
flexWrap: 'wrap',
|
||
alignItems: 'flex-end',
|
||
gap: '10px',
|
||
marginBottom: '10px',
|
||
}}
|
||
>
|
||
<div className="form-row" style={{ marginBottom: 0, minWidth: 'min(160px, 100%)' }}>
|
||
<label className="form-label" style={{ fontSize: '0.82rem' }}>
|
||
Neue Vorlage: Sichtbarkeit
|
||
</label>
|
||
<select
|
||
className="form-input"
|
||
value={newTplVisibility}
|
||
onChange={(e) => {
|
||
const v = e.target.value
|
||
setNewTplVisibility(v)
|
||
if (v === 'club' && !newTplClubId && planningModalClubId != null) {
|
||
setNewTplClubId(String(planningModalClubId))
|
||
}
|
||
}}
|
||
>
|
||
<option value="private">Privat (nur du)</option>
|
||
<option value="club">Verein</option>
|
||
{isSuperadmin ? <option value="official">Offiziell (global)</option> : null}
|
||
</select>
|
||
</div>
|
||
{newTplVisibility === 'club' ? (
|
||
<div className="form-row" style={{ marginBottom: 0, flex: '1 1 200px' }}>
|
||
<label className="form-label" style={{ fontSize: '0.82rem' }}>
|
||
Verein
|
||
</label>
|
||
<select
|
||
className="form-input"
|
||
value={newTplClubId}
|
||
onChange={(e) => setNewTplClubId(e.target.value)}
|
||
>
|
||
<option value="">— Verein wählen —</option>
|
||
{memberClubs.map((c) => (
|
||
<option key={c.id} value={String(c.id)}>
|
||
{c.name || `Verein #${c.id}`}
|
||
</option>
|
||
))}
|
||
</select>
|
||
</div>
|
||
) : null}
|
||
<button
|
||
type="button"
|
||
className="btn btn-secondary"
|
||
style={{ marginBottom: '2px' }}
|
||
onClick={() =>
|
||
onSaveAsTemplate?.({
|
||
visibility: newTplVisibility,
|
||
club_id:
|
||
newTplVisibility === 'club' && newTplClubId
|
||
? parseInt(newTplClubId, 10)
|
||
: null,
|
||
})
|
||
}
|
||
>
|
||
Vorlage aus Aufbau speichern
|
||
</button>
|
||
</div>
|
||
</>
|
||
}
|
||
sections={formData.sections}
|
||
wideExerciseGrid
|
||
onSectionsChange={(updater) =>
|
||
setFormData((prev) => ({
|
||
...prev,
|
||
sections: updater(prev.sections),
|
||
}))
|
||
}
|
||
onRequestTrainingModulePick={onRequestTrainingModulePick}
|
||
onRequestExercisePick={onRequestExercisePick}
|
||
onPeekExercise={onPeekExercise}
|
||
showExecutionExtras={Boolean(editingUnit) && sectionsEditMode === 'debrief'}
|
||
enableParallelPhaseControls
|
||
/>
|
||
</div>
|
||
|
||
<div style={{ marginBottom: '1.75rem' }} />
|
||
|
||
{editingUnit && (
|
||
<>
|
||
<h3 style={{ marginTop: '0.5rem', marginBottom: '1rem' }}>Durchführung</h3>
|
||
|
||
<div className="responsive-grid-4" style={{ marginBottom: '1rem' }}>
|
||
<div className="form-row">
|
||
<label className="form-label">Tatsächliches Datum</label>
|
||
<input
|
||
type="date"
|
||
className="form-input"
|
||
value={formData.actual_date}
|
||
onChange={(e) => updateFormField('actual_date', e.target.value)}
|
||
/>
|
||
</div>
|
||
|
||
<div className="form-row">
|
||
<label className="form-label">Von</label>
|
||
<input
|
||
type="time"
|
||
className="form-input"
|
||
value={formData.actual_time_start}
|
||
onChange={(e) => updateFormField('actual_time_start', e.target.value)}
|
||
/>
|
||
</div>
|
||
|
||
<div className="form-row">
|
||
<label className="form-label">Bis</label>
|
||
<input
|
||
type="time"
|
||
className="form-input"
|
||
value={formData.actual_time_end}
|
||
onChange={(e) => updateFormField('actual_time_end', e.target.value)}
|
||
/>
|
||
</div>
|
||
|
||
<div className="form-row">
|
||
<label className="form-label">Teilnehmer</label>
|
||
<input
|
||
type="number"
|
||
className="form-input"
|
||
value={formData.attendance_count}
|
||
onChange={(e) => updateFormField('attendance_count', e.target.value)}
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<div className="form-row">
|
||
<label className="form-label">Status</label>
|
||
<select
|
||
className="form-input"
|
||
value={formData.status}
|
||
onChange={(e) => updateFormField('status', e.target.value)}
|
||
>
|
||
<option value="planned">Geplant</option>
|
||
<option value="completed">Durchgeführt</option>
|
||
<option value="cancelled">Abgesagt</option>
|
||
</select>
|
||
</div>
|
||
|
||
{formData.status === 'completed' ? (
|
||
<div className="form-row" style={{ marginTop: '0.75rem' }}>
|
||
<label
|
||
style={{
|
||
display: 'flex',
|
||
alignItems: 'flex-start',
|
||
gap: '10px',
|
||
cursor: 'pointer',
|
||
lineHeight: 1.45,
|
||
}}
|
||
>
|
||
<input
|
||
type="checkbox"
|
||
checked={!!formData.debrief_completed}
|
||
onChange={(e) => updateFormField('debrief_completed', e.target.checked)}
|
||
style={{ marginTop: '3px' }}
|
||
/>
|
||
<span>
|
||
<strong>Rückschau erledigt</strong>
|
||
<span className="muted" style={{ display: 'block', fontSize: '0.82rem', marginTop: '5px' }}>
|
||
Wenn angehakt, erscheint die Einheit nicht mehr unter „Offene Rückschau“ auf dem Dashboard
|
||
(Nachbereitung gilt als abgeschlossen).
|
||
</span>
|
||
</span>
|
||
</label>
|
||
</div>
|
||
) : null}
|
||
</>
|
||
)}
|
||
|
||
<h3 style={{ marginTop: '2rem', marginBottom: '1rem' }}>Notizen</h3>
|
||
|
||
<div className="form-row">
|
||
<label className="form-label">Öffentliche Notizen</label>
|
||
<textarea
|
||
className="form-input"
|
||
rows={3}
|
||
value={formData.notes}
|
||
onChange={(e) => updateFormField('notes', e.target.value)}
|
||
placeholder="Für Teilnehmer"
|
||
/>
|
||
</div>
|
||
|
||
<div className="form-row">
|
||
<label className="form-label">Trainernotizen</label>
|
||
<textarea
|
||
className="form-input"
|
||
rows={3}
|
||
value={formData.trainer_notes}
|
||
onChange={(e) => updateFormField('trainer_notes', e.target.value)}
|
||
/>
|
||
</div>
|
||
|
||
<div style={{ display: 'flex', gap: '0.5rem', marginTop: '1.5rem' }}>
|
||
<button type="submit" className="btn btn-primary" style={{ flex: 1 }}>
|
||
{editingUnit ? 'Speichern' : 'Erstellen'}
|
||
</button>
|
||
<button type="button" className="btn btn-secondary" onClick={onCancel}>
|
||
Abbrechen
|
||
</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|