import api from './api' export function defaultSection(title = 'Hauptteil') { return { title, guidance_notes: '', items: [] } } export function exerciseRow() { return { item_type: 'exercise', exercise_id: '', exercise_variant_id: '', exercise_title: '', variants: [], planned_duration_min: '', actual_duration_min: '', notes: '', modifications: '', source_training_module_id: '', source_module_title: '', } } export async function hydrateExercisePlanningRow(exercise) { let variants = Array.isArray(exercise?.variants) ? exercise.variants : [] let title = exercise?.title || '' const id = exercise?.id if (!id) return null let meta = {} if (!variants.length) { try { const full = await api.getExercise(id) variants = Array.isArray(full?.variants) ? full.variants : [] title = full?.title || title meta = { exercise_visibility: full?.visibility || 'private', exercise_club_id: full?.club_id ?? null, exercise_created_by: full?.created_by ?? null, exercise_status: full?.status || 'draft', } } catch { variants = [] } } else { meta = { exercise_visibility: exercise?.visibility ?? null, exercise_club_id: exercise?.club_id ?? null, exercise_created_by: exercise?.created_by ?? null, exercise_status: exercise?.status ?? null, } if (meta.exercise_visibility == null || meta.exercise_created_by == null) { try { const full = await api.getExercise(id) if (meta.exercise_visibility == null) meta.exercise_visibility = full?.visibility || 'private' if (meta.exercise_club_id == null) meta.exercise_club_id = full?.club_id ?? null if (meta.exercise_created_by == null) meta.exercise_created_by = full?.created_by ?? null if (meta.exercise_status == null) meta.exercise_status = full?.status || 'draft' } catch { /* keep partial meta */ } } meta.exercise_visibility = meta.exercise_visibility || 'private' meta.exercise_status = meta.exercise_status || 'draft' } const row = exerciseRow() row.exercise_id = id row.exercise_variant_id = '' row.exercise_title = title row.variants = variants Object.assign(row, meta) return row } export function noteRow() { return { item_type: 'note', note_body: '', source_training_module_id: '', source_module_title: '' } } /** Zur Serialisierung in die Planungs-API (persistente Modul-Herkunft). */ function parseOptionalSourceTrainingModuleIdForPayload(v) { if (v === null || v === undefined || v === '') return null const n = typeof v === 'number' ? v : parseInt(String(v).trim(), 10) return Number.isFinite(n) && n >= 1 ? n : null } export function normalizeUnitToForm(fullUnit) { if (fullUnit.sections && fullUnit.sections.length) { return fullUnit.sections.map((sec) => ({ title: sec.title, guidance_notes: sec.guidance_notes || '', items: (sec.items || []).map((it) => { if (it.item_type === 'note') { const sm = parseOptionalSourceTrainingModuleIdForPayload(it.source_training_module_id) const rowNote = { item_type: 'note', note_body: it.note_body || '', source_training_module_id: '', source_module_title: '', } if (sm != null) { rowNote.source_training_module_id = sm rowNote.source_module_title = ( it.source_module_title || it.source_training_module_title || '' ).trim() } return rowNote } const smEx = parseOptionalSourceTrainingModuleIdForPayload(it.source_training_module_id) return { item_type: 'exercise', exercise_id: it.exercise_id, exercise_variant_id: it.exercise_variant_id ?? '', exercise_title: it.exercise_title || '', variants: [], planned_duration_min: it.planned_duration_min !== null && it.planned_duration_min !== undefined ? String(it.planned_duration_min) : '', actual_duration_min: it.actual_duration_min !== null && it.actual_duration_min !== undefined ? String(it.actual_duration_min) : '', notes: it.notes ?? '', modifications: it.modifications ?? '', ...(smEx != null ? { source_training_module_id: smEx, source_module_title: ( it.source_module_title || it.source_training_module_title || '' ).trim(), } : {}), } }), })) } if (fullUnit.exercises && fullUnit.exercises.length) { return [ { title: 'Übungen', guidance_notes: '', items: fullUnit.exercises.map((ex) => ({ item_type: 'exercise', exercise_id: ex.exercise_id, exercise_variant_id: ex.exercise_variant_id ?? '', exercise_title: ex.exercise_title || '', variants: [], planned_duration_min: ex.planned_duration_min !== null && ex.planned_duration_min !== undefined ? String(ex.planned_duration_min) : '', actual_duration_min: ex.actual_duration_min !== null && ex.actual_duration_min !== undefined ? String(ex.actual_duration_min) : '', notes: ex.notes ?? '', modifications: ex.modifications ?? '', })), }, ] } return [defaultSection()] } export async function enrichSectionsWithVariants(sections) { if (!sections?.length) return sections const ids = [] for (const sec of sections) { for (const it of sec.items || []) { if (it.item_type === 'note') continue if (it.exercise_id) ids.push(it.exercise_id) } } const unique = [...new Set(ids)] const cache = new Map() await Promise.all( unique.map(async (id) => { try { const ex = await api.getExercise(id) cache.set(id, { title: ex.title || '', variants: Array.isArray(ex.variants) ? ex.variants : [], visibility: ex.visibility || 'private', club_id: ex.club_id ?? null, created_by: ex.created_by ?? null, status: ex.status || 'draft', }) } catch { cache.set(id, { title: '', variants: [], visibility: 'private', club_id: null, created_by: null, status: 'draft', }) } }) ) return sections.map((sec) => ({ ...sec, items: (sec.items || []).map((it) => { if (it.item_type === 'note') return it if (!it.exercise_id) return it const c = cache.get(it.exercise_id) if (!c) return it return { ...it, exercise_title: it.exercise_title || c.title, variants: Array.isArray(it.variants) && it.variants.length > 0 ? it.variants : c.variants, exercise_visibility: c.visibility, exercise_club_id: c.club_id, exercise_created_by: c.created_by, exercise_status: c.status, } }), })) } export function parseMin(v) { if (v === '' || v === null || v === undefined) return null const n = parseInt(String(v), 10) return Number.isFinite(n) ? n : null } export function buildSectionsPayload(sections) { return sections.map((sec, si) => ({ order_index: si, title: (sec.title || '').trim() || 'Abschnitt', guidance_notes: sec.guidance_notes?.trim() ? sec.guidance_notes.trim() : null, items: (sec.items || []) .map((it, ii) => { if (it.item_type === 'note') { const sm = parseOptionalSourceTrainingModuleIdForPayload(it.source_training_module_id) const row = { item_type: 'note', order_index: ii, note_body: it.note_body ?? '', } if (sm != null) row.source_training_module_id = sm return row } if (it.exercise_id === '' || it.exercise_id == null || Number.isNaN(Number(it.exercise_id))) { return null } const vid = it.exercise_variant_id const smEx = parseOptionalSourceTrainingModuleIdForPayload(it.source_training_module_id) const rowEx = { item_type: 'exercise', order_index: ii, exercise_id: parseInt(it.exercise_id, 10), exercise_variant_id: vid !== '' && vid != null && !Number.isNaN(Number(vid)) ? parseInt(vid, 10) : null, planned_duration_min: parseMin(it.planned_duration_min), actual_duration_min: parseMin(it.actual_duration_min), notes: it.notes?.trim() ? it.notes.trim() : null, modifications: it.modifications?.trim() ? it.modifications.trim() : null, } if (smEx != null) rowEx.source_training_module_id = smEx return rowEx }) .filter(Boolean), })) } /** Fügt die Positionen eines Moduls in lokale Abschnitte ein (wie eine Übung, ohne Zwischenspeichern der Einheit). */ export async function insertTrainingModuleIntoPlanningSections({ sections, moduleDetail, sectionIndex, insertBeforeItemIndex, }) { const secIx = typeof sectionIndex === 'number' ? sectionIndex : parseInt(String(sectionIndex), 10) if ( !Array.isArray(sections) || !Number.isFinite(secIx) || secIx < 0 || secIx >= sections.length || !moduleDetail || typeof moduleDetail !== 'object' ) { return sections } const prev = [...(sections[secIx].items || [])] let beforeIx if (insertBeforeItemIndex === null || insertBeforeItemIndex === undefined || insertBeforeItemIndex === 'end') { beforeIx = prev.length } else if (insertBeforeItemIndex === 'start') { beforeIx = 0 } else { const n = typeof insertBeforeItemIndex === 'number' ? insertBeforeItemIndex : parseInt(String(insertBeforeItemIndex), 10) beforeIx = Number.isFinite(n) ? Math.min(Math.max(n, 0), prev.length) : prev.length } const midRaw = moduleDetail.id const midNum = typeof midRaw === 'number' ? midRaw : parseInt(String(midRaw), 10) if (!Number.isFinite(midNum) || midNum < 1) return sections const modTitle = (moduleDetail.title || '').trim() || `Modul #${midNum}` const modItems = [...(moduleDetail.items || [])].sort( (a, b) => (a.order_index ?? 0) - (b.order_index ?? 0) ) const appendRows = [] for (const mi of modItems) { if (mi.item_type === 'note') { appendRows.push({ item_type: 'note', note_body: mi.note_body || '', source_training_module_id: midNum, source_module_title: modTitle, }) continue } if (!mi.exercise_id) continue const hydrated = await hydrateExercisePlanningRow({ id: mi.exercise_id }) if (!hydrated) continue hydrated.source_training_module_id = midNum hydrated.source_module_title = modTitle if (mi.exercise_variant_id) hydrated.exercise_variant_id = String(mi.exercise_variant_id) hydrated.planned_duration_min = mi.planned_duration_min !== null && mi.planned_duration_min !== undefined ? String(mi.planned_duration_min) : '' hydrated.notes = mi.notes ?? '' appendRows.push(hydrated) } const mergedItems = [...prev.slice(0, beforeIx), ...appendRows, ...prev.slice(beforeIx)] return sections.map((sec, idx) => (idx === secIx ? { ...sec, items: mergedItems } : sec)) } export function sectionPlannedMinutes(sec) { return (sec.items || []).reduce((sum, it) => { if (it.item_type !== 'exercise') return sum const m = parseMin(it.planned_duration_min) return sum + (m || 0) }, 0) }