shinkan-jinkendo/frontend/src/utils/trainingUnitSectionsForm.js
Lars 4080088d42
Some checks failed
Deploy Development / deploy (push) Successful in 33s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 6s
Test Suite / playwright-tests (push) Failing after 39s
feat: enhance TrainingUnitSectionsEditor with heading accessory support
- Added a new `headingAccessory` prop to TrainingUnitSectionsEditor for customizable header content.
- Updated the component to conditionally render the heading and accessory in a flexible toolbar layout.
- Refactored TrainingFrameworkProgramEditPage and TrainingPlanningPage to utilize the new heading accessory feature, improving user interaction and layout consistency.
2026-05-05 14:47:38 +02:00

192 lines
5.7 KiB
JavaScript

import api from './api'
export function defaultSection(title = 'Hauptteil') {
return { title, guidance_notes: '', items: [] }
}
export function exerciseRow() {
return {
item_type: 'exercise',
exercise_id: '',
exercise_variant_id: '',
exercise_title: '',
variants: [],
planned_duration_min: '',
actual_duration_min: '',
notes: '',
modifications: '',
}
}
export async function hydrateExercisePlanningRow(exercise) {
let variants = Array.isArray(exercise?.variants) ? exercise.variants : []
let title = exercise?.title || ''
const id = exercise?.id
if (!id) return null
if (!variants.length) {
try {
const full = await api.getExercise(id)
variants = Array.isArray(full?.variants) ? full.variants : []
title = full?.title || title
} catch {
variants = []
}
}
const row = exerciseRow()
row.exercise_id = id
row.exercise_variant_id = ''
row.exercise_title = title
row.variants = variants
return row
}
export function noteRow() {
return { item_type: 'note', note_body: '' }
}
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') {
return { item_type: 'note', note_body: it.note_body || '' }
}
return {
item_type: 'exercise',
exercise_id: it.exercise_id,
exercise_variant_id: 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 ?? '',
}
}),
}))
}
if (fullUnit.exercises && fullUnit.exercises.length) {
return [
{
title: 'Übungen',
guidance_notes: '',
items: fullUnit.exercises.map((ex) => ({
item_type: 'exercise',
exercise_id: ex.exercise_id,
exercise_variant_id: 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 ?? '',
})),
},
]
}
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)
cache.set(id, {
title: ex.title || '',
variants: Array.isArray(ex.variants) ? ex.variants : [],
})
} catch {
cache.set(id, { title: '', variants: [] })
}
})
)
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
return {
...it,
exercise_title: it.exercise_title || c.title,
variants:
Array.isArray(it.variants) && it.variants.length > 0 ? it.variants : c.variants,
}
}),
}))
}
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') {
return {
item_type: 'note',
order_index: ii,
note_body: it.note_body ?? '',
}
}
if (it.exercise_id === '' || it.exercise_id == null || Number.isNaN(Number(it.exercise_id))) {
return null
}
const vid = it.exercise_variant_id
return {
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,
}
})
.filter(Boolean),
}))
}
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)
}