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);
|
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 {
|
.tu-planning-mod-tag {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
|
||||||
|
|
@ -65,18 +65,13 @@ function gatherPlanningModuleOutline(items, startIdx, moduleId) {
|
||||||
|
|
||||||
const MODULE_OUTLINE_PREVIEW_MAX = 8
|
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) {
|
function compactComboPlanningCaption(it) {
|
||||||
const overridden =
|
const overridden =
|
||||||
it.planning_method_profile != null &&
|
it.planning_method_profile != null &&
|
||||||
typeof it.planning_method_profile === 'object' &&
|
typeof it.planning_method_profile === 'object' &&
|
||||||
!Array.isArray(it.planning_method_profile)
|
!Array.isArray(it.planning_method_profile)
|
||||||
const archRaw = String(it.catalog_method_archetype || '').trim()
|
return overridden ? 'Planung angepasst' : 'wie Katalog'
|
||||||
const archLbl = archRaw ? combinationArchetypeLabel(archRaw) : null
|
|
||||||
if (overridden) {
|
|
||||||
return archLbl ? `${archLbl} · Planung angepasst` : 'Planung angepasst'
|
|
||||||
}
|
|
||||||
return archLbl ? `${archLbl} · wie Katalog` : 'wie im Katalog'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Globale Eckdaten aus effective profile (optional unter Stationenliste). */
|
/** Globale Eckdaten aus effective profile (optional unter Stationenliste). */
|
||||||
|
|
@ -1223,10 +1218,6 @@ export default function TrainingUnitSectionsEditor({
|
||||||
<div
|
<div
|
||||||
className="tu-combo-planning-strip"
|
className="tu-combo-planning-strip"
|
||||||
style={{
|
style={{
|
||||||
display: 'flex',
|
|
||||||
flexWrap: 'wrap',
|
|
||||||
alignItems: 'flex-start',
|
|
||||||
gap: '10px',
|
|
||||||
padding: '8px 12px 10px',
|
padding: '8px 12px 10px',
|
||||||
paddingLeft: enableItemDragReorder ? 44 : 12,
|
paddingLeft: enableItemDragReorder ? 44 : 12,
|
||||||
borderTop: '1px solid var(--border)',
|
borderTop: '1px solid var(--border)',
|
||||||
|
|
@ -1234,9 +1225,8 @@ export default function TrainingUnitSectionsEditor({
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
|
className="tu-combo-planning-strip__meta"
|
||||||
style={{
|
style={{
|
||||||
flex: '1 1 200px',
|
|
||||||
minWidth: 0,
|
|
||||||
fontSize: '0.78rem',
|
fontSize: '0.78rem',
|
||||||
color: 'var(--text2)',
|
color: 'var(--text2)',
|
||||||
lineHeight: 1.45,
|
lineHeight: 1.45,
|
||||||
|
|
@ -1248,7 +1238,9 @@ export default function TrainingUnitSectionsEditor({
|
||||||
<span style={{ color: 'var(--text1)' }}>
|
<span style={{ color: 'var(--text1)' }}>
|
||||||
{stripArchLbl || stripArchRaw || '—'}
|
{stripArchLbl || stripArchRaw || '—'}
|
||||||
</span>
|
</span>
|
||||||
<span style={{ marginLeft: 10, fontWeight: 500 }}>{compactComboPlanningCaption(it)}</span>
|
<span style={{ marginLeft: 10, fontWeight: 500, whiteSpace: 'nowrap' }}>
|
||||||
|
{compactComboPlanningCaption(it)}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{stripGlobalRough ? (
|
{stripGlobalRough ? (
|
||||||
<div
|
<div
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,31 @@ export function normalizeAdvanceMode(v) {
|
||||||
return 'timed'
|
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). */
|
/** UI: Serien-Anzahl aus Formularfeld; leer/ungültig ⇒ 1 (eine Serie). */
|
||||||
export function parseComboRepSeriesCountUi(raw) {
|
export function parseComboRepSeriesCountUi(raw) {
|
||||||
if (raw === '' || raw === undefined || raw === null) return 1
|
if (raw === '' || raw === undefined || raw === null) return 1
|
||||||
|
|
@ -267,10 +292,10 @@ export function readSlotProfilesV1(profileObj) {
|
||||||
return raw.map((row) => {
|
return raw.map((row) => {
|
||||||
if (!row || typeof row !== 'object') return null
|
if (!row || typeof row !== 'object') return null
|
||||||
const si = Number(row.slot_index)
|
const si = Number(row.slot_index)
|
||||||
const mode = normalizeAdvanceMode(row.advance_mode)
|
const inferredMode = inferAdvanceModeFromStoredSlotRow(row)
|
||||||
const out = {
|
const out = {
|
||||||
slot_index: Number.isFinite(si) ? si : 0,
|
slot_index: Number.isFinite(si) ? si : 0,
|
||||||
advance_mode: mode,
|
advance_mode: inferredMode,
|
||||||
load_sec: normalizeOptionalNonNegInt(row.load_sec),
|
load_sec: normalizeOptionalNonNegInt(row.load_sec),
|
||||||
consecutive_reps: normalizeOptionalPositiveInt(row.consecutive_reps),
|
consecutive_reps: normalizeOptionalPositiveInt(row.consecutive_reps),
|
||||||
rep_series_count: normalizeOptionalPositiveInt(row.rep_series_count),
|
rep_series_count: normalizeOptionalPositiveInt(row.rep_series_count),
|
||||||
|
|
@ -289,6 +314,8 @@ export function summarizeSlotProfileBrief(r) {
|
||||||
if (adv === 'timed') {
|
if (adv === 'timed') {
|
||||||
bits.push('Zeit')
|
bits.push('Zeit')
|
||||||
if (r.load_sec != null) bits.push(`${r.load_sec}s Arbeit`)
|
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') {
|
} else if (adv === 'rep') {
|
||||||
bits.push('Ziel‑Wdh.')
|
bits.push('Ziel‑Wdh.')
|
||||||
const nSer = r.rep_series_count != null && r.rep_series_count >= 1 ? r.rep_series_count : 1
|
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 (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)
|
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`)
|
bits.push(`Pause zw. Serien ${r.intra_rep_rest_sec}s`)
|
||||||
else if (
|
else if (
|
||||||
|
|
@ -330,6 +358,7 @@ export function stationPrimaryLoadLabel(slotRow) {
|
||||||
const adv = slotRow.advance_mode || 'timed'
|
const adv = slotRow.advance_mode || 'timed'
|
||||||
if (adv === 'timed') {
|
if (adv === 'timed') {
|
||||||
if (slotRow.load_sec != null) return `${slotRow.load_sec}s`
|
if (slotRow.load_sec != null) return `${slotRow.load_sec}s`
|
||||||
|
if (slotRow.consecutive_reps != null) return `${slotRow.consecutive_reps}×`
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
if (adv === 'rep') {
|
if (adv === 'rep') {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
/** Effektives Ablaufprofil für Kombination im Coach/in der Planung */
|
/** 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). */
|
/** Top-Level: null/undefined aus Planungs-Snapshot löschen keine Katalog-Felder (API liefert oft JSON-null). */
|
||||||
function omitNullUndefinedTop(planObj) {
|
function omitNullUndefinedTop(planObj) {
|
||||||
const out = {}
|
const out = {}
|
||||||
|
|
@ -11,16 +13,52 @@ function omitNullUndefinedTop(planObj) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Je Slot-Zeile: null/undefined aus Planung überschreiben keine Katalog-Werte (z. B. consecutive_reps). */
|
/** 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) {
|
function mergeSlotProfileFields(prev, patch) {
|
||||||
const base = prev && typeof prev === 'object' ? { ...prev } : {}
|
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)) {
|
for (const [k, v] of Object.entries(patch)) {
|
||||||
if (v === null || v === undefined) continue
|
if (v === null || v === undefined) continue
|
||||||
base[k] = v
|
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)
|
const ix = Number(base.slot_index)
|
||||||
if (Number.isFinite(ix)) base.slot_index = ix
|
if (Number.isFinite(ix)) base.slot_index = ix
|
||||||
return base
|
|
||||||
|
return coerceMergedSlotProfileRow(base)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user