import React, { useEffect, useState } from 'react' import { useNavigate, useParams } from 'react-router-dom' import api, { buildExerciseApiPayload } from '../utils/api' import RichTextEditor from '../components/RichTextEditor' const INTENSITY_OPTIONS = [ { value: '', label: '—' }, { value: 'niedrig', label: 'niedrig' }, { value: 'mittel', label: 'mittel' }, { value: 'hoch', label: 'hoch' }, ] const LEVEL_OPTIONS = [ { value: '', label: '—' }, { value: 'einsteiger', label: 'Einsteiger' }, { value: 'grundlagen', label: 'Grundlagen' }, { value: 'aufbau', label: 'Aufbau' }, { value: 'fortgeschritten', label: 'Fortgeschritten' }, { value: 'experte', label: 'Experte' }, ] function emptyForm() { return { title: '', summary: '', goal: '', execution: '', preparation: '', trainer_notes: '', equipmentLines: '', duration_min: '', duration_max: '', group_size_min: '', group_size_max: '', focus_areas_multi: [], training_styles_multi: [], training_types_multi: [], target_groups_multi: [], visibility: 'private', status: 'draft', skills: [], } } function detailToForm(exercise) { return { title: exercise.title || '', summary: exercise.summary || '', goal: exercise.goal || '', execution: exercise.execution || '', preparation: exercise.preparation || '', trainer_notes: exercise.trainer_notes || '', equipmentLines: (exercise.equipment || []).join('\n'), duration_min: exercise.duration_min ?? '', duration_max: exercise.duration_max ?? '', group_size_min: exercise.group_size_min ?? '', group_size_max: exercise.group_size_max ?? '', focus_areas_multi: (exercise.focus_areas || []).map((f) => ({ focus_area_id: f.focus_area_id, is_primary: !!f.is_primary, })), training_styles_multi: (exercise.training_styles || []).map((t) => ({ training_style_id: t.training_style_id, is_primary: !!t.is_primary, })), training_types_multi: (exercise.training_types || []).map((t) => ({ training_type_id: t.training_type_id, is_primary: !!t.is_primary, })), target_groups_multi: (exercise.target_groups || []).map((g) => ({ target_group_id: g.target_group_id, is_primary: !!g.is_primary, })), visibility: exercise.visibility || 'private', status: exercise.status || 'draft', skills: exercise.skills?.map((s) => ({ skill_id: s.skill_id, is_primary: s.is_primary || false, intensity: s.intensity || '', required_level: s.required_level || '', target_level: s.target_level || '', })) || [], } } function MultiAssocBlock({ title, rows, setRows, options, idKey, emptyLabel }) { const setPrimary = (idx) => { setRows(rows.map((r, i) => ({ ...r, is_primary: i === idx }))) } const updateRow = (idx, patch) => { const next = rows.map((r, i) => (i === idx ? { ...r, ...patch } : r)) if (patch.is_primary === true) { next.forEach((r, i) => { if (i !== idx) r.is_primary = false }) } setRows(next) } const addRow = () => setRows([...rows, { [idKey]: '', is_primary: rows.length === 0 }]) const removeRow = (idx) => { const next = rows.filter((_, i) => i !== idx) if (next.length && !next.some((r) => r.is_primary)) next[0].is_primary = true setRows(next) } return (

{title}

{rows.length === 0 && (

{emptyLabel}

)} {rows.map((row, idx) => (
))}
) } function ExerciseFormPage() { const { id: routeId } = useParams() const navigate = useNavigate() const exerciseId = routeId && !Number.isNaN(parseInt(routeId, 10)) ? parseInt(routeId, 10) : null const isEdit = exerciseId != null const [formData, setFormData] = useState(emptyForm) const [skillsCatalog, setSkillsCatalog] = useState([]) const [focusAreas, setFocusAreas] = useState([]) const [styleDirections, setStyleDirections] = useState([]) const [trainingTypes, setTrainingTypes] = useState([]) const [targetGroups, setTargetGroups] = useState([]) const [mediaList, setMediaList] = useState([]) const [loading, setLoading] = useState(!!isEdit) const [saving, setSaving] = useState(false) const [skillPick, setSkillPick] = useState('') const [mediaFile, setMediaFile] = useState(null) const [mediaType, setMediaType] = useState('image') const [mediaTitle, setMediaTitle] = useState('') const [mediaContext, setMediaContext] = useState('ablauf') const [embedUrl, setEmbedUrl] = useState('') const [embedTitle, setEmbedTitle] = useState('') useEffect(() => { let cancelled = false const boot = async () => { try { const [skillsData, faData, sdData, ttData, tgData] = await Promise.all([ api.listSkills(), api.listFocusAreas(), api.listTrainingStyles(), api.listTrainingTypes(), api.listTargetGroups(), ]) if (cancelled) return setSkillsCatalog(skillsData) setFocusAreas(faData) setStyleDirections(sdData) setTrainingTypes(ttData) setTargetGroups(tgData) } catch (e) { if (!cancelled) { console.error(e) alert( 'Kataloge (Fokus, Stile, Zielgruppen, Fähigkeiten) konnten nicht geladen werden: ' + (e.message || e), ) } } } boot() return () => { cancelled = true } }, []) useEffect(() => { if (!isEdit) { setFormData(emptyForm()) setMediaList([]) setLoading(false) return } let cancelled = false const load = async () => { setLoading(true) try { const exercise = await api.getExercise(exerciseId) if (cancelled) return setFormData(detailToForm(exercise)) setMediaList(exercise.media || []) } catch (err) { if (!cancelled) { alert(err.message || 'Übung nicht ladbar') navigate('/exercises') } } finally { if (!cancelled) setLoading(false) } } load() return () => { cancelled = true } }, [isEdit, exerciseId, navigate]) const updateFormField = (field, value) => { setFormData((prev) => ({ ...prev, [field]: value })) } const addSkillRow = () => { const id = skillPick ? parseInt(skillPick, 10) : null if (!id) { alert('Fähigkeit wählen') return } if (formData.skills.some((s) => s.skill_id === id)) { alert('Bereits zugeordnet') return } updateFormField('skills', [ ...formData.skills, { skill_id: id, is_primary: formData.skills.length === 0, intensity: '', required_level: '', target_level: '', }, ]) setSkillPick('') } const setSkillPrimary = (idx) => { updateFormField( 'skills', formData.skills.map((s, i) => ({ ...s, is_primary: i === idx })), ) } const updateSkillField = (idx, field, value) => { updateFormField( 'skills', formData.skills.map((s, i) => (i === idx ? { ...s, [field]: value } : s)), ) } const removeSkillRow = (idx) => { const next = formData.skills.filter((_, i) => i !== idx) if (next.length && !next.some((s) => s.is_primary)) next[0].is_primary = true updateFormField('skills', next) } const handleSubmit = async (e) => { e.preventDefault() if (!formData.title || formData.title.trim().length < 3) { alert('Titel mindestens 3 Zeichen') return } const payloadBase = { ...formData, equipment: typeof formData.equipmentLines === 'string' ? formData.equipmentLines .split(/[\n,]+/) .map((s) => s.trim()) .filter(Boolean) : [], } let payload try { payload = buildExerciseApiPayload(payloadBase) } catch (err) { alert(err.message) return } setSaving(true) try { if (isEdit) { await api.updateExercise(exerciseId, payload) const ex = await api.getExercise(exerciseId) setMediaList(ex.media || []) alert('Gespeichert.') } else { const created = await api.createExercise(payload) navigate(`/exercises/${created.id}/edit`, { replace: true }) } } catch (err) { alert('Fehler beim Speichern: ' + err.message) } finally { setSaving(false) } } const refreshMedia = async () => { if (!exerciseId) return const ex = await api.getExercise(exerciseId) setMediaList(ex.media || []) } const handleUploadFile = async () => { if (!exerciseId || !mediaFile) { alert('Datei wählen') return } const fd = new FormData() fd.append('file', mediaFile) fd.append('media_type', mediaType) fd.append('title', mediaTitle) fd.append('description', '') fd.append('context', mediaContext) fd.append('is_primary', 'false') try { await api.uploadExerciseMedia(exerciseId, fd) setMediaFile(null) setMediaTitle('') await refreshMedia() } catch (err) { alert('Upload: ' + err.message) } } const handleAddEmbed = async () => { if (!exerciseId || !embedUrl.trim()) { alert('Embed-URL eingeben') return } const fd = new FormData() fd.append('embed_url', embedUrl.trim()) fd.append('media_type', 'video') fd.append('title', embedTitle) fd.append('description', '') fd.append('context', mediaContext) fd.append('is_primary', 'false') try { await api.uploadExerciseMedia(exerciseId, fd) setEmbedUrl('') setEmbedTitle('') await refreshMedia() } catch (err) { alert('Embed: ' + err.message) } } const handleDeleteMedia = async (mid) => { if (!confirm('Medium löschen?')) return try { await api.deleteExerciseMedia(exerciseId, mid) await refreshMedia() } catch (err) { alert(err.message) } } const availableSkills = skillsCatalog.filter((s) => !formData.skills.some((x) => x.skill_id === s.id)) if (loading) { return (

Laden...

) } return (
{isEdit && ( )}

{isEdit ? 'Übung bearbeiten' : 'Neue Übung'}

updateFormField('title', e.target.value)} required minLength={3} />
updateFormField('summary', html)} placeholder="Kurzbeschreibung (optional)" minHeight="80px" />
updateFormField('goal', html)} placeholder="Trainingsziel" minHeight="120px" />
updateFormField('execution', html)} placeholder="Ablauf Schritt für Schritt" minHeight="180px" />
updateFormField('preparation', html)} placeholder="Matten, Raum, …" minHeight="100px" />
updateFormField('trainer_notes', html)} placeholder="Sicherheit, Varianten-Hinweise, …" minHeight="100px" />