feat(version): bump to 0.8.103 and enhance planning method profile integration
All checks were successful
Deploy Development / deploy (push) Successful in 38s
Test Suite / pytest-backend (push) Successful in 35s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 11s
Test Suite / playwright-tests (push) Successful in 59s

- Updated app version to 0.8.103, reflecting recent enhancements in training planning.
- Incremented database schema version to 20260512057, ensuring compatibility with new features.
- Introduced optional `planning_method_profile` for combination exercises, allowing for detailed planning and coaching support.
- Enhanced frontend components to manage and display planning method profiles effectively in the Training Unit Sections Editor and ExerciseFullContent.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Lars 2026-05-13 07:21:33 +02:00
parent 12fd3926b2
commit 4e654e50c0
9 changed files with 233 additions and 17 deletions

View File

@ -0,0 +1,8 @@
-- 057: Terminspezifisches Ablaufprofil fuer Kombinationsuebungen in der Planung
-- NULL = method_profile vom Katalog (exercises) verwenden; sonst dieser JSONB-Stand gilt fuer diese Platzierung.
ALTER TABLE training_unit_section_items
ADD COLUMN IF NOT EXISTS planning_method_profile JSONB NULL;
COMMENT ON COLUMN training_unit_section_items.planning_method_profile IS
'Snapshots des Ablaufprofils fuer diese Einheit/Zeile; NULL = exercises.method_profile.';

View File

