import api from './api' import { sortCombinationSlotsForDisplay } from '../constants/combinationArchetypes' export function defaultSection(title = 'Hauptteil') { return { title, guidance_notes: '', items: [] } } function normalizeCatalogMethodProfile(cp) { if (cp && typeof cp === 'object' && !Array.isArray(cp)) return { ...cp } return {} } /** NULL = Planung folgt Katalogprofil der Übung */ function normalizePlanningMethodProfile(pm) { if (pm == null) return null if (typeof pm === 'object' && !Array.isArray(pm)) return { ...pm } return null } export function exerciseRow() { return { item_type: 'exercise', exercise_id: '', exercise_variant_id: '', exercise_kind: 'simple', exercise_title: '', variants: [], planned_duration_min: '', actual_duration_min: '', notes: '', modifications: '', source_training_module_id: '', source_module_title: '', catalog_method_archetype: '', catalog_method_profile: {}, planning_method_profile: null, } } export async function hydrateExercisePlanningRow(exercise) { let variants = Array.isArray(exercise?.variants) ? exercise.variants : [] let title = exercise?.title || '' let exerciseKind = exercise?.exercise_kind const id = exercise?.id if (!id) return null let meta = {} let full async function ensureFull() { if (full !== undefined) return full try { full = await api.getExercise(id) } catch { full = null } return full } if (!variants.length) { await ensureFull() if (full) { variants = Array.isArray(full.variants) ? full.variants : [] title = full.title || title if (exerciseKind == null) exerciseKind = full.exercise_kind 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', catalog_method_archetype: typeof full.method_archetype === 'string' ? full.method_archetype.trim() : '', catalog_method_profile: normalizeCatalogMethodProfile(full.method_profile), } } } 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 || exerciseKind == null ) { await ensureFull() if (full) { 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' if (exerciseKind == null) exerciseKind = full.exercise_kind if (!variants.length) variants = Array.isArray(full.variants) ? full.variants : [] } } 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.exercise_kind = String(exerciseKind || 'simple').toLowerCase().trim() === 'combination' ? 'combination' : 'simple' if (row.exercise_kind === 'combination') { row.variants = [] row.exercise_variant_id = '' } else { row.variants = variants } Object.assign(row, meta) if (row.exercise_kind === 'combination') { if (full === undefined) await ensureFull() if (full) { row.catalog_method_archetype = typeof full.method_archetype === 'string' ? full.method_archetype.trim() : '' row.catalog_method_profile = normalizeCatalogMethodProfile(full.method_profile) row.combination_slots = Array.isArray(full.combination_slots) ? full.combination_slots : [] } } row.planning_method_profile = null 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) const ek = String(it.exercise_kind || 'simple').toLowerCase().trim() const isCombo = ek === 'combination' return { item_type: 'exercise', exercise_id: it.exercise_id, exercise_kind: isCombo ? 'combination' : 'simple', exercise_variant_id: isCombo ? '' : 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 ?? '', catalog_method_archetype: String(it.catalog_method_archetype ?? '').trim(), catalog_method_profile: normalizeCatalogMethodProfile(it.catalog_method_profile), planning_method_profile: normalizePlanningMethodProfile(it.planning_method_profile), ...(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) => { const ek = String(ex.exercise_kind || 'simple').toLowerCase().trim() const isCombo = ek === 'combination' return { item_type: 'exercise', exercise_kind: ek, exercise_id: ex.exercise_id, exercise_variant_id: isCombo ? '' : (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 ?? '', catalog_method_archetype: String(ex.catalog_method_archetype ?? '').trim(), catalog_method_profile: normalizeCatalogMethodProfile(ex.catalog_method_profile), planning_method_profile: normalizePlanningMethodProfile(ex.planning_method_profile), } }), }, ] } 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) const ek = String(ex.exercise_kind || 'simple').toLowerCase().trim() cache.set(id, { title: ex.title || '', exercise_kind: ek, 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', method_archetype: typeof ex.method_archetype === 'string' ? ex.method_archetype.trim() : '', method_profile: normalizeCatalogMethodProfile(ex.method_profile), combination_slots: ek === 'combination' && Array.isArray(ex.combination_slots) ? ex.combination_slots : [], }) } catch { cache.set(id, { title: '', exercise_kind: 'simple', variants: [], visibility: 'private', club_id: null, created_by: null, status: 'draft', method_archetype: '', method_profile: {}, combination_slots: [], }) } }) ) 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 const ek = String(c.exercise_kind || 'simple').toLowerCase().trim() const isCombo = ek === 'combination' const itemCatalog = normalizeCatalogMethodProfile(it.catalog_method_profile) const catalog_method_profile = Object.keys(itemCatalog).length > 0 ? itemCatalog : normalizeCatalogMethodProfile(c.method_profile) const rowArche = String(it.catalog_method_archetype ?? '').trim() const catalog_method_archetype = rowArche || String(c.method_archetype ?? '').trim() return { ...it, catalog_method_archetype, catalog_method_profile, planning_method_profile: normalizePlanningMethodProfile(it.planning_method_profile), exercise_kind: isCombo ? 'combination' : 'simple', exercise_title: it.exercise_title || c.title, exercise_variant_id: isCombo ? '' : it.exercise_variant_id, variants: isCombo ? [] : 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, ...(isCombo ? { combination_slots: c.combination_slots || [] } : {}), } }), })) } /** * Outline für CombinationMethodProfileEditor: pro‑Slot‑Zeiten nur sichtbar, wenn Stationen übergeben werden. */ export function comboSlotsOutlineForProfileEditor(combinationSlots) { if (!Array.isArray(combinationSlots) || combinationSlots.length === 0) return null const sorted = sortCombinationSlotsForDisplay(combinationSlots) return sorted.map((s, i) => { const rawIx = s.slot_index const si = rawIx === '' || rawIx == null ? null : typeof rawIx === 'number' ? rawIx : parseInt(String(rawIx), 10) return { slot_index: Number.isFinite(si) ? si : i, title: (s.title != null ? String(s.title) : '').trim(), } }) } 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 isCombo = String(it.exercise_kind || 'simple').toLowerCase().trim() === 'combination' const vid = isCombo ? null : 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 (isCombo) { const pmp = it.planning_method_profile if (pmp != null && typeof pmp === 'object' && !Array.isArray(pmp)) { rowEx.planning_method_profile = { ...pmp } } } 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 ( hydrated.exercise_kind !== 'combination' && 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) }