Enhance TrainingCoachPage and trainingPlanUtils with split rejoin transition logic
All checks were successful
Deploy Development / deploy (push) Successful in 39s
Test Suite / pytest-backend (push) Successful in 37s
Test Suite / lint-backend (push) Successful in 0s
Test Suite / build-frontend (push) Successful in 13s
Test Suite / k6 /health Baseline (push) Successful in 33s
Test Suite / playwright-tests (push) Successful in 1m11s
Test Suite / pytest-backend (pull_request) Successful in 34s
Test Suite / lint-backend (pull_request) Successful in 0s
Test Suite / build-frontend (pull_request) Successful in 12s
Test Suite / k6 /health Baseline (pull_request) Successful in 34s
Test Suite / playwright-tests (pull_request) Successful in 1m20s

- Introduced a new utility function to determine when to prompt for a split rejoin transition between phases, improving user guidance during training sessions.
- Updated TrainingCoachPage to incorporate this logic, enhancing the flow of navigation through training timelines.
- Refactored button actions to provide clearer options for users when managing group transitions, optimizing the user experience during training.
This commit is contained in:
Lars 2026-05-15 18:52:27 +02:00
parent 5e5350d5ac
commit a4f11a8225
2 changed files with 63 additions and 24 deletions

View File

@ -13,6 +13,7 @@ import {
coachBranchPicksStorageKey,
coachOutlineGroupsFromTimeline,
coachShouldPromptSplitRejoin,
coachShouldPromptSplitRejoinTransition,
durationOverridesMapFromDeltas,
findCoachTimelineJumpIndexForPhase,
flattenPlanTimeline,
@ -549,6 +550,14 @@ export default function TrainingCoachPage() {
}
return
}
const nextIdx = safeStep + 1
if (nextIdx < timeline.length) {
const rejoinMid = coachShouldPromptSplitRejoinTransition(unit, timeline[safeStep], timeline[nextIdx])
if (rejoinMid) {
setSplitRejoinPrompt(rejoinMid)
return
}
}
setStep((s) => clampStep(s + 1, timeline.length))
}
@ -620,7 +629,6 @@ export default function TrainingCoachPage() {
.trim()
}
await api.updateTrainingUnit(idNum, payload)
await reloadUnit()
setTrainerAppend('')
try {
sessionStorage.removeItem(storageDeltasKey(idNum))
@ -630,14 +638,15 @@ export default function TrainingCoachPage() {
} catch {
/* ignore */
}
setSearchParams({}, { replace: true })
setStep(0)
setDeltas({})
setBranchPicks({})
setStreamChoiceHint(null)
setSplitRejoinPrompt(null)
setCoachDebriefPhase(false)
setSaveOk('Gespeichert.')
setDebriefOpen(false)
setSaveOk('Gespeichert.')
setSearchParams({}, { replace: true })
navigate(`/planning/run/${unitId}`, { replace: true })
} catch (e) {
setSaveOk(`Fehler: ${e.message || e}`)
} finally {
@ -771,11 +780,11 @@ export default function TrainingCoachPage() {
? String(splitRejoinPrompt.phaseTitle).trim()
: `Phase ${splitRejoinPrompt.phaseOrderIndex}`}
{' — '}
alle Gruppen fertig?
alle Gruppen zusammen?
</p>
<p style={{ fontSize: '0.84rem', color: 'var(--text2)', margin: '0 0 12px', lineHeight: 1.45 }}>
Diese Phase hat mehrere Streams. Kurz mit dem anderen Trainer klären, dann gemeinsam Ist-Zeiten und Speichern
(gilt auch, wenn danach kein weiterer Block mehr kommt).
Diese Phase hat mehrere Streams. Kurz mit dem anderen Trainer klären, dann gemeinsam weitermachen. Ist-Zeiten
und Speichern erfolgen in der Nachbereitung am Ende der Einheit.
</p>
<ul style={{ margin: '0 0 14px', paddingLeft: '1.2rem', fontSize: '0.82rem', color: 'var(--text2)' }}>
{splitRejoinPrompt.streams.map((st) => (
@ -786,22 +795,36 @@ export default function TrainingCoachPage() {
))}
</ul>
<div style={{ display: 'flex', flexDirection: 'column', gap: '8px' }}>
<button
type="button"
className="btn btn-primary"
style={{ minHeight: '48px', fontWeight: 700 }}
onClick={() => {
setSplitRejoinPrompt(null)
setCoachDebriefPhase(true)
try {
sessionStorage.setItem(storageDebriefKey(idNum), '1')
} catch {
/* ignore */
}
}}
>
Alle Gruppen fertig zur Nachbereitung
</button>
{safeStep < timeline.length - 1 ? (
<button
type="button"
className="btn btn-primary"
style={{ minHeight: '48px', fontWeight: 700 }}
onClick={() => {
setSplitRejoinPrompt(null)
setStep((s) => clampStep(s + 1, timeline.length))
}}
>
Gruppen zusammengeführt weiter mit dem Plan
</button>
) : (
<button
type="button"
className="btn btn-primary"
style={{ minHeight: '48px', fontWeight: 700 }}
onClick={() => {
setSplitRejoinPrompt(null)
setCoachDebriefPhase(true)
try {
sessionStorage.setItem(storageDebriefKey(idNum), '1')
} catch {
/* ignore */
}
}}
>
Alle Gruppen fertig zur Nachbereitung
</button>
)}
<button type="button" className="btn btn-secondary" style={{ minHeight: '44px' }} onClick={() => setSplitRejoinPrompt(null)}>
Zurück andere Gruppe läuft noch
</button>
@ -819,7 +842,9 @@ export default function TrainingCoachPage() {
}
}}
>
Ausnahme: trotzdem zur Nachbereitung
{safeStep < timeline.length - 1
? 'Ausnahme: jetzt schon zur Nachbereitung'
: 'Ausnahme: trotzdem zur Nachbereitung'}
</button>
</div>
</div>

View File

@ -519,6 +519,20 @@ export function coachShouldPromptSplitRejoin(unit, lastTimelineEntry) {
}
}
/**
* Nach dem letzten Block eines gewählten Streams: Rückfrage vor Ganzgruppenphase oder vor dem nächsten Split,
* wenn die aktuelle Parallelphase mehrere Streams hat.
*/
export function coachShouldPromptSplitRejoinTransition(unit, currentEntry, nextEntry) {
if (!currentEntry || !nextEntry) return null
const cRm = currentEntry.runMeta
if (!cRm || cRm.kind !== 'parallel' || cRm.streamOrder == null) return null
const intoWholeGroup = nextEntry.runMeta?.kind === 'whole_group'
const intoNextSplit = nextEntry.entryKind === COACH_ENTRY_BRANCH_GATE
if (!intoWholeGroup && !intoNextSplit) return null
return coachShouldPromptSplitRejoin(unit, currentEntry)
}
export function summarizeTimelineEntry(ent) {
if (!ent) return ''
if (ent.entryKind === COACH_ENTRY_BRANCH_GATE) {