Parlellsession- Plan #35
|
|
@ -13,6 +13,12 @@ import {
|
||||||
maxPhaseOrderIndexFromSections,
|
maxPhaseOrderIndexFromSections,
|
||||||
buildPlanTargetOptions,
|
buildPlanTargetOptions,
|
||||||
planLocKey,
|
planLocKey,
|
||||||
|
MAX_PARALLEL_STREAMS_PER_PHASE,
|
||||||
|
parallelStreamVisual,
|
||||||
|
streamTabLabelFromIndices,
|
||||||
|
streamsForParallelPhaseOrders,
|
||||||
|
sectionIndicesForParallelStream,
|
||||||
|
reorderWithinBucketIndices,
|
||||||
exerciseRow,
|
exerciseRow,
|
||||||
noteRow,
|
noteRow,
|
||||||
sectionPlannedMinutes,
|
sectionPlannedMinutes,
|
||||||
|
|
@ -279,6 +285,8 @@ export default function TrainingUnitSectionsEditor({
|
||||||
if (!par.length) return prev
|
if (!par.length) return prev
|
||||||
const maxP = Math.max(...par.map((s) => s.planLoc.phaseOrderIndex ?? 0))
|
const maxP = Math.max(...par.map((s) => s.planLoc.phaseOrderIndex ?? 0))
|
||||||
const inPhase = par.filter((s) => (s.planLoc.phaseOrderIndex ?? 0) === maxP)
|
const inPhase = par.filter((s) => (s.planLoc.phaseOrderIndex ?? 0) === maxP)
|
||||||
|
const distinctStreams = new Set(inPhase.map((s) => s.planLoc.parallelStreamOrderIndex ?? 0))
|
||||||
|
if (distinctStreams.size >= MAX_PARALLEL_STREAMS_PER_PHASE) return prev
|
||||||
const maxS = Math.max(...inPhase.map((s) => s.planLoc.parallelStreamOrderIndex ?? 0))
|
const maxS = Math.max(...inPhase.map((s) => s.planLoc.parallelStreamOrderIndex ?? 0))
|
||||||
const newSo = maxS + 1
|
const newSo = maxS + 1
|
||||||
const tmpl = {
|
const tmpl = {
|
||||||
|
|
@ -313,13 +321,27 @@ export default function TrainingUnitSectionsEditor({
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Ganzgruppe: global tauschen; parallele Phase: nur innerhalb desselben Streams sortieren. */
|
||||||
const moveSection = (sIdx, dir) => {
|
const moveSection = (sIdx, dir) => {
|
||||||
patch((prev) => {
|
patch((prev) => {
|
||||||
const p = [...prev]
|
const p = ensure(prev)
|
||||||
|
const sec = p[sIdx]
|
||||||
|
const L = sec?.planLoc
|
||||||
|
if (L?.phaseKind === 'parallel') {
|
||||||
|
const po = L.phaseOrderIndex ?? 0
|
||||||
|
const so = L.parallelStreamOrderIndex ?? 0
|
||||||
|
const bucket = sectionIndicesForParallelStream(p, po, so)
|
||||||
|
const pos = bucket.indexOf(sIdx)
|
||||||
|
if (pos < 0) return p
|
||||||
|
const newPos = pos + dir
|
||||||
|
if (newPos < 0 || newPos >= bucket.length) return p
|
||||||
|
return reorderWithinBucketIndices(p, bucket, pos, newPos)
|
||||||
|
}
|
||||||
|
const arr = [...p]
|
||||||
const ta = sIdx + dir
|
const ta = sIdx + dir
|
||||||
if (ta < 0 || ta >= p.length) return p
|
if (ta < 0 || ta >= arr.length) return arr
|
||||||
;[p[sIdx], p[ta]] = [p[ta], p[sIdx]]
|
;[arr[sIdx], arr[ta]] = [arr[ta], arr[sIdx]]
|
||||||
return p
|
return arr
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -399,6 +421,8 @@ export default function TrainingUnitSectionsEditor({
|
||||||
const [dropTargetPos, setDropTargetPos] = useState(null)
|
const [dropTargetPos, setDropTargetPos] = useState(null)
|
||||||
|
|
||||||
const [dropSectionBand, setDropSectionBand] = useState(null)
|
const [dropSectionBand, setDropSectionBand] = useState(null)
|
||||||
|
/** Aktiver Reiter pro paralleler Phase (phaseOrder → streamOrder). */
|
||||||
|
const [parallelStreamTabByPhase, setParallelStreamTabByPhase] = useState({})
|
||||||
/** { slot: number, beforeIdx: number } */
|
/** { slot: number, beforeIdx: number } */
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -694,6 +718,87 @@ export default function TrainingUnitSectionsEditor({
|
||||||
[list]
|
[list]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const firstSectionIndexByParallelPhase = useMemo(() => {
|
||||||
|
const m = new Map()
|
||||||
|
list.forEach((s, i) => {
|
||||||
|
const L = s?.planLoc
|
||||||
|
if (L?.phaseKind !== 'parallel') return
|
||||||
|
const po = L.phaseOrderIndex ?? 0
|
||||||
|
if (!m.has(po)) m.set(po, i)
|
||||||
|
})
|
||||||
|
return m
|
||||||
|
}, [list])
|
||||||
|
|
||||||
|
const parallelPhaseOrdersPresent = useMemo(() => {
|
||||||
|
const set = new Set()
|
||||||
|
for (const s of list) {
|
||||||
|
if (s?.planLoc?.phaseKind === 'parallel') set.add(s.planLoc.phaseOrderIndex ?? 0)
|
||||||
|
}
|
||||||
|
return [...set].sort((a, b) => a - b)
|
||||||
|
}, [list])
|
||||||
|
|
||||||
|
const cannotAddMoreStreams = useMemo(() => {
|
||||||
|
const par = list.filter((s) => s?.planLoc?.phaseKind === 'parallel')
|
||||||
|
if (!par.length) return true
|
||||||
|
const maxP = Math.max(...par.map((s) => s.planLoc.phaseOrderIndex ?? 0))
|
||||||
|
const inPhase = par.filter((s) => (s.planLoc.phaseOrderIndex ?? 0) === maxP)
|
||||||
|
const distinct = new Set(inPhase.map((s) => s.planLoc.parallelStreamOrderIndex ?? 0))
|
||||||
|
return distinct.size >= MAX_PARALLEL_STREAMS_PER_PHASE
|
||||||
|
}, [list])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!enableParallelPhaseControls || !parallelPhaseOrdersPresent.length) return
|
||||||
|
setParallelStreamTabByPhase((prev) => {
|
||||||
|
const next = { ...prev }
|
||||||
|
let changed = false
|
||||||
|
for (const po of parallelPhaseOrdersPresent) {
|
||||||
|
const orders = streamsForParallelPhaseOrders(list, po)
|
||||||
|
if (!orders.length) continue
|
||||||
|
if (next[po] === undefined) {
|
||||||
|
next[po] = orders[0]
|
||||||
|
changed = true
|
||||||
|
} else if (!orders.includes(next[po])) {
|
||||||
|
next[po] = orders[0]
|
||||||
|
changed = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const k of Object.keys(next)) {
|
||||||
|
const poi = Number(k)
|
||||||
|
if (!Number.isFinite(poi) || !parallelPhaseOrdersPresent.includes(poi)) {
|
||||||
|
delete next[k]
|
||||||
|
changed = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return changed ? next : prev
|
||||||
|
})
|
||||||
|
}, [list, parallelPhaseOrdersPresent, enableParallelPhaseControls])
|
||||||
|
|
||||||
|
const sectionMoveDisabledUp = (sIdx) => {
|
||||||
|
const sec = list[sIdx]
|
||||||
|
const L = sec?.planLoc
|
||||||
|
if (L?.phaseKind === 'parallel') {
|
||||||
|
const po = L.phaseOrderIndex ?? 0
|
||||||
|
const so = L.parallelStreamOrderIndex ?? 0
|
||||||
|
const bucket = sectionIndicesForParallelStream(list, po, so)
|
||||||
|
const pos = bucket.indexOf(sIdx)
|
||||||
|
return pos <= 0
|
||||||
|
}
|
||||||
|
return sIdx === 0
|
||||||
|
}
|
||||||
|
|
||||||
|
const sectionMoveDisabledDown = (sIdx) => {
|
||||||
|
const sec = list[sIdx]
|
||||||
|
const L = sec?.planLoc
|
||||||
|
if (L?.phaseKind === 'parallel') {
|
||||||
|
const po = L.phaseOrderIndex ?? 0
|
||||||
|
const so = L.parallelStreamOrderIndex ?? 0
|
||||||
|
const bucket = sectionIndicesForParallelStream(list, po, so)
|
||||||
|
const pos = bucket.indexOf(sIdx)
|
||||||
|
return pos < 0 || pos >= bucket.length - 1
|
||||||
|
}
|
||||||
|
return sIdx === list.length - 1
|
||||||
|
}
|
||||||
|
|
||||||
const comboPlanningModalDerived = useMemo(() => {
|
const comboPlanningModalDerived = useMemo(() => {
|
||||||
if (!comboPlanningModal) {
|
if (!comboPlanningModal) {
|
||||||
return { item: null, sIdx: null, iIdx: null }
|
return { item: null, sIdx: null, iIdx: null }
|
||||||
|
|
@ -791,8 +896,9 @@ export default function TrainingUnitSectionsEditor({
|
||||||
Breakout: Phasen und parallele Streams
|
Breakout: Phasen und parallele Streams
|
||||||
</div>
|
</div>
|
||||||
<p style={{ margin: '0 0 12px', fontSize: '0.8rem', color: 'var(--text2)', lineHeight: 1.5 }}>
|
<p style={{ margin: '0 0 12px', fontSize: '0.8rem', color: 'var(--text2)', lineHeight: 1.5 }}>
|
||||||
Legt fest, ob Abschnitte zur ganzen Gruppe oder zu parallelen Gruppen gehören. „Abschnitt hinzufügen“ unten
|
Legt fest, ob Abschnitte zur ganzen Gruppe oder zu parallelen Gruppen gehören. Pro paralleler Phase erscheinen
|
||||||
übernimmt die Zuordnung des letzten Abschnitts. Speichern erzeugt bei Bedarf automatisch den{' '}
|
Reiter je Stream — nur der aktive Stream ist sichtbar (farbig am linken Rand). „Abschnitt hinzufügen“ übernimmt
|
||||||
|
die Zuordnung des letzten Abschnitts. Speichern erzeugt bei Bedarf automatisch den{' '}
|
||||||
<code style={{ fontSize: '0.78em' }}>phases</code>-Payload fürs Backend.
|
<code style={{ fontSize: '0.78em' }}>phases</code>-Payload fürs Backend.
|
||||||
</p>
|
</p>
|
||||||
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '8px', alignItems: 'center' }}>
|
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '8px', alignItems: 'center' }}>
|
||||||
|
|
@ -806,11 +912,13 @@ export default function TrainingUnitSectionsEditor({
|
||||||
type="button"
|
type="button"
|
||||||
className="btn btn-secondary"
|
className="btn btn-secondary"
|
||||||
onClick={addStreamToLastParallelPhase}
|
onClick={addStreamToLastParallelPhase}
|
||||||
disabled={!hasParallelPhase}
|
disabled={!hasParallelPhase || cannotAddMoreStreams}
|
||||||
title={
|
title={
|
||||||
hasParallelPhase
|
!hasParallelPhase
|
||||||
? 'Weiterer Stream in der letzten parallelen Phase (höchster Phasen-Index)'
|
? 'Zuerst eine parallele Phase anlegen'
|
||||||
: 'Zuerst eine parallele Phase anlegen'
|
: cannotAddMoreStreams
|
||||||
|
? `Höchstens ${MAX_PARALLEL_STREAMS_PER_PHASE} Streams pro Phase`
|
||||||
|
: 'Weiterer Stream in der letzten parallelen Phase (höchster Phasen-Index)'
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
Stream hinzufügen
|
Stream hinzufügen
|
||||||
|
|
@ -828,6 +936,29 @@ export default function TrainingUnitSectionsEditor({
|
||||||
dropSectionBand.slot === sectionToSlot &&
|
dropSectionBand.slot === sectionToSlot &&
|
||||||
dropSectionBand.beforeIdx === bx
|
dropSectionBand.beforeIdx === bx
|
||||||
|
|
||||||
|
const pl = sec?.planLoc
|
||||||
|
const parallelPhaseOrder =
|
||||||
|
enableParallelPhaseControls && pl?.phaseKind === 'parallel' ? pl.phaseOrderIndex ?? 0 : null
|
||||||
|
const streamOrdersForParallelPhase =
|
||||||
|
parallelPhaseOrder != null ? streamsForParallelPhaseOrders(list, parallelPhaseOrder) : []
|
||||||
|
const activeParallelStream =
|
||||||
|
parallelPhaseOrder != null
|
||||||
|
? parallelStreamTabByPhase[parallelPhaseOrder] ?? streamOrdersForParallelPhase[0] ?? 0
|
||||||
|
: null
|
||||||
|
const hideParallelSection =
|
||||||
|
enableParallelPhaseControls &&
|
||||||
|
pl?.phaseKind === 'parallel' &&
|
||||||
|
(pl.parallelStreamOrderIndex ?? 0) !== activeParallelStream
|
||||||
|
const isFirstSectionOfParallelPhase =
|
||||||
|
parallelPhaseOrder != null &&
|
||||||
|
firstSectionIndexByParallelPhase.get(parallelPhaseOrder) === sIdx
|
||||||
|
const streamVisual =
|
||||||
|
enableParallelPhaseControls && pl?.phaseKind === 'parallel'
|
||||||
|
? parallelStreamVisual(pl.parallelStreamOrderIndex ?? 0)
|
||||||
|
: null
|
||||||
|
const allowSectionDragGrip =
|
||||||
|
enableSectionDragReorder && !(enableParallelPhaseControls && pl?.phaseKind === 'parallel')
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment key={`secFrag-${sIdx}`}>
|
<Fragment key={`secFrag-${sIdx}`}>
|
||||||
{enableSectionDragReorder ? (
|
{enableSectionDragReorder ? (
|
||||||
|
|
@ -846,14 +977,69 @@ export default function TrainingUnitSectionsEditor({
|
||||||
onDrop={(e) => onSectionBandDrop(e, sIdx)}
|
onDrop={(e) => onSectionBandDrop(e, sIdx)}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
|
{isFirstSectionOfParallelPhase &&
|
||||||
|
enableParallelPhaseControls &&
|
||||||
|
streamOrdersForParallelPhase.length ? (
|
||||||
|
<div
|
||||||
|
role="tablist"
|
||||||
|
aria-label={`Parallele Streams · Phase ${parallelPhaseOrder}`}
|
||||||
|
style={{
|
||||||
|
display: 'flex',
|
||||||
|
flexWrap: 'wrap',
|
||||||
|
gap: '6px',
|
||||||
|
marginBottom: '10px',
|
||||||
|
padding: '8px 10px',
|
||||||
|
background: 'var(--surface2)',
|
||||||
|
borderRadius: '10px',
|
||||||
|
border: '1px solid var(--border, rgba(0,0,0,0.08))',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{streamOrdersForParallelPhase.map((so) => {
|
||||||
|
const sel =
|
||||||
|
(parallelStreamTabByPhase[parallelPhaseOrder] ??
|
||||||
|
streamOrdersForParallelPhase[0] ??
|
||||||
|
0) === so
|
||||||
|
const pv = parallelStreamVisual(so)
|
||||||
|
const lab = streamTabLabelFromIndices(
|
||||||
|
list,
|
||||||
|
sectionIndicesForParallelStream(list, parallelPhaseOrder, so),
|
||||||
|
)
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={`p${parallelPhaseOrder}-tab-s${so}`}
|
||||||
|
type="button"
|
||||||
|
role="tab"
|
||||||
|
aria-selected={sel}
|
||||||
|
className="btn framework-ctrl framework-ctrl--xs"
|
||||||
|
style={{
|
||||||
|
margin: 0,
|
||||||
|
border: sel ? `2px solid ${pv.border}` : `1px solid ${pv.border}`,
|
||||||
|
background: sel ? pv.tabBgActive : pv.tabBg,
|
||||||
|
color: 'var(--text1)',
|
||||||
|
fontWeight: sel ? 600 : 500,
|
||||||
|
}}
|
||||||
|
onClick={() =>
|
||||||
|
setParallelStreamTabByPhase((prev) => ({ ...prev, [parallelPhaseOrder]: so }))
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{lab}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
{!hideParallelSection ? (
|
||||||
<div
|
<div
|
||||||
className="tu-section-shell"
|
className="tu-section-shell"
|
||||||
style={{
|
style={{
|
||||||
marginBottom: '1rem',
|
marginBottom: '1rem',
|
||||||
padding: '0.75rem',
|
padding: '0.75rem',
|
||||||
background: 'var(--surface2)',
|
background: streamVisual ? streamVisual.soft : 'var(--surface2)',
|
||||||
borderRadius: '10px',
|
borderRadius: '10px',
|
||||||
border: '1px solid var(--border, rgba(0,0,0,0.08))',
|
border: streamVisual
|
||||||
|
? `1px solid ${streamVisual.border}`
|
||||||
|
: '1px solid var(--border, rgba(0,0,0,0.08))',
|
||||||
|
borderLeft: streamVisual ? `5px solid ${streamVisual.border}` : undefined,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
|
|
@ -865,7 +1051,7 @@ export default function TrainingUnitSectionsEditor({
|
||||||
alignItems: 'flex-start',
|
alignItems: 'flex-start',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{enableSectionDragReorder ? (
|
{allowSectionDragGrip ? (
|
||||||
<span
|
<span
|
||||||
className="tu-sec-drag-grip"
|
className="tu-sec-drag-grip"
|
||||||
draggable
|
draggable
|
||||||
|
|
@ -890,8 +1076,11 @@ export default function TrainingUnitSectionsEditor({
|
||||||
type="button"
|
type="button"
|
||||||
aria-label="Abschnitt hoch"
|
aria-label="Abschnitt hoch"
|
||||||
onClick={() => moveSection(sIdx, -1)}
|
onClick={() => moveSection(sIdx, -1)}
|
||||||
disabled={sIdx === 0}
|
disabled={sectionMoveDisabledUp(sIdx)}
|
||||||
style={{ padding: '4px 10px', opacity: sIdx === 0 ? 0.35 : 1 }}
|
style={{
|
||||||
|
padding: '4px 10px',
|
||||||
|
opacity: sectionMoveDisabledUp(sIdx) ? 0.35 : 1,
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
▲
|
▲
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -899,10 +1088,10 @@ export default function TrainingUnitSectionsEditor({
|
||||||
type="button"
|
type="button"
|
||||||
aria-label="Abschnitt runter"
|
aria-label="Abschnitt runter"
|
||||||
onClick={() => moveSection(sIdx, 1)}
|
onClick={() => moveSection(sIdx, 1)}
|
||||||
disabled={sIdx === list.length - 1}
|
disabled={sectionMoveDisabledDown(sIdx)}
|
||||||
style={{
|
style={{
|
||||||
padding: '4px 10px',
|
padding: '4px 10px',
|
||||||
opacity: sIdx === list.length - 1 ? 0.35 : 1,
|
opacity: sectionMoveDisabledDown(sIdx) ? 0.35 : 1,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
▼
|
▼
|
||||||
|
|
@ -1479,6 +1668,7 @@ export default function TrainingUnitSectionsEditor({
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
|
) : null}
|
||||||
</Fragment>
|
</Fragment>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
|
|
|
||||||
|
|
@ -86,6 +86,76 @@ export function buildPlanTargetOptions(sections) {
|
||||||
return [...map.values()].sort((a, b) => a.key.localeCompare(b.key, undefined, { numeric: true }))
|
return [...map.values()].sort((a, b) => a.key.localeCompare(b.key, undefined, { numeric: true }))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Max. Streams pro paralleler Phase (UI + API-Schutz). */
|
||||||
|
export const MAX_PARALLEL_STREAMS_PER_PHASE = 5
|
||||||
|
|
||||||
|
/** Farben pro Stream-Index (max. 5 unterschiedliche Farbzyklen). */
|
||||||
|
export function parallelStreamVisual(streamOrderIndex) {
|
||||||
|
const n = Math.max(0, Number(streamOrderIndex) || 0)
|
||||||
|
const hues = [200, 135, 38, 285, 22]
|
||||||
|
const h = hues[n % hues.length]
|
||||||
|
return {
|
||||||
|
border: `hsl(${h} 50% 36%)`,
|
||||||
|
soft: `hsl(${h} 36% 94%)`,
|
||||||
|
tabBg: `hsl(${h} 34% 92%)`,
|
||||||
|
tabBgActive: `hsl(${h} 40% 82%)`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function streamTabLabelFromIndices(sections, globalIndices) {
|
||||||
|
const first = globalIndices?.[0]
|
||||||
|
if (first === undefined || !sections?.[first]) return 'Stream'
|
||||||
|
const pl = sections[first].planLoc
|
||||||
|
const t = pl?.streamTitle != null && String(pl.streamTitle).trim() ? String(pl.streamTitle).trim() : ''
|
||||||
|
if (t) return t
|
||||||
|
const so = pl?.parallelStreamOrderIndex ?? 0
|
||||||
|
return `Stream ${so + 1}`
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Sortierte Stream-Indizes innerhalb einer parallelen Phase (für Reiter). */
|
||||||
|
export function streamsForParallelPhaseOrders(sections, phaseOrderIndex) {
|
||||||
|
const set = new Set()
|
||||||
|
const po = Number(phaseOrderIndex) || 0
|
||||||
|
for (const s of sections || []) {
|
||||||
|
const L = s?.planLoc
|
||||||
|
if (L?.phaseKind === 'parallel' && (L.phaseOrderIndex ?? 0) === po) {
|
||||||
|
set.add(L.parallelStreamOrderIndex ?? 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [...set].sort((a, b) => a - b)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Globale Abschnitts-Indizes eines Streams. */
|
||||||
|
export function sectionIndicesForParallelStream(sections, phaseOrderIndex, streamOrderIndex) {
|
||||||
|
const out = []
|
||||||
|
const po = Number(phaseOrderIndex) || 0
|
||||||
|
const so = Number(streamOrderIndex) || 0
|
||||||
|
;(sections || []).forEach((s, i) => {
|
||||||
|
const L = s?.planLoc
|
||||||
|
if (L?.phaseKind === 'parallel' && (L.phaseOrderIndex ?? 0) === po && (L.parallelStreamOrderIndex ?? 0) === so) {
|
||||||
|
out.push(i)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Reihenfolge innerhalb eines Stream-Buckets (globale Indizes) ändern. */
|
||||||
|
export function reorderWithinBucketIndices(prev, bucketGlobalIndicesSorted, oldPos, newPos) {
|
||||||
|
const sortedIdx = [...bucketGlobalIndicesSorted].sort((a, b) => a - b)
|
||||||
|
if (oldPos === newPos || oldPos < 0 || newPos < 0 || oldPos >= sortedIdx.length || newPos >= sortedIdx.length) {
|
||||||
|
return prev
|
||||||
|
}
|
||||||
|
const values = sortedIdx.map((gi) => prev[gi])
|
||||||
|
const arr = [...values]
|
||||||
|
const [x] = arr.splice(oldPos, 1)
|
||||||
|
arr.splice(newPos, 0, x)
|
||||||
|
const next = [...prev]
|
||||||
|
sortedIdx.forEach((gi, k) => {
|
||||||
|
next[gi] = arr[k]
|
||||||
|
})
|
||||||
|
return next
|
||||||
|
}
|
||||||
|
|
||||||
function normalizeCatalogMethodProfile(cp) {
|
function normalizeCatalogMethodProfile(cp) {
|
||||||
if (cp && typeof cp === 'object' && !Array.isArray(cp)) return { ...cp }
|
if (cp && typeof cp === 'object' && !Array.isArray(cp)) return { ...cp }
|
||||||
return {}
|
return {}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user