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>
135 lines
4.2 KiB
JavaScript
135 lines
4.2 KiB
JavaScript
/** 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 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 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 '{}'
|
||
}
|
||
}
|