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);
|
||||
}
|
||||
.combo-plan-bracket__stations {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
list-style: decimal;
|
||||
list-style-position: outside;
|
||||
padding: 0 0 0 1.35rem;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
.combo-plan-bracket__station {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
align-items: flex-start;
|
||||
display: block;
|
||||
padding: 10px 10px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid var(--border);
|
||||
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 {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
.combo-plan-bracket__station-title {
|
||||
|
|
@ -6463,10 +6447,6 @@ a.analysis-split__nav-item {
|
|||
border-color: #444 !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 */
|
||||
|
|
|
|||
|
|
@ -10,7 +10,24 @@ import {
|
|||
combinationArchetypeLabel,
|
||||
sortCombinationSlotsForDisplay,
|
||||
} 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({
|
||||
combinationSlots,
|
||||
|
|
@ -93,12 +110,25 @@ export default function CombinationCoachSlots({
|
|||
return m
|
||||
}, [methodProfile])
|
||||
|
||||
const methodProfileKvSansSlots = useMemo(() => {
|
||||
const globalComboRows = useMemo(
|
||||
() => describeGlobalComboProfile(archeKey, methodProfile || {}),
|
||||
[archeKey, methodProfile],
|
||||
)
|
||||
|
||||
const profileExtraEntries = useMemo(() => {
|
||||
if (!methodProfile || typeof methodProfile !== 'object' || Array.isArray(methodProfile)) return []
|
||||
return Object.entries(methodProfile)
|
||||
.filter(([k]) => k !== 'slot_profiles_v1')
|
||||
.sort(([a], [b]) => a.localeCompare(b, 'de'))
|
||||
}, [methodProfile])
|
||||
const known = new Set(['slot_profiles_v1'])
|
||||
for (const f of METHOD_PROFILE_GUI_FIELDS[archeKey] || []) {
|
||||
known.add(f.key)
|
||||
}
|
||||
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 (
|
||||
<section
|
||||
|
|
@ -122,14 +152,7 @@ export default function CombinationCoachSlots({
|
|||
{compactPlanningView ? 'Stationen & Einzelübungen (Katalog)' : 'Kombination · Stationen & Einzelübungen'}
|
||||
</h3>
|
||||
{archDisplay ? (
|
||||
<p style={{ margin: '0 0 6px', fontSize: '0.95rem', fontWeight: 700 }}>
|
||||
{archDisplay}
|
||||
{archeKey && archDisplay !== archeKey ? (
|
||||
<span style={{ marginLeft: 8, fontSize: '0.78rem', fontWeight: 500, color: 'var(--text3)' }}>
|
||||
({archeKey})
|
||||
</span>
|
||||
) : null}
|
||||
</p>
|
||||
<p style={{ margin: '0 0 6px', fontSize: '0.95rem', fontWeight: 700 }}>{archDisplay}</p>
|
||||
) : null}
|
||||
{compactPlanningView ? null : (
|
||||
<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' }}>
|
||||
Geplantes Ablaufprofil (Katalog)
|
||||
Globale Eckdaten (wie im Editor)
|
||||
</div>
|
||||
{methodProfileKvSansSlots.length === 0 ? (
|
||||
{globalComboRows.length === 0 && profileExtraEntries.length === 0 ? (
|
||||
<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>
|
||||
) : (
|
||||
<dl style={{ margin: 0, fontSize: '0.82rem', lineHeight: 1.45 }}>
|
||||
{methodProfileKvSansSlots.map(([k, val]) => (
|
||||
<div key={k} style={{ marginBottom: '4px', display: 'grid', gridTemplateColumns: 'minmax(72px,1fr) minmax(0,2fr)', gap: '6px 10px' }}>
|
||||
{globalComboRows.map((row) => (
|
||||
<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>
|
||||
<dd style={{ margin: 0, color: 'var(--text1)' }}>
|
||||
{typeof val === 'boolean'
|
||||
? val
|
||||
? 'ja'
|
||||
: 'nein'
|
||||
: typeof val === 'number'
|
||||
? String(val)
|
||||
: typeof val === 'string'
|
||||
? val
|
||||
: JSON.stringify(val)}
|
||||
</dd>
|
||||
<dd style={{ margin: 0, color: 'var(--text1)' }}>{formatInlineProfileValue(val)}</dd>
|
||||
</div>
|
||||
))}
|
||||
</dl>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import React, { useMemo, useState } from 'react'
|
||||
import { archetypeCoachHint, combinationArchetypeLabel, sortCombinationSlotsForDisplay, defaultRepSeriesCountForArchetype } from '../constants/combinationArchetypes'
|
||||
import { archetypeCoachHint, combinationArchetypeLabel, sortCombinationSlotsForDisplay } from '../constants/combinationArchetypes'
|
||||
import {
|
||||
METHOD_PROFILE_GUI_FIELDS,
|
||||
parseProfileJson,
|
||||
|
|
@ -299,13 +299,13 @@ export default function CombinationMethodProfileEditor({
|
|||
Zirkel erst die globalen Arbeit‑Sekunden.
|
||||
</p>
|
||||
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
|
||||
{outlineSorted.map((slot) => {
|
||||
{outlineSorted.map((slot, ordIdx) => {
|
||||
const siRaw = slot.slot_index
|
||||
const si =
|
||||
siRaw === '' || siRaw == null ? null : typeof siRaw === 'number' ? siRaw : parseInt(String(siRaw), 10)
|
||||
if (!Number.isFinite(si)) return null
|
||||
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 serieLabel =
|
||||
slotAdv === 'timed' ? 'Wdh. ohne Wechsel' : slotAdv === 'rep' ? 'Wdh. / Serie' : 'Richtwert'
|
||||
|
|
@ -323,10 +323,7 @@ export default function CombinationMethodProfileEditor({
|
|||
background: 'var(--surface2)',
|
||||
}}
|
||||
>
|
||||
<div style={{ marginBottom: '10px', fontWeight: 700, color: 'var(--text1)', fontSize: '0.9rem' }}>
|
||||
Station {si}
|
||||
<span style={{ fontWeight: 400, marginLeft: 8, color: 'var(--text2)', fontSize: '0.86rem' }}>{ttl}</span>
|
||||
</div>
|
||||
<div style={{ marginBottom: '10px', fontWeight: 700, color: 'var(--text1)', fontSize: '0.9rem' }}>{ttl}</div>
|
||||
<div className="form-row" style={{ marginBottom: '10px', maxWidth: '22rem' }}>
|
||||
<label className="form-label" style={{ fontSize: '11px' }}>
|
||||
Steuerung
|
||||
|
|
@ -392,11 +389,7 @@ export default function CombinationMethodProfileEditor({
|
|||
min={slotAdv === 'rep' ? 1 : undefined}
|
||||
className="form-input"
|
||||
placeholder="1"
|
||||
value={
|
||||
row.rep_series_count != null && String(row.rep_series_count) !== ''
|
||||
? String(row.rep_series_count)
|
||||
: String(defaultRepSeriesCountForArchetype(methodArchetype))
|
||||
}
|
||||
value={String(parseComboRepSeriesCountUi(row.rep_series_count))}
|
||||
onChange={(e) => onSlotRepSeriesCount(si, e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -58,12 +58,7 @@ export default function CombinationPlanBracket({
|
|||
<header className="combo-plan-bracket__head">
|
||||
<div className="combo-plan-bracket__head-main">
|
||||
<span className="combo-plan-bracket__kicker">Kombinations‑Plan</span>
|
||||
<span className="combo-plan-bracket__archetype">
|
||||
{archLabel || arch || 'Archetyp'}
|
||||
{arch && archLabel && archLabel !== arch ? (
|
||||
<span className="combo-plan-bracket__archetype-id"> ({arch})</span>
|
||||
) : null}
|
||||
</span>
|
||||
<span className="combo-plan-bracket__archetype">{archLabel || arch || 'Archetyp'}</span>
|
||||
{planningAdjusted ? (
|
||||
<span className="combo-plan-bracket__badge">Planung angepasst</span>
|
||||
) : null}
|
||||
|
|
@ -102,12 +97,6 @@ export default function CombinationPlanBracket({
|
|||
|
||||
return (
|
||||
<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-title">{stationTitle}</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) {
|
||||
return (
|
||||
<div style={{ textAlign: 'center', padding: '1rem' }}>
|
||||
|
|
@ -81,8 +81,17 @@ export default function ExerciseFullContent({ exercise, loading, error, exercise
|
|||
const isCombination =
|
||||
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
|
||||
? effectiveComboMethodProfile(exercise.method_profile, planningComboMethodProfile)
|
||||
? effectiveComboMethodProfile({ ...catalogFromExercise, ...catalogFromUnit }, planningComboMethodProfile)
|
||||
: null
|
||||
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import api from '../utils/api'
|
|||
import ExerciseRichTextBlock from '../components/ExerciseRichTextBlock'
|
||||
import ExerciseAttachmentMediaStrip from '../components/ExerciseAttachmentMediaStrip'
|
||||
import { formatSkillLevelSlug } from '../constants/skillLevels'
|
||||
import { sortCombinationSlotsForDisplay } from '../constants/combinationArchetypes'
|
||||
|
||||
function TagRow({ exercise }) {
|
||||
const tags = []
|
||||
|
|
@ -147,11 +148,9 @@ function ExerciseDetailPage() {
|
|||
</p>
|
||||
) : null}
|
||||
<ol style={{ paddingLeft: '1.25rem', marginBottom: 0 }}>
|
||||
{exercise.combination_slots.map((s) => (
|
||||
<li key={`${s.slot_index}-${(s.title || '').slice(0, 8)}`} style={{ marginBottom: '10px' }}>
|
||||
<strong>
|
||||
Station {s.slot_index != null ? s.slot_index : '?'}{s.title ? ` — ${s.title}` : ''}
|
||||
</strong>
|
||||
{sortCombinationSlotsForDisplay(exercise.combination_slots).map((s, idx) => (
|
||||
<li key={`${s.slot_index}-${idx}-${(s.title || '').slice(0, 8)}`} style={{ marginBottom: '10px' }}>
|
||||
<strong>{(s.title || '').trim() || `Station ${idx + 1}`}</strong>
|
||||
<ul style={{ margin: '4px 0 0', paddingLeft: '1.2rem' }}>
|
||||
{(s.candidates && s.candidates.length
|
||||
? s.candidates
|
||||
|
|
|
|||
|
|
@ -740,6 +740,11 @@ export default function TrainingCoachPage() {
|
|||
exercise={catalogExercise}
|
||||
exerciseId={currentEntry?.item?.exercise_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={
|
||||
String(currentEntry?.item?.exercise_kind || 'simple').toLowerCase().trim() === 'combination'
|
||||
? currentEntry?.item?.planning_method_profile ?? null
|
||||
|
|
|
|||
|
|
@ -1,19 +1,56 @@
|
|||
/** 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) {
|
||||
const cat =
|
||||
catalogDict && typeof catalogDict === 'object' && !Array.isArray(catalogDict)
|
||||
? catalogDict
|
||||
: {}
|
||||
catalogDict && typeof catalogDict === 'object' && !Array.isArray(catalogDict) ? { ...catalogDict } : {}
|
||||
|
||||
if (
|
||||
planningSnapshot !== null &&
|
||||
planningSnapshot !== undefined &&
|
||||
typeof planningSnapshot === 'object' &&
|
||||
!Array.isArray(planningSnapshot)
|
||||
planningSnapshot === null ||
|
||||
planningSnapshot === undefined ||
|
||||
typeof planningSnapshot !== 'object' ||
|
||||
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) {
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user