shinkan-jinkendo/frontend/src/utils/comboPlanningMethodProfile.js
Lars 81fd7d9b3b
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
feat(training-unit-editor): enhance combo planning UI and functionality
- 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>
2026-05-13 15:49:14 +02:00

135 lines
4.2 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/** 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 = {}
for (const [k, v] of Object.entries(planObj)) {
if (v === null || v === undefined) continue
out[k] = v
}
return out
}
/** 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 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 Katalogadvance_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 coerceMergedSlotProfileRow(base)
}
/**
* 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, mergeSlotProfileFields(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 } : {}
if (planningSnapshot === null || planningSnapshot === undefined) {
return { ...cat }
}
if (typeof planningSnapshot === 'string') {
const t = planningSnapshot.trim()
if (!t || t === 'null') return { ...cat }
try {
const p = JSON.parse(t)
return effectiveComboMethodProfile(catalogDict, p)
} catch {
return { ...cat }
}
}
if (typeof planningSnapshot !== 'object' || Array.isArray(planningSnapshot)) {
return { ...cat }
}
const planRaw = planningSnapshot
const merged = { ...cat, ...omitNullUndefinedTop(planRaw) }
if (Object.prototype.hasOwnProperty.call(planRaw, 'slot_profiles_v1')) {
merged.slot_profiles_v1 = mergeSlotProfilesV1(cat.slot_profiles_v1, planRaw.slot_profiles_v1)
}
return merged
}
export function comboPlanningProfileJsonForEditor(catalogDict, planningSnapshot) {
const o = effectiveComboMethodProfile(catalogDict, planningSnapshot)
try {
return JSON.stringify(Object.keys(o).length ? o : {})
} catch {
return '{}'
}
}