import React, { useState, useEffect, useCallback } from 'react' import { Link } from 'react-router-dom' import api from '../utils/api' import { useAuth } from '../context/AuthContext' import ExercisePickerModal from '../components/ExercisePickerModal' import ExercisePeekModal from '../components/ExercisePeekModal' function defaultSection(title = 'Hauptteil') { return { title, guidance_notes: '', items: [] } } function exerciseRow() { return { item_type: 'exercise', exercise_id: '', exercise_variant_id: '', exercise_title: '', variants: [], planned_duration_min: '', actual_duration_min: '', notes: '', modifications: '' } } function noteRow() { return { item_type: 'note', note_body: '' } } function normalizeUnitToForm(fullUnit) { if (fullUnit.sections && fullUnit.sections.length) { return fullUnit.sections.map((sec) => ({ title: sec.title, guidance_notes: sec.guidance_notes || '', items: (sec.items || []).map((it) => { if (it.item_type === 'note') { return { item_type: 'note', note_body: it.note_body || '' } } return { item_type: 'exercise', exercise_id: it.exercise_id, exercise_variant_id: it.exercise_variant_id ?? '', exercise_title: it.exercise_title || '', variants: [], planned_duration_min: it.planned_duration_min !== null && it.planned_duration_min !== undefined ? String(it.planned_duration_min) : '', actual_duration_min: it.actual_duration_min !== null && it.actual_duration_min !== undefined ? String(it.actual_duration_min) : '', notes: it.notes ?? '', modifications: it.modifications ?? '' } }) })) } if (fullUnit.exercises && fullUnit.exercises.length) { return [ { title: 'Übungen', guidance_notes: '', items: fullUnit.exercises.map((ex) => ({ item_type: 'exercise', exercise_id: ex.exercise_id, exercise_variant_id: ex.exercise_variant_id ?? '', exercise_title: ex.exercise_title || '', variants: [], planned_duration_min: ex.planned_duration_min !== null && ex.planned_duration_min !== undefined ? String(ex.planned_duration_min) : '', actual_duration_min: ex.actual_duration_min !== null && ex.actual_duration_min !== undefined ? String(ex.actual_duration_min) : '', notes: ex.notes ?? '', modifications: ex.modifications ?? '' })) } ] } return [defaultSection()] } /** Lädt Varianten/Titel nach, wenn Einheit vom Server ohne variants[] im Client-State ist. */ async function enrichSectionsWithVariants(sections) { if (!sections?.length) return sections const ids = [] for (const sec of sections) { for (const it of sec.items || []) { if (it.item_type === 'note') continue if (it.exercise_id) ids.push(it.exercise_id) } } const unique = [...new Set(ids)] const cache = new Map() await Promise.all( unique.map(async (id) => { try { const ex = await api.getExercise(id) cache.set(id, { title: ex.title || '', variants: Array.isArray(ex.variants) ? ex.variants : [], }) } catch { cache.set(id, { title: '', variants: [] }) } }) ) return sections.map((sec) => ({ ...sec, items: (sec.items || []).map((it) => { if (it.item_type === 'note') return it if (!it.exercise_id) return it const c = cache.get(it.exercise_id) if (!c) return it return { ...it, exercise_title: it.exercise_title || c.title, variants: Array.isArray(it.variants) && it.variants.length > 0 ? it.variants : c.variants, } }), })) } function parseMin(v) { if (v === '' || v === null || v === undefined) return null const n = parseInt(String(v), 10) return Number.isFinite(n) ? n : null } function buildSectionsPayload(sections) { return sections.map((sec, si) => ({ order_index: si, title: (sec.title || '').trim() || 'Abschnitt', guidance_notes: sec.guidance_notes?.trim() ? sec.guidance_notes.trim() : null, items: (sec.items || []) .map((it, ii) => { if (it.item_type === 'note') { return { item_type: 'note', order_index: ii, note_body: it.note_body ?? '' } } if (it.exercise_id === '' || it.exercise_id == null || Number.isNaN(Number(it.exercise_id))) { return null } const vid = it.exercise_variant_id return { item_type: 'exercise', order_index: ii, exercise_id: parseInt(it.exercise_id, 10), exercise_variant_id: vid !== '' && vid != null && !Number.isNaN(Number(vid)) ? parseInt(vid, 10) : null, planned_duration_min: parseMin(it.planned_duration_min), actual_duration_min: parseMin(it.actual_duration_min), notes: it.notes?.trim() ? it.notes.trim() : null, modifications: it.modifications?.trim() ? it.modifications.trim() : null } }) .filter(Boolean) })) } function sectionPlannedMinutes(sec) { return (sec.items || []).reduce((sum, it) => { if (it.item_type !== 'exercise') return sum const m = parseMin(it.planned_duration_min) return sum + (m || 0) }, 0) } function TrainingPlanningPage() { const { user } = useAuth() const [groups, setGroups] = useState([]) const [selectedGroupId, setSelectedGroupId] = useState('') const [units, setUnits] = useState([]) const [planTemplates, setPlanTemplates] = useState([]) const [loading, setLoading] = useState(true) const [showModal, setShowModal] = useState(false) const [editingUnit, setEditingUnit] = useState(null) const [draftPlanTemplateId, setDraftPlanTemplateId] = useState('') const [quickTemplateId, setQuickTemplateId] = useState('') const [exercisePickerOpen, setExercisePickerOpen] = useState(false) const [exercisePickerTarget, setExercisePickerTarget] = useState(null) const [planningPeekExerciseId, setPlanningPeekExerciseId] = useState(null) const today = new Date().toISOString().split('T')[0] const thirtyDaysLater = new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString().split('T')[0] const [startDate, setStartDate] = useState(today) const [endDate, setEndDate] = useState(thirtyDaysLater) const [formData, setFormData] = useState({ group_id: '', planned_date: '', planned_time_start: '', planned_time_end: '', planned_focus: '', actual_date: '', actual_time_start: '', actual_time_end: '', attendance_count: '', status: 'planned', notes: '', trainer_notes: '', sections: [defaultSection()] }) useEffect(() => { loadData() }, []) useEffect(() => { if (selectedGroupId) { loadUnits() } }, [selectedGroupId, startDate, endDate]) const loadPlanTemplates = useCallback(async () => { try { const tpl = await api.listTrainingPlanTemplates() setPlanTemplates(tpl) } catch (e) { console.error('Vorlagen laden:', e) } }, []) const loadData = async () => { try { const groupsData = await api.listTrainingGroups({ status: 'active' }) setGroups(groupsData) await loadPlanTemplates() if (groupsData.length > 0) { const ownGroup = groupsData.find((g) => g.trainer_id === user?.id) if (ownGroup) { setSelectedGroupId(ownGroup.id) } else if (groupsData.length === 1) { setSelectedGroupId(groupsData[0].id) } } } catch (err) { console.error('Failed to load data:', err) alert('Fehler beim Laden: ' + err.message) } finally { setLoading(false) } } const loadUnits = async () => { if (!selectedGroupId) return try { const unitsData = await api.listTrainingUnits({ group_id: selectedGroupId, start_date: startDate, end_date: endDate }) setUnits(unitsData) } catch (err) { console.error('Failed to load units:', err) } } 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 = () => { if (!selectedGroupId) { alert('Bitte wähle zuerst eine Trainingsgruppe') return } const group = groups.find((g) => g.id === parseInt(selectedGroupId, 10)) setEditingUnit(null) setDraftPlanTemplateId('') setFormData({ group_id: selectedGroupId, planned_date: today, planned_time_start: group?.time_start?.slice(0, 5) || '', planned_time_end: group?.time_end?.slice(0, 5) || '', planned_focus: '', actual_date: '', actual_time_start: '', actual_time_end: '', attendance_count: '', status: 'planned', notes: '', trainer_notes: '', sections: [defaultSection('Hauptteil')] }) setShowModal(true) } const applyTemplateFromSelect = async (templateId) => { setDraftPlanTemplateId(templateId) if (!templateId) return try { const tpl = await api.getTrainingPlanTemplate(parseInt(templateId, 10)) setFormData((fd) => ({ ...fd, sections: (tpl.sections || []).length ? tpl.sections.map((s) => ({ title: s.title, guidance_notes: s.guidance_text || '', items: [] })) : [defaultSection()] })) } catch (err) { alert('Vorlage laden: ' + err.message) } } const handleEdit = async (unit) => { try { const fullUnit = await api.getTrainingUnit(unit.id) setEditingUnit(fullUnit) setDraftPlanTemplateId(fullUnit.plan_template_id ? String(fullUnit.plan_template_id) : '') let sections = normalizeUnitToForm(fullUnit) sections = await enrichSectionsWithVariants(sections) setFormData({ group_id: fullUnit.group_id, planned_date: fullUnit.planned_date || '', planned_time_start: fullUnit.planned_time_start?.slice(0, 5) || '', planned_time_end: fullUnit.planned_time_end?.slice(0, 5) || '', planned_focus: fullUnit.planned_focus || '', actual_date: fullUnit.actual_date || '', actual_time_start: fullUnit.actual_time_start?.slice(0, 5) || '', actual_time_end: fullUnit.actual_time_end?.slice(0, 5) || '', attendance_count: fullUnit.attendance_count ?? '', status: fullUnit.status || 'planned', notes: fullUnit.notes || '', trainer_notes: fullUnit.trainer_notes || '', sections, }) setShowModal(true) } catch (err) { alert('Fehler beim Laden: ' + err.message) } } const handleSaveAsTemplate = async () => { const name = window.prompt('Name für die neue Trainingsvorlage (nur Abschnitts-Gliederung):') if (!name?.trim()) return try { await api.createTrainingPlanTemplate({ name: name.trim(), sections: formData.sections.map((s) => ({ title: s.title || 'Abschnitt', guidance_text: s.guidance_notes?.trim() ? s.guidance_notes.trim() : null })) }) await loadPlanTemplates() alert('Vorlage gespeichert.') } catch (err) { alert('Speichern: ' + err.message) } } const handleDelete = async (unit) => { if (!confirm(`Trainingseinheit vom ${unit.planned_date} wirklich löschen?`)) return try { await api.deleteTrainingUnit(unit.id) await loadUnits() } catch (err) { alert('Fehler beim Löschen: ' + err.message) } } const handleSubmit = async (e) => { e.preventDefault() if (!formData.group_id || !formData.planned_date) { alert('Gruppe und Datum sind Pflichtfelder') return } try { const sectionsPayload = buildSectionsPayload(formData.sections) const payload = { planned_date: formData.planned_date, planned_time_start: formData.planned_time_start || null, planned_time_end: formData.planned_time_end || null, planned_focus: formData.planned_focus || null, actual_date: formData.actual_date || null, actual_time_start: formData.actual_time_start || null, actual_time_end: formData.actual_time_end || null, attendance_count: formData.attendance_count ? parseInt(formData.attendance_count, 10) : null, status: formData.status || 'planned', notes: formData.notes || null, trainer_notes: formData.trainer_notes || null, sections: sectionsPayload } if (!editingUnit) { payload.group_id = parseInt(formData.group_id, 10) if (draftPlanTemplateId) { payload.plan_template_id = parseInt(draftPlanTemplateId, 10) } } if (editingUnit) { await api.updateTrainingUnit(editingUnit.id, payload) } else { await api.createTrainingUnit(payload) } setShowModal(false) await loadUnits() } catch (err) { alert('Fehler beim Speichern: ' + err.message) } } const updateFormField = (field, value) => { setFormData((prev) => ({ ...prev, [field]: value })) } const updateSectionField = (sIdx, field, value) => { setFormData((prev) => ({ ...prev, sections: prev.sections.map((s, i) => (i === sIdx ? { ...s, [field]: value } : s)) })) } const addSection = () => { setFormData((prev) => ({ ...prev, sections: [...prev.sections, defaultSection(`Abschnitt ${prev.sections.length + 1}`)] })) } const removeSection = (sIdx) => { setFormData((prev) => { const next = prev.sections.filter((_, i) => i !== sIdx) return { ...prev, sections: next.length ? next : [defaultSection()] } }) } const moveSection = (sIdx, dir) => { setFormData((prev) => { const ta = sIdx + dir if (ta < 0 || ta >= prev.sections.length) return prev const copy = [...prev.sections] ;[copy[sIdx], copy[ta]] = [copy[ta], copy[sIdx]] return { ...prev, sections: copy } }) } const addItem = (sIdx, kind) => { setFormData((prev) => ({ ...prev, sections: prev.sections.map((s, i) => { if (i !== sIdx) return s const row = kind === 'note' ? noteRow() : exerciseRow() return { ...s, items: [...s.items, row] } }) })) } const updateItem = (sIdx, iIdx, field, value) => { setFormData((prev) => ({ ...prev, sections: prev.sections.map((s, si) => { if (si !== sIdx) return s return { ...s, items: s.items.map((it, ii) => { if (ii !== iIdx) return it const next = { ...it, [field]: value } if (field === 'exercise_id') { next.exercise_variant_id = '' next.exercise_title = '' next.variants = [] } return next }) } }) })) } const removeItem = (sIdx, iIdx) => { setFormData((prev) => ({ ...prev, sections: prev.sections.map((s, i) => i !== sIdx ? s : { ...s, items: s.items.filter((_, j) => j !== iIdx) } ) })) } const moveItem = (sIdx, iIdx, dir) => { setFormData((prev) => ({ ...prev, sections: prev.sections.map((s, si) => { if (si !== sIdx) return s const items = [...s.items] const ta = iIdx + dir if (ta < 0 || ta >= items.length) return s ;[items[iIdx], items[ta]] = [items[ta], items[iIdx]] return { ...s, items } }) })) } if (loading) { return (

Laden...

) } const selectedGroup = groups.find((g) => g.id === parseInt(selectedGroupId, 10)) return (

Trainingsplanung

Wähle eine Trainingsgruppe, lege dann Termine mit Inhalt (Abschnitte und Übungen) an — ein Plan entsteht aus einer oder mehreren{' '} Trainingseinheiten im gewählten Zeitraum.

Mehrere Einheiten strukturieren auf einmal:{' '} Trainingsrahmenprogramme {' '} (Ziele, Slots, Übungen als Vorlage).

{!loading && groups.length === 0 && (

Erst Verein & Gruppe anlegen

Ohne Trainingsgruppe kann hier nichts gebucht werden. Unter Vereine legst du einen Verein an (kurzer Name genügt), optional eine Sparte, dann eine Trainingsgruppe. Wochentage, feste Zeiten oder Eigenschaften sind optional und kannst du später ergänzen.

Zu Vereinen & Trainingsgruppen
)}
setStartDate(e.target.value)} />
setEndDate(e.target.value)} />
{selectedGroup && (

{selectedGroup.location || 'Kein Ort angegeben'} {selectedGroup.weekday && ` · ${selectedGroup.weekday}`} {selectedGroup.time_start && ` · ${selectedGroup.time_start.slice(0, 5)} - ${selectedGroup.time_end?.slice(0, 5)}`}

)}

