feat(combo-planning): enhance combination profile handling and UI improvements
All checks were successful
Deploy Development / deploy (push) Successful in 36s
Test Suite / pytest-backend (push) Successful in 35s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 12s
Test Suite / playwright-tests (push) Successful in 59s
All checks were successful
Deploy Development / deploy (push) Successful in 36s
Test Suite / pytest-backend (push) Successful in 35s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 12s
Test Suite / playwright-tests (push) Successful in 59s
- Updated CombinationCoachSlots to integrate a new function for formatting inline profile values, improving data display consistency. - Refactored CombinationMethodProfileEditor to streamline slot index handling and enhance title clarity. - Improved CombinationPlanBracket by removing unnecessary elements for a cleaner UI. - Enhanced ExerciseFullContent to support additional catalog method profile snapshots, improving exercise detail accuracy. - Updated CSS for combo plan brackets to enhance visual presentation and alignment. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
79dabbca5a
commit
d3ddc52118
|
|
@ -6318,38 +6318,22 @@ a.analysis-split__nav-item {
|
||||||
color: var(--text1);
|
color: var(--text1);
|
||||||
}
|
}
|
||||||
.combo-plan-bracket__stations {
|
.combo-plan-bracket__stations {
|
||||||
list-style: none;
|
list-style: decimal;
|
||||||
padding: 0;
|
list-style-position: outside;
|
||||||
|
padding: 0 0 0 1.35rem;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
}
|
}
|
||||||
.combo-plan-bracket__station {
|
.combo-plan-bracket__station {
|
||||||
display: flex;
|
display: block;
|
||||||
gap: 10px;
|
|
||||||
align-items: flex-start;
|
|
||||||
padding: 10px 10px;
|
padding: 10px 10px;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
background: var(--surface2);
|
background: var(--surface2);
|
||||||
}
|
}
|
||||||
.combo-plan-bracket__station-index {
|
|
||||||
flex-shrink: 0;
|
|
||||||
min-width: 2.25rem;
|
|
||||||
height: 2.25rem;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
border-radius: 8px;
|
|
||||||
font-size: 0.72rem;
|
|
||||||
font-weight: 800;
|
|
||||||
background: var(--surface);
|
|
||||||
border: 1px solid var(--border);
|
|
||||||
color: var(--accent-dark);
|
|
||||||
}
|
|
||||||
.combo-plan-bracket__station-main {
|
.combo-plan-bracket__station-main {
|
||||||
flex: 1;
|
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
.combo-plan-bracket__station-title {
|
.combo-plan-bracket__station-title {
|
||||||
|
|
@ -6463,10 +6447,6 @@ a.analysis-split__nav-item {
|
||||||
border-color: #444 !important;
|
border-color: #444 !important;
|
||||||
background: #f4f6f8 !important;
|
background: #f4f6f8 !important;
|
||||||
}
|
}
|
||||||
.combo-plan-bracket__station-index {
|
|
||||||
border-color: #444 !important;
|
|
||||||
color: #06352a !important;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Coach — volle Übung, Nur-Mittelbereich scrollt; Steuerung oben/unten sichtbar */
|
/* Coach — volle Übung, Nur-Mittelbereich scrollt; Steuerung oben/unten sichtbar */
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,24 @@ import {
|
||||||
combinationArchetypeLabel,
|
combinationArchetypeLabel,
|
||||||
sortCombinationSlotsForDisplay,
|
sortCombinationSlotsForDisplay,
|
||||||
} from '../constants/combinationArchetypes'
|
} from '../constants/combinationArchetypes'
|
||||||
import { effectiveStationTimingSummary, readSlotProfilesV1 } from '../utils/combinationMethodProfileUi'
|
import {
|
||||||
|
describeGlobalComboProfile,
|
||||||
|
effectiveStationTimingSummary,
|
||||||
|
METHOD_PROFILE_GUI_FIELDS,
|
||||||
|
readSlotProfilesV1,
|
||||||
|
} from '../utils/combinationMethodProfileUi'
|
||||||
|
|
||||||
|
function formatInlineProfileValue(val) {
|
||||||
|
if (val === null || val === undefined) return '—'
|
||||||
|
if (typeof val === 'boolean') return val ? 'ja' : 'nein'
|
||||||
|
if (typeof val === 'number' && Number.isFinite(val)) return String(val)
|
||||||
|
if (typeof val === 'string') return val.trim() === '' ? '—' : val
|
||||||
|
try {
|
||||||
|
return JSON.stringify(val)
|
||||||
|
} catch {
|
||||||
|
return String(val)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default function CombinationCoachSlots({
|
export default function CombinationCoachSlots({
|
||||||
combinationSlots,
|
combinationSlots,
|
||||||
|
|
@ -93,12 +110,25 @@ export default function CombinationCoachSlots({
|
||||||
return m
|
return m
|
||||||
}, [methodProfile])
|
}, [methodProfile])
|
||||||
|
|
||||||
const methodProfileKvSansSlots = useMemo(() => {
|
const globalComboRows = useMemo(
|
||||||
|
() => describeGlobalComboProfile(archeKey, methodProfile || {}),
|
||||||
|
[archeKey, methodProfile],
|
||||||
|
)
|
||||||
|
|
||||||
|
const profileExtraEntries = useMemo(() => {
|
||||||
if (!methodProfile || typeof methodProfile !== 'object' || Array.isArray(methodProfile)) return []
|
if (!methodProfile || typeof methodProfile !== 'object' || Array.isArray(methodProfile)) return []
|
||||||
return Object.entries(methodProfile)
|
const known = new Set(['slot_profiles_v1'])
|
||||||
.filter(([k]) => k !== 'slot_profiles_v1')
|
for (const f of METHOD_PROFILE_GUI_FIELDS[archeKey] || []) {
|
||||||
.sort(([a], [b]) => a.localeCompare(b, 'de'))
|
known.add(f.key)
|
||||||
}, [methodProfile])
|
}
|
||||||
|
const out = []
|
||||||
|
for (const [k, val] of Object.entries(methodProfile)) {
|
||||||
|
if (known.has(k)) continue
|
||||||
|
if (val === null || val === undefined || val === '') continue
|
||||||
|
out.push([k, val])
|
||||||
|
}
|
||||||
|
return out.sort((a, b) => String(a[0]).localeCompare(String(b[0]), 'de'))
|
||||||
|
}, [methodProfile, archeKey])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section
|
<section
|
||||||
|
|
@ -122,14 +152,7 @@ export default function CombinationCoachSlots({
|
||||||
{compactPlanningView ? 'Stationen & Einzelübungen (Katalog)' : 'Kombination · Stationen & Einzelübungen'}
|
{compactPlanningView ? 'Stationen & Einzelübungen (Katalog)' : 'Kombination · Stationen & Einzelübungen'}
|
||||||
</h3>
|
</h3>
|
||||||
{archDisplay ? (
|
{archDisplay ? (
|
||||||
<p style={{ margin: '0 0 6px', fontSize: '0.95rem', fontWeight: 700 }}>
|
<p style={{ margin: '0 0 6px', fontSize: '0.95rem', fontWeight: 700 }}>{archDisplay}</p>
|
||||||
{archDisplay}
|
|
||||||
{archeKey && archDisplay !== archeKey ? (
|
|
||||||
<span style={{ marginLeft: 8, fontSize: '0.78rem', fontWeight: 500, color: 'var(--text3)' }}>
|
|
||||||
({archeKey})
|
|
||||||
</span>
|
|
||||||
) : null}
|
|
||||||
</p>
|
|
||||||
) : null}
|
) : null}
|
||||||
{compactPlanningView ? null : (
|
{compactPlanningView ? null : (
|
||||||
<p style={{ margin: '0 0 14px', fontSize: '0.88rem', color: 'var(--text2)', lineHeight: 1.48 }}>
|
<p style={{ margin: '0 0 14px', fontSize: '0.88rem', color: 'var(--text2)', lineHeight: 1.48 }}>
|
||||||
|
|
@ -148,28 +171,40 @@ export default function CombinationCoachSlots({
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div style={{ fontSize: '0.7rem', fontWeight: 700, color: 'var(--text3)', textTransform: 'uppercase', marginBottom: '6px' }}>
|
<div style={{ fontSize: '0.7rem', fontWeight: 700, color: 'var(--text3)', textTransform: 'uppercase', marginBottom: '6px' }}>
|
||||||
Geplantes Ablaufprofil (Katalog)
|
Globale Eckdaten (wie im Editor)
|
||||||
</div>
|
</div>
|
||||||
{methodProfileKvSansSlots.length === 0 ? (
|
{globalComboRows.length === 0 && profileExtraEntries.length === 0 ? (
|
||||||
<p style={{ margin: 0, fontSize: '0.82rem', color: 'var(--text3)', lineHeight: 1.42 }}>
|
<p style={{ margin: 0, fontSize: '0.82rem', color: 'var(--text3)', lineHeight: 1.42 }}>
|
||||||
Nur stationsbezogene Daten (Zeiten/Zähl‑Steuerung) — siehe je Station unter der Überschrift „Plan:“.
|
Keine globalen Zahlenfelder gesetzt — Zeiten und Steuerung siehe je Station unter „Plan:“ (oder nur im Freitext der Kombination).
|
||||||
</p>
|
</p>
|
||||||
) : (
|
) : (
|
||||||
<dl style={{ margin: 0, fontSize: '0.82rem', lineHeight: 1.45 }}>
|
<dl style={{ margin: 0, fontSize: '0.82rem', lineHeight: 1.45 }}>
|
||||||
{methodProfileKvSansSlots.map(([k, val]) => (
|
{globalComboRows.map((row) => (
|
||||||
<div key={k} style={{ marginBottom: '4px', display: 'grid', gridTemplateColumns: 'minmax(72px,1fr) minmax(0,2fr)', gap: '6px 10px' }}>
|
<div
|
||||||
|
key={row.key}
|
||||||
|
style={{
|
||||||
|
marginBottom: '6px',
|
||||||
|
display: 'grid',
|
||||||
|
gridTemplateColumns: 'minmax(88px,1fr) minmax(0,1.4fr)',
|
||||||
|
gap: '6px 10px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<dt style={{ margin: 0, fontWeight: 600, color: 'var(--text3)' }}>{row.detailLabel}</dt>
|
||||||
|
<dd style={{ margin: 0, color: 'var(--text1)', fontWeight: 600 }}>{row.value}</dd>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
{profileExtraEntries.map(([k, val]) => (
|
||||||
|
<div
|
||||||
|
key={k}
|
||||||
|
style={{
|
||||||
|
marginBottom: '4px',
|
||||||
|
display: 'grid',
|
||||||
|
gridTemplateColumns: 'minmax(88px,1fr) minmax(0,1.4fr)',
|
||||||
|
gap: '6px 10px',
|
||||||
|
}}
|
||||||
|
>
|
||||||
<dt style={{ margin: 0, fontWeight: 600, color: 'var(--text3)', wordBreak: 'break-all' }}>{k}</dt>
|
<dt style={{ margin: 0, fontWeight: 600, color: 'var(--text3)', wordBreak: 'break-all' }}>{k}</dt>
|
||||||
<dd style={{ margin: 0, color: 'var(--text1)' }}>
|
<dd style={{ margin: 0, color: 'var(--text1)' }}>{formatInlineProfileValue(val)}</dd>
|
||||||
{typeof val === 'boolean'
|
|
||||||
? val
|
|
||||||
? 'ja'
|
|
||||||
: 'nein'
|
|
||||||
: typeof val === 'number'
|
|
||||||
? String(val)
|
|
||||||
: typeof val === 'string'
|
|
||||||
? val
|
|
||||||
: JSON.stringify(val)}
|
|
||||||
</dd>
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</dl>
|
</dl>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import React, { useMemo, useState } from 'react'
|
import React, { useMemo, useState } from 'react'
|
||||||
import { archetypeCoachHint, combinationArchetypeLabel, sortCombinationSlotsForDisplay, defaultRepSeriesCountForArchetype } from '../constants/combinationArchetypes'
|
import { archetypeCoachHint, combinationArchetypeLabel, sortCombinationSlotsForDisplay } from '../constants/combinationArchetypes'
|
||||||
import {
|
import {
|
||||||
METHOD_PROFILE_GUI_FIELDS,
|
METHOD_PROFILE_GUI_FIELDS,
|
||||||
parseProfileJson,
|
parseProfileJson,
|
||||||
|
|
@ -299,13 +299,13 @@ export default function CombinationMethodProfileEditor({
|
||||||
Zirkel erst die globalen Arbeit‑Sekunden.
|
Zirkel erst die globalen Arbeit‑Sekunden.
|
||||||
</p>
|
</p>
|
||||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
|
||||||
{outlineSorted.map((slot) => {
|
{outlineSorted.map((slot, ordIdx) => {
|
||||||
const siRaw = slot.slot_index
|
const siRaw = slot.slot_index
|
||||||
const si =
|
const si =
|
||||||
siRaw === '' || siRaw == null ? null : typeof siRaw === 'number' ? siRaw : parseInt(String(siRaw), 10)
|
siRaw === '' || siRaw == null ? null : typeof siRaw === 'number' ? siRaw : parseInt(String(siRaw), 10)
|
||||||
if (!Number.isFinite(si)) return null
|
if (!Number.isFinite(si)) return null
|
||||||
const row = lookupSlotTiming(si)
|
const row = lookupSlotTiming(si)
|
||||||
const ttl = ((slot.title || '').trim() || `Station ${si}`).trim()
|
const ttl = ((slot.title || '').trim() || `Station ${ordIdx + 1}`).trim()
|
||||||
const slotAdv = normalizeAdvanceMode(row.advance_mode)
|
const slotAdv = normalizeAdvanceMode(row.advance_mode)
|
||||||
const serieLabel =
|
const serieLabel =
|
||||||
slotAdv === 'timed' ? 'Wdh. ohne Wechsel' : slotAdv === 'rep' ? 'Wdh. / Serie' : 'Richtwert'
|
slotAdv === 'timed' ? 'Wdh. ohne Wechsel' : slotAdv === 'rep' ? 'Wdh. / Serie' : 'Richtwert'
|
||||||
|
|
@ -323,10 +323,7 @@ export default function CombinationMethodProfileEditor({
|
||||||
background: 'var(--surface2)',
|
background: 'var(--surface2)',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div style={{ marginBottom: '10px', fontWeight: 700, color: 'var(--text1)', fontSize: '0.9rem' }}>
|
<div style={{ marginBottom: '10px', fontWeight: 700, color: 'var(--text1)', fontSize: '0.9rem' }}>{ttl}</div>
|
||||||
Station {si}
|
|
||||||
<span style={{ fontWeight: 400, marginLeft: 8, color: 'var(--text2)', fontSize: '0.86rem' }}>{ttl}</span>
|
|
||||||
</div>
|
|
||||||
<div className="form-row" style={{ marginBottom: '10px', maxWidth: '22rem' }}>
|
<div className="form-row" style={{ marginBottom: '10px', maxWidth: '22rem' }}>
|
||||||
<label className="form-label" style={{ fontSize: '11px' }}>
|
<label className="form-label" style={{ fontSize: '11px' }}>
|
||||||
Steuerung
|
Steuerung
|
||||||
|
|
@ -392,11 +389,7 @@ export default function CombinationMethodProfileEditor({
|
||||||
min={slotAdv === 'rep' ? 1 : undefined}
|
min={slotAdv === 'rep' ? 1 : undefined}
|
||||||
className="form-input"
|
className="form-input"
|
||||||
placeholder="1"
|
placeholder="1"
|
||||||
value={
|
value={String(parseComboRepSeriesCountUi(row.rep_series_count))}
|
||||||
row.rep_series_count != null && String(row.rep_series_count) !== ''
|
|
||||||
? String(row.rep_series_count)
|
|
||||||
: String(defaultRepSeriesCountForArchetype(methodArchetype))
|
|
||||||
}
|
|
||||||
onChange={(e) => onSlotRepSeriesCount(si, e.target.value)}
|
onChange={(e) => onSlotRepSeriesCount(si, e.target.value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -58,12 +58,7 @@ export default function CombinationPlanBracket({
|
||||||
<header className="combo-plan-bracket__head">
|
<header className="combo-plan-bracket__head">
|
||||||
<div className="combo-plan-bracket__head-main">
|
<div className="combo-plan-bracket__head-main">
|
||||||
<span className="combo-plan-bracket__kicker">Kombinations‑Plan</span>
|
<span className="combo-plan-bracket__kicker">Kombinations‑Plan</span>
|
||||||
<span className="combo-plan-bracket__archetype">
|
<span className="combo-plan-bracket__archetype">{archLabel || arch || 'Archetyp'}</span>
|
||||||
{archLabel || arch || 'Archetyp'}
|
|
||||||
{arch && archLabel && archLabel !== arch ? (
|
|
||||||
<span className="combo-plan-bracket__archetype-id"> ({arch})</span>
|
|
||||||
) : null}
|
|
||||||
</span>
|
|
||||||
{planningAdjusted ? (
|
{planningAdjusted ? (
|
||||||
<span className="combo-plan-bracket__badge">Planung angepasst</span>
|
<span className="combo-plan-bracket__badge">Planung angepasst</span>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
@ -102,12 +97,6 @@ export default function CombinationPlanBracket({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li key={`slot-${stationIx}-${si}`} className="combo-plan-bracket__station">
|
<li key={`slot-${stationIx}-${si}`} className="combo-plan-bracket__station">
|
||||||
<div
|
|
||||||
className="combo-plan-bracket__station-index"
|
|
||||||
title={`Technischer Slot-Index (slot_index): ${stationIx}`}
|
|
||||||
>
|
|
||||||
S{displayStep}
|
|
||||||
</div>
|
|
||||||
<div className="combo-plan-bracket__station-main">
|
<div className="combo-plan-bracket__station-main">
|
||||||
<div className="combo-plan-bracket__station-title">{stationTitle}</div>
|
<div className="combo-plan-bracket__station-title">{stationTitle}</div>
|
||||||
<div className="combo-plan-bracket__station-exercises">{names || '(keine Einzelübung)'}</div>
|
<div className="combo-plan-bracket__station-exercises">{names || '(keine Einzelübung)'}</div>
|
||||||
|
|
|
||||||
|
|
@ -54,9 +54,9 @@ function metaParts(exercise) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {{ exercise?: object|null, loading?: boolean, error?: string|null, exerciseId?: number, variantId?: number|string|null, planningComboMethodProfile?: object|null }} props
|
* @param {{ exercise?: object|null, loading?: boolean, error?: string|null, exerciseId?: number, variantId?: number|string|null, planningComboMethodProfile?: object|null, catalogMethodProfileSnapshot?: object|null }} props
|
||||||
*/
|
*/
|
||||||
export default function ExerciseFullContent({ exercise, loading, error, exerciseId, variantId, planningComboMethodProfile }) {
|
export default function ExerciseFullContent({ exercise, loading, error, exerciseId, variantId, planningComboMethodProfile, catalogMethodProfileSnapshot }) {
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div style={{ textAlign: 'center', padding: '1rem' }}>
|
<div style={{ textAlign: 'center', padding: '1rem' }}>
|
||||||
|
|
@ -81,8 +81,17 @@ 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 catalogFromExercise =
|
||||||
|
exercise.method_profile && typeof exercise.method_profile === 'object' && !Array.isArray(exercise.method_profile)
|
||||||
|
? exercise.method_profile
|
||||||
|
: {}
|
||||||
|
const catalogFromUnit =
|
||||||
|
catalogMethodProfileSnapshot && typeof catalogMethodProfileSnapshot === 'object' && !Array.isArray(catalogMethodProfileSnapshot)
|
||||||
|
? catalogMethodProfileSnapshot
|
||||||
|
: {}
|
||||||
|
|
||||||
const coachComboProfile = isCombination
|
const coachComboProfile = isCombination
|
||||||
? effectiveComboMethodProfile(exercise.method_profile, planningComboMethodProfile)
|
? effectiveComboMethodProfile({ ...catalogFromExercise, ...catalogFromUnit }, planningComboMethodProfile)
|
||||||
: null
|
: null
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import api from '../utils/api'
|
||||||
import ExerciseRichTextBlock from '../components/ExerciseRichTextBlock'
|
import ExerciseRichTextBlock from '../components/ExerciseRichTextBlock'
|
||||||
import ExerciseAttachmentMediaStrip from '../components/ExerciseAttachmentMediaStrip'
|
import ExerciseAttachmentMediaStrip from '../components/ExerciseAttachmentMediaStrip'
|
||||||
import { formatSkillLevelSlug } from '../constants/skillLevels'
|
import { formatSkillLevelSlug } from '../constants/skillLevels'
|
||||||
|
import { sortCombinationSlotsForDisplay } from '../constants/combinationArchetypes'
|
||||||
|
|
||||||
function TagRow({ exercise }) {
|
function TagRow({ exercise }) {
|
||||||
const tags = []
|
const tags = []
|
||||||
|
|
@ -147,11 +148,9 @@ function ExerciseDetailPage() {
|
||||||
</p>
|
</p>
|
||||||
) : null}
|
) : null}
|
||||||
<ol style={{ paddingLeft: '1.25rem', marginBottom: 0 }}>
|
<ol style={{ paddingLeft: '1.25rem', marginBottom: 0 }}>
|
||||||
{exercise.combination_slots.map((s) => (
|
{sortCombinationSlotsForDisplay(exercise.combination_slots).map((s, idx) => (
|
||||||
<li key={`${s.slot_index}-${(s.title || '').slice(0, 8)}`} style={{ marginBottom: '10px' }}>
|
<li key={`${s.slot_index}-${idx}-${(s.title || '').slice(0, 8)}`} style={{ marginBottom: '10px' }}>
|
||||||
<strong>
|
<strong>{(s.title || '').trim() || `Station ${idx + 1}`}</strong>
|
||||||
Station {s.slot_index != null ? s.slot_index : '?'}{s.title ? ` — ${s.title}` : ''}
|
|
||||||
</strong>
|
|
||||||
<ul style={{ margin: '4px 0 0', paddingLeft: '1.2rem' }}>
|
<ul style={{ margin: '4px 0 0', paddingLeft: '1.2rem' }}>
|
||||||
{(s.candidates && s.candidates.length
|
{(s.candidates && s.candidates.length
|
||||||
? s.candidates
|
? s.candidates
|
||||||
|
|
|
||||||
|
|
@ -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}
|
||||||
|
catalogMethodProfileSnapshot={
|
||||||
|
String(currentEntry?.item?.exercise_kind || 'simple').toLowerCase().trim() === 'combination'
|
||||||
|
? currentEntry?.item?.catalog_method_profile ?? null
|
||||||
|
: null
|
||||||
|
}
|
||||||
planningComboMethodProfile={
|
planningComboMethodProfile={
|
||||||
String(currentEntry?.item?.exercise_kind || 'simple').toLowerCase().trim() === 'combination'
|
String(currentEntry?.item?.exercise_kind || 'simple').toLowerCase().trim() === 'combination'
|
||||||
? currentEntry?.item?.planning_method_profile ?? null
|
? currentEntry?.item?.planning_method_profile ?? null
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,56 @@
|
||||||
/** Effektives Ablaufprofil für Kombination im Coach/in der Planung */
|
/** Effektives Ablaufprofil für Kombination im Coach/in der Planung */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vereinigt slot_profiles_v1 aus Katalog und Planungs-Overlay (je slot_index).
|
||||||
|
* @param {unknown} catArr
|
||||||
|
* @param {unknown} planArr
|
||||||
|
*/
|
||||||
|
function mergeSlotProfilesV1(catArr, planArr) {
|
||||||
|
const c = Array.isArray(catArr) ? catArr : []
|
||||||
|
const p = Array.isArray(planArr) ? planArr : []
|
||||||
|
const byIx = new Map()
|
||||||
|
for (const r of c) {
|
||||||
|
if (!r || typeof r !== 'object') continue
|
||||||
|
const ix = Number(r.slot_index)
|
||||||
|
if (!Number.isFinite(ix)) continue
|
||||||
|
byIx.set(ix, { ...r })
|
||||||
|
}
|
||||||
|
for (const r of p) {
|
||||||
|
if (!r || typeof r !== 'object') continue
|
||||||
|
const ix = Number(r.slot_index)
|
||||||
|
if (!Number.isFinite(ix)) continue
|
||||||
|
const prev = byIx.get(ix) || {}
|
||||||
|
byIx.set(ix, { ...prev, ...r })
|
||||||
|
}
|
||||||
|
return [...byIx.entries()].sort((a, b) => a[0] - b[0]).map(([, row]) => row)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Katalog-Basis + optionaler Planungs-Snapshot.
|
||||||
|
* Wichtig: `planning_method_profile` darf nur **Überschreibungen** sein — nie den Katalog komplett ersetzen
|
||||||
|
* (sonst verschwinden Zeiten/Runden bei leerem Objekt oder Teil-Payload).
|
||||||
|
*/
|
||||||
export function effectiveComboMethodProfile(catalogDict, planningSnapshot) {
|
export function effectiveComboMethodProfile(catalogDict, planningSnapshot) {
|
||||||
const cat =
|
const cat =
|
||||||
catalogDict && typeof catalogDict === 'object' && !Array.isArray(catalogDict)
|
catalogDict && typeof catalogDict === 'object' && !Array.isArray(catalogDict) ? { ...catalogDict } : {}
|
||||||
? catalogDict
|
|
||||||
: {}
|
|
||||||
if (
|
if (
|
||||||
planningSnapshot !== null &&
|
planningSnapshot === null ||
|
||||||
planningSnapshot !== undefined &&
|
planningSnapshot === undefined ||
|
||||||
typeof planningSnapshot === 'object' &&
|
typeof planningSnapshot !== 'object' ||
|
||||||
!Array.isArray(planningSnapshot)
|
Array.isArray(planningSnapshot)
|
||||||
) {
|
) {
|
||||||
return { ...planningSnapshot }
|
return { ...cat }
|
||||||
}
|
}
|
||||||
return { ...cat }
|
|
||||||
|
const plan = planningSnapshot
|
||||||
|
const merged = { ...cat, ...plan }
|
||||||
|
|
||||||
|
if (Object.prototype.hasOwnProperty.call(plan, 'slot_profiles_v1')) {
|
||||||
|
merged.slot_profiles_v1 = mergeSlotProfilesV1(cat.slot_profiles_v1, plan.slot_profiles_v1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return merged
|
||||||
}
|
}
|
||||||
|
|
||||||
export function comboPlanningProfileJsonForEditor(catalogDict, planningSnapshot) {
|
export function comboPlanningProfileJsonForEditor(catalogDict, planningSnapshot) {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user