2026-05-13 07:16:58 +02:00
|
|
|
|
import React, { useMemo, useState } from 'react'
|
2026-05-13 07:38:44 +02:00
|
|
|
|
import { archetypeCoachHint, combinationArchetypeLabel, sortCombinationSlotsForDisplay } from '../constants/combinationArchetypes'
|
2026-05-13 07:16:58 +02:00
|
|
|
|
import {
|
|
|
|
|
|
METHOD_PROFILE_GUI_FIELDS,
|
|
|
|
|
|
parseProfileJson,
|
|
|
|
|
|
setFullProfileRawJson,
|
|
|
|
|
|
updateProfileGuided,
|
2026-05-13 07:38:44 +02:00
|
|
|
|
patchMethodProfile,
|
|
|
|
|
|
readSlotProfilesV1,
|
|
|
|
|
|
patchSlotTimingField,
|
|
|
|
|
|
applyCircuitRotateQuickRatio,
|
|
|
|
|
|
applyIntervalDomainQuickRatio,
|
2026-05-13 07:16:58 +02:00
|
|
|
|
} from '../utils/combinationMethodProfileUi'
|
|
|
|
|
|
|
|
|
|
|
|
function clampInt(n, min, max) {
|
|
|
|
|
|
if (!Number.isFinite(n)) return null
|
|
|
|
|
|
let x = n
|
|
|
|
|
|
if (typeof min === 'number' && x < min) x = min
|
|
|
|
|
|
if (typeof max === 'number' && x > max) x = max
|
|
|
|
|
|
return Math.round(x)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-13 07:38:44 +02:00
|
|
|
|
/** Archetypen mit klar bezifferbarer Stationslogik · alle mit Slot-Liste sinnvoll */
|
|
|
|
|
|
const ARCHETYPES_WITH_SLOT_TIMING = new Set([
|
|
|
|
|
|
'circuit_rotate_time',
|
|
|
|
|
|
'sequence_linear',
|
|
|
|
|
|
'station_parcour',
|
|
|
|
|
|
'time_domain_interval',
|
|
|
|
|
|
'circuit_all_parallel',
|
|
|
|
|
|
'pair_superset',
|
|
|
|
|
|
'free_method_block',
|
|
|
|
|
|
])
|
|
|
|
|
|
|
2026-05-13 07:16:58 +02:00
|
|
|
|
/**
|
2026-05-13 07:38:44 +02:00
|
|
|
|
* Kombination: geführtes method_profile (+ optional Stationszeilen, ohne JSON für Trainer).
|
|
|
|
|
|
*
|
|
|
|
|
|
* @param {boolean} [props.plannerMode] — z. B. Planungs‑Override: keine Roh‑JSON‑Sektion.
|
|
|
|
|
|
* @param {boolean} [props.allowExpertJson] — wenn true und nicht plannerMode: Roh‑JSON (Support).
|
|
|
|
|
|
* @param {{ slot_index?: number|string, title?: string }[]} [props.comboSlotsOutline] — für Slot‑Felder aus der Übung
|
2026-05-13 07:16:58 +02:00
|
|
|
|
*/
|
|
|
|
|
|
export default function CombinationMethodProfileEditor({
|
|
|
|
|
|
methodArchetype,
|
|
|
|
|
|
methodProfileJson,
|
|
|
|
|
|
onChangeMethodProfileJson,
|
2026-05-13 07:38:44 +02:00
|
|
|
|
plannerMode = false,
|
|
|
|
|
|
allowExpertJson = false,
|
|
|
|
|
|
comboSlotsOutline = null,
|
2026-05-13 07:53:00 +02:00
|
|
|
|
omitPerSlotTiming = false,
|
2026-05-13 07:16:58 +02:00
|
|
|
|
}) {
|
|
|
|
|
|
const arch = typeof methodArchetype === 'string' ? methodArchetype.trim() : ''
|
|
|
|
|
|
const fieldsGui = METHOD_PROFILE_GUI_FIELDS[arch]
|
|
|
|
|
|
const fields = Array.isArray(fieldsGui) ? fieldsGui : null
|
|
|
|
|
|
const parseState = useMemo(() => parseProfileJson(methodProfileJson || '{}'), [methodProfileJson])
|
|
|
|
|
|
const [rawOpenError, setRawOpenError] = useState(null)
|
|
|
|
|
|
const [rawDraft, setRawDraft] = useState(null)
|
2026-05-13 07:38:44 +02:00
|
|
|
|
const [presetHint, setPresetHint] = useState(null)
|
2026-05-13 07:16:58 +02:00
|
|
|
|
|
|
|
|
|
|
const profileObj = parseState.ok ? parseState.obj : {}
|
|
|
|
|
|
|
2026-05-13 07:38:44 +02:00
|
|
|
|
const outlineSorted = useMemo(() => {
|
|
|
|
|
|
if (!comboSlotsOutline || !Array.isArray(comboSlotsOutline) || comboSlotsOutline.length === 0) return []
|
|
|
|
|
|
return sortCombinationSlotsForDisplay(comboSlotsOutline)
|
|
|
|
|
|
}, [comboSlotsOutline])
|
|
|
|
|
|
|
|
|
|
|
|
const showSlotTiming =
|
2026-05-13 07:53:00 +02:00
|
|
|
|
!omitPerSlotTiming && ARCHETYPES_WITH_SLOT_TIMING.has(arch) && outlineSorted.length > 0
|
2026-05-13 07:38:44 +02:00
|
|
|
|
|
|
|
|
|
|
const slotRowsModel = useMemo(() => readSlotProfilesV1(profileObj), [profileObj])
|
|
|
|
|
|
|
|
|
|
|
|
const lookupSlotTiming = (slotIndex) =>
|
|
|
|
|
|
slotRowsModel.find((r) => Number(r.slot_index) === Number(slotIndex)) || {}
|
|
|
|
|
|
|
2026-05-13 07:16:58 +02:00
|
|
|
|
const applyGuided = (key, value, kind) => {
|
|
|
|
|
|
if (kind === 'bool') {
|
|
|
|
|
|
const res = updateProfileGuided(arch, methodProfileJson || '{}', key, value, 'bool')
|
|
|
|
|
|
if (!res.ok) return
|
|
|
|
|
|
onChangeMethodProfileJson(res.json)
|
2026-05-13 07:38:44 +02:00
|
|
|
|
setPresetHint(null)
|
2026-05-13 07:16:58 +02:00
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
if (value === '' || value === undefined || value === null) {
|
|
|
|
|
|
const res = updateProfileGuided(arch, methodProfileJson || '{}', key, '', 'int')
|
|
|
|
|
|
if (!res.ok) return
|
|
|
|
|
|
onChangeMethodProfileJson(res.json)
|
2026-05-13 07:38:44 +02:00
|
|
|
|
setPresetHint(null)
|
2026-05-13 07:16:58 +02:00
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
const num = typeof value === 'number' ? value : parseInt(String(value), 10)
|
|
|
|
|
|
if (!Number.isFinite(num)) return
|
|
|
|
|
|
const def = METHOD_PROFILE_GUI_FIELDS[arch]?.find((f) => f.key === key && f.kind === 'int')
|
|
|
|
|
|
const c = clampInt(num, def?.min, def?.max)
|
|
|
|
|
|
if (c == null) return
|
|
|
|
|
|
const res = updateProfileGuided(arch, methodProfileJson || '{}', key, c, 'int')
|
|
|
|
|
|
if (!res.ok) return
|
|
|
|
|
|
onChangeMethodProfileJson(res.json)
|
2026-05-13 07:38:44 +02:00
|
|
|
|
setPresetHint(null)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const onSlotField = (slotIx, field, rawStr) => {
|
|
|
|
|
|
const patched = patchMethodProfile(methodProfileJson || '{}', (d) =>
|
|
|
|
|
|
patchSlotTimingField(d, slotIx, field, rawStr)
|
|
|
|
|
|
)
|
|
|
|
|
|
if (!patched.ok) return
|
|
|
|
|
|
onChangeMethodProfileJson(patched.json)
|
|
|
|
|
|
setPresetHint(null)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const runCircuitPreset = (presetId) => {
|
|
|
|
|
|
const r = patchMethodProfile(methodProfileJson || '{}', (draft) => {
|
|
|
|
|
|
const pr = applyCircuitRotateQuickRatio(draft, presetId)
|
|
|
|
|
|
if (!pr.ok) setPresetHint(pr.error || '')
|
|
|
|
|
|
else setPresetHint(null)
|
|
|
|
|
|
})
|
|
|
|
|
|
if (!r.ok) return
|
|
|
|
|
|
onChangeMethodProfileJson(r.json)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const runIntervalPreset = (presetId) => {
|
|
|
|
|
|
const r = patchMethodProfile(methodProfileJson || '{}', (draft) => {
|
|
|
|
|
|
const pr = applyIntervalDomainQuickRatio(draft, presetId)
|
|
|
|
|
|
if (!pr.ok) setPresetHint(pr.error || '')
|
|
|
|
|
|
else setPresetHint(null)
|
|
|
|
|
|
})
|
|
|
|
|
|
if (!r.ok) return
|
|
|
|
|
|
onChangeMethodProfileJson(r.json)
|
2026-05-13 07:16:58 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const archeLabel = arch ? combinationArchetypeLabel(arch) : null
|
|
|
|
|
|
|
|
|
|
|
|
const openAdvanced = () => {
|
|
|
|
|
|
setRawOpenError(null)
|
|
|
|
|
|
const p = parseProfileJson(methodProfileJson || '{}')
|
|
|
|
|
|
setRawDraft(p.ok ? JSON.stringify(p.obj, null, 2) : String(methodProfileJson || ''))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-05-13 07:38:44 +02:00
|
|
|
|
const showExpertSection = allowExpertJson && !plannerMode
|
|
|
|
|
|
|
2026-05-13 07:16:58 +02:00
|
|
|
|
return (
|
|
|
|
|
|
<div style={{ marginTop: '10px' }}>
|
2026-05-13 07:38:44 +02:00
|
|
|
|
{presetHint ? (
|
|
|
|
|
|
<p style={{ fontSize: '12px', color: 'var(--danger)', margin: '0 0 8px', lineHeight: 1.4 }}>{presetHint}</p>
|
|
|
|
|
|
) : null}
|
|
|
|
|
|
|
2026-05-13 07:16:58 +02:00
|
|
|
|
{arch ? (
|
|
|
|
|
|
<p style={{ fontSize: '12px', color: 'var(--text2)', lineHeight: 1.48, margin: '0 0 12px' }}>
|
|
|
|
|
|
<strong style={{ color: 'var(--text1)' }}>
|
|
|
|
|
|
Coach & Planung:{' '}
|
|
|
|
|
|
{archeLabel && archeLabel !== arch ? `${archeLabel} · ` : ''}
|
|
|
|
|
|
</strong>
|
|
|
|
|
|
{archetypeCoachHint(arch)}
|
|
|
|
|
|
</p>
|
|
|
|
|
|
) : (
|
|
|
|
|
|
<p style={{ fontSize: '12px', color: 'var(--text3)', margin: '0 0 12px' }}>
|
2026-05-13 07:38:44 +02:00
|
|
|
|
Wähle einen Methoden‑Archetyp — besonders beim <strong>freien Methodenblock</strong> stehen alle
|
|
|
|
|
|
typischen Stations‑Zeiten zur Verfügung. Ohne Archetyp keine geführten Eingaben.
|
2026-05-13 07:16:58 +02:00
|
|
|
|
</p>
|
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
|
|
{!parseState.ok ? (
|
|
|
|
|
|
<p style={{ color: 'var(--danger)', fontSize: '13px', marginBottom: '10px' }}>{parseState.error}</p>
|
|
|
|
|
|
) : null}
|
|
|
|
|
|
|
|
|
|
|
|
{fields && fields.length > 0 ? (
|
|
|
|
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '10px', marginBottom: '12px' }}>
|
2026-05-13 07:38:44 +02:00
|
|
|
|
{arch === 'circuit_rotate_time' ? (
|
|
|
|
|
|
<div
|
|
|
|
|
|
style={{
|
|
|
|
|
|
display: 'flex',
|
|
|
|
|
|
flexWrap: 'wrap',
|
|
|
|
|
|
gap: '6px',
|
|
|
|
|
|
marginBottom: '4px',
|
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
<span style={{ fontSize: '11px', color: 'var(--text3)', marginRight: '6px' }}>Schnellwahl:</span>
|
|
|
|
|
|
<button type="button" className="btn btn-secondary framework-ctrl framework-ctrl--xs" onClick={() => runCircuitPreset('transition_equals_work')}>
|
|
|
|
|
|
Wechsel ≈ Arbeit
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button type="button" className="btn btn-secondary framework-ctrl framework-ctrl--xs" onClick={() => runCircuitPreset('round_rest_equals_work')}>
|
|
|
|
|
|
Runden‑Pause ≈ Arbeit
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button type="button" className="btn btn-secondary framework-ctrl framework-ctrl--xs" onClick={() => runCircuitPreset('round_rest_two_thirds_work')}>
|
|
|
|
|
|
Runden‑Pause ≈ ⅔ Arbeit
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
) : null}
|
|
|
|
|
|
{arch === 'time_domain_interval' ? (
|
|
|
|
|
|
<div
|
|
|
|
|
|
style={{
|
|
|
|
|
|
display: 'flex',
|
|
|
|
|
|
flexWrap: 'wrap',
|
|
|
|
|
|
gap: '6px',
|
|
|
|
|
|
marginBottom: '4px',
|
|
|
|
|
|
alignItems: 'center',
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
<span style={{ fontSize: '11px', color: 'var(--text3)', marginRight: '6px' }}>Schnellwahl:</span>
|
|
|
|
|
|
<button type="button" className="btn btn-secondary framework-ctrl framework-ctrl--xs" onClick={() => runIntervalPreset('rest_equals_work')}>
|
|
|
|
|
|
Erholung = Belastung
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<button type="button" className="btn btn-secondary framework-ctrl framework-ctrl--xs" onClick={() => runIntervalPreset('rest_two_thirds_work')}>
|
|
|
|
|
|
Erholung ≈ ⅔ Belastung
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
) : null}
|
|
|
|
|
|
|
2026-05-13 07:16:58 +02:00
|
|
|
|
{fields.map((def) => {
|
|
|
|
|
|
if (def.kind === 'bool') {
|
|
|
|
|
|
const ck = !!profileObj[def.key]
|
|
|
|
|
|
return (
|
|
|
|
|
|
<label
|
|
|
|
|
|
key={def.key}
|
|
|
|
|
|
style={{ display: 'flex', gap: '8px', alignItems: 'center', cursor: 'pointer', fontSize: '0.9rem' }}
|
|
|
|
|
|
>
|
|
|
|
|
|
<input type="checkbox" checked={ck} onChange={(e) => applyGuided(def.key, e.target.checked, 'bool')} />
|
|
|
|
|
|
<span>{def.label}</span>
|
|
|
|
|
|
</label>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
const v = profileObj[def.key]
|
|
|
|
|
|
const str =
|
|
|
|
|
|
v === undefined || v === null ? '' : typeof v === 'number' && Number.isFinite(v) ? String(v) : String(v)
|
|
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div className="form-row" key={def.key}>
|
|
|
|
|
|
<label className="form-label" style={{ fontSize: '12px' }}>
|
|
|
|
|
|
{def.label}
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<input
|
|
|
|
|
|
type="number"
|
|
|
|
|
|
className="form-input"
|
|
|
|
|
|
min={def.min}
|
|
|
|
|
|
max={def.max}
|
|
|
|
|
|
value={str}
|
|
|
|
|
|
placeholder="optional"
|
|
|
|
|
|
onChange={(e) => {
|
|
|
|
|
|
const t = e.target.value
|
|
|
|
|
|
if (t.trim() === '') applyGuided(def.key, '', 'int')
|
|
|
|
|
|
else applyGuided(def.key, parseInt(t, 10), 'int')
|
|
|
|
|
|
}}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
})}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
) : arch && fields && fields.length === 0 ? (
|
2026-05-13 07:38:44 +02:00
|
|
|
|
<div style={{ marginBottom: '12px' }}>
|
|
|
|
|
|
<p style={{ fontSize: '12px', color: 'var(--text3)', margin: '0 0 10px', lineHeight: 1.45 }}>
|
|
|
|
|
|
Dieser Archetyp ist für <strong>maximal flexible</strong> Stationsblöcke gedacht — die Zeit‑Eckdaten sind
|
|
|
|
|
|
unten je Station möglich. Freitexte der Kombination beschreiben alles Organisatorische, was nicht in
|
|
|
|
|
|
Sekunden gefasst wird.
|
|
|
|
|
|
</p>
|
|
|
|
|
|
</div>
|
2026-05-13 07:16:58 +02:00
|
|
|
|
) : null}
|
|
|
|
|
|
|
2026-05-13 07:38:44 +02:00
|
|
|
|
{showSlotTiming ? (
|
|
|
|
|
|
<div
|
|
|
|
|
|
style={{
|
|
|
|
|
|
marginTop: '6px',
|
|
|
|
|
|
marginBottom: '14px',
|
|
|
|
|
|
padding: '12px 14px',
|
|
|
|
|
|
borderRadius: '10px',
|
|
|
|
|
|
border: '1px solid var(--border)',
|
|
|
|
|
|
background: 'var(--surface)',
|
2026-05-13 07:16:58 +02:00
|
|
|
|
}}
|
2026-05-13 07:38:44 +02:00
|
|
|
|
>
|
|
|
|
|
|
<div style={{ fontSize: '0.88rem', fontWeight: 700, marginBottom: '10px', color: 'var(--text1)' }}>
|
|
|
|
|
|
Pro Station / Slot (Zeiten in Sekunden)
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<p style={{ fontSize: '11px', color: 'var(--text3)', margin: '0 0 12px', lineHeight: 1.45 }}>
|
|
|
|
|
|
Belastungsdauer, wie oft die Übung an der gleichen Station hintereinander, kurze Pause dazwischen, Zeit bis
|
|
|
|
|
|
zur nächsten Station. Felder können leer bleiben — z. B. nutzt der Zirkel oben erst die globalen Arbeit‑/
|
|
|
|
|
|
Rotations‑Sekunden.
|
|
|
|
|
|
</p>
|
|
|
|
|
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '12px' }}>
|
|
|
|
|
|
{outlineSorted.map((slot) => {
|
|
|
|
|
|
const siRaw = slot.slot_index
|
|
|
|
|
|
const si =
|
|
|
|
|
|
siRaw === '' || siRaw == null ? null : typeof siRaw === 'number' ? siRaw : parseInt(String(siRaw), 10)
|
|
|
|
|
|
if (!Number.isFinite(si)) return null
|
|
|
|
|
|
const row = lookupSlotTiming(si)
|
|
|
|
|
|
const ttl = ((slot.title || '').trim() || `Station ${si}`).trim()
|
|
|
|
|
|
return (
|
|
|
|
|
|
<div
|
|
|
|
|
|
key={`slot-timing-${si}`}
|
|
|
|
|
|
style={{
|
|
|
|
|
|
padding: '10px',
|
|
|
|
|
|
borderRadius: '10px',
|
|
|
|
|
|
border: '1px solid var(--border)',
|
|
|
|
|
|
background: 'var(--surface2)',
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
<div style={{ marginBottom: '10px', fontWeight: 700, color: 'var(--text1)', fontSize: '0.9rem' }}>
|
|
|
|
|
|
Station {si}
|
|
|
|
|
|
<span style={{ fontWeight: 400, marginLeft: 8, color: 'var(--text2)', fontSize: '0.86rem' }}>{ttl}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div
|
|
|
|
|
|
style={{
|
|
|
|
|
|
display: 'grid',
|
|
|
|
|
|
gridTemplateColumns: 'repeat(auto-fill, minmax(140px, 1fr))',
|
|
|
|
|
|
gap: '10px',
|
|
|
|
|
|
alignItems: 'end',
|
|
|
|
|
|
}}
|
|
|
|
|
|
>
|
|
|
|
|
|
<div className="form-row" style={{ marginBottom: 0 }}>
|
|
|
|
|
|
<label className="form-label" style={{ fontSize: '11px' }}>
|
|
|
|
|
|
Belastung (s)
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<input
|
|
|
|
|
|
type="number"
|
|
|
|
|
|
min={0}
|
|
|
|
|
|
className="form-input"
|
|
|
|
|
|
placeholder="–"
|
|
|
|
|
|
value={row.load_sec != null ? String(row.load_sec) : ''}
|
|
|
|
|
|
onChange={(e) => onSlotField(si, 'load_sec', e.target.value)}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="form-row" style={{ marginBottom: 0 }}>
|
|
|
|
|
|
<label className="form-label" style={{ fontSize: '11px' }}>
|
|
|
|
|
|
Wdh. ohne Wechsel
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<input
|
|
|
|
|
|
type="number"
|
|
|
|
|
|
min={1}
|
|
|
|
|
|
className="form-input"
|
|
|
|
|
|
placeholder="oft 1"
|
|
|
|
|
|
value={row.consecutive_reps != null ? String(row.consecutive_reps) : ''}
|
|
|
|
|
|
onChange={(e) => onSlotField(si, 'consecutive_reps', e.target.value)}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="form-row" style={{ marginBottom: 0 }}>
|
|
|
|
|
|
<label className="form-label" style={{ fontSize: '11px' }}>
|
|
|
|
|
|
Pause zwischen Wdh. (s)
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<input
|
|
|
|
|
|
type="number"
|
|
|
|
|
|
min={0}
|
|
|
|
|
|
className="form-input"
|
|
|
|
|
|
placeholder="–"
|
|
|
|
|
|
value={row.intra_rep_rest_sec != null ? String(row.intra_rep_rest_sec) : ''}
|
|
|
|
|
|
onChange={(e) => onSlotField(si, 'intra_rep_rest_sec', e.target.value)}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div className="form-row" style={{ marginBottom: 0 }}>
|
|
|
|
|
|
<label className="form-label" style={{ fontSize: '11px' }}>
|
|
|
|
|
|
Pause / Wechsel (s)
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<input
|
|
|
|
|
|
type="number"
|
|
|
|
|
|
min={0}
|
|
|
|
|
|
className="form-input"
|
|
|
|
|
|
placeholder="–"
|
|
|
|
|
|
value={row.transition_after_sec != null ? String(row.transition_after_sec) : ''}
|
|
|
|
|
|
onChange={(e) => onSlotField(si, 'transition_after_sec', e.target.value)}
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
})}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
) : null}
|
|
|
|
|
|
|
|
|
|
|
|
{showExpertSection ? (
|
|
|
|
|
|
<details
|
|
|
|
|
|
style={{
|
|
|
|
|
|
marginTop: '4px',
|
|
|
|
|
|
borderRadius: '8px',
|
|
|
|
|
|
border: '1px solid var(--border)',
|
|
|
|
|
|
padding: '8px 10px',
|
|
|
|
|
|
background: 'var(--surface2)',
|
|
|
|
|
|
}}
|
|
|
|
|
|
onToggle={(ev) => {
|
|
|
|
|
|
if (ev.target.open) openAdvanced()
|
2026-05-13 07:16:58 +02:00
|
|
|
|
}}
|
2026-05-13 07:38:44 +02:00
|
|
|
|
>
|
|
|
|
|
|
<summary style={{ cursor: 'pointer', fontSize: '0.82rem', fontWeight: 600 }}>
|
|
|
|
|
|
Support / Entwicklung: Rohdaten (JSON)
|
|
|
|
|
|
</summary>
|
|
|
|
|
|
<p style={{ fontSize: '11px', color: 'var(--text3)', margin: '8px 0 6px', lineHeight: 1.4 }}>
|
|
|
|
|
|
Für Migrationen und Sonderfälle. Geführte Felder setzen weiterhin gültige Standardschlüssel.
|
|
|
|
|
|
</p>
|
|
|
|
|
|
<textarea
|
|
|
|
|
|
className="form-input"
|
|
|
|
|
|
rows={8}
|
|
|
|
|
|
style={{ fontFamily: 'Consolas,monospace', fontSize: '12px' }}
|
|
|
|
|
|
value={rawDraft != null ? rawDraft : methodProfileJson || '{}'}
|
|
|
|
|
|
onChange={(e) => {
|
|
|
|
|
|
setRawDraft(e.target.value)
|
|
|
|
|
|
setRawOpenError(null)
|
|
|
|
|
|
}}
|
|
|
|
|
|
spellCheck={false}
|
|
|
|
|
|
onBlur={() => {
|
|
|
|
|
|
const src = rawDraft != null ? rawDraft : methodProfileJson
|
|
|
|
|
|
const res = setFullProfileRawJson(src || '{}')
|
|
|
|
|
|
if (!res.ok) {
|
|
|
|
|
|
setRawOpenError(res.error)
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
setRawOpenError(null)
|
|
|
|
|
|
setRawDraft(null)
|
|
|
|
|
|
onChangeMethodProfileJson(res.json)
|
|
|
|
|
|
}}
|
|
|
|
|
|
/>
|
|
|
|
|
|
{rawOpenError ? (
|
|
|
|
|
|
<p style={{ color: 'var(--danger)', fontSize: '12px', marginTop: '6px' }}>{rawOpenError}</p>
|
|
|
|
|
|
) : null}
|
|
|
|
|
|
</details>
|
|
|
|
|
|
) : null}
|
2026-05-13 07:16:58 +02:00
|
|
|
|
</div>
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|