diff --git a/frontend/src/app.css b/frontend/src/app.css index fb4d05e..ed100e8 100644 --- a/frontend/src/app.css +++ b/frontend/src/app.css @@ -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; diff --git a/frontend/src/components/TrainingUnitSectionsEditor.jsx b/frontend/src/components/TrainingUnitSectionsEditor.jsx index 6431ee1..f6fdea9 100644 --- a/frontend/src/components/TrainingUnitSectionsEditor.jsx +++ b/frontend/src/components/TrainingUnitSectionsEditor.jsx @@ -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({
{stripArchLbl || stripArchRaw || '—'} - {compactComboPlanningCaption(it)} + + {compactComboPlanningCaption(it)} +
{stripGlobalRough ? (
{ 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') { diff --git a/frontend/src/utils/comboPlanningMethodProfile.js b/frontend/src/utils/comboPlanningMethodProfile.js index e461284..24c272d 100644 --- a/frontend/src/utils/comboPlanningMethodProfile.js +++ b/frontend/src/utils/comboPlanningMethodProfile.js @@ -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) } /**