/** 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 '{}' } }