import React, { useState, useEffect, useCallback, useMemo } 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' import TrainingUnitSectionsEditor from '../components/TrainingUnitSectionsEditor' import { defaultSection, normalizeUnitToForm, enrichSectionsWithVariants, buildSectionsPayload, hydrateExercisePlanningRow, } from '../utils/trainingUnitSectionsForm' 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 [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 [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 [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: '', sections: [defaultSection()], ...sessionAssignDefaults() }) 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 = (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 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 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')], ...sessionAssignDefaults() }) 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: '', sections: [defaultSection('Hauptteil')], ...sessionAssignDefaults() }) 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, 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 })(), }) 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 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 } 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') return { ...prev, [field]: value } 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...
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).
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{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. )}
Wähle oben eine Trainingsgruppe — danach kannst du mit „Neue Trainingseinheit planen“ starten.
Im sichtbaren Monatsbereich liegt noch keine Einheit. Über + in einem Tag legst du einen neuen Termin mit Datum an.
) : null}+{dayUnits.length - 3} weitere
) : null}Keine Trainingseinheiten in diesem Zeitraum. Nutze oben „Neue Trainingseinheit planen“ oder{' '} „Schnell erstellen“, um den ersten Termin anzulegen.
{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.notes}
)}{(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}
Mitgliederverzeichnis konnte nicht geladen werden.
) : null}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.
Laden der Sessions…
) : fwImportDetail?.slots?.length ? ( <>Keine Sessions in diesem Programm.
) : null}Inhalt stammt aus dem Session-Blueprint des Rahmenprogramms. Änderungen gelten nur für diese geplante Einheit; die Zuordnung zum Rahmen bleibt zur Nachverfolgung erhalten.
Lädt die Abschnitte und Hinweise aus der Vorlage; Übungen fügst du hier ein.