@ -407,6 +407,18 @@ def _normalize_assistant_trainer_profile_ids(
) )
return uniq return uniq
def _normalize_planning_method_profile_payload(raw) -> Optional[Dict[str, Any]]:
"""None = Katalog wirksam; Dict = Snapshot fuer diese Platzierung."""
if raw is None:
return None
if isinstance(raw, dict):
return dict(raw)
raise HTTPException(
status_code=400,
detail="planning_method_profile muss ein JSON-Objekt oder null sein",
)
# Nachverfolgbarkeit: Übernahmen aus Rahmenprogramm über origin_framework_slot_id # Nachverfolgbarkeit: Übernahmen aus Rahmenprogramm über origin_framework_slot_id
_ORIGIN_LINEAGE_JOIN = """ _ORIGIN_LINEAGE_JOIN = """
LEFT JOIN training_framework_slots origin_slot ON origin_slot.id = tu.origin_framework_slot_id LEFT JOIN training_framework_slots origin_slot ON origin_slot.id = tu.origin_framework_slot_id
@ -452,6 +464,8 @@ def _fetch_sections(cur, unit_id: int) -> List[Dict[str, Any]]:
e.title AS exercise_title, e.title AS exercise_title,
e.exercise_kind AS exercise_kind, e.exercise_kind AS exercise_kind,
e.summary AS exercise_summary, e.summary AS exercise_summary,
e.method_archetype AS catalog_method_archetype,
e.method_profile AS catalog_method_profile,
( (
SELECT fa.name FROM exercise_focus_areas efa SELECT fa.name FROM exercise_focus_areas efa
JOIN focus_areas fa ON fa.id = efa.focus_area_id JOIN focus_areas fa ON fa.id = efa.focus_area_id
@ -471,6 +485,14 @@ def _fetch_sections(cur, unit_id: int) -> List[Dict[str, Any]]:
(sec["id"],), (sec["id"],),
) )
sec["items"] = [r2d(r) for r in cur.fetchall()] sec["items"] = [r2d(r) for r in cur.fetchall()]
for it in sec["items"]:
if it.get("item_type") != "exercise":
continue
cmp_raw = it.get("catalog_method_profile")
if not isinstance(cmp_raw, dict):
it["catalog_method_profile"] = {}
else:
it["catalog_method_profile"] = dict(cmp_raw)
secs.append(sec) secs.append(sec)
return secs return secs
@ -506,6 +528,7 @@ def _sections_clone_payload(cur, unit_id: int) -> List[Dict[str, Any]]:
"actual_duration_min": it.get("actual_duration_min"), "actual_duration_min": it.get("actual_duration_min"),
"notes": it.get("notes"), "notes": it.get("notes"),
"modifications": it.get("modifications"), "modifications": it.get("modifications"),
"planning_method_profile": it.get("planning_method_profile"),
} }
sm = _optional_source_training_module_id_payload(it.get("source_training_module_id")) sm = _optional_source_training_module_id_payload(it.get("source_training_module_id"))
if sm is not None: if sm is not None:
@ -676,9 +699,10 @@ def _append_copied_module_items_to_section(
section_id, order_index, item_type, section_id, order_index, item_type,
exercise_id, exercise_variant_id, exercise_id, exercise_variant_id,
planned_duration_min, actual_duration_min, planned_duration_min, actual_duration_min,
notes, modifications, note_body, source_training_module_id notes, modifications, note_body,
source_training_module_id, planning_method_profile
) VALUES (%s, %s, 'exercise', ) VALUES (%s, %s, 'exercise',
%s, %s, %s, NULL, %s, NULL, NULL, %s) %s, %s, %s, NULL, %s, NULL, NULL, %s, NULL)
""", """,
( (
section_id, section_id,
@ -728,6 +752,15 @@ def _insert_section_items(cur, section_id: int, items_in: Optional[List[Any]], s
eid = int(eid) eid = int(eid)
vid = _optional_positive_int(raw.get("exercise_variant_id"), "exercise_variant_id") vid = _optional_positive_int(raw.get("exercise_variant_id"), "exercise_variant_id")
_validate_variant_for_exercise(cur, eid, vid) _validate_variant_for_exercise(cur, eid, vid)
cur.execute(
"""SELECT COALESCE(exercise_kind, 'simple') AS k FROM exercises WHERE id = %s""",
(eid,),
)
er = cur.fetchone()
ek = str(er["k"] if er and er.get("k") is not None else "simple").strip().lower()
planning_mp = _normalize_planning_method_profile_payload(raw.get("planning_method_profile"))
if ek != "combination":
planning_mp = None
src_mod = _optional_source_training_module_id_payload(raw.get("source_training_module_id")) src_mod = _optional_source_training_module_id_payload(raw.get("source_training_module_id"))
cur.execute( cur.execute(
""" """
@ -735,10 +768,10 @@ def _insert_section_items(cur, section_id: int, items_in: Optional[List[Any]], s
section_id, order_index, item_type, section_id, order_index, item_type,
exercise_id, exercise_variant_id, exercise_id, exercise_variant_id,
planned_duration_min, actual_duration_min, planned_duration_min, actual_duration_min,
notes, modifications, note_body, source_training_module_id notes, modifications, note_body,
source_training_module_id, planning_method_profile
) VALUES (%s, %s, 'exercise', ) VALUES (%s, %s, 'exercise',
%s, %s, %s, %s, %s, %s, NULL, %s %s, %s, %s, %s, %s, %s, NULL, %s, %s)
)
""", """,
( (
section_id, section_id,
@ -750,6 +783,7 @@ def _insert_section_items(cur, section_id: int, items_in: Optional[List[Any]], s
raw.get("notes"), raw.get("notes"),
raw.get("modifications"), raw.get("modifications"),
src_mod, src_mod,
planning_mp,
), ),
) )

View File

@ -1,8 +1,8 @@
# Shinkan Jinkendo Version Information # Shinkan Jinkendo Version Information
APP_VERSION = "0.8.102" APP_VERSION = "0.8.103"
BUILD_DATE = "2026-05-12" BUILD_DATE = "2026-05-12"
DB_SCHEMA_VERSION = "20260512056" DB_SCHEMA_VERSION = "20260512057"
MODULE_VERSIONS = { MODULE_VERSIONS = {
"legal_documents": "1.4.0", # Admin: Live-Vorschau pro Abschnitt + modale Vollvorschau (Editor + Dokumentenliste) "legal_documents": "1.4.0", # Admin: Live-Vorschau pro Abschnitt + modale Vollvorschau (Editor + Dokumentenliste)
@ -24,7 +24,7 @@ MODULE_VERSIONS = {
"exercises": "2.24.2", # Kombi: geführtes method_profile im Übungsformular nach Archetyp + Coach zeigt Profil als Key/Wert "exercises": "2.24.2", # Kombi: geführtes method_profile im Übungsformular nach Archetyp + Coach zeigt Profil als Key/Wert
"training_units": "0.2.0", "training_units": "0.2.0",
"training_programs": "0.1.0", "training_programs": "0.1.0",
"planning": "0.9.1", # Kombinationsübungen: Sektionen PATCH/validator + exercise_kind GET; Frontend KEINE Varianten bei combination "planning": "0.9.2", # Kombi: planning_method_profile auf Sektions-Items (Migration 057); Form-Payload + Coach-PUT
"training_modules": "1.0.0", "training_modules": "1.0.0",
"import_wiki": "1.0.0", "import_wiki": "1.0.0",
"admin": "1.0.0", "admin": "1.0.0",
@ -35,6 +35,13 @@ MODULE_VERSIONS = {
} }
CHANGELOG = [ CHANGELOG = [
{
"version": "0.8.103",
"date": "2026-05-12",
"changes": [
"Trainingsplanung: bei Kombinationszeilen optionales `planning_method_profile` (Migration 057); Planungs-Editor mit Ablaufprofil-Details, „wie Katalog“ / „aus Katalog kopieren“; Payload/Coach-PUT übernehmen Snapshot.",
],
},
{ {
"version": "0.8.102", "version": "0.8.102",
"date": "2026-05-12", "date": "2026-05-12",

View File

@ -6,6 +6,7 @@ import React from 'react'
import { Link } from 'react-router-dom' import { Link } from 'react-router-dom'
import ExerciseRichTextBlock from './ExerciseRichTextBlock' import ExerciseRichTextBlock from './ExerciseRichTextBlock'
import CombinationCoachSlots from './CombinationCoachSlots' import CombinationCoachSlots from './CombinationCoachSlots'
import { effectiveComboMethodProfile } from '../utils/comboPlanningMethodProfile'
function TagRow({ exercise }) { function TagRow({ exercise }) {
const tags = [] const tags = []
@ -53,9 +54,9 @@ function metaParts(exercise) {
} }
/** /**
* @param {{ exercise?: object|null, loading?: boolean, error?: string|null, exerciseId?: number, variantId?: number|string|null }} props * @param {{ exercise?: object|null, loading?: boolean, error?: string|null, exerciseId?: number, variantId?: number|string|null, planningComboMethodProfile?: object|null }} props
*/ */
export default function ExerciseFullContent({ exercise, loading, error, exerciseId, variantId }) { export default function ExerciseFullContent({ exercise, loading, error, exerciseId, variantId, planningComboMethodProfile }) {
if (loading) { if (loading) {
return ( return (
<div style={{ textAlign: 'center', padding: '1rem' }}> <div style={{ textAlign: 'center', padding: '1rem' }}>
@ -80,6 +81,10 @@ export default function ExerciseFullContent({ exercise, loading, error, exercise
const isCombination = const isCombination =
String(exercise.exercise_kind || 'simple').toLowerCase().trim() === 'combination' String(exercise.exercise_kind || 'simple').toLowerCase().trim() === 'combination'
const coachComboProfile = isCombination
? effectiveComboMethodProfile(exercise.method_profile, planningComboMethodProfile)
: null
return ( return (
<div className="exercise-coach-catalog" style={{ fontSize: '0.93rem', lineHeight: 1.5 }}> <div className="exercise-coach-catalog" style={{ fontSize: '0.93rem', lineHeight: 1.5 }}>
{variant ? ( {variant ? (
@ -114,7 +119,7 @@ export default function ExerciseFullContent({ exercise, loading, error, exercise
<CombinationCoachSlots <CombinationCoachSlots
combinationSlots={exercise.combination_slots} combinationSlots={exercise.combination_slots}
methodArchetype={exercise.method_archetype} methodArchetype={exercise.method_archetype}
methodProfile={exercise.method_profile} methodProfile={coachComboProfile}
/> />
) : null} ) : null}
<h2 style={{ margin: '0 0 8px', fontSize: '1.2rem', lineHeight: 1.35 }}>{exercise.title}</h2> <h2 style={{ margin: '0 0 8px', fontSize: '1.2rem', lineHeight: 1.35 }}>{exercise.title}</h2>

View File

@ -1,5 +1,7 @@
import React, { Fragment, useCallback, useEffect, useState } from 'react' import React, { Fragment, useCallback, useEffect, useState } from 'react'
import { GripVertical, Pencil } from 'lucide-react' import { GripVertical, Pencil } from 'lucide-react'
import CombinationMethodProfileEditor from './CombinationMethodProfileEditor'
import { comboPlanningProfileJsonForEditor } from '../utils/comboPlanningMethodProfile'
import { import {
defaultSection, defaultSection,
exerciseRow, exerciseRow,
@ -1019,6 +1021,72 @@ export default function TrainingUnitSectionsEditor({
</div> </div>
</div> </div>
{isCombination && it.exercise_id ? (
<div
className="tu-combo-planning-profile"
style={{
padding: '4px 12px 4px',
paddingLeft: enableItemDragReorder ? 44 : 12,
borderTop: '1px solid var(--border)',
}}
>
<details className="card" style={{ padding: '12px 14px', background: 'var(--surface2)' }}>
<summary
style={{ cursor: 'pointer', fontSize: '0.88rem', color: 'var(--text2)', fontWeight: 600 }}
>
Ablaufprofil für diese Planung (Kombination)
<span style={{ marginLeft: 10, fontWeight: 400, fontSize: '0.82rem' }}>
{it.planning_method_profile != null &&
typeof it.planning_method_profile === 'object' &&
!Array.isArray(it.planning_method_profile)
? '— Anpassung aktiv'
: '— wie im Katalog'}
</span>
</summary>
<div style={{ marginTop: 12 }}>
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 8, marginBottom: 10 }}>
<button
type="button"
className="btn btn-secondary framework-ctrl framework-ctrl--xs"
onClick={() => updateItem(sIdx, iIdx, 'planning_method_profile', null)}
>
Planung wie Katalog
</button>
<button
type="button"
className="btn btn-secondary framework-ctrl framework-ctrl--xs"
title="Bearbeitbare Kopie der Katalog-Vorgaben setzen"
onClick={() =>
updateItem(sIdx, iIdx, 'planning_method_profile', {
...(it.catalog_method_profile || {}),
})
}
>
Aus Katalog kopieren
</button>
</div>
<CombinationMethodProfileEditor
methodArchetype={(it.catalog_method_archetype || '').trim()}
methodProfileJson={comboPlanningProfileJsonForEditor(
it.catalog_method_profile || {},
it.planning_method_profile
)}
onChangeMethodProfileJson={(json) => {
try {
const obj = JSON.parse(json || '{}')
if (obj && typeof obj === 'object' && !Array.isArray(obj)) {
updateItem(sIdx, iIdx, 'planning_method_profile', obj)
}
} catch {
/* Ungültiges JSON — Hinweis im Editor */
}
}}
/>
</div>
</details>
</div>
) : null}
{showExecutionExtras ? ( {showExecutionExtras ? (
<div className="tu-ex-debrief"> <div className="tu-ex-debrief">
<div className="tu-ex-debrief__grow"> <div className="tu-ex-debrief__grow">

View File

@ -740,6 +740,11 @@ export default function TrainingCoachPage() {
exercise={catalogExercise} exercise={catalogExercise}
exerciseId={currentEntry?.item?.exercise_id ?? null} exerciseId={currentEntry?.item?.exercise_id ?? null}
variantId={currentEntry?.item?.exercise_variant_id ?? null} variantId={currentEntry?.item?.exercise_variant_id ?? null}
planningComboMethodProfile={
String(currentEntry?.item?.exercise_kind || 'simple').toLowerCase().trim() === 'combination'
? currentEntry?.item?.planning_method_profile ?? null
: null
}
/> />
</div> </div>
</> </>

View File

@ -0,0 +1,26 @@
/** Effektives Ablaufprofil für Kombination im Coach/in der Planung */
export function effectiveComboMethodProfile(catalogDict, planningSnapshot) {
const cat =
catalogDict && typeof catalogDict === 'object' && !Array.isArray(catalogDict)
? catalogDict
: {}
if (
planningSnapshot !== null &&
planningSnapshot !== undefined &&
typeof planningSnapshot === 'object' &&
!Array.isArray(planningSnapshot)
) {
return { ...planningSnapshot }
}
return { ...cat }
}
export function comboPlanningProfileJsonForEditor(catalogDict, planningSnapshot) {
const o = effectiveComboMethodProfile(catalogDict, planningSnapshot)
try {
return JSON.stringify(Object.keys(o).length ? o : {})
} catch {
return '{}'
}
}

View File

@ -81,7 +81,7 @@ export function sectionsToPutPayload(unit, durationOverridesByItemId = {}) {
else actual = typeof actual === 'number' ? actual : parseInt(String(actual), 10) else actual = typeof actual === 'number' ? actual : parseInt(String(actual), 10)
if (actual !== null && !Number.isFinite(actual)) actual = null if (actual !== null && !Number.isFinite(actual)) actual = null
return { const row = {
item_type: 'exercise', item_type: 'exercise',
order_index: it.order_index ?? ii, order_index: it.order_index ?? ii,
exercise_id: parseInt(String(eid), 10), exercise_id: parseInt(String(eid), 10),
@ -92,6 +92,13 @@ export function sectionsToPutPayload(unit, durationOverridesByItemId = {}) {
notes: trimOrNull(it.notes), notes: trimOrNull(it.notes),
modifications: trimOrNull(it.modifications), 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), .filter(Boolean),
})) }))

View File

@ -4,6 +4,18 @@ export function defaultSection(title = 'Hauptteil') {
return { title, guidance_notes: '', items: [] } 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() { export function exerciseRow() {
return { return {
item_type: 'exercise', item_type: 'exercise',
@ -18,6 +30,9 @@ export function exerciseRow() {
modifications: '', modifications: '',
source_training_module_id: '', source_training_module_id: '',
source_module_title: '', source_module_title: '',
catalog_method_archetype: '',
catalog_method_profile: {},
planning_method_profile: null,
} }
} }
@ -29,16 +44,20 @@ export async function hydrateExercisePlanningRow(exercise) {
if (!id) return null if (!id) return null
let meta = {} let meta = {}
async function fetchFull() { let full
async function ensureFull() {
if (full !== undefined) return full
try { try {
return await api.getExercise(id) full = await api.getExercise(id)
} catch { } catch {
return null full = null
} }
return full
} }
if (!variants.length) { if (!variants.length) {
const full = await fetchFull() await ensureFull()
if (full) { if (full) {
variants = Array.isArray(full.variants) ? full.variants : [] variants = Array.isArray(full.variants) ? full.variants : []
title = full.title || title title = full.title || title
@ -48,6 +67,8 @@ export async function hydrateExercisePlanningRow(exercise) {
exercise_club_id: full.club_id ?? null, exercise_club_id: full.club_id ?? null,
exercise_created_by: full.created_by ?? null, exercise_created_by: full.created_by ?? null,
exercise_status: full.status || 'draft', 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 { } else {
@ -62,7 +83,7 @@ export async function hydrateExercisePlanningRow(exercise) {
meta.exercise_created_by == null || meta.exercise_created_by == null ||
exerciseKind == null exerciseKind == null
) { ) {
const full = await fetchFull() await ensureFull()
if (full) { if (full) {
if (meta.exercise_visibility == null) meta.exercise_visibility = full.visibility || 'private' 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_club_id == null) meta.exercise_club_id = full.club_id ?? null
@ -89,6 +110,15 @@ export async function hydrateExercisePlanningRow(exercise) {
row.variants = variants row.variants = variants
} }
Object.assign(row, meta) 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.planning_method_profile = null
return row return row
} }
@ -147,6 +177,9 @@ export function normalizeUnitToForm(fullUnit) {
: '', : '',
notes: it.notes ?? '', notes: it.notes ?? '',
modifications: it.modifications ?? '', 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 ...(smEx != null
? { ? {
source_training_module_id: smEx, source_training_module_id: smEx,
@ -186,6 +219,9 @@ export function normalizeUnitToForm(fullUnit) {
: '', : '',
notes: ex.notes ?? '', notes: ex.notes ?? '',
modifications: ex.modifications ?? '', 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),
} }
}), }),
}, },
@ -217,6 +253,8 @@ export async function enrichSectionsWithVariants(sections) {
club_id: ex.club_id ?? null, club_id: ex.club_id ?? null,
created_by: ex.created_by ?? null, created_by: ex.created_by ?? null,
status: ex.status || 'draft', status: ex.status || 'draft',
method_archetype: typeof ex.method_archetype === 'string' ? ex.method_archetype.trim() : '',
method_profile: normalizeCatalogMethodProfile(ex.method_profile),
}) })
} catch { } catch {
cache.set(id, { cache.set(id, {
@ -227,6 +265,8 @@ export async function enrichSectionsWithVariants(sections) {
club_id: null, club_id: null,
created_by: null, created_by: null,
status: 'draft', status: 'draft',
method_archetype: '',
method_profile: {},
}) })
} }
}) })
@ -240,8 +280,18 @@ export async function enrichSectionsWithVariants(sections) {
if (!c) return it if (!c) return it
const ek = String(c.exercise_kind || 'simple').toLowerCase().trim() const ek = String(c.exercise_kind || 'simple').toLowerCase().trim()
const isCombo = ek === 'combination' 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 { return {
...it, ...it,
catalog_method_archetype,
catalog_method_profile,
planning_method_profile: normalizePlanningMethodProfile(it.planning_method_profile),
exercise_kind: isCombo ? 'combination' : 'simple', exercise_kind: isCombo ? 'combination' : 'simple',
exercise_title: it.exercise_title || c.title, exercise_title: it.exercise_title || c.title,
exercise_variant_id: isCombo ? '' : it.exercise_variant_id, exercise_variant_id: isCombo ? '' : it.exercise_variant_id,
@ -296,6 +346,12 @@ export function buildSectionsPayload(sections) {
notes: it.notes?.trim() ? it.notes.trim() : null, notes: it.notes?.trim() ? it.notes.trim() : null,
modifications: it.modifications?.trim() ? it.modifications.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 if (smEx != null) rowEx.source_training_module_id = smEx
return rowEx return rowEx
}) })