import React, { useState, useEffect, useCallback, useMemo, useRef } from 'react' import { Link, useSearchParams } from 'react-router-dom' import api from '../utils/api' import { useAuth } from '../context/AuthContext' import { activeClubMemberships } from '../utils/activeClub' import ExercisePickerModal from '../components/ExercisePickerModal' import ExercisePeekModal from '../components/ExercisePeekModal' import TrainingUnitSectionsEditor from '../components/TrainingUnitSectionsEditor' import TrainingPlanExerciseVisibilityPanel from '../components/TrainingPlanExerciseVisibilityPanel' import PageSectionNav from '../components/PageSectionNav' import { defaultSection, normalizeUnitToForm, enrichSectionsWithVariants, buildSectionsPayload, hydrateExercisePlanningRow, insertTrainingModuleIntoPlanningSections, } from '../utils/trainingUnitSectionsForm' /** Kurz-Anzeige Sichtbarkeit (Trainingsmodule, Übungen) */ function trainingVisibilityShortDE(visibility) { const v = String(visibility || '').trim().toLowerCase() if (v === 'official') return 'Öffentliche Bibliothek' if (v === 'club') return 'Verein' if (v === 'private') return 'Privat' return visibility ? String(visibility) : '' } function addDaysIsoDate(isoDay, daysDelta) { const d = new Date(`${isoDay}T12:00:00`) d.setDate(d.getDate() + daysDelta) return d.toISOString().slice(0, 10) } function pad2(n) { return String(n).padStart(2, '0') } function toIsoLocal(d) { return `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())}` } /** Montag = erster Wochentag (ISO-Woche UI) */ function mondayIndex(d) { return (d.getDay() + 6) % 7 } /** Kalendarische Monatsansicht: erster und letzter Tag des sichtbaren Rasters (Mo–So) */ function getCalendarGridRange(ym) { const parts = (ym || '').split('-').map(Number) const y = parts[0] const m = parts[1] if (!y || !m || m < 1 || m > 12) { const t = new Date() return { gridStart: toIsoLocal(t), gridEnd: toIsoLocal(t) } } const first = new Date(y, m - 1, 1) const last = new Date(y, m, 0) const gridStart = new Date(first) gridStart.setDate(first.getDate() - mondayIndex(first)) const lastMon = mondayIndex(last) const gridEnd = new Date(last) gridEnd.setDate(last.getDate() + (6 - lastMon)) return { gridStart: toIsoLocal(gridStart), gridEnd: toIsoLocal(gridEnd) } } function shiftCalendarMonth(ym, delta) { const parts = (ym || '').split('-').map(Number) const y = parts[0] || new Date().getFullYear() const m = parts[1] || 1 const d = new Date(y, m - 1 + delta, 1) return `${d.getFullYear()}-${pad2(d.getMonth() + 1)}` } function enumerateIsoDays(fromIso, toIso) { const out = [] const cur = new Date(`${fromIso}T12:00:00`) const end = new Date(`${toIso}T12:00:00`) while (cur <= end) { out.push(toIsoLocal(cur)) cur.setDate(cur.getDate() + 1) } return out } const WEEKDAYS_DE = ['Mo', 'Di', 'Mi', 'Do', 'Fr', 'Sa', 'So'] function toNumList(arr) { if (!Array.isArray(arr)) return [] const out = [] for (const x of arr) { const n = Number(x) if (Number.isFinite(n) && n >= 1) out.push(n) } return out } const sessionAssignDefaults = () => ({ lead_trainer_profile_id: '', session_assistants_inherit: true, session_assistant_profile_ids: [], }) /** Co_trainer_ids aus TrainingGroups (Liste/JSON) → Zahlenliste */ function normalizeGroupCoTrainerIds(raw) { if (raw == null) return [] const arr = Array.isArray(raw) ? raw : [] const out = [] for (const x of arr) { const n = Number(x) if (Number.isFinite(n) && n >= 1) out.push(n) } return out } /** Mitgliederverzeichnis-Einträge ohne effektiven Leitungsträger als Co‑Option */ function filterDirectoryExcludingLead(directory, excludeLeadPid) { const ex = excludeLeadPid != null && excludeLeadPid !== '' && Number.isFinite(Number(excludeLeadPid)) ? Number(excludeLeadPid) : null if (ex == null) return directory return directory.filter((m) => Number(m.id) !== ex) } function TrainingPlanningPage() { const { user } = useAuth() const [searchParams, setSearchParams] = useSearchParams() const unitDeepLinkHandledRef = useRef(null) 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) /** Abschnitts-Editor bei Bearbeitung: Planung vs. Nachbereitung (Ist & Abweichungen) */ const [sectionsEditMode, setSectionsEditMode] = useState('planning') const [draftPlanTemplateId, setDraftPlanTemplateId] = useState('') const [exercisePickerOpen, setExercisePickerOpen] = useState(false) const [exercisePickerTarget, setExercisePickerTarget] = useState(null) const [planningPeekCtx, setPlanningPeekCtx] = 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 [frameworkImportOpen, setFrameworkImportOpen] = useState(false) const [frameworkProgramsList, setFrameworkProgramsList] = useState([]) const [fwImportProgramId, setFwImportProgramId] = useState('') const [fwImportDetail, setFwImportDetail] = useState(null) const [fwImportLoading, setFwImportLoading] = useState(false) const [fwImportSelectedSlots, setFwImportSelectedSlots] = useState(() => new Set()) const [fwImportSlotDates, setFwImportSlotDates] = useState({}) const [fwImportStartDate, setFwImportStartDate] = useState(today) const [fwImportIntervalDays, setFwImportIntervalDays] = useState(7) const [fwImportSubmitting, setFwImportSubmitting] = useState(false) const [moduleApplyOpen, setModuleApplyOpen] = useState(false) const [moduleApplyBusy, setModuleApplyBusy] = useState(false) const [moduleApplyList, setModuleApplyList] = useState([]) const [moduleApplyModuleId, setModuleApplyModuleId] = useState('') const [moduleApplySectionIx, setModuleApplySectionIx] = useState(0) const [moduleApplyInsertSlot, setModuleApplyInsertSlot] = useState('before:0') const [moduleApplyErr, setModuleApplyErr] = useState('') const [moduleApplyPlacementLocked, setModuleApplyPlacementLocked] = useState(false) const [moduleApplySearchQuery, setModuleApplySearchQuery] = useState('') const [modulePickPreview, setModulePickPreview] = useState({ loading: false, moduleId: '', exercises: [], notes: 0, err: '', }) const [startDate, setStartDate] = useState(today) const [endDate, setEndDate] = useState(thirtyDaysLater) const [planView, setPlanView] = useState('list') const [calendarMonthStr, setCalendarMonthStr] = useState(() => today.slice(0, 7)) const [planScope, setPlanScope] = useState('group') const [assignedToMeOnly, setAssignedToMeOnly] = useState(false) const [clubDirectory, setClubDirectory] = useState([]) const [assignModalOpen, setAssignModalOpen] = useState(false) const [assignDraft, setAssignDraft] = useState({ unit: null, lead_trainer_profile_id: '', session_assistants_inherit: true, session_assistant_profile_ids: [], }) const [assignSaving, setAssignSaving] = useState(false) 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: '', debrief_completed: false, sections: [defaultSection()], ...sessionAssignDefaults() }) const planningFormRef = useRef(formData) planningFormRef.current = formData const moduleApplyFilteredList = useMemo(() => { const q = moduleApplySearchQuery.trim().toLowerCase().replace(/\s+/g, ' ') const words = q ? q.split(' ').filter(Boolean) : [] const list = Array.isArray(moduleApplyList) ? moduleApplyList : [] if (!words.length) return list return list.filter((m) => { const blob = [ m.title, m.summary, m.goal, m.target_group_notes, m.deployment_context_notes, ] .map((x) => String(x ?? '').toLowerCase()) .join('\n') return words.every((w) => blob.includes(w)) }) }, [moduleApplySearchQuery, moduleApplyList]) const modulePlacementSummary = useMemo(() => { const secs = Array.isArray(formData.sections) ? formData.sections : [] let si = typeof moduleApplySectionIx === 'number' ? moduleApplySectionIx : parseInt(String(moduleApplySectionIx), 10) if (!Number.isFinite(si)) si = 0 si = Math.max(0, Math.min(si, secs.length ? secs.length - 1 : 0)) const cap = secs[si]?.items?.length ?? 0 let beforeIx = cap if (typeof moduleApplyInsertSlot === 'string' && moduleApplyInsertSlot.startsWith('before:')) { const zi = parseInt(moduleApplyInsertSlot.slice('before:'.length), 10) if (Number.isFinite(zi)) beforeIx = Math.min(Math.max(0, zi), cap) } const rawTitle = (secs[si]?.title || '').trim() const secTitle = rawTitle || `Abschnitt ${si + 1}` let positionDescription if (cap <= 0) positionDescription = 'als erste Einträge dieses Abschnitts' else if (beforeIx <= 0) positionDescription = 'vor dem ersten Eintrag dieses Abschnitts' else if (beforeIx >= cap) positionDescription = 'nach dem letzten Eintrag dieses Abschnitts' else positionDescription = `unmittelbar vor Eintrag ${beforeIx + 1} (${cap} Einträge im Abschnitt)` return { secTitle, positionDescription } }, [formData.sections, moduleApplySectionIx, moduleApplyInsertSlot]) useEffect(() => { if (!moduleApplyOpen || !moduleApplyFilteredList.length) return if (moduleApplyFilteredList.some((m) => String(m.id) === String(moduleApplyModuleId))) return setModuleApplyModuleId(String(moduleApplyFilteredList[0].id)) }, [moduleApplyOpen, moduleApplyFilteredList, moduleApplyModuleId]) const planningModalClubId = useMemo(() => { const gid = Number(formData.group_id) if (!Number.isFinite(gid) || gid < 1) return null const g = groups.find((x) => Number(x.id) === gid) if (!g || g.club_id == null || g.club_id === '') return null const c = Number(g.club_id) return Number.isFinite(c) ? c : null }, [groups, formData.group_id]) const moduleApplyTargetItems = useMemo(() => { const secs = formData.sections || [] if (!secs.length) return [] let ix = typeof moduleApplySectionIx === 'number' ? moduleApplySectionIx : parseInt(String(moduleApplySectionIx), 10) if (!Number.isFinite(ix)) ix = 0 if (ix < 0 || ix >= secs.length) return [] const sec = secs[ix] return Array.isArray(sec?.items) ? sec.items : [] }, [formData.sections, moduleApplySectionIx]) const refreshPlanningSectionMeta = useCallback(async () => { const next = await enrichSectionsWithVariants(planningFormRef.current.sections) setFormData((prev) => ({ ...prev, sections: next })) }, []) 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 = useCallback(async () => { if (!selectedGroupId) return let start = startDate let end = endDate if (planView === 'calendar') { const r = getCalendarGridRange(calendarMonthStr) start = r.gridStart end = r.gridEnd } const gid = parseInt(selectedGroupId, 10) const groupRow = groups.find((g) => g.id === gid) const clubId = groupRow?.club_id try { const filters = { start_date: start, end_date: end } if (assignedToMeOnly) { filters.assigned_to_me = true } if (planScope === 'club' && clubId) { filters.club_id = clubId } else { filters.group_id = gid } const unitsData = await api.listTrainingUnits(filters) setUnits(unitsData) } catch (err) { console.error('Failed to load units:', err) } }, [ selectedGroupId, groups, startDate, endDate, planView, calendarMonthStr, planScope, assignedToMeOnly ]) useEffect(() => { loadData() }, []) useEffect(() => { if (selectedGroupId) { loadUnits() } }, [selectedGroupId, loadUnits]) const selectedGroupClubIdMemo = useMemo(() => { const g = groups.find((gr) => gr.id === parseInt(selectedGroupId, 10)) return g?.club_id != null ? Number(g.club_id) : null }, [groups, selectedGroupId]) const canClubOrgTraining = useMemo(() => { const r = (user?.role || '').toLowerCase() if (r === 'admin' || r === 'superadmin') return true if (selectedGroupClubIdMemo == null || !Number.isFinite(selectedGroupClubIdMemo)) return false const row = activeClubMemberships(user?.clubs).find((c) => Number(c.id) === selectedGroupClubIdMemo) return Array.isArray(row?.roles) && row.roles.includes('club_admin') }, [user?.role, user?.clubs, selectedGroupClubIdMemo]) const clubAdminClubIdSet = useMemo(() => { const ids = [] for (const c of activeClubMemberships(user?.clubs)) { if (Array.isArray(c.roles) && c.roles.includes('club_admin')) { const id = Number(c.id) if (Number.isFinite(id)) ids.push(id) } } return new Set(ids) }, [user?.clubs]) useEffect(() => { const gid = parseInt(formData.group_id || selectedGroupId || '0', 10) const gModal = Number.isFinite(gid) && gid >= 1 ? groups.find((x) => x.id === gid) : null const clubForModal = gModal?.club_id != null ? Number(gModal.club_id) : null let assignModalClubId = null if (assignModalOpen && assignDraft.unit?.group_id != null) { const ug = Number(assignDraft.unit.group_id) const gAssign = Number.isFinite(ug) ? groups.find((x) => x.id === ug) : null if (gAssign?.club_id != null) assignModalClubId = Number(gAssign.club_id) } const loadClubId = showModal && clubForModal != null && Number.isFinite(clubForModal) ? clubForModal : assignModalOpen && assignModalClubId != null && Number.isFinite(assignModalClubId) ? assignModalClubId : canClubOrgTraining && selectedGroupClubIdMemo != null && Number.isFinite(selectedGroupClubIdMemo) ? selectedGroupClubIdMemo : null if (loadClubId == null || !Number.isFinite(loadClubId)) { setClubDirectory([]) return undefined } let cancelled = false ;(async () => { try { const d = await api.clubMembersDirectory(loadClubId) if (!cancelled) setClubDirectory(Array.isArray(d) ? d : []) } catch (err) { if (!cancelled) { console.error('Mitgliederverzeichnis:', err) setClubDirectory([]) } } })() return () => { cancelled = true } }, [ showModal, assignModalOpen, assignDraft.unit, formData.group_id, selectedGroupId, groups, canClubOrgTraining, selectedGroupClubIdMemo, ]) useEffect(() => { if (!frameworkImportOpen) return let cancelled = false ;(async () => { try { const list = await api.listTrainingFrameworkPrograms() if (!cancelled) setFrameworkProgramsList(Array.isArray(list) ? list : []) } catch (e) { if (!cancelled) { console.error('Rahmenprogramme laden:', e) setFrameworkProgramsList([]) } } })() return () => { cancelled = true } }, [frameworkImportOpen]) const openFrameworkImportModal = useCallback(() => { setFwImportProgramId('') setFwImportDetail(null) setFwImportSelectedSlots(new Set()) setFwImportSlotDates({}) setFwImportStartDate(new Date().toISOString().split('T')[0]) setFwImportIntervalDays(7) setFrameworkImportOpen(true) }, []) const onFwImportProgramChange = async (idStr) => { setFwImportProgramId(idStr) if (!idStr) { setFwImportDetail(null) return } setFwImportLoading(true) try { const d = await api.getTrainingFrameworkProgram(parseInt(idStr, 10)) setFwImportDetail(d) setFwImportSelectedSlots(new Set()) setFwImportSlotDates({}) } catch (e) { alert(e.message || 'Rahmenprogramm laden fehlgeschlagen') setFwImportDetail(null) } finally { setFwImportLoading(false) } } const toggleFwImportSlot = (slot) => { if (!slot?.blueprint_training_unit_id) return const sid = slot.id setFwImportSelectedSlots((prev) => { const n = new Set(prev) if (n.has(sid)) n.delete(sid) else n.add(sid) return n }) } const applyFwImportDateSuggestions = () => { if (!fwImportDetail?.slots?.length) return const sorted = [...fwImportDetail.slots].sort( (a, b) => (a.sort_order ?? 0) - (b.sort_order ?? 0) ) let offset = 0 const iv = Math.max(0, Number(fwImportIntervalDays) || 0) const next = {} for (const s of sorted) { if (!fwImportSelectedSlots.has(s.id)) continue if (!s.blueprint_training_unit_id) continue next[String(s.id)] = addDaysIsoDate(fwImportStartDate, offset) offset += iv } setFwImportSlotDates((prev) => ({ ...prev, ...next })) } const submitFrameworkImport = async () => { if (!selectedGroupId) { alert('Bitte zuerst eine Trainingsgruppe wählen.') return } const gid = parseInt(selectedGroupId, 10) if (!fwImportDetail?.slots?.length) return const sorted = [...fwImportDetail.slots].sort( (a, b) => (a.sort_order ?? 0) - (b.sort_order ?? 0) ) const picks = sorted.filter( (s) => fwImportSelectedSlots.has(s.id) && s.blueprint_training_unit_id ) if (!picks.length) { alert('Bitte mindestens eine Session mit Ablauf (Blueprint) auswählen.') return } for (const s of picks) { const key = String(s.id) const date = fwImportSlotDates[key] || fwImportStartDate if (!date) { alert('Bitte für jede ausgewählte Session ein Datum setzen (oder Datumsvorschläge nutzen).') return } } setFwImportSubmitting(true) try { for (const s of picks) { const key = String(s.id) const date = fwImportSlotDates[key] || fwImportStartDate await api.createTrainingUnitFromFrameworkSlot({ group_id: gid, planned_date: date, framework_slot_id: s.id, }) } setFrameworkImportOpen(false) await loadUnits() } catch (e) { alert(e.message || 'Übernahme fehlgeschlagen') } finally { setFwImportSubmitting(false) } } const frameworkLineageText = (unit) => { const fpTitle = (unit.origin_framework_program_title || '').trim() || 'Rahmenprogramm' const st = (unit.origin_framework_slot_title || '').trim() const idx = unit.origin_framework_slot_sort_order const slotBit = st || (typeof idx === 'number' ? `Session ${idx + 1}` : 'Session') return { fpTitle, slotBit, fpId: unit.origin_framework_program_id } } 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: '', debrief_completed: false, sections: [defaultSection('Hauptteil')], ...sessionAssignDefaults() }) setSectionsEditMode('planning') setShowModal(true) } const handleCreateForDate = (isoDay) => { 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: isoDay, 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: '', debrief_completed: false, sections: [defaultSection('Hauptteil')], ...sessionAssignDefaults() }) setSectionsEditMode('planning') 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 = useCallback(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 || '', debrief_completed: Boolean(fullUnit.debrief_completed_at), sections, lead_trainer_profile_id: fullUnit.lead_trainer_profile_id != null && fullUnit.lead_trainer_profile_id !== '' ? String(fullUnit.lead_trainer_profile_id) : '', session_assistants_inherit: fullUnit.assistant_trainer_profile_ids == null || fullUnit.assistant_trainer_profile_ids === undefined, session_assistant_profile_ids: (() => { const efLead = fullUnit.effective_lead_trainer_profile_id != null ? Number(fullUnit.effective_lead_trainer_profile_id) : null let xs = toNumList(fullUnit.assistant_trainer_profile_ids) if (efLead != null && Number.isFinite(efLead)) xs = xs.filter((id) => id !== efLead) return xs })(), }) setSectionsEditMode(fullUnit.status === 'completed' ? 'debrief' : 'planning') setShowModal(true) } catch (err) { alert('Fehler beim Laden: ' + err.message) throw err } }, []) useEffect(() => { if (!user?.id || loading) return const uid = searchParams.get('unit') if (!uid) { unitDeepLinkHandledRef.current = null return } if (unitDeepLinkHandledRef.current === uid) return const idNum = parseInt(uid, 10) if (!Number.isFinite(idNum)) return unitDeepLinkHandledRef.current = uid handleEdit({ id: idNum }) .then(() => { setSearchParams( (prev) => { const next = new URLSearchParams(prev) next.delete('unit') next.delete('debrief') return next }, { replace: true } ) }) .catch(() => { unitDeepLinkHandledRef.current = null setSearchParams( (prev) => { const next = new URLSearchParams(prev) next.delete('unit') next.delete('debrief') return next }, { replace: true } ) }) }, [user?.id, loading, searchParams, handleEdit, setSearchParams]) 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 openModuleApplyModal = useCallback(async (placement) => { setModuleApplyErr('') setModuleApplySearchQuery('') const placementLocked = placement != null && typeof placement.sectionIndex === 'number' && typeof placement.insertBeforeIndex === 'number' setModuleApplyPlacementLocked(placementLocked) const secs = planningFormRef.current?.sections ?? [] let secIx = 0 let before = 0 if (secs.length) { if (placement && typeof placement.sectionIndex === 'number') { secIx = Math.min(Math.max(0, placement.sectionIndex), secs.length - 1) const items = Array.isArray(secs[secIx]?.items) ? secs[secIx].items : [] const cap = items.length if (typeof placement.insertBeforeIndex === 'number' && Number.isFinite(placement.insertBeforeIndex)) { before = Math.min(Math.max(0, placement.insertBeforeIndex), cap) } else before = cap } else { const items = Array.isArray(secs[0]?.items) ? secs[0].items : [] before = items.length secIx = 0 } } setModuleApplySectionIx(secIx) setModuleApplyInsertSlot(`before:${before}`) setModuleApplyOpen(true) try { const list = await api.listTrainingModules() const arr = Array.isArray(list) ? list : [] setModuleApplyList(arr) setModuleApplyModuleId(arr.length ? String(arr[0].id) : '') } catch (e) { setModuleApplyErr(e.message || 'Module konnten nicht geladen werden') setModuleApplyList([]) } }, []) const handleApplyTrainingModuleConfirm = useCallback(async () => { const mid = parseInt(moduleApplyModuleId, 10) if (!Number.isFinite(mid)) { alert('Bitte ein Trainingsmodul wählen.') return } let secIx = parseInt(String(moduleApplySectionIx), 10) if (!Number.isFinite(secIx)) secIx = 0 const baseSections = planningFormRef.current?.sections ?? formData.sections ?? [] if (!baseSections.length) { alert('Keine Abschnitte im Formular.') return } if (secIx < 0 || secIx >= baseSections.length) secIx = 0 const secItems = Array.isArray(baseSections[secIx]?.items) ? baseSections[secIx].items : [] const itemCap = secItems.length let insertBefore = itemCap if (typeof moduleApplyInsertSlot === 'string' && moduleApplyInsertSlot.startsWith('before:')) { const zi = parseInt(moduleApplyInsertSlot.slice('before:'.length), 10) if (Number.isFinite(zi)) insertBefore = Math.min(Math.max(0, zi), itemCap) } setModuleApplyBusy(true) setModuleApplyErr('') try { const detail = await api.getTrainingModule(mid) let nextSections = await insertTrainingModuleIntoPlanningSections({ sections: baseSections, moduleDetail: detail, sectionIndex: secIx, insertBeforeItemIndex: insertBefore, }) nextSections = await enrichSectionsWithVariants(nextSections) setFormData((fd) => ({ ...fd, sections: nextSections })) setModuleApplyOpen(false) setModuleApplyPlacementLocked(false) } catch (e) { setModuleApplyErr(e.message || 'Einfügen fehlgeschlagen') } finally { setModuleApplyBusy(false) } }, [moduleApplyModuleId, moduleApplySectionIx, moduleApplyInsertSlot]) useEffect(() => { if (!moduleApplyOpen) { setModulePickPreview({ loading: false, moduleId: '', exercises: [], notes: 0, err: '', }) return undefined } const mid = parseInt(String(moduleApplyModuleId), 10) if (!Number.isFinite(mid) || mid < 1) { setModulePickPreview({ loading: false, moduleId: '', exercises: [], notes: 0, err: '', }) return undefined } let cancelled = false setModulePickPreview({ loading: true, moduleId: String(mid), exercises: [], notes: 0, err: '', }) ;(async () => { try { const detail = await api.getTrainingModule(mid) if (cancelled) return const itemsSorted = [...(detail.items ?? [])].sort( (a, b) => (a.order_index ?? 0) - (b.order_index ?? 0) ) const uniqueEx = new Set() let notes = 0 for (const row of itemsSorted) { if ((row.item_type || '') !== 'note') { const eid = row.exercise_id if (eid) uniqueEx.add(Number(eid)) continue } const b = String(row.note_body ?? '').trim() if (b === '---') continue notes += 1 } const titleById = new Map() await Promise.all( [...uniqueEx].map(async (eid) => { try { const ex = await api.getExercise(eid) titleById.set(eid, (ex?.title || '').trim() || `Übung #${eid}`) } catch { titleById.set(eid, `Übung #${eid}`) } }) ) if (cancelled) return const exTitlesInOrder = [] for (const row of itemsSorted) { if ((row.item_type || '') !== 'exercise') continue const eid = Number(row.exercise_id) if (!Number.isFinite(eid)) continue exTitlesInOrder.push(titleById.get(eid) || `Übung #${eid}`) } setModulePickPreview({ loading: false, moduleId: String(mid), exercises: exTitlesInOrder, notes, err: '', }) } catch (e) { if (!cancelled) { setModulePickPreview({ loading: false, moduleId: String(mid), exercises: [], notes: 0, err: e?.message || 'Vorschau fehlgeschlagen', }) } } })() return () => { cancelled = true } }, [moduleApplyOpen, moduleApplyModuleId]) const handleTakeLead = async (unit) => { if (!user?.id) return try { await api.updateTrainingUnit(unit.id, { lead_trainer_profile_id: user.id }) await loadUnits() } catch (err) { alert(err.message || 'Leitung konnte nicht übernommen werden') } } const openTrainerAssignModal = (unit) => { const effLead = unit.effective_lead_trainer_profile_id != null ? Number(unit.effective_lead_trainer_profile_id) : null let coIds = toNumList(unit.assistant_trainer_profile_ids) if (effLead != null && Number.isFinite(effLead)) { coIds = coIds.filter((id) => id !== effLead) } setAssignDraft({ unit, lead_trainer_profile_id: unit.lead_trainer_profile_id != null && unit.lead_trainer_profile_id !== '' ? String(unit.lead_trainer_profile_id) : '', session_assistants_inherit: unit.assistant_trainer_profile_ids == null || unit.assistant_trainer_profile_ids === undefined, session_assistant_profile_ids: coIds, }) setAssignModalOpen(true) } const saveTrainerAssignModal = async () => { if (!assignDraft.unit) return setAssignSaving(true) try { const payload = {} const leadStr = String(assignDraft.lead_trainer_profile_id || '').trim() if (leadStr) payload.lead_trainer_profile_id = parseInt(leadStr, 10) else payload.lead_trainer_profile_id = null if (assignDraft.session_assistants_inherit) { payload.assistant_trainer_profile_ids = null } else { payload.assistant_trainer_profile_ids = [...assignDraft.session_assistant_profile_ids].sort((a, b) => a - b) } await api.updateTrainingUnit(assignDraft.unit.id, payload) setAssignModalOpen(false) setAssignDraft({ unit: null, ...sessionAssignDefaults(), }) await loadUnits() } catch (err) { alert(err.message || 'Zuweisung konnte nicht gespeichert werden') } finally { setAssignSaving(false) } } 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.debrief_completed = (formData.status || '') === 'completed' ? !!formData.debrief_completed : false } const leadStr = String(formData.lead_trainer_profile_id || '').trim() if (leadStr) { payload.lead_trainer_profile_id = parseInt(leadStr, 10) } else if (editingUnit) { payload.lead_trainer_profile_id = null } if (formData.session_assistants_inherit) { if (editingUnit) payload.assistant_trainer_profile_ids = null } else { payload.assistant_trainer_profile_ids = [...formData.session_assistant_profile_ids].sort( (a, b) => a - b ) } 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) => { if (field !== 'lead_trainer_profile_id') { const patch = { ...prev, [field]: value } if (field === 'status' && value !== 'completed') { patch.debrief_completed = false } return patch } const ts = typeof value === 'string' ? value.trim() : String(value ?? '').trim() const strip = new Set() if (ts !== '') { const nid = parseInt(ts, 10) if (Number.isFinite(nid)) strip.add(nid) } else { const gidParsed = parseInt(prev.group_id || selectedGroupId || '0', 10) const gr = Number.isFinite(gidParsed) && gidParsed >= 1 ? groups.find((xg) => xg.id === gidParsed) : null if (gr?.trainer_id != null) { const ht = Number(gr.trainer_id) if (Number.isFinite(ht)) strip.add(ht) } } const assistants = prev.session_assistant_profile_ids.filter((id) => !strip.has(id)) return { ...prev, lead_trainer_profile_id: value, session_assistant_profile_ids: assistants } }) } const calendarGridDays = useMemo(() => { const r = getCalendarGridRange(calendarMonthStr) return enumerateIsoDays(r.gridStart, r.gridEnd) }, [calendarMonthStr]) const unitsByPlannedDate = useMemo(() => { const m = new Map() for (const u of units) { const raw = u.planned_date if (!raw) continue const key = String(raw).slice(0, 10) if (!m.has(key)) m.set(key, []) m.get(key).push(u) } return m }, [units]) const calendarMonthTitle = useMemo(() => { const p = calendarMonthStr.split('-').map(Number) const y = p[0] const mo = p[1] if (!y || !mo) return '' return new Date(y, mo - 1, 1).toLocaleDateString('de-DE', { month: 'long', year: 'numeric' }) }, [calendarMonthStr]) const mayConfigureSessionAssignments = useCallback( (unit) => { if (!unit) return false const pid = Number(user?.id) if (!Number.isFinite(pid)) return false const r = (user?.role || '').toLowerCase() if (r === 'admin' || r === 'superadmin') return true const gClub = unit.group_club_id != null ? Number(unit.group_club_id) : null if (Number.isFinite(gClub) && clubAdminClubIdSet.has(gClub)) return true const gid = Number(unit.group_id) const g = groups.find((gr) => gr.id === gid) if (!g) return false const cb = unit.created_by != null ? Number(unit.created_by) : NaN if (Number.isFinite(cb) && cb === pid) return true const ht = g.trainer_id != null ? Number(g.trainer_id) : NaN if (Number.isFinite(ht) && ht === pid) return true return normalizeGroupCoTrainerIds(g.co_trainer_ids).includes(pid) }, [user?.id, user?.role, groups, clubAdminClubIdSet] ) if (loading) { return (

Laden...

) } const selectedGroup = groups.find((g) => g.id === parseInt(selectedGroupId, 10)) const gidTrainerForm = parseInt(formData.group_id || selectedGroupId || '0', 10) const groupForTrainerForm = Number.isFinite(gidTrainerForm) && gidTrainerForm >= 1 ? groups.find((gr) => gr.id === gidTrainerForm) : null let formTrainerAssignLeadExcludeId = null if (groupForTrainerForm?.trainer_id != null) formTrainerAssignLeadExcludeId = Number(groupForTrainerForm.trainer_id) const leadDraftTrim = String(formData.lead_trainer_profile_id || '').trim() if (leadDraftTrim !== '') { const nl = parseInt(leadDraftTrim, 10) if (Number.isFinite(nl)) formTrainerAssignLeadExcludeId = nl } if (editingUnit?.effective_lead_trainer_profile_id != null && leadDraftTrim === '') { const el = Number(editingUnit.effective_lead_trainer_profile_id) if (Number.isFinite(el)) formTrainerAssignLeadExcludeId = el } const clubDirectoryForCo = filterDirectoryExcludingLead(clubDirectory, formTrainerAssignLeadExcludeId) let assignExcludeLeadPid = null if (assignModalOpen && assignDraft.unit) { const dl = String(assignDraft.lead_trainer_profile_id || '').trim() if (dl !== '') { const n = parseInt(dl, 10) assignExcludeLeadPid = Number.isFinite(n) ? n : null } else if (assignDraft.unit.effective_lead_trainer_profile_id != null) { const n = Number(assignDraft.unit.effective_lead_trainer_profile_id) assignExcludeLeadPid = Number.isFinite(n) ? n : null } } const clubDirectoryForAssignCo = filterDirectoryExcludingLead(clubDirectory, assignExcludeLeadPid) return (

Trainingsplanung

Ansicht { if (id === 'calendar') { setPlanView('calendar') setCalendarMonthStr((prev) => { const fromList = (startDate || '').slice(0, 7) if (/^\d{4}-\d{2}$/.test(fromList)) return fromList return prev || new Date().toISOString().slice(0, 7) }) } else { setPlanView('list') } }} items={[ { id: 'list', label: 'Liste' }, { id: 'calendar', label: 'Kalender' }, ]} className="page-section-nav--inline planning-ansicht-nav" /> {planView === 'list' ? 'Zeitraum unten mit Von/Bis filtern.' : 'Monat unten wechseln; Termine erscheinen im Raster.'}

