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
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:
parent
12fd3926b2
commit
4e654e50c0
|
|
@ -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.';
|
||||||
|
|
@ -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,
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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",
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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">
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
</>
|
</>
|
||||||
|
|
|
||||||
26
frontend/src/utils/comboPlanningMethodProfile.js
Normal file
26
frontend/src/utils/comboPlanningMethodProfile.js
Normal 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 '{}'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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),
|
||||||
}))
|
}))
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
})
|
})
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user