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>
609 lines
20 KiB
JavaScript
609 lines
20 KiB
JavaScript
/**
|
||
* Geführtes method_profile für Kombinationsübungen — Felder nach method_archetype.
|
||
* Unbekannte JSON-Schlüssel bleiben beim Zusammenführen erhalten (Erweiterbarkeit).
|
||
*/
|
||
|
||
const INT_MAX = 86400
|
||
|
||
/** Pro Station: zeitlich (Standard), mengenorientiert oder coachgeführt (ohne Arbeits-Countdown). */
|
||
export const SLOT_ADVANCE_MODES = Object.freeze(['timed', 'rep', 'manual'])
|
||
|
||
export function normalizeAdvanceMode(v) {
|
||
const s = typeof v === 'string' ? v.trim().toLowerCase() : ''
|
||
if (s === 'rep' || s === 'reps' || s === 'count') return 'rep'
|
||
if (s === 'manual' || s === 'coach' || s === 'coach_led') return 'manual'
|
||
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). */
|
||
export function parseComboRepSeriesCountUi(raw) {
|
||
if (raw === '' || raw === undefined || raw === null) return 1
|
||
const n =
|
||
typeof raw === 'number' && Number.isFinite(raw) ? Math.round(raw) : parseInt(String(raw).trim(), 10)
|
||
if (!Number.isFinite(n) || n < 1) return 1
|
||
return n
|
||
}
|
||
|
||
function parseProfileJson(raw) {
|
||
if (typeof raw !== 'string' || !raw.trim()) return { ok: true, obj: {} }
|
||
try {
|
||
const p = JSON.parse(raw)
|
||
if (!p || typeof p !== 'object' || Array.isArray(p)) {
|
||
return { ok: false, error: 'Ablaufprofil muss ein JSON-Objekt sein.' }
|
||
}
|
||
return { ok: true, obj: { ...p } }
|
||
} catch {
|
||
return { ok: false, error: 'Ablaufprofil (JSON): Syntax ungültig.' }
|
||
}
|
||
}
|
||
|
||
/** Pro Archetyp: UI-Feldbeschreibungen (Werte werden in method_profile geschrieben) */
|
||
export const METHOD_PROFILE_GUI_FIELDS = Object.freeze({
|
||
sequence_linear: [
|
||
{
|
||
key: 'rounds',
|
||
kind: 'int',
|
||
label: 'Anzahl Gesamtdurchläufe (komplette Sequenz, alle Stationen nacheinander)',
|
||
min: 1,
|
||
max: 999,
|
||
},
|
||
{
|
||
key: 'hint_step_duration_sec',
|
||
kind: 'int',
|
||
label: 'Orientierung: Sekunden je Station/Schritt (optional)',
|
||
min: 5,
|
||
max: INT_MAX,
|
||
},
|
||
{
|
||
key: 'block_intro_sec',
|
||
kind: 'int',
|
||
label: 'Einführung / Demon am Block Gesamt (Sek., optional)',
|
||
min: 0,
|
||
max: INT_MAX,
|
||
},
|
||
],
|
||
circuit_rotate_time: [
|
||
{
|
||
key: 'rounds',
|
||
kind: 'int',
|
||
label:
|
||
'Anzahl Gesamtdurchläufe (jede Station pro Sportler mehrfach beim Umlauf, z. B. 4 Stationen × 2 = zwei komplette Runden)',
|
||
min: 1,
|
||
max: 999,
|
||
},
|
||
{
|
||
key: 'work_seconds',
|
||
kind: 'int',
|
||
label: 'Arbeitszeit pro Station (Sek.)',
|
||
min: 5,
|
||
max: INT_MAX,
|
||
},
|
||
{
|
||
key: 'transition_seconds',
|
||
kind: 'int',
|
||
label: 'Wechsel / Rotation (Sek., optional)',
|
||
min: 0,
|
||
max: INT_MAX,
|
||
},
|
||
{
|
||
key: 'rest_seconds',
|
||
kind: 'int',
|
||
label: 'Pause zwischen Runden oder Stationen-Folgen (Sek., optional)',
|
||
min: 0,
|
||
max: INT_MAX,
|
||
},
|
||
],
|
||
circuit_all_parallel: [
|
||
{
|
||
key: 'rounds',
|
||
kind: 'int',
|
||
label: 'Anzahl Durchläufe (wenn alle parallel dieselbe Rundenlogik haben, optional)',
|
||
min: 1,
|
||
max: 999,
|
||
},
|
||
{
|
||
key: 'explain_before_seconds',
|
||
kind: 'int',
|
||
label: 'Zeitfenster Vorab‑Erklärung aller Stationen (Sek., optional)',
|
||
min: 0,
|
||
max: INT_MAX,
|
||
},
|
||
{
|
||
key: 'simultaneous_start',
|
||
kind: 'bool',
|
||
label: 'Alle Stationen starten zusammen nach Erklärung',
|
||
},
|
||
],
|
||
station_parcour: [
|
||
{
|
||
key: 'rounds',
|
||
kind: 'int',
|
||
label: 'Anzahl Durchläufe des Parcours (Start Station 1, alle Bahnpunkte, Wiederholung bei Bedarf)',
|
||
min: 1,
|
||
max: 999,
|
||
},
|
||
{
|
||
key: 'allow_free_visit_order',
|
||
kind: 'bool',
|
||
label: 'Reihenfolge der Besuche frei (Parcours / Abhaken-Logik später im Coach)',
|
||
},
|
||
],
|
||
pair_superset: [
|
||
{
|
||
key: 'switch_seconds',
|
||
kind: 'int',
|
||
label: 'Orientierung: Wechselpause A↔B (Sek., optional)',
|
||
min: 0,
|
||
max: INT_MAX,
|
||
},
|
||
{
|
||
key: 'work_seconds_per_side',
|
||
kind: 'int',
|
||
label: 'Arbeit pro Rolle oder Seite (Sek., optional)',
|
||
min: 5,
|
||
max: INT_MAX,
|
||
},
|
||
],
|
||
time_domain_interval: [
|
||
{
|
||
key: 'work_seconds',
|
||
kind: 'int',
|
||
label: 'Intervall: Belastungszeit (Sek.)',
|
||
min: 5,
|
||
max: INT_MAX,
|
||
},
|
||
{
|
||
key: 'rest_seconds',
|
||
kind: 'int',
|
||
label: 'Intervall: Erholungszeit (Sek., optional)',
|
||
min: 0,
|
||
max: INT_MAX,
|
||
},
|
||
{
|
||
key: 'interval_rounds',
|
||
kind: 'int',
|
||
label: 'Anzahl Wiederholungen der Intervalldomäne (komplette Zyklen Arbeit/Pause)',
|
||
min: 1,
|
||
max: 999,
|
||
},
|
||
],
|
||
free_method_block: [],
|
||
})
|
||
|
||
function shortenComboGuiCaption(label) {
|
||
const t = (label || '').trim()
|
||
if (!t) return ''
|
||
const cut = t.split('(')[0].trim()
|
||
return cut.length > 52 ? `${cut.slice(0, 50)}…` : cut
|
||
}
|
||
|
||
/**
|
||
* Globale Archetyp-Felder aus method_profile für Lesetext (Vorschau, Druck).
|
||
* Ignoriert slot_profiles_v1 (kommt separat je Station).
|
||
*/
|
||
export function describeGlobalComboProfile(archetypeKey, profileObj) {
|
||
const arch = typeof archetypeKey === 'string' ? archetypeKey.trim() : ''
|
||
if (!profileObj || typeof profileObj !== 'object' || Array.isArray(profileObj)) return []
|
||
const defs = METHOD_PROFILE_GUI_FIELDS[arch] || []
|
||
const rows = []
|
||
for (const def of defs) {
|
||
const val = profileObj[def.key]
|
||
if (val === undefined || val === null || val === '') continue
|
||
if (def.kind === 'bool') {
|
||
const on = val === true || val === 'true' || val === 1 || val === '1'
|
||
rows.push({
|
||
key: def.key,
|
||
caption: shortenComboGuiCaption(def.label),
|
||
detailLabel: def.label,
|
||
value: on ? 'Ja' : 'Nein',
|
||
})
|
||
} else {
|
||
rows.push({
|
||
key: def.key,
|
||
caption: shortenComboGuiCaption(def.label),
|
||
detailLabel: def.label,
|
||
value: String(val),
|
||
})
|
||
}
|
||
}
|
||
return rows
|
||
}
|
||
|
||
/**
|
||
* Aktualisiert method_profile unter Beibehaltung nicht-GUI Schlüssel.
|
||
*/
|
||
export function updateProfileGuided(archetype, rawJson, key, parsedValue, kind) {
|
||
const arch = typeof archetype === 'string' ? archetype.trim() : ''
|
||
const parsed = parseProfileJson(rawJson)
|
||
if (!parsed.ok) return parsed
|
||
const next = { ...parsed.obj }
|
||
|
||
if (kind === 'bool') {
|
||
if (parsedValue) next[key] = true
|
||
else delete next[key]
|
||
} else if (kind === 'int') {
|
||
if (parsedValue === null || parsedValue === undefined || parsedValue === '') {
|
||
delete next[key]
|
||
} else {
|
||
const n = typeof parsedValue === 'number' ? parsedValue : parseInt(String(parsedValue), 10)
|
||
if (!Number.isFinite(n)) delete next[key]
|
||
else next[key] = n
|
||
}
|
||
}
|
||
|
||
const outJson = JSON.stringify(next)
|
||
return { ok: true, obj: next, json: outJson === '{}' ? '{}' : outJson }
|
||
}
|
||
|
||
export function setFullProfileRawJson(rawEditable) {
|
||
const parsed = parseProfileJson(rawEditable)
|
||
if (!parsed.ok) return parsed
|
||
const j = JSON.stringify(parsed.obj)
|
||
return { ok: true, obj: parsed.obj, json: j === '{}' ? '{}' : j }
|
||
}
|
||
|
||
/**
|
||
* Pfad für slot_profiles_v1 und ähnliche strukturierte Erweiterungen.
|
||
* Ungültiges JSON gibt { ok:false } zurück; mutator erhält geklontes Profil‑Objekt.
|
||
*/
|
||
export function patchMethodProfile(rawJson, mutator) {
|
||
const parsed = parseProfileJson(rawJson || '{}')
|
||
if (!parsed.ok) return parsed
|
||
const draft = { ...parsed.obj }
|
||
mutator(draft)
|
||
try {
|
||
const j = JSON.stringify(draft)
|
||
return { ok: true, obj: draft, json: j === '{}' ? '{}' : j }
|
||
} catch {
|
||
return { ok: false, error: 'Ablaufprofil konnte nicht gespeichert werden.' }
|
||
}
|
||
}
|
||
|
||
/** Normalisiert slot_profiles_v1 aus dem gespeicherten Profil */
|
||
export function readSlotProfilesV1(profileObj) {
|
||
if (!profileObj || typeof profileObj !== 'object') return []
|
||
const raw = profileObj.slot_profiles_v1
|
||
if (!Array.isArray(raw)) return []
|
||
return raw.map((row) => {
|
||
if (!row || typeof row !== 'object') return null
|
||
const si = Number(row.slot_index)
|
||
const inferredMode = inferAdvanceModeFromStoredSlotRow(row)
|
||
const out = {
|
||
slot_index: Number.isFinite(si) ? si : 0,
|
||
advance_mode: inferredMode,
|
||
load_sec: normalizeOptionalNonNegInt(row.load_sec),
|
||
consecutive_reps: normalizeOptionalPositiveInt(row.consecutive_reps),
|
||
rep_series_count: normalizeOptionalPositiveInt(row.rep_series_count),
|
||
intra_rep_rest_sec: normalizeOptionalNonNegInt(row.intra_rep_rest_sec),
|
||
transition_after_sec: normalizeOptionalNonNegInt(row.transition_after_sec),
|
||
}
|
||
return out
|
||
}).filter(Boolean)
|
||
}
|
||
|
||
/** Kurztext für Listen/strip (Coach „Plan:“ — gleiche Logik). */
|
||
export function summarizeSlotProfileBrief(r) {
|
||
if (!r) return null
|
||
const adv = r.advance_mode || 'timed'
|
||
const bits = []
|
||
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
|
||
if (r.consecutive_reps != null) {
|
||
if (nSer >= 2) bits.push(`${nSer} Serien à ${r.consecutive_reps}×`)
|
||
else bits.push(`${r.consecutive_reps}×`)
|
||
}
|
||
} else {
|
||
bits.push('Coach')
|
||
const nSerMan = r.rep_series_count != null && r.rep_series_count >= 2 ? r.rep_series_count : 1
|
||
if (r.consecutive_reps != null) {
|
||
if (nSerMan >= 2) bits.push(`${nSerMan} Serien à Richtwert ${r.consecutive_reps}×`)
|
||
else bits.push(`Richtwert ${r.consecutive_reps}×`)
|
||
} else if (r.rep_series_count != null && r.rep_series_count >= 2) {
|
||
bits.push(`${r.rep_series_count} Serien`)
|
||
}
|
||
}
|
||
if (r.intra_rep_rest_sec != null) {
|
||
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 (
|
||
adv === 'manual' &&
|
||
r.rep_series_count != null &&
|
||
r.rep_series_count >= 2
|
||
) {
|
||
bits.push(`Pause zw. Serien ${r.intra_rep_rest_sec}s`)
|
||
}
|
||
}
|
||
if (r.transition_after_sec != null) bits.push(`Wechsel ${r.transition_after_sec}s`)
|
||
return bits.join(' · ')
|
||
}
|
||
|
||
/**
|
||
* Kompakte Stations-Belastung für die Plan-Klammer (links): Sekunden oder Wdh., nicht Slot-ID.
|
||
*/
|
||
export function stationPrimaryLoadLabel(slotRow) {
|
||
if (!slotRow || typeof slotRow !== 'object') return null
|
||
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') {
|
||
if (slotRow.consecutive_reps != null) return `${slotRow.consecutive_reps}×`
|
||
return null
|
||
}
|
||
if (adv === 'manual') {
|
||
if (slotRow.consecutive_reps != null) return `~${slotRow.consecutive_reps}×`
|
||
return null
|
||
}
|
||
return null
|
||
}
|
||
|
||
function globalTimingHintsForArchetype(arch, mp) {
|
||
if (!mp || typeof mp !== 'object' || Array.isArray(mp)) return []
|
||
const bits = []
|
||
switch (arch) {
|
||
case 'circuit_rotate_time':
|
||
if (mp.work_seconds != null && mp.work_seconds !== '') bits.push(`${mp.work_seconds}s Arbeit je Station`)
|
||
if (mp.transition_seconds != null && mp.transition_seconds !== '')
|
||
bits.push(`Rotation ${mp.transition_seconds}s`)
|
||
if (mp.rest_seconds != null && mp.rest_seconds !== '') bits.push(`Pause ${mp.rest_seconds}s`)
|
||
if (mp.rounds != null && mp.rounds !== '') bits.push(`${mp.rounds} Umlauf‑Runden`)
|
||
break
|
||
case 'sequence_linear':
|
||
if (mp.hint_step_duration_sec != null && mp.hint_step_duration_sec !== '')
|
||
bits.push(`~${mp.hint_step_duration_sec}s je Station`)
|
||
if (mp.rounds != null && mp.rounds !== '') bits.push(`${mp.rounds} Sequenz‑Durchläufe`)
|
||
if (mp.block_intro_sec != null && mp.block_intro_sec !== '') bits.push(`Block‑Intro ${mp.block_intro_sec}s`)
|
||
break
|
||
case 'time_domain_interval':
|
||
if (mp.work_seconds != null && mp.work_seconds !== '') bits.push(`${mp.work_seconds}s Intervall‑Arbeit`)
|
||
if (mp.rest_seconds != null && mp.rest_seconds !== '') bits.push(`${mp.rest_seconds}s Erholung`)
|
||
if (mp.interval_rounds != null && mp.interval_rounds !== '') bits.push(`${mp.interval_rounds} Intervall‑Zyklen`)
|
||
break
|
||
case 'pair_superset':
|
||
if (mp.work_seconds_per_side != null && mp.work_seconds_per_side !== '')
|
||
bits.push(`${mp.work_seconds_per_side}s Arbeit`)
|
||
if (mp.switch_seconds != null && mp.switch_seconds !== '') bits.push(`Wechsel ${mp.switch_seconds}s`)
|
||
break
|
||
case 'station_parcour':
|
||
if (mp.rounds != null && mp.rounds !== '') bits.push(`${mp.rounds} Parcours‑Runden`)
|
||
break
|
||
case 'circuit_all_parallel':
|
||
if (mp.rounds != null && mp.rounds !== '') bits.push(`${mp.rounds} Runden`)
|
||
if (mp.explain_before_seconds != null && mp.explain_before_seconds !== '')
|
||
bits.push(`Erklärung ${mp.explain_before_seconds}s`)
|
||
break
|
||
default:
|
||
break
|
||
}
|
||
return bits
|
||
}
|
||
|
||
function isWeakSlotTimingSummary(txt) {
|
||
if (!txt || typeof txt !== 'string') return true
|
||
const t = txt.trim()
|
||
return t === 'Zeit' || t === 'Coach' || t === 'Ziel‑Wdh.'
|
||
}
|
||
|
||
/**
|
||
* Stationszeile für Lesetext: Slot‑Zeiten + bei Bedarf globale Eckdaten (Zirkel‑Sekunden, Runden …).
|
||
*/
|
||
export function effectiveStationTimingSummary(archetypeKey, profileObj, slotRow) {
|
||
const arch = typeof archetypeKey === 'string' ? archetypeKey.trim() : ''
|
||
const mp = profileObj && typeof profileObj === 'object' && !Array.isArray(profileObj) ? profileObj : {}
|
||
const slotTxt = summarizeSlotProfileBrief(slotRow)
|
||
const hints = globalTimingHintsForArchetype(arch, mp)
|
||
const hintStr = hints.join(' · ')
|
||
|
||
if (!isWeakSlotTimingSummary(slotTxt)) {
|
||
const extras = []
|
||
for (const h of hints) {
|
||
if (
|
||
/Runden|Durchläufe|Zyklen|Umlauf/i.test(h) &&
|
||
slotTxt &&
|
||
!/Runden|Serien|×|\d+s Arbeit|\d+s Erholung|\d+s Intervall/i.test(slotTxt)
|
||
) {
|
||
extras.push(h)
|
||
}
|
||
}
|
||
return extras.length ? `${slotTxt} · ${extras.join(' · ')}` : slotTxt
|
||
}
|
||
|
||
if (hintStr) return hintStr
|
||
if (slotTxt) return slotTxt
|
||
return null
|
||
}
|
||
|
||
function normalizeOptionalNonNegInt(v) {
|
||
if (v === '' || v === undefined || v === null) return undefined
|
||
const n = typeof v === 'number' ? v : parseInt(String(v), 10)
|
||
if (!Number.isFinite(n) || n < 0) return undefined
|
||
return Math.round(n)
|
||
}
|
||
|
||
function normalizeOptionalPositiveInt(v) {
|
||
const n = normalizeOptionalNonNegInt(v)
|
||
if (n === undefined) return undefined
|
||
if (n < 1) return undefined
|
||
return n
|
||
}
|
||
|
||
const SLOT_TIMING_FIELDS = /** @type {const} */ ([
|
||
'load_sec',
|
||
'consecutive_reps',
|
||
'rep_series_count',
|
||
'intra_rep_rest_sec',
|
||
'transition_after_sec',
|
||
])
|
||
|
||
function slotProfileRowShouldKeep(nextRow) {
|
||
if (!nextRow || typeof nextRow !== 'object') return false
|
||
const mode = normalizeAdvanceMode(nextRow.advance_mode)
|
||
if (mode !== 'timed') return true
|
||
return SLOT_TIMING_FIELDS.some((k) => nextRow[k] !== undefined && nextRow[k] !== null)
|
||
}
|
||
|
||
function writeSlotProfilesV1Arr(profileDraft, arr) {
|
||
const sorted = [...arr].sort((a, b) => Number(a.slot_index) - Number(b.slot_index))
|
||
if (sorted.length === 0) delete profileDraft.slot_profiles_v1
|
||
else profileDraft.slot_profiles_v1 = sorted
|
||
}
|
||
|
||
/** Steuert Ende der Arbeitsphase: Zeit, Wiederholungsziel oder nur manuell weiter. */
|
||
export function patchSlotAdvanceMode(profileDraft, slotIndex, modeRaw) {
|
||
const ix =
|
||
typeof slotIndex === 'number' && Number.isFinite(slotIndex)
|
||
? slotIndex
|
||
: parseInt(String(slotIndex), 10)
|
||
if (!Number.isFinite(ix)) return
|
||
|
||
const mode = normalizeAdvanceMode(modeRaw)
|
||
let arr = Array.isArray(profileDraft.slot_profiles_v1) ? [...profileDraft.slot_profiles_v1] : []
|
||
const found = arr.findIndex((r) => r && typeof r === 'object' && Number(r.slot_index) === ix)
|
||
|
||
const nextRow = {}
|
||
if (found >= 0 && arr[found] && typeof arr[found] === 'object') {
|
||
Object.assign(nextRow, arr[found])
|
||
}
|
||
nextRow.slot_index = ix
|
||
if (mode === 'timed') delete nextRow.advance_mode
|
||
else {
|
||
nextRow.advance_mode = mode
|
||
delete nextRow.load_sec
|
||
}
|
||
|
||
let nextArr
|
||
if (!slotProfileRowShouldKeep(nextRow)) {
|
||
nextArr = found >= 0 ? arr.filter((_, i) => i !== found) : arr
|
||
} else if (found >= 0) {
|
||
nextArr = [...arr]
|
||
nextArr[found] = nextRow
|
||
} else {
|
||
nextArr = [...arr, nextRow]
|
||
}
|
||
|
||
writeSlotProfilesV1Arr(profileDraft, nextArr)
|
||
}
|
||
|
||
/** '', null = Feld entfernen; sonst gültige Zahl setzen */
|
||
export function patchSlotTimingField(profileDraft, slotIndex, field, rawInput) {
|
||
if (!SLOT_TIMING_FIELDS.includes(field)) return
|
||
const ix =
|
||
typeof slotIndex === 'number' && Number.isFinite(slotIndex)
|
||
? slotIndex
|
||
: parseInt(String(slotIndex), 10)
|
||
if (!Number.isFinite(ix)) return
|
||
|
||
let arr = Array.isArray(profileDraft.slot_profiles_v1) ? [...profileDraft.slot_profiles_v1] : []
|
||
let found = arr.findIndex((r) => r && typeof r === 'object' && Number(r.slot_index) === ix)
|
||
|
||
const nextRow = {}
|
||
if (found >= 0 && arr[found] && typeof arr[found] === 'object') {
|
||
Object.assign(nextRow, arr[found])
|
||
}
|
||
nextRow.slot_index = ix
|
||
|
||
if (rawInput === null || rawInput === undefined || String(rawInput).trim() === '') {
|
||
delete nextRow[field]
|
||
} else if (field === 'consecutive_reps' || field === 'rep_series_count') {
|
||
const n = normalizeOptionalPositiveInt(rawInput)
|
||
if (n === undefined) delete nextRow[field]
|
||
else nextRow[field] = n
|
||
} else {
|
||
const n = normalizeOptionalNonNegInt(rawInput)
|
||
if (n === undefined) delete nextRow[field]
|
||
else nextRow[field] = n
|
||
}
|
||
|
||
const keep = slotProfileRowShouldKeep(nextRow)
|
||
|
||
if (found >= 0) {
|
||
if (!keep) {
|
||
arr = arr.filter((_, i) => i !== found)
|
||
} else {
|
||
arr[found] = nextRow
|
||
}
|
||
} else if (keep) {
|
||
arr.push(nextRow)
|
||
}
|
||
|
||
writeSlotProfilesV1Arr(profileDraft, arr)
|
||
}
|
||
|
||
/** Rotierender Zirkel: typische Ableitungen (setzt Sekunden konkret). */
|
||
export function applyCircuitRotateQuickRatio(profileDraft, preset) {
|
||
const wRaw = profileDraft.work_seconds
|
||
const work =
|
||
typeof wRaw === 'number' && Number.isFinite(wRaw) ? Math.round(wRaw) : parseInt(String(wRaw), 10)
|
||
if (!Number.isFinite(work) || work <= 0)
|
||
return { ok: false, error: 'Zuerst Arbeitszeit pro Station (Sek.) setzen.' }
|
||
profileDraft.timing_schema = profileDraft.timing_schema ?? 1
|
||
if (preset === 'transition_equals_work') {
|
||
profileDraft.transition_seconds = work
|
||
return { ok: true }
|
||
}
|
||
if (preset === 'round_rest_equals_work') {
|
||
profileDraft.rest_seconds = work
|
||
return { ok: true }
|
||
}
|
||
if (preset === 'round_rest_two_thirds_work') {
|
||
profileDraft.rest_seconds = Math.round((work * 2) / 3)
|
||
return { ok: true }
|
||
}
|
||
return { ok: false, error: 'Unbekannte Schnellwahl.' }
|
||
}
|
||
|
||
export function applyIntervalDomainQuickRatio(profileDraft, preset) {
|
||
const wRaw = profileDraft.work_seconds
|
||
const work =
|
||
typeof wRaw === 'number' && Number.isFinite(wRaw) ? Math.round(wRaw) : parseInt(String(wRaw), 10)
|
||
if (!Number.isFinite(work) || work <= 0)
|
||
return { ok: false, error: 'Zuerst Belastungszeit Intervall (Sek.) setzen.' }
|
||
profileDraft.timing_schema = profileDraft.timing_schema ?? 1
|
||
if (preset === 'rest_equals_work') {
|
||
profileDraft.rest_seconds = work
|
||
return { ok: true }
|
||
}
|
||
if (preset === 'rest_two_thirds_work') {
|
||
profileDraft.rest_seconds = Math.round((work * 2) / 3)
|
||
return { ok: true }
|
||
}
|
||
return { ok: false, error: 'Unbekannte Schnellwahl.' }
|
||
}
|
||
|
||
export { parseProfileJson, INT_MAX }
|