Wähle eine Trainingsgruppe und lege Trainingseinheiten für den Zeitraum an (Inhalt: Abschnitte und Übungen).

Mehrere Einheiten strukturieren auf einmal:{' '} Trainingsrahmenprogramme {' '} (Ziele, Sessions, Vorlagen‑Ablauf).

Wiederverwendbare Blöcke innerhalb einer Einheit:{' '} Trainingsmodule {' '} (übernahme als Kopie beim Bearbeiten einer Einheit).

{!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
)}
{planView === 'list' ? ( <>
setStartDate(e.target.value)} />
setEndDate(e.target.value)} />
) : (
{calendarMonthTitle}
)}
Einblenden

Neue Termine gelten immer für die gewählte Gruppe. „Ganzer Verein“ zeigt zusätzlich Termine anderer Gruppen desselben Vereins.

Mehr zu Ansicht & Trainerzuordnung

„Ganzer Verein“ bezieht sich auf denselben Verein wie die gewählte Gruppe. Neu angelegte Termine beziehen sich weiterhin auf die Gruppe, die du oben gewählt hast.

{selectedGroupId ? (

Über Trainer oder Trainer zuweisen bearbeitest du Leitung und Co je Einheit (berechtigt: Vereinsorganisation, Haupt-/Co‑Trainer der Gruppe sowie Erstellung der Einheit). Das Mitgliederverzeichnis listet nur eigene Vereinsmitglieder; die Leitung erscheint nicht unter Co‑Trainer.

) : (

Wähle zuerst eine Gruppe — dann erweitert sich die Hilfe zu Trainer und Berechtigungen.

)}
{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)}`}

)}

Neue Trainingseinheit

Datum, Zeiten und Ablauf (Abschnitte & Übungen) — optional{' '} Trainingsvorlage oder Inhalte aus einem Rahmenprogramm im Dialog.

{!selectedGroupId && (

Wähle oben eine Trainingsgruppe, um fortzufahren.

)} {groups.length === 0 && (

Es gibt noch keine aktive Trainingsgruppe — unter{' '} Vereine anlegen oder aktivieren.

)}
{!selectedGroupId ? (

Wähle oben eine Trainingsgruppe — danach kannst du unter{' '} „Trainingseinheit planen…“ einen Termin anlegen.

) : planView === 'calendar' ? (
{units.length === 0 ? (

Im sichtbaren Monatsbereich liegt noch keine Einheit. Über + in einem Tag legst du einen neuen Termin mit Datum an.

) : null}
{WEEKDAYS_DE.map((w) => (
{w}
))} {calendarGridDays.map((dayIso) => { const inMonth = dayIso.slice(0, 7) === calendarMonthStr const dayNum = parseInt(dayIso.slice(8, 10), 10) const isTodayMarker = dayIso === today const dayUnits = unitsByPlannedDate.get(dayIso) || [] return (
{dayNum}
{dayUnits.slice(0, 3).map((unit) => (
{mayConfigureSessionAssignments(unit) ? ( ) : null}
))}
{dayUnits.length > 3 ? (

+{dayUnits.length - 3} weitere

) : null}
) })}
) : units.length === 0 ? (

Keine Trainingseinheiten in diesem Zeitraum. Unten unter „Neue Trainingseinheit“ einen Termin anlegen — optional mit Vorlage im Dialog.

) : (
{units.map((unit) => { const lineage = unit.origin_framework_slot_id ? frameworkLineageText(unit) : null const uid = user?.id != null ? Number(user.id) : null const effLead = unit.effective_lead_trainer_profile_id != null ? Number(unit.effective_lead_trainer_profile_id) : null const showTakeLead = unit.status === 'planned' && uid != null && effLead != null && effLead !== uid return (

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

{planScope === 'club' && (unit.group_name || '').trim() ? (

{unit.group_name}

) : null}

Leitung: {(unit.lead_trainer_name || '').trim() || '—'}

{(() => { const coRaw = unit.effective_assistant_trainer_profile_ids const co = Array.isArray(coRaw) ? coRaw.map(Number).filter((x) => Number.isFinite(x) && x >= 1) : [] if (!co.length) return null const src = unit.assistant_trainer_profile_ids != null ? 'Session-Zuweisung' : 'über Trainingsgruppe' return (

Co-Trainer ({src}): {co.length}

) })()} {unit.planned_focus && (

Fokus: {unit.planned_focus}

)} {lineage ? (

Aus Rahmen: {unit.origin_framework_program_id ? ( {lineage.fpTitle} ) : ( {lineage.fpTitle} )} · {lineage.slotBit}

) : null}
{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) {mayConfigureSessionAssignments(unit) ? ( ) : null} {showTakeLead ? ( ) : null}
{unit.notes && (

{unit.notes}

)}
) })}
)} {assignModalOpen && assignDraft.unit ? (
{ if (!assignSaving) setAssignModalOpen(false) }} >
e.stopPropagation()} style={{ background: 'var(--surface)', borderRadius: '12px', padding: 'clamp(14px, 3vw, 1.75rem)', maxWidth: 'min(460px, 100%)', width: '100%', maxHeight: '90vh', overflowY: 'auto', boxSizing: 'border-box', }} >

Trainer zuweisen (organisatorisch)

{(assignDraft.unit.planned_date || '').toString().slice(0, 10)} {assignDraft.unit.planned_time_start ? ` · ${String(assignDraft.unit.planned_time_start).slice(0, 5)}` : ''} {(assignDraft.unit.group_name || '').trim() ? ` · ${(assignDraft.unit.group_name || '').trim()}` : null}

{!assignDraft.session_assistants_inherit ? (
{clubDirectoryForAssignCo.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) && assignDraft.session_assistant_profile_ids.includes(mid) return ( ) })}
) : null} {!clubDirectory.length ? (

Mitgliederverzeichnis konnte nicht geladen werden.

) : null}
) : null} {moduleApplyOpen && (
{ if (ev.target !== ev.currentTarget || moduleApplyBusy) return setModuleApplyOpen(false) setModuleApplyPlacementLocked(false) }} >

Trainingsmodul einfügen

Alle Positionen des gewählten Moduls werden als neue Zeilen eingefügt (Kopie, mit klarer Herkunft im Ablauf). Die Einheit brauchst du dafür nicht vorher gespeichert zu haben — Speichern am Ende wie gewohnt. Vollständige Textsuche oder Modulkategorien planen wir serverseitig für eine spätere Iteration; vorerst steht hier eine{' '} Schnellsuche über Titel und Freitext-Felder zur Verfügung.

{moduleApplyErr ? (

{moduleApplyErr}

) : null} {moduleApplyPlacementLocked ? ( <>

Aktuelle Einfügeposition: Abschnitt {modulePlacementSummary.secTitle}{' '} / {modulePlacementSummary.positionDescription}

Abschnitt oder Position ändern
) : ( <>
)}
setModuleApplySearchQuery(e.target.value)} disabled={moduleApplyBusy} aria-label="Module durch Freitext filtern" />
{!moduleApplyFilteredList.length ? (

{!moduleApplyList.length ? 'Keine Module verfügbar oder keine Berechtigung.' : 'Kein Modul entspricht der Suche.'}

) : ( moduleApplyFilteredList.map((m) => { const title = ((m.title || '').trim() || `Modul #${m.id}`).trim() const visLbl = trainingVisibilityShortDE(m.visibility) const nPos = typeof m.items_count === 'number' ? m.items_count : '—' const selected = String(m.id) === String(moduleApplyModuleId) return ( ) }) )}
{moduleApplyModuleId ? (
Ablauf-Vorschau (Bibliotheksmodul)
{modulePickPreview.loading ? (

Übungen und Hinweise laden …

) : modulePickPreview.err ? (

{modulePickPreview.err}

) : !modulePickPreview.exercises.length && !modulePickPreview.notes ? (

Keine Übungspositionen in diesem Eintrag gefunden (prüfen, ob Übungen im Modul gültige IDs haben).

) : ( <>
    {(modulePickPreview.exercises.slice(0, 12)).map((t, qi) => (
  1. {t}
  2. ))}
{modulePickPreview.exercises.length > 12 ? (

… und noch {modulePickPreview.exercises.length - 12} weitere Übungen in genau dieser Modulreihenfolge.

) : null} {modulePickPreview.notes > 0 ? (

zusätzlich {modulePickPreview.notes}{' '} {modulePickPreview.notes === 1 ? 'Position mit Hinweis' : 'Positionen mit Hinweisen'}{' '} (ohne Aufzählung)

) : null} )}
) : null}

