feat(training-unit-editor): enhance combo planning UI and functionality
All checks were successful
Deploy Development / deploy (push) Successful in 39s
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 1m2s
Test Suite / pytest-backend (pull_request) Successful in 33s
Test Suite / lint-backend (pull_request) Successful in 0s
Test Suite / build-frontend (pull_request) Successful in 10s
Test Suite / playwright-tests (pull_request) Successful in 1m2s
All checks were successful
Deploy Development / deploy (push) Successful in 39s
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 1m2s
Test Suite / pytest-backend (pull_request) Successful in 33s
Test Suite / lint-backend (pull_request) Successful in 0s
Test Suite / build-frontend (pull_request) Successful in 10s
Test Suite / playwright-tests (pull_request) Successful in 1m2s
- Updated CSS for the combo planning strip to improve layout and visual consistency. - Refactored `compactComboPlanningCaption` to simplify the display of planning status. - Introduced a new utility function to infer advance mode from stored slot rows, enhancing profile handling. - Improved merging logic for slot profiles to ensure accurate representation of advance modes. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
3898e8bc2c
commit
81fd7d9b3b
|
|
@ -5412,6 +5412,24 @@ a.analysis-split__nav-item {
|
|||
0 2px 12px rgba(15, 23, 42, 0.05);
|
||||
}
|
||||
|
||||
/* Kombinations‑Strip: volle Breite unter der Zeile, begrenzte Textbreite — Hauptzeile (Name/Min.) nicht verdrängen */
|
||||
.training-unit-sections-editor .tu-combo-planning-strip {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.training-unit-sections-editor .tu-combo-planning-strip__meta {
|
||||
width: 100%;
|
||||
max-width: min(100%, 42rem);
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.training-unit-sections-editor .tu-combo-planning-strip > .btn {
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
.tu-planning-mod-tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
|
|
|
|||
|
|
@ -65,18 +65,13 @@ function gatherPlanningModuleOutline(items, startIdx, moduleId) {
|
|||
|
||||
const MODULE_OUTLINE_PREVIEW_MAX = 8
|
||||
|
||||
/** Statuszeile: Planungs‑Override vs. Katalog, inkl. Archetyp‑Label wenn bekannt. */
|
||||
/** Statuszeile: nur Planungs‑Override vs. Katalog (Archetyp steht bereits links). */
|
||||
function compactComboPlanningCaption(it) {
|
||||
const overridden =
|
||||
it.planning_method_profile != null &&
|
||||
typeof it.planning_method_profile === 'object' &&
|
||||
!Array.isArray(it.planning_method_profile)
|
||||
const archRaw = String(it.catalog_method_archetype || '').trim()
|
||||
const archLbl = archRaw ? combinationArchetypeLabel(archRaw) : null
|
||||
if (overridden) {
|
||||
return archLbl ? `${archLbl} · Planung angepasst` : 'Planung angepasst'
|
||||
}
|
||||
return archLbl ? `${archLbl} · wie Katalog` : 'wie im Katalog'
|
||||
return overridden ? 'Planung angepasst' : 'wie Katalog'
|
||||
}
|
||||
|
||||
/** Globale Eckdaten aus effective profile (optional unter Stationenliste). */
|
||||
|
|
@ -1223,10 +1218,6 @@ export default function TrainingUnitSectionsEditor({
|
|||
<div
|
||||
className="tu-combo-planning-strip"
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
alignItems: 'flex-start',
|
||||
gap: '10px',
|
||||
padding: '8px 12px 10px',
|
||||
paddingLeft: enableItemDragReorder ? 44 : 12,
|
||||
borderTop: '1px solid var(--border)',
|
||||
|
|
@ -1234,9 +1225,8 @@ export default function TrainingUnitSectionsEditor({
|
|||
}}
|
||||
>
|
||||
<div
|
||||
className="tu-combo-planning-strip__meta"
|
||||
style={{
|
||||
flex: '1 1 200px',
|
||||
minWidth: 0,
|
||||
fontSize: '0.78rem',
|
||||
color: 'var(--text2)',
|
||||
lineHeight: 1.45,
|
||||
|
|
@ -1248,7 +1238,9 @@ export default function TrainingUnitSectionsEditor({
|
|||
<span style={{ color: 'var(--text1)' }}>
|
||||
{stripArchLbl || stripArchRaw || '—'}
|
||||
</span>
|
||||
<span style={{ marginLeft: 10, fontWeight: 500 }}>{compactComboPlanningCaption(it)}</span>
|
||||
<span style={{ marginLeft: 10, fontWeight: 500, whiteSpace: 'nowrap' }}>
|
||||
{compactComboPlanningCaption(it)}
|
||||
</span>
|
||||
</div>
|
||||
{stripGlobalRough ? (
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -15,6 +15,31 @@ export function normalizeAdvanceMode(v) {
|
|||
return 'timed'
|
||||
}
|
||||
|
||||
/**
|
||||
* Modus aus Roh-Zeile (Legacy ohne advance_mode; oder nur Ziel‑Wdh. ohne Sekunden).
|
||||
*/
|
||||
export function inferAdvanceModeFromStoredSlotRow(row) {
|
||||
if (!row || typeof row !== 'object') return 'timed'
|
||||
const explicitRaw = row.advance_mode
|
||||
if (explicitRaw !== undefined && explicitRaw !== null && String(explicitRaw).trim() !== '') {
|
||||
const e = normalizeAdvanceMode(explicitRaw)
|
||||
return e === 'rep' || e === 'manual' ? e : 'timed'
|
||||
}
|
||||
const load =
|
||||
row.load_sec !== undefined && row.load_sec !== null && row.load_sec !== ''
|
||||
? normalizeOptionalNonNegInt(row.load_sec)
|
||||
: undefined
|
||||
const reps =
|
||||
row.consecutive_reps !== undefined && row.consecutive_reps !== null && row.consecutive_reps !== ''
|
||||
? normalizeOptionalPositiveInt(row.consecutive_reps)
|
||||
: undefined
|
||||
|
||||
if (load != null && reps == null) return 'timed'
|
||||
if (reps != null && load == null) return 'rep'
|
||||
if (load != null && reps != null) return 'timed'
|
||||
return 'timed'
|
||||
}
|
||||
|
||||
/** UI: Serien-Anzahl aus Formularfeld; leer/ungültig ⇒ 1 (eine Serie). */
|
||||
export function parseComboRepSeriesCountUi(raw) {
|
||||
if (raw === '' || raw === undefined || raw === null) return 1
|
||||
|
|
@ -267,10 +292,10 @@ export function readSlotProfilesV1(profileObj) {
|
|||
return raw.map((row) => {
|
||||
if (!row || typeof row !== 'object') return null
|
||||
const si = Number(row.slot_index)
|
||||
const mode = normalizeAdvanceMode(row.advance_mode)
|
||||
const inferredMode = inferAdvanceModeFromStoredSlotRow(row)
|
||||
const out = {
|
||||
slot_index: Number.isFinite(si) ? si : 0,
|
||||
advance_mode: mode,
|
||||
advance_mode: inferredMode,
|
||||
load_sec: normalizeOptionalNonNegInt(row.load_sec),
|
||||
consecutive_reps: normalizeOptionalPositiveInt(row.consecutive_reps),
|
||||
rep_series_count: normalizeOptionalPositiveInt(row.rep_series_count),
|
||||
|
|
@ -289,6 +314,8 @@ export function summarizeSlotProfileBrief(r) {
|
|||
if (adv === 'timed') {
|
||||
bits.push('Zeit')
|
||||
if (r.load_sec != null) bits.push(`${r.load_sec}s Arbeit`)
|
||||
if (r.consecutive_reps != null)
|
||||
bits.push(`${r.consecutive_reps}× Wdh. ohne Wechsel zur nächsten Station`)
|
||||
} else if (adv === 'rep') {
|
||||
bits.push('Ziel‑Wdh.')
|
||||
const nSer = r.rep_series_count != null && r.rep_series_count >= 1 ? r.rep_series_count : 1
|
||||
|
|
@ -307,7 +334,8 @@ export function summarizeSlotProfileBrief(r) {
|
|||
}
|
||||
}
|
||||
if (r.intra_rep_rest_sec != null) {
|
||||
if (adv === 'timed') bits.push(`Pause ${r.intra_rep_rest_sec}s`)
|
||||
if (adv === 'timed')
|
||||
bits.push(`Pause zw. Wdh. ${r.intra_rep_rest_sec}s (nicht Stationswechsel)`)
|
||||
else if (adv === 'rep' && r.rep_series_count != null && r.rep_series_count >= 2)
|
||||
bits.push(`Pause zw. Serien ${r.intra_rep_rest_sec}s`)
|
||||
else if (
|
||||
|
|
@ -330,6 +358,7 @@ export function stationPrimaryLoadLabel(slotRow) {
|
|||
const adv = slotRow.advance_mode || 'timed'
|
||||
if (adv === 'timed') {
|
||||
if (slotRow.load_sec != null) return `${slotRow.load_sec}s`
|
||||
if (slotRow.consecutive_reps != null) return `${slotRow.consecutive_reps}×`
|
||||
return null
|
||||
}
|
||||
if (adv === 'rep') {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
/** Effektives Ablaufprofil für Kombination im Coach/in der Planung */
|
||||
|
||||
import { inferAdvanceModeFromStoredSlotRow } from './combinationMethodProfileUi'
|
||||
|
||||
/** Top-Level: null/undefined aus Planungs-Snapshot löschen keine Katalog-Felder (API liefert oft JSON-null). */
|
||||
function omitNullUndefinedTop(planObj) {
|
||||
const out = {}
|
||||
|
|
@ -11,16 +13,52 @@ function omitNullUndefinedTop(planObj) {
|
|||
}
|
||||
|
||||
/** Je Slot-Zeile: null/undefined aus Planung überschreiben keine Katalog-Werte (z. B. consecutive_reps). */
|
||||
function patchHasExplicitAdvanceMode(patch) {
|
||||
return (
|
||||
Object.prototype.hasOwnProperty.call(patch, 'advance_mode') &&
|
||||
patch.advance_mode !== null &&
|
||||
patch.advance_mode !== undefined &&
|
||||
String(patch.advance_mode).trim() !== ''
|
||||
)
|
||||
}
|
||||
|
||||
/** Nach Merge: Rep/Manual ohne Sekunden-Arbeit; Zeit ohne advance_mode-Schlüssel. */
|
||||
function coerceMergedSlotProfileRow(row) {
|
||||
if (!row || typeof row !== 'object') return row
|
||||
const inferred = inferAdvanceModeFromStoredSlotRow(row)
|
||||
if (inferred === 'rep' || inferred === 'manual') {
|
||||
delete row.load_sec
|
||||
row.advance_mode = inferred
|
||||
} else {
|
||||
delete row.advance_mode
|
||||
}
|
||||
return row
|
||||
}
|
||||
|
||||
function mergeSlotProfileFields(prev, patch) {
|
||||
const base = prev && typeof prev === 'object' ? { ...prev } : {}
|
||||
if (!patch || typeof patch !== 'object') return base
|
||||
if (!patch || typeof patch !== 'object') return coerceMergedSlotProfileRow(base)
|
||||
|
||||
for (const [k, v] of Object.entries(patch)) {
|
||||
if (v === null || v === undefined) continue
|
||||
base[k] = v
|
||||
}
|
||||
|
||||
const loadExplicit =
|
||||
Object.prototype.hasOwnProperty.call(patch, 'load_sec') &&
|
||||
patch.load_sec !== null &&
|
||||
patch.load_sec !== undefined &&
|
||||
String(patch.load_sec).trim() !== ''
|
||||
|
||||
// Nur Arbeitsssekunden aus Planung → Zeitmodus soll Katalog‑advance_mode rep nicht „festhalten“
|
||||
if (loadExplicit && !patchHasExplicitAdvanceMode(patch)) {
|
||||
delete base.advance_mode
|
||||
}
|
||||
|
||||
const ix = Number(base.slot_index)
|
||||
if (Number.isFinite(ix)) base.slot_index = ix
|
||||
return base
|
||||
|
||||
return coerceMergedSlotProfileRow(base)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user