/** * Hilfen für Trainingsplan-Ansicht, Coach-Modus und API-Payload für PUT /training-units/:id. */ export function sortedSections(unit) { const raw = unit?.sections if (!Array.isArray(raw)) return [] return [...raw].sort((a, b) => (a.order_index ?? 0) - (b.order_index ?? 0)) } export function sortedItems(sec) { const raw = sec?.items if (!Array.isArray(raw)) return [] return [...raw].sort((a, b) => (a.order_index ?? 0) - (b.order_index ?? 0)) } export function itemStableKey(it, secOrder, ix) { if (it && it.id != null) return String(it.id) return `${secOrder}-${it?.item_type || 'row'}-${ix}` } /** Flache Reihenfolge wie auf der Matte: alle Notizen und Übungen nacheinander. */ export function flattenPlanTimeline(unit) { const list = [] sortedSections(unit).forEach((sec, si) => { const secOrder = sec.order_index ?? si sortedItems(sec).forEach((item, ii) => { list.push({ si, ii, secOrder, flatIndex: list.length, sec, item, }) }) }) return list } export function summarizeTimelineEntry({ item }) { if (!item) return '' if (item.item_type === 'note') { const t = String(item.note_body || '').trim() return t.length > 72 ? `${t.slice(0, 70)}…` : t || 'Notiz' } const title = item.exercise_title || (item.exercise_id ? `Übung #${item.exercise_id}` : 'Übung') const vn = item.exercise_variant_name ? ` · ${item.exercise_variant_name}` : '' return `${title}${vn}` } /** Payload für PUT (schließt bestehendes Unit mit optionalen Overrides pro Abschnitt-Item-ID ab). */ export function sectionsToPutPayload(unit, durationOverridesByItemId = {}) { return sortedSections(unit).map((sec, si) => ({ order_index: sec.order_index ?? si, title: ((sec.title || '').trim() || 'Abschnitt'), guidance_notes: sec.guidance_notes?.trim() ? sec.guidance_notes.trim() : null, ...(sec.source_template_section_id != null ? { source_template_section_id: sec.source_template_section_id } : {}), items: sortedItems(sec) .map((it, ii) => { if (it.item_type === 'note') { return { item_type: 'note', order_index: it.order_index ?? ii, note_body: it.note_body ?? '', } } const eid = it.exercise_id if (eid === '' || eid == null || Number.isNaN(Number(eid))) { return null } const isCombo = String(it.exercise_kind || 'simple').toLowerCase().trim() === 'combination' const vid = isCombo ? null : it.exercise_variant_id let actual = durationOverridesByItemId[String(it.id)]?.actual_duration_min ?? it.actual_duration_min if (actual === '' || actual === undefined) actual = null else actual = typeof actual === 'number' ? actual : parseInt(String(actual), 10) if (actual !== null && !Number.isFinite(actual)) actual = null const row = { item_type: 'exercise', order_index: it.order_index ?? ii, exercise_id: parseInt(String(eid), 10), exercise_variant_id: vid !== '' && vid != null && !Number.isNaN(Number(vid)) ? parseInt(String(vid), 10) : null, planned_duration_min: coalescePositiveInt(it.planned_duration_min), actual_duration_min: actual, notes: trimOrNull(it.notes), modifications: trimOrNull(it.modifications), } if (isCombo) { const pmp = it.planning_method_profile if (pmp != null && typeof pmp === 'object' && !Array.isArray(pmp)) { row.planning_method_profile = { ...pmp } } } return row }) .filter(Boolean), })) } function coalescePositiveInt(v) { if (v === '' || v === null || v === undefined) return null const n = parseInt(String(v), 10) return Number.isFinite(n) ? n : null } function trimOrNull(v) { const s = v != null ? String(v).trim() : '' return s ? s : null }