Plan anlegen: neue Trainingseinheit mit Datum, Zeit und Ablauf — oder schnell nur mit Datum (Zeiten aus der Gruppe). {!selectedGroupId && ( Wähle oben eine Trainingsgruppe, um die Schaltflächen zu aktivieren. )} {groups.length === 0 && ( Es gibt noch keine aktive Trainingsgruppe — unter{' '} Vereine {' '} anlegen oder aktivieren. )}

{!selectedGroupId ? (

Wähle oben eine Trainingsgruppe — danach kannst du mit „Neue Trainingseinheit planen“ starten.

) : units.length === 0 ? (

Keine Trainingseinheiten in diesem Zeitraum. Nutze oben „Neue Trainingseinheit planen“ oder{' '} „Schnell erstellen“, um den ersten Termin anzulegen.

) : (
{units.map((unit) => (

{unit.planned_date} {unit.planned_time_start && ` · ${unit.planned_time_start.slice(0, 5)} - ${unit.planned_time_end?.slice(0, 5)}`}

{unit.planned_focus && (

Fokus: {unit.planned_focus}

)}
{unit.status === 'planned' && 'Geplant'} {unit.status === 'completed' && 'Durchgeführt'} {unit.status === 'cancelled' && 'Abgesagt'} {unit.attendance_count !== null && unit.attendance_count !== undefined && ( {unit.attendance_count} Teilnehmer )}
Plan & Ablauf Im Training (Coach)
{unit.notes && (

{unit.notes}

)}
))}
)} {showModal && (

{editingUnit ? 'Trainingseinheit bearbeiten' : 'Neue Trainingseinheit'}

{!editingUnit && (

Lädt die Abschnitte und Hinweise aus der Vorlage; Übungen fügst du hier ein.

)}

Planung

updateFormField('planned_date', e.target.value)} required />
updateFormField('planned_time_start', e.target.value)} />
updateFormField('planned_time_end', e.target.value)} />
updateFormField('planned_focus', e.target.value)} placeholder="z.B. Grundlagen, Kinder altersgerecht" />

Abschnitte & Übungen

{formData.sections.map((sec, sIdx) => { const planMin = sectionPlannedMinutes(sec) return (
updateSectionField(sIdx, 'title', e.target.value)} placeholder="Abschnittstitel (z. B. Aufwärmen)" />