2026-05-05 13:39:30 +02:00
import api from './api'
2026-05-13 09:58:59 +02:00
import { sortCombinationSlotsForDisplay } from '../constants/combinationArchetypes'
2026-05-05 13:39:30 +02:00
export function defaultSection ( title = 'Hauptteil' ) {
return { title , guidance _notes : '' , items : [ ] }
}
2026-05-13 07:21:33 +02:00
function normalizeCatalogMethodProfile ( cp ) {
if ( cp && typeof cp === 'object' && ! Array . isArray ( cp ) ) return { ... cp }
return { }
}
2026-05-13 15:28:37 +02:00
/** NULL = Planung folgt Katalogprofil der Übung (Reihenfolge beibehalten: zuerst String-JSON auflösen). */
2026-05-13 07:21:33 +02:00
function normalizePlanningMethodProfile ( pm ) {
if ( pm == null ) return null
2026-05-13 15:28:37 +02:00
if ( typeof pm === 'string' ) {
const t = pm . trim ( )
if ( ! t || t === 'null' ) return null
try {
const p = JSON . parse ( t )
if ( p && typeof p === 'object' && ! Array . isArray ( p ) ) return { ... p }
return null
} catch {
return null
}
}
2026-05-13 07:21:33 +02:00
if ( typeof pm === 'object' && ! Array . isArray ( pm ) ) return { ... pm }
return null
}
2026-05-13 15:28:37 +02:00
/** Reines JSON für PUT /training-units (vermeidet nicht serialisierbare Werte → 500). */
export function cloneJsonSerializablePlanningProfile ( obj ) {
if ( obj == null || typeof obj !== 'object' || Array . isArray ( obj ) ) return null
try {
return JSON . parse ( JSON . stringify ( obj ) )
} catch {
return { }
}
}
2026-05-05 13:39:30 +02:00
export function exerciseRow ( ) {
return {
item _type : 'exercise' ,
exercise _id : '' ,
exercise _variant _id : '' ,
2026-05-13 06:30:53 +02:00
exercise _kind : 'simple' ,
2026-05-05 13:39:30 +02:00
exercise _title : '' ,
variants : [ ] ,
planned _duration _min : '' ,
actual _duration _min : '' ,
notes : '' ,
modifications : '' ,
2026-05-12 21:53:41 +02:00
source _training _module _id : '' ,
source _module _title : '' ,
2026-05-13 07:21:33 +02:00
catalog _method _archetype : '' ,
catalog _method _profile : { } ,
planning _method _profile : null ,
2026-05-05 13:39:30 +02:00
}
}
2026-05-05 14:47:38 +02:00
export async function hydrateExercisePlanningRow ( exercise ) {
let variants = Array . isArray ( exercise ? . variants ) ? exercise . variants : [ ]
let title = exercise ? . title || ''
2026-05-13 06:30:53 +02:00
let exerciseKind = exercise ? . exercise _kind
2026-05-05 14:47:38 +02:00
const id = exercise ? . id
if ( ! id ) return null
2026-05-07 10:09:25 +02:00
let meta = { }
2026-05-13 06:30:53 +02:00
2026-05-13 07:21:33 +02:00
let full
async function ensureFull ( ) {
if ( full !== undefined ) return full
2026-05-05 14:47:38 +02:00
try {
2026-05-13 07:21:33 +02:00
full = await api . getExercise ( id )
2026-05-13 06:30:53 +02:00
} catch {
2026-05-13 07:21:33 +02:00
full = null
2026-05-13 06:30:53 +02:00
}
2026-05-13 07:21:33 +02:00
return full
2026-05-13 06:30:53 +02:00
}
if ( ! variants . length ) {
2026-05-13 07:21:33 +02:00
await ensureFull ( )
2026-05-13 06:30:53 +02:00
if ( full ) {
variants = Array . isArray ( full . variants ) ? full . variants : [ ]
title = full . title || title
if ( exerciseKind == null ) exerciseKind = full . exercise _kind
2026-05-07 10:09:25 +02:00
meta = {
2026-05-13 06:30:53 +02:00
exercise _visibility : full . visibility || 'private' ,
exercise _club _id : full . club _id ? ? null ,
exercise _created _by : full . created _by ? ? null ,
exercise _status : full . status || 'draft' ,
2026-05-13 07:21:33 +02:00
catalog _method _archetype : typeof full . method _archetype === 'string' ? full . method _archetype . trim ( ) : '' ,
catalog _method _profile : normalizeCatalogMethodProfile ( full . method _profile ) ,
2026-05-07 10:09:25 +02:00
}
2026-05-05 14:47:38 +02:00
}
2026-05-07 10:09:25 +02:00
} 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 ,
}
2026-05-13 06:30:53 +02:00
if (
meta . exercise _visibility == null ||
meta . exercise _created _by == null ||
exerciseKind == null
) {
2026-05-13 07:21:33 +02:00
await ensureFull ( )
2026-05-13 06:30:53 +02:00
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 : [ ]
2026-05-07 10:09:25 +02:00
}
}
meta . exercise _visibility = meta . exercise _visibility || 'private'
meta . exercise _status = meta . exercise _status || 'draft'
2026-05-05 14:47:38 +02:00
}
2026-05-13 06:30:53 +02:00
2026-05-05 14:47:38 +02:00
const row = exerciseRow ( )
row . exercise _id = id
row . exercise _variant _id = ''
row . exercise _title = title
2026-05-13 06:30:53 +02:00
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
}
2026-05-07 10:09:25 +02:00
Object . assign ( row , meta )
2026-05-13 07:21:33 +02:00
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 )
2026-05-13 09:58:59 +02:00
row . combination _slots = Array . isArray ( full . combination _slots ) ? full . combination _slots : [ ]
2026-05-13 07:21:33 +02:00
}
}
row . planning _method _profile = null
2026-05-05 14:47:38 +02:00
return row
}
2026-05-05 13:39:30 +02:00
export function noteRow ( ) {
2026-05-12 21:53:41 +02:00
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
2026-05-05 13:39:30 +02:00
}
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' ) {
2026-05-12 21:53:41 +02:00
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
2026-05-05 13:39:30 +02:00
}
2026-05-12 21:53:41 +02:00
const smEx = parseOptionalSourceTrainingModuleIdForPayload ( it . source _training _module _id )
2026-05-13 06:30:53 +02:00
const ek = String ( it . exercise _kind || 'simple' ) . toLowerCase ( ) . trim ( )
const isCombo = ek === 'combination'
2026-05-05 13:39:30 +02:00
return {
item _type : 'exercise' ,
exercise _id : it . exercise _id ,
2026-05-13 06:30:53 +02:00
exercise _kind : isCombo ? 'combination' : 'simple' ,
exercise _variant _id : isCombo ? '' : it . exercise _variant _id ? ? '' ,
2026-05-05 13:39:30 +02:00
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 ? ? '' ,
2026-05-13 07:21:33 +02:00
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 ) ,
2026-05-12 21:53:41 +02:00
... ( smEx != null
? {
source _training _module _id : smEx ,
source _module _title : (
it . source _module _title ||
it . source _training _module _title ||
''
) . trim ( ) ,
}
: { } ) ,
2026-05-05 13:39:30 +02:00
}
} ) ,
} ) )
}
if ( fullUnit . exercises && fullUnit . exercises . length ) {
return [
{
title : 'Übungen' ,
guidance _notes : '' ,
2026-05-13 06:30:53 +02:00
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 ? ? '' ,
2026-05-13 07:21:33 +02:00
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 ) ,
2026-05-13 06:30:53 +02:00
}
} ) ,
2026-05-05 13:39:30 +02:00
} ,
]
}
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 )
2026-05-13 09:58:59 +02:00
const ek = String ( ex . exercise _kind || 'simple' ) . toLowerCase ( ) . trim ( )
2026-05-05 13:39:30 +02:00
cache . set ( id , {
title : ex . title || '' ,
2026-05-13 09:58:59 +02:00
exercise _kind : ek ,
2026-05-05 13:39:30 +02:00
variants : Array . isArray ( ex . variants ) ? ex . variants : [ ] ,
2026-05-07 10:09:25 +02:00
visibility : ex . visibility || 'private' ,
club _id : ex . club _id ? ? null ,
created _by : ex . created _by ? ? null ,
status : ex . status || 'draft' ,
2026-05-13 07:21:33 +02:00
method _archetype : typeof ex . method _archetype === 'string' ? ex . method _archetype . trim ( ) : '' ,
method _profile : normalizeCatalogMethodProfile ( ex . method _profile ) ,
2026-05-13 09:58:59 +02:00
combination _slots : ek === 'combination' && Array . isArray ( ex . combination _slots ) ? ex . combination _slots : [ ] ,
2026-05-05 13:39:30 +02:00
} )
} catch {
2026-05-07 10:09:25 +02:00
cache . set ( id , {
title : '' ,
2026-05-13 06:30:53 +02:00
exercise _kind : 'simple' ,
2026-05-07 10:09:25 +02:00
variants : [ ] ,
visibility : 'private' ,
club _id : null ,
created _by : null ,
status : 'draft' ,
2026-05-13 07:21:33 +02:00
method _archetype : '' ,
method _profile : { } ,
2026-05-13 09:58:59 +02:00
combination _slots : [ ] ,
2026-05-07 10:09:25 +02:00
} )
2026-05-05 13:39:30 +02:00
}
} )
)
2026-05-13 14:11:53 +02:00
const titleById = new Map ( )
for ( const id of unique ) {
const row = cache . get ( id )
const t = ( row ? . title || '' ) . trim ( )
if ( t ) titleById . set ( Number ( id ) , t )
}
const comboCandidateExtra = new Set ( )
for ( const id of unique ) {
const row = cache . get ( id )
if ( String ( row ? . exercise _kind || '' ) . toLowerCase ( ) . trim ( ) !== 'combination' ) continue
for ( const slot of row . combination _slots || [ ] ) {
for ( const raw of slot . candidate _exercise _ids || [ ] ) {
const n = typeof raw === 'number' ? raw : parseInt ( String ( raw ) , 10 )
if ( Number . isFinite ( n ) && ! titleById . has ( n ) ) comboCandidateExtra . add ( n )
}
}
}
await Promise . all (
[ ... comboCandidateExtra ] . map ( async ( cid ) => {
try {
const ex = await api . getExercise ( cid )
titleById . set ( cid , ( ( ex . title || '' ) . trim ( ) || ` Übung # ${ cid } ` ) )
} catch {
titleById . set ( cid , ` Übung # ${ cid } ` )
}
} ) ,
)
function comboMemberTitleByIdForSlots ( slots ) {
const o = { }
for ( const slot of slots || [ ] ) {
for ( const raw of slot . candidate _exercise _ids || [ ] ) {
const n = typeof raw === 'number' ? raw : parseInt ( String ( raw ) , 10 )
if ( ! Number . isFinite ( n ) ) continue
const key = String ( n )
if ( ! o [ key ] ) o [ key ] = titleById . get ( n ) || ` Übung # ${ n } `
}
}
return o
}
2026-05-05 13:39:30 +02:00
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
2026-05-13 06:30:53 +02:00
const ek = String ( c . exercise _kind || 'simple' ) . toLowerCase ( ) . trim ( )
const isCombo = ek === 'combination'
2026-05-13 07:21:33 +02:00
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 ( )
2026-05-05 13:39:30 +02:00
return {
... it ,
2026-05-13 07:21:33 +02:00
catalog _method _archetype ,
catalog _method _profile ,
planning _method _profile : normalizePlanningMethodProfile ( it . planning _method _profile ) ,
2026-05-13 06:30:53 +02:00
exercise _kind : isCombo ? 'combination' : 'simple' ,
2026-05-05 13:39:30 +02:00
exercise _title : it . exercise _title || c . title ,
2026-05-13 06:30:53 +02:00
exercise _variant _id : isCombo ? '' : it . exercise _variant _id ,
2026-05-05 13:39:30 +02:00
variants :
2026-05-13 06:30:53 +02:00
isCombo ? [ ] : Array . isArray ( it . variants ) && it . variants . length > 0 ? it . variants : c . variants ,
2026-05-07 10:09:25 +02:00
exercise _visibility : c . visibility ,
exercise _club _id : c . club _id ,
exercise _created _by : c . created _by ,
exercise _status : c . status ,
2026-05-13 14:11:53 +02:00
... ( isCombo ? { combination _slots : c . combination _slots || [ ] , combo _member _title _by _id : comboMemberTitleByIdForSlots ( c . combination _slots || [ ] ) } : { } ) ,
2026-05-05 13:39:30 +02:00
}
} ) ,
} ) )
}
2026-05-13 09:58:59 +02:00
/ * *
* 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 ( ) ,
}
} )
}
2026-05-05 13:39:30 +02:00
export function parseMin ( v ) {
if ( v === '' || v === null || v === undefined ) return null
const n = parseInt ( String ( v ) , 10 )
return Number . isFinite ( n ) ? n : null
}
2026-05-14 22:24:55 +02:00
/** PUT /api/training-units/:id `sections` — flache order_index pro Einheit; Parallelität bricht am DB-UNIQUE (training_unit_id, order_index) bis Migration. */
2026-05-05 13:39:30 +02:00
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' ) {
2026-05-12 21:53:41 +02:00
const sm = parseOptionalSourceTrainingModuleIdForPayload ( it . source _training _module _id )
const row = {
2026-05-05 13:39:30 +02:00
item _type : 'note' ,
order _index : ii ,
note _body : it . note _body ? ? '' ,
}
2026-05-12 21:53:41 +02:00
if ( sm != null ) row . source _training _module _id = sm
return row
2026-05-05 13:39:30 +02:00
}
if ( it . exercise _id === '' || it . exercise _id == null || Number . isNaN ( Number ( it . exercise _id ) ) ) {
return null
}
2026-05-13 06:30:53 +02:00
const isCombo = String ( it . exercise _kind || 'simple' ) . toLowerCase ( ) . trim ( ) === 'combination'
const vid = isCombo ? null : it . exercise _variant _id
2026-05-12 21:53:41 +02:00
const smEx = parseOptionalSourceTrainingModuleIdForPayload ( it . source _training _module _id )
const rowEx = {
2026-05-05 13:39:30 +02:00
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 ,
}
2026-05-13 07:21:33 +02:00
if ( isCombo ) {
const pmp = it . planning _method _profile
if ( pmp != null && typeof pmp === 'object' && ! Array . isArray ( pmp ) ) {
2026-05-13 15:28:37 +02:00
const cleaned = cloneJsonSerializablePlanningProfile ( pmp )
if ( cleaned && Object . keys ( cleaned ) . length > 0 ) {
rowEx . planning _method _profile = cleaned
}
2026-05-13 07:21:33 +02:00
}
}
2026-05-12 21:53:41 +02:00
if ( smEx != null ) rowEx . source _training _module _id = smEx
return rowEx
2026-05-05 13:39:30 +02:00
} )
. filter ( Boolean ) ,
} ) )
}
2026-05-12 21:53:41 +02:00
/** 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
2026-05-13 06:30:53 +02:00
if (
hydrated . exercise _kind !== 'combination' &&
mi . exercise _variant _id
) {
hydrated . exercise _variant _id = String ( mi . exercise _variant _id )
}
2026-05-12 21:53:41 +02:00
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 ) )
}
2026-05-05 13:39:30 +02:00
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 )
}