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