Neue Module kannst du unter{' '} Trainingsmodule {' '} anlegen.

)} {frameworkImportOpen && (

Sessions aus Rahmen übernehmen

Wähle ein Trainingsrahmenprogramm und eine oder mehrere Sessions. Pro Session entsteht eine{' '} eigene geplante Einheit in der aktuellen Gruppe (Kopie des Ablaufs). Die{' '} Verknüpfung zum Rahmen-Slot wird gespeichert, damit die Herkunft sichtbar bleibt.

{fwImportLoading ? (

Laden der Sessions…

) : fwImportDetail?.slots?.length ? ( <>
Sessions (mit Ablauf)
    {[...fwImportDetail.slots] .sort((a, b) => (a.sort_order ?? 0) - (b.sort_order ?? 0)) .map((slot) => { const hasBp = !!slot.blueprint_training_unit_id const checked = fwImportSelectedSlots.has(slot.id) const label = (slot.title || '').trim() || `Session ${(slot.sort_order ?? 0) + 1}` return (
  • ) })}
setFwImportStartDate(e.target.value)} disabled={fwImportSubmitting} />
setFwImportIntervalDays(parseInt(e.target.value, 10) || 0)} disabled={fwImportSubmitting} />
) : fwImportProgramId ? (

Keine Sessions in diesem Programm.

) : null}
)} {showModal && (

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

{editingUnit?.origin_framework_slot_id ? (() => { const L = frameworkLineageText(editingUnit) return (
Herkunft:{' '} {editingUnit.origin_framework_program_id ? ( {L.fpTitle} ) : ( L.fpTitle )} · {L.slotBit}

Inhalt stammt aus dem Session-Blueprint des Rahmenprogramms. Änderungen gelten nur für diese geplante Einheit; die Zuordnung zum Rahmen bleibt zur Nachverfolgung erhalten.

) })() : null} {!editingUnit && (

Übernimmt nur die Sektionsstruktur aus der Bibliothek; Übungen trägst du unten bei den Abschnitten ein. Gespeicherte Vorlagen kannst du unter Planung später erweitern.

)}

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" />

Trainerzuordnung (diese Einheit)

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.

{!formData.session_assistants_inherit ? (
{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 ( ) })}
) : null} {!clubDirectory.length && showModal ? (

Keine Einträge im Vereins-Mitgliederverzeichnis oder noch nicht geladen (nur für Vereinsinterne).

) : null}
{editingUnit ? (
Ablauf bearbeiten als
{[ { id: 'planning', label: 'Planung' }, { id: 'debrief', label: 'Nachbereitung' }, ].map((opt, i) => ( ))}

{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“.'}

) : null} } sections={formData.sections} wideExerciseGrid onSectionsChange={(updater) => setFormData((prev) => ({ ...prev, sections: updater(prev.sections), })) } onRequestTrainingModulePick={(ctx) => { void openModuleApplyModal(ctx) }} onRequestExercisePick={({ sectionIndex, itemIndex, insertBeforeIndex }) => { setExercisePickerTarget({ sIdx: sectionIndex, iIdx: typeof itemIndex === 'number' ? itemIndex : undefined, insertBeforeIndex: typeof insertBeforeIndex === 'number' && Number.isFinite(insertBeforeIndex) ? insertBeforeIndex : undefined, }) setExercisePickerOpen(true) }} onPeekExercise={(id, variantId, peekExtras) => setPlanningPeekCtx({ exerciseId: id, variantId: variantId ?? null, peekExtras: peekExtras ?? null, }) } showExecutionExtras={Boolean(editingUnit) && sectionsEditMode === 'debrief'} />
{editingUnit && ( <>

Durchführung

updateFormField('actual_date', e.target.value)} />
updateFormField('actual_time_start', e.target.value)} />
updateFormField('actual_time_end', e.target.value)} />
updateFormField('attendance_count', e.target.value)} />
{formData.status === 'completed' ? (
) : null} )}

Notizen