diff --git a/backend/routers/training_planning.py b/backend/routers/training_planning.py index cc9a34c..6208193 100644 --- a/backend/routers/training_planning.py +++ b/backend/routers/training_planning.py @@ -679,6 +679,17 @@ def update_training_unit(unit_id: int, data: dict, session=Depends(require_auth) _template_access(cur, tid, profile_id, role) tpl_id_val = tid + trainer_notes_val = None + if "trainer_notes" not in data: + cur.execute( + "SELECT trainer_notes FROM training_units WHERE id = %s", + (unit_id,), + ) + row_tn = cur.fetchone() + trainer_notes_val = row_tn["trainer_notes"] if row_tn else None + else: + trainer_notes_val = data.get("trainer_notes") + cur.execute( """ UPDATE training_units SET @@ -708,7 +719,7 @@ def update_training_unit(unit_id: int, data: dict, session=Depends(require_auth) data.get("attendance_count"), data.get("status"), data.get("notes"), - data.get("trainer_notes"), + trainer_notes_val, tpl_id_val, unit_id, ), diff --git a/frontend/src/pages/TrainingCoachPage.jsx b/frontend/src/pages/TrainingCoachPage.jsx index b09d0d6..b5e1419 100644 --- a/frontend/src/pages/TrainingCoachPage.jsx +++ b/frontend/src/pages/TrainingCoachPage.jsx @@ -20,6 +20,10 @@ function storageDeltasKey(unitId) { return `sj_coach_deltas_${unitId}` } +function storageDebriefKey(unitId) { + return `sj_coach_debrief_${unitId}` +} + function formatClock(totalSec) { const m = Math.floor(totalSec / 60) const s = totalSec % 60 @@ -30,22 +34,119 @@ function mergeDelta(setDeltas, itemKey, patch) { setDeltas((prev) => ({ ...prev, [itemKey]: { ...prev[itemKey], ...patch } })) } -function CoachStepNavBar({ step, timelineLength, onPrev, onNext, onDone }) { +function CoachControlsBand({ + step, + timelineLength, + onPrev, + onNext, + onDone, + clockStr, + runStartAt, + pausedAccumMs, + onTimerStart, + onTimerPause, + onTimerReset, + onApplyActual, + roundedMinForApply, + isLastCoachStep, + showJumpToTimerOwner, + showJumpToTimerOwnerRow = true, + onJumpToTimerOwner, + timerOwnerLabelIndex, +}) { const disPrev = step <= 0 const disNext = step >= timelineLength - 1 + const alive = runStartAt != null || pausedAccumMs > 0 + const canApplyForOwner = roundedMinForApply != null && roundedMinForApply >= 1 + const istLabelMin = roundedMinForApply == null ? '—' : String(roundedMinForApply) + const doneLabel = + timelineLength <= 1 + ? 'Nachbereitung öffnen' + : isLastCoachStep + ? 'Nachbereitung & Ist-Zeit' + : 'Abgeschlossen & weiter' + return (
-
- +
+ )} +
+ - + ) : ( + + )} + + {alive && ( + + )} +
+ +
+ - ) } @@ -64,12 +165,14 @@ export default function TrainingCoachPage() { const [catalogLoading, setCatalogLoading] = useState(false) const [catalogError, setCatalogError] = useState(null) const [debriefOpen, setDebriefOpen] = useState(false) + const [coachDebriefPhase, setCoachDebriefPhase] = useState(false) const [step, setStep] = useState(0) const [deltas, setDeltas] = useState({}) const [runStartAt, setRunStartAt] = useState(null) const [pausedAccumMs, setPausedAccumMs] = useState(0) + const [timerOwningStep, setTimerOwningStep] = useState(null) const [, setPulse] = useState(0) const [trainerAppend, setTrainerAppend] = useState('') @@ -110,6 +213,11 @@ export default function TrainingCoachPage() { } catch { /* ignore */ } + try { + if (sessionStorage.getItem(storageDebriefKey(idNum)) === '1') setCoachDebriefPhase(true) + } catch { + /* ignore */ + } } catch (e) { if (!cancelled) setLoadError(e.message || 'Laden fehlgeschlagen') } finally { @@ -131,9 +239,14 @@ export default function TrainingCoachPage() { } catch { /* quota */ } - }, [idNum, deltas]) - useEffect(() => { + try { + sessionStorage.setItem(storageDebriefKey(idNum), coachDebriefPhase ? '1' : '0') + } catch { + /* quota */ + } + }, [idNum, coachDebriefPhase]) + if (runStartAt == null) return undefined const iv = setInterval(() => setPulse((p) => p + 1), 380) return () => clearInterval(iv) @@ -150,8 +263,14 @@ export default function TrainingCoachPage() { setStep(0) return } + if (coachDebriefPhase) return setStep((prev) => clampStep(prev, timeline.length)) - }, [unit, timeline.length]) + }, [unit, timeline.length, coachDebriefPhase]) + + useEffect(() => { + if (!coachDebriefPhase || !unit || timeline.length === 0) return + setStep(timeline.length - 1) + }, [coachDebriefPhase, unit, timeline.length]) const elapsedMs = pausedAccumMs + (runStartAt != null ? Date.now() - runStartAt : 0) @@ -162,8 +281,17 @@ export default function TrainingCoachPage() { const nextEntry = timeline[step + 1] || null const next2Entry = timeline[step + 2] || null + const clockStr = formatClock(tickDisplaySec) + const roundedMinApply = elapsedMs <= 650 ? null : Math.max(1, Math.round(elapsedMs / 60000)) + const showJumpToTimerOwner = + timerOwningStep != null && + step !== timerOwningStep && + (runStartAt != null || pausedAccumMs > 0) + const isLastCoachStep = timeline.length > 0 && step >= timeline.length - 1 + const timerStart = () => { setRunStartAt(Date.now()) + setTimerOwningStep(step) } const timerPause = () => { @@ -176,13 +304,18 @@ export default function TrainingCoachPage() { const timerReset = () => { setRunStartAt(null) setPausedAccumMs(0) + setTimerOwningStep(null) } const applySuggestedDuration = () => { - if (!currentEntry?.item || currentEntry.item.item_type !== 'exercise') return - const key = itemStableKey(currentEntry.item, currentEntry.secOrder, currentEntry.ii) - const min = Math.max(1, Math.round(elapsedMs / 60000)) || null - if (min != null) mergeDelta(setDeltas, key, { actual_duration_min: min }) + const idx = timerOwningStep != null ? timerOwningStep : step + const ent = timeline[idx] + const item = ent?.item + if (!item || item.item_type !== 'exercise') return + if (elapsedMs <= 650) return + const min = Math.max(1, Math.round(elapsedMs / 60000)) + const key = itemStableKey(item, ent.secOrder, ent.ii) + mergeDelta(setDeltas, key, { actual_duration_min: min }) timerPause() } @@ -190,8 +323,27 @@ export default function TrainingCoachPage() { const goNext = () => setStep((s) => clampStep(s + 1)) const markCurrentDoneAdvance = () => { - timerPause() - if (step < timeline.length - 1) setStep((s) => s + 1) + const ownerIdx = timerOwningStep != null ? timerOwningStep : step + const ent = timeline[ownerIdx] + const item = ent?.item + if (item?.item_type === 'exercise' && elapsedMs > 650) { + const key = itemStableKey(item, ent.secOrder, ent.ii) + const min = Math.max(1, Math.round(elapsedMs / 60000)) + mergeDelta(setDeltas, key, { actual_duration_min: min }) + } + timerReset() + + const lastIdx = timeline.length - 1 + if (step >= lastIdx && lastIdx >= 0) { + setCoachDebriefPhase(true) + try { + sessionStorage.setItem(storageDebriefKey(idNum), '1') + } catch { + /* ignore */ + } + return + } + setStep((s) => clampStep(s + 1, timeline.length)) } const durationOverridesForApi = useMemo(() => { @@ -253,23 +405,27 @@ export default function TrainingCoachPage() { try { const sectionsPayload = sectionsToPutPayload(unit, durationOverridesForApi) const tn = trainerAppend.trim() - const mergedTrainer = tn - ? [unit.trainer_notes || '', `--- (${new Date().toLocaleString('de-DE')}) ---`, tn].filter(Boolean).join('\n') - : unit.trainer_notes - - await api.updateTrainingUnit(idNum, { - trainer_notes: mergedTrainer.trim() ? mergedTrainer.trim() : unit.trainer_notes, - ...(saveMarkDone ? { status: 'completed' } : {}), + const payload = { sections: sectionsPayload, - }) + ...(saveMarkDone ? { status: 'completed' } : {}), + } + if (tn) { + payload.trainer_notes = [unit.trainer_notes || '', `--- (${new Date().toLocaleString('de-DE')}) ---`, tn] + .filter(Boolean) + .join('\n') + .trim() + } + await api.updateTrainingUnit(idNum, payload) await reloadUnit() setTrainerAppend('') try { sessionStorage.removeItem(storageDeltasKey(idNum)) + sessionStorage.removeItem(storageDebriefKey(idNum)) } catch { /* ignore */ } setDeltas({}) + setCoachDebriefPhase(false) setSaveOk('Gespeichert.') setDebriefOpen(false) } catch (e) { @@ -341,7 +497,8 @@ export default function TrainingCoachPage() { }} >
- Coach · Schritt {(step || 0) + 1} / {Math.max(timeline.length, 1)} + Coach ·{' '} + {coachDebriefPhase ? 'Nachbereitung · abschließend speichern' : `Schritt ${(step || 0) + 1} / ${Math.max(timeline.length, 1)}`}

{unit.planned_date} @@ -357,7 +514,7 @@ export default function TrainingCoachPage() { {timeline.map((ent, ix) => { const lbl = summarizeTimelineEntry(ent) const secTitle = ent.sec.title || `Abschnitt ${ent.si + 1}` - const active = ix === step + const active = coachDebriefPhase ? ix === timeline.length - 1 : ix === step return (