import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useLocation, useNavigate, useParams, useSearchParams } from 'react-router-dom' import api from '../utils/api' import { useAuth } from '../context/AuthContext' import { useToast } from '../context/ToastContext' import { getTenantClubDependencyKey } from '../utils/activeClub' import ExercisePickerModal from '../components/ExercisePickerModal' import ExercisePeekModal from '../components/ExercisePeekModal' import TrainingUnitFormShell from '../components/planning/TrainingUnitFormShell' import TrainingPlanningModuleApplyModal from '../components/planning/TrainingPlanningModuleApplyModal' import TrainingPublishToFrameworkModal from '../components/planning/TrainingPublishToFrameworkModal' import SaveExercisesAsModuleModal from '../components/planning/SaveExercisesAsModuleModal' import { defaultSection, enrichSectionsWithVariants, formSectionsFromPlanTemplateRows, hydrateExercisePlanningRow, insertTrainingModuleIntoPlanningSections, normalizeUnitToForm, templateSectionsPayloadFromFormSections, } from '../utils/trainingUnitSectionsForm' import { filterDirectoryExcludingLead, } from '../utils/trainingPlanningPageHelpers' import { createEmptyTrainingUnitFormData, buildTrainingUnitSavePayload, trainingUnitToFormFields, trainingUnitFormSnapshot, validateTrainingUnitFormForSave, } from '../utils/trainingUnitEditorCore' import PageFormEditorChrome from '../components/PageFormEditorChrome' import UnsavedChangesPrompt from '../components/UnsavedChangesPrompt' import { useBeforeUnloadWhen, useUnsavedChangesBlocker } from '../hooks/useUnsavedChangesBlocker' import { buildPlanUnitEditPath } from '../utils/planningUnitRoutes' import { PLANNING_HUB_PATH, buildCurrentLocationReturnContext, goNavReturn, } from '../utils/navReturnContext' export default function TrainingUnitEditPage() { const { id: routeId } = useParams() const isNew = !routeId || routeId === 'new' const unitId = !isNew ? parseInt(routeId, 10) : NaN const navigate = useNavigate() const location = useLocation() const [searchParams] = useSearchParams() const { user } = useAuth() const toast = useToast() const tenantClubDepKey = useMemo(() => getTenantClubDependencyKey(user), [user]) const [loading, setLoading] = useState(!isNew) const [saving, setSaving] = useState(false) const [groups, setGroups] = useState([]) const [planTemplates, setPlanTemplates] = useState([]) const [editingUnit, setEditingUnit] = useState(null) const [draftPlanTemplateId, setDraftPlanTemplateId] = useState('') const [sectionsEditMode, setSectionsEditMode] = useState('planning') const [clubDirectory, setClubDirectory] = useState([]) const [formData, setFormData] = useState(() => createEmptyTrainingUnitFormData()) const formRef = useRef(formData) formRef.current = formData const baselineRef = useRef(null) const [baselineReady, setBaselineReady] = useState(false) const [bypassDirty, setBypassDirty] = useState(false) const dirtySignature = trainingUnitFormSnapshot(formRef.current, { editingUnit, draftPlanTemplateId, }) useEffect(() => { baselineRef.current = null setBaselineReady(false) setBypassDirty(false) }, [isNew, unitId]) useEffect(() => { if (loading) return undefined const handle = window.setTimeout(() => { baselineRef.current = trainingUnitFormSnapshot(formRef.current, { editingUnit, draftPlanTemplateId, }) setBaselineReady(true) }, 120) return () => clearTimeout(handle) // Baseline nur nach initialem Laden — nicht bei Template-/Form-Änderungen im Editor // eslint-disable-next-line react-hooks/exhaustive-deps -- editingUnit/draftPlanTemplateId bewusst ausgeschlossen }, [loading, isNew, unitId]) const formDirtyEffective = baselineReady && baselineRef.current != null && !bypassDirty && !loading && dirtySignature !== baselineRef.current const blocker = useUnsavedChangesBlocker(Boolean(formDirtyEffective && !saving)) useBeforeUnloadWhen(Boolean(formDirtyEffective && !saving)) const [exercisePickerOpen, setExercisePickerOpen] = useState(false) const [exercisePickerTarget, setExercisePickerTarget] = useState(null) const [planningPeekCtx, setPlanningPeekCtx] = useState(null) 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 [publishFrameworkOpen, setPublishFrameworkOpen] = useState(false) const [saveModuleOpen, setSaveModuleOpen] = useState(false) const goBack = useCallback(() => { goNavReturn(navigate, location, { path: PLANNING_HUB_PATH, label: 'Zurück zur Planung', }) }, [location, navigate]) const moduleSaveReturnContext = useMemo( () => buildCurrentLocationReturnContext(location, 'Zurück zur Trainingseinheit'), [location] ) const planningClubId = 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?.club_id) return null const c = Number(g.club_id) return Number.isFinite(c) ? c : null }, [groups, formData.group_id]) 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]) 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]) useEffect(() => { if (!moduleApplyOpen || !moduleApplyFilteredList.length) return if (moduleApplyFilteredList.some((m) => String(m.id) === String(moduleApplyModuleId))) return setModuleApplyModuleId(String(moduleApplyFilteredList[0].id)) }, [moduleApplyOpen, moduleApplyFilteredList, moduleApplyModuleId]) 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]) useEffect(() => { if (planningClubId == null) { setClubDirectory([]) return undefined } let cancelled = false ;(async () => { try { const d = await api.clubMembersDirectory(planningClubId) if (!cancelled) setClubDirectory(Array.isArray(d) ? d : []) } catch { if (!cancelled) setClubDirectory([]) } })() return () => { cancelled = true } }, [planningClubId]) const clubDirectoryForCo = useMemo(() => { let exclude = null const leadTrim = String(formData.lead_trainer_profile_id || '').trim() if (leadTrim) { const n = parseInt(leadTrim, 10) if (Number.isFinite(n)) exclude = n } else if (editingUnit?.effective_lead_trainer_profile_id != null) { exclude = Number(editingUnit.effective_lead_trainer_profile_id) } else { const gid = parseInt(formData.group_id || '0', 10) const g = groups.find((gr) => gr.id === gid) if (g?.trainer_id != null) exclude = Number(g.trainer_id) } return filterDirectoryExcludingLead(clubDirectory, exclude) }, [clubDirectory, formData.lead_trainer_profile_id, formData.group_id, editingUnit, groups]) const loadCatalogs = useCallback(async () => { const [groupsData, tpl] = await Promise.all([ api.listTrainingGroups({ status: 'active' }), api.listTrainingPlanTemplates(), ]) setGroups(Array.isArray(groupsData) ? groupsData : []) setPlanTemplates(Array.isArray(tpl) ? tpl : []) return Array.isArray(groupsData) ? groupsData : [] }, []) useEffect(() => { let cancelled = false async function init() { setLoading(true) try { const groupsData = await loadCatalogs() if (cancelled) return if (isNew) { const qGroup = searchParams.get('group') || '' const qDate = searchParams.get('date') || new Date().toISOString().slice(0, 10) const qTemplate = searchParams.get('template') || '' const gid = qGroup || (groupsData.length === 1 ? String(groupsData[0].id) : '') || (groupsData.find((g) => g.trainer_id === user?.id) ? String(groupsData.find((g) => g.trainer_id === user?.id).id) : '') const group = groupsData.find((g) => String(g.id) === String(gid)) setEditingUnit(null) setDraftPlanTemplateId(qTemplate) setSectionsEditMode(searchParams.get('mode') === 'debrief' ? 'debrief' : 'planning') setFormData( createEmptyTrainingUnitFormData({ groupId: gid, plannedDate: qDate, timeStart: group?.time_start?.slice(0, 5) || '', timeEnd: group?.time_end?.slice(0, 5) || '', }) ) if (qTemplate) { const tpl = await api.getTrainingPlanTemplate(parseInt(qTemplate, 10)) if (!cancelled && tpl?.sections?.length) { setFormData((fd) => ({ ...fd, sections: formSectionsFromPlanTemplateRows(tpl.sections), })) } } } else if (Number.isFinite(unitId)) { const fullUnit = await api.getTrainingUnit(unitId) if (cancelled) return setEditingUnit(fullUnit) setDraftPlanTemplateId(fullUnit.plan_template_id ? String(fullUnit.plan_template_id) : '') let sections = normalizeUnitToForm(fullUnit) sections = await enrichSectionsWithVariants(sections) if (cancelled) return setFormData(trainingUnitToFormFields(fullUnit, sections)) const modeParam = searchParams.get('mode') setSectionsEditMode( modeParam === 'debrief' || fullUnit.status === 'completed' ? 'debrief' : 'planning' ) } } catch (e) { if (!cancelled) toast.error(e.message || 'Laden fehlgeschlagen') } finally { if (!cancelled) setLoading(false) } } init() return () => { cancelled = true } }, [isNew, unitId, searchParams, loadCatalogs, user?.id, tenantClubDepKey, toast]) const updateFormField = useCallback( (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 || '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) } } return { ...prev, lead_trainer_profile_id: value, session_assistant_profile_ids: prev.session_assistant_profile_ids.filter((id) => !strip.has(id)), } }) }, [groups] ) 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 ? formSectionsFromPlanTemplateRows(tpl.sections) : [defaultSection()], })) } catch (err) { toast.error('Vorlage laden: ' + err.message) } } const reloadUnitAfterSave = useCallback( async (savedId) => { const fullUnit = await api.getTrainingUnit(savedId) const nextDraftTemplateId = fullUnit.plan_template_id ? String(fullUnit.plan_template_id) : '' setEditingUnit(fullUnit) setDraftPlanTemplateId(nextDraftTemplateId) let sections = normalizeUnitToForm(fullUnit) sections = await enrichSectionsWithVariants(sections) const nextForm = trainingUnitToFormFields(fullUnit, sections) setFormData(nextForm) baselineRef.current = trainingUnitFormSnapshot(nextForm, { editingUnit: fullUnit, draftPlanTemplateId: nextDraftTemplateId, }) setBypassDirty(false) if (!isNew && savedId !== unitId) { navigate(buildPlanUnitEditPath(savedId), { replace: true, state: location.state }) } }, [isNew, unitId, navigate, location.state] ) const handleSubmit = useCallback( async (e, { closeAfter = true } = {}) => { e?.preventDefault?.() const v = validateTrainingUnitFormForSave(formData) if (!v.ok) { toast.error(v.message) return false } setSaving(true) try { const payload = buildTrainingUnitSavePayload(formData, { editingUnit, draftPlanTemplateId, }) let savedUnit if (editingUnit) { savedUnit = await api.updateTrainingUnit(editingUnit.id, payload) } else { savedUnit = await api.createTrainingUnit(payload) } toast.success('Gespeichert.') if (closeAfter) { goBack() } else if (savedUnit?.id) { await reloadUnitAfterSave(savedUnit.id) } return true } catch (err) { toast.error('Fehler beim Speichern: ' + err.message) return false } finally { setSaving(false) } }, [formData, editingUnit, draftPlanTemplateId, toast, goBack, reloadUnitAfterSave] ) const handleUnsavedDialogSave = async () => { const ok = await handleSubmit(null, { closeAfter: false }) if (ok) blocker.proceed() } const actionConfig = useMemo( () => ({ formId: 'planning-unit-form', saving, isNew: !editingUnit, onSave: (e) => handleSubmit(e, { closeAfter: false }), onSaveAndClose: (e) => handleSubmit(e, { closeAfter: true }), onCancel: goBack, showSave: true, showSaveAndClose: true, }), [saving, editingUnit, handleSubmit, goBack] ) const hubBackPath = PLANNING_HUB_PATH const pageTitle = editingUnit ? 'Trainingseinheit bearbeiten' : 'Neue Trainingseinheit' const handleSaveAsTemplate = async (opts = {}) => { const name = window.prompt('Name für die neue Trainingsvorlage (nur Abschnitts-Gliederung):') if (!name?.trim()) return const descRaw = window.prompt('Kurzbeschreibung (optional, leer lassen zum Überspringen):') const visibility = typeof opts.visibility === 'string' && opts.visibility.trim() ? String(opts.visibility).trim().toLowerCase() : 'private' let club_id = opts.club_id != null && opts.club_id !== '' ? Number(opts.club_id) : null if (visibility === 'club') { if (!Number.isFinite(club_id) || club_id < 1) club_id = planningClubId if (!Number.isFinite(club_id) || club_id < 1) { toast.error('Bitte einen Verein wählen (Sichtbarkeit „Verein“).') return } } else { club_id = null } try { await api.createTrainingPlanTemplate({ name: name.trim(), description: descRaw?.trim() ? descRaw.trim() : null, visibility, club_id: visibility === 'club' ? club_id : null, sections: templateSectionsPayloadFromFormSections(formData.sections), }) toast.success('Vorlage gespeichert.') } catch (err) { toast.error('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 = formRef.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 : [] before = typeof placement.insertBeforeIndex === 'number' ? Math.min(Math.max(0, placement.insertBeforeIndex), items.length) : items.length } else { before = Array.isArray(secs[0]?.items) ? secs[0].items.length : 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 onModuleApplySectionIndexChange = useCallback((newIx) => { setModuleApplySectionIx(newIx) const secsNow = formRef.current?.sections ?? [] const len = Array.isArray(secsNow[newIx]?.items) ? secsNow[newIx].items.length : 0 setModuleApplyInsertSlot(`before:${len}`) }, []) const handleApplyTrainingModuleConfirm = useCallback(async () => { const mid = parseInt(moduleApplyModuleId, 10) if (!Number.isFinite(mid)) { toast.error('Bitte ein Trainingsmodul wählen.') return } let secIx = parseInt(String(moduleApplySectionIx), 10) const baseSections = formRef.current?.sections ?? [] if (!baseSections.length) return if (!Number.isFinite(secIx) || secIx < 0 || secIx >= baseSections.length) secIx = 0 const itemCap = Array.isArray(baseSections[secIx]?.items) ? baseSections[secIx].items.length : 0 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) 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, toast]) const refreshPlanningSectionMeta = useCallback(async () => { const next = await enrichSectionsWithVariants(formRef.current.sections) setFormData((prev) => ({ ...prev, sections: next })) }, []) if (loading) { return (

Laden …

) } return ( handleSubmit(e, { closeAfter: false })} onSaveAndClose={(e) => handleSubmit(e, { closeAfter: true })} draftPlanTemplateId={draftPlanTemplateId} onDraftTemplateSelect={applyTemplateFromSelect} planTemplates={planTemplates} clubDirectory={clubDirectory} clubDirectoryForCo={clubDirectoryForCo} planningClubId={planningClubId} user={user} onMetaRefresh={refreshPlanningSectionMeta} sectionsEditMode={sectionsEditMode} setSectionsEditMode={setSectionsEditMode} onSaveAsTemplate={handleSaveAsTemplate} onRequestPublishToFramework={() => editingUnit?.id && setPublishFrameworkOpen(true)} onRequestSaveAsModule={() => editingUnit?.id && setSaveModuleOpen(true)} 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 }) } /> { setModuleApplyOpen(false) setModuleApplyPlacementLocked(false) }} /> setPublishFrameworkOpen(false)} onSuccess={() => setPublishFrameworkOpen(false)} unitId={editingUnit?.id} planningModalClubId={planningClubId} returnContext={moduleSaveReturnContext} /> setSaveModuleOpen(false)} onSuccess={() => setSaveModuleOpen(false)} unitId={editingUnit?.id} planningModalClubId={planningClubId} returnContext={moduleSaveReturnContext} /> { setExercisePickerOpen(false) setExercisePickerTarget(null) }} onSelectExercises={async (picked) => { if (!exercisePickerTarget || !picked?.length) return const rows = [] for (const ex of picked) { const row = await hydrateExercisePlanningRow(ex) if (row) rows.push(row) } if (!rows.length) return const { sIdx, iIdx, insertBeforeIndex } = exercisePickerTarget setFormData((prev) => ({ ...prev, sections: prev.sections.map((s, si) => { if (si !== sIdx) return s const items = [...(s.items || [])] if (typeof iIdx === 'number') { const [first, ...tail] = rows items[iIdx] = { ...items[iIdx], ...first, item_type: 'exercise' } if (tail.length) items.splice(iIdx + 1, 0, ...tail) return { ...s, items } } const at = Math.min( typeof insertBeforeIndex === 'number' ? insertBeforeIndex : items.length, items.length ) items.splice(at, 0, ...rows) return { ...s, items } }), })) setExercisePickerOpen(false) setExercisePickerTarget(null) }} /> setPlanningPeekCtx(null)} /> setBypassDirty(true)} detail="Du hast ungespeicherte Änderungen vorgenommen. Möchtest du die Seite wirklich verlassen?" /> ) }