feat: enhance training unit update functionality and improve UI controls
- Added logic to retrieve existing trainer notes if not provided during the update of training units. - Updated the TrainingCoachPage to include new controls for managing training sessions, including timer functionalities and navigation enhancements. - Improved user experience with clearer button labels and conditional rendering based on the training session state.
This commit is contained in:
parent
6fd316e985
commit
0dfc08459e
|
|
@ -679,6 +679,17 @@ def update_training_unit(unit_id: int, data: dict, session=Depends(require_auth)
|
||||||
_template_access(cur, tid, profile_id, role)
|
_template_access(cur, tid, profile_id, role)
|
||||||
tpl_id_val = tid
|
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(
|
cur.execute(
|
||||||
"""
|
"""
|
||||||
UPDATE training_units SET
|
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("attendance_count"),
|
||||||
data.get("status"),
|
data.get("status"),
|
||||||
data.get("notes"),
|
data.get("notes"),
|
||||||
data.get("trainer_notes"),
|
trainer_notes_val,
|
||||||
tpl_id_val,
|
tpl_id_val,
|
||||||
unit_id,
|
unit_id,
|
||||||
),
|
),
|
||||||
|
|
|
||||||
|
|
@ -20,6 +20,10 @@ function storageDeltasKey(unitId) {
|
||||||
return `sj_coach_deltas_${unitId}`
|
return `sj_coach_deltas_${unitId}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function storageDebriefKey(unitId) {
|
||||||
|
return `sj_coach_debrief_${unitId}`
|
||||||
|
}
|
||||||
|
|
||||||
function formatClock(totalSec) {
|
function formatClock(totalSec) {
|
||||||
const m = Math.floor(totalSec / 60)
|
const m = Math.floor(totalSec / 60)
|
||||||
const s = totalSec % 60
|
const s = totalSec % 60
|
||||||
|
|
@ -30,22 +34,119 @@ function mergeDelta(setDeltas, itemKey, patch) {
|
||||||
setDeltas((prev) => ({ ...prev, [itemKey]: { ...prev[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 disPrev = step <= 0
|
||||||
const disNext = step >= timelineLength - 1
|
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 (
|
return (
|
||||||
<div className="training-coach-stepbar" style={{ marginBottom: '10px' }}>
|
<div className="training-coach-stepbar" style={{ marginBottom: '10px' }}>
|
||||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '10px', marginBottom: '8px' }}>
|
{showJumpToTimerOwner && showJumpToTimerOwnerRow && (
|
||||||
<button type="button" className="btn btn-secondary" style={{ minHeight: '50px', fontWeight: 700 }} disabled={disPrev} onClick={onPrev}>
|
<div style={{ textAlign: 'center', marginBottom: '8px' }}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-secondary"
|
||||||
|
style={{ fontSize: '0.82rem', fontWeight: 600, padding: '8px 12px', lineHeight: 1.3 }}
|
||||||
|
onClick={onJumpToTimerOwner}
|
||||||
|
>
|
||||||
|
← Zur laufenden Übung ({timerOwnerLabelIndex + 1}/{timelineLength})
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
display: 'grid',
|
||||||
|
gridTemplateColumns: 'minmax(92px,1fr) minmax(0,2fr) minmax(92px,1fr)',
|
||||||
|
gap: '8px',
|
||||||
|
alignItems: 'stretch',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-secondary"
|
||||||
|
style={{ minHeight: '46px', fontWeight: 700, padding: '6px 8px' }}
|
||||||
|
disabled={disPrev}
|
||||||
|
onClick={onPrev}
|
||||||
|
>
|
||||||
← zurück
|
← zurück
|
||||||
</button>
|
</button>
|
||||||
<button type="button" className="btn btn-secondary" style={{ minHeight: '50px', fontWeight: 700 }} disabled={disNext} onClick={onNext}>
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '6px', justifyContent: 'center' }}>
|
||||||
|
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '6px', justifyContent: 'center', alignItems: 'center' }}>
|
||||||
|
{runStartAt == null ? (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-primary"
|
||||||
|
style={{ minHeight: '44px', flex: '1 1 auto', fontWeight: 700 }}
|
||||||
|
onClick={onTimerStart}
|
||||||
|
>
|
||||||
|
▶ Start{alive ? ` · ${clockStr}` : ''}
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<button type="button" className="btn btn-secondary" style={{ minHeight: '44px', flex: '1 1 auto', fontWeight: 700 }} onClick={onTimerPause}>
|
||||||
|
Pause · {clockStr}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
<button type="button" className="btn btn-secondary" style={{ minHeight: '44px', flex: '0 1 auto', fontWeight: 600 }} disabled={!canApplyForOwner} onClick={onApplyActual}>
|
||||||
|
Ist ({istLabelMin} Min)
|
||||||
|
</button>
|
||||||
|
{alive && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-secondary"
|
||||||
|
title="Zeit löschen"
|
||||||
|
style={{ minHeight: '44px', flex: '0 0 auto', padding: '0 10px' }}
|
||||||
|
onClick={onTimerReset}
|
||||||
|
>
|
||||||
|
↺
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-primary"
|
||||||
|
style={{ width: '100%', minHeight: '44px', fontWeight: 700, lineHeight: 1.2, padding: '6px 10px', whiteSpace: 'normal', textAlign: 'center' }}
|
||||||
|
onClick={onDone}
|
||||||
|
>
|
||||||
|
{doneLabel}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-secondary"
|
||||||
|
style={{ minHeight: '46px', fontWeight: 700, padding: '6px 8px' }}
|
||||||
|
disabled={disNext}
|
||||||
|
onClick={onNext}
|
||||||
|
>
|
||||||
weiter →
|
weiter →
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<button type="button" className="btn btn-primary" style={{ width: '100%', minHeight: '48px', fontWeight: 700 }} onClick={onDone}>
|
|
||||||
Abgeschlossen & weiter
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -64,12 +165,14 @@ export default function TrainingCoachPage() {
|
||||||
const [catalogLoading, setCatalogLoading] = useState(false)
|
const [catalogLoading, setCatalogLoading] = useState(false)
|
||||||
const [catalogError, setCatalogError] = useState(null)
|
const [catalogError, setCatalogError] = useState(null)
|
||||||
const [debriefOpen, setDebriefOpen] = useState(false)
|
const [debriefOpen, setDebriefOpen] = useState(false)
|
||||||
|
const [coachDebriefPhase, setCoachDebriefPhase] = useState(false)
|
||||||
|
|
||||||
const [step, setStep] = useState(0)
|
const [step, setStep] = useState(0)
|
||||||
const [deltas, setDeltas] = useState({})
|
const [deltas, setDeltas] = useState({})
|
||||||
|
|
||||||
const [runStartAt, setRunStartAt] = useState(null)
|
const [runStartAt, setRunStartAt] = useState(null)
|
||||||
const [pausedAccumMs, setPausedAccumMs] = useState(0)
|
const [pausedAccumMs, setPausedAccumMs] = useState(0)
|
||||||
|
const [timerOwningStep, setTimerOwningStep] = useState(null)
|
||||||
const [, setPulse] = useState(0)
|
const [, setPulse] = useState(0)
|
||||||
|
|
||||||
const [trainerAppend, setTrainerAppend] = useState('')
|
const [trainerAppend, setTrainerAppend] = useState('')
|
||||||
|
|
@ -110,6 +213,11 @@ export default function TrainingCoachPage() {
|
||||||
} catch {
|
} catch {
|
||||||
/* ignore */
|
/* ignore */
|
||||||
}
|
}
|
||||||
|
try {
|
||||||
|
if (sessionStorage.getItem(storageDebriefKey(idNum)) === '1') setCoachDebriefPhase(true)
|
||||||
|
} catch {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (!cancelled) setLoadError(e.message || 'Laden fehlgeschlagen')
|
if (!cancelled) setLoadError(e.message || 'Laden fehlgeschlagen')
|
||||||
} finally {
|
} finally {
|
||||||
|
|
@ -131,9 +239,14 @@ export default function TrainingCoachPage() {
|
||||||
} catch {
|
} catch {
|
||||||
/* quota */
|
/* quota */
|
||||||
}
|
}
|
||||||
}, [idNum, deltas])
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
try {
|
||||||
|
sessionStorage.setItem(storageDebriefKey(idNum), coachDebriefPhase ? '1' : '0')
|
||||||
|
} catch {
|
||||||
|
/* quota */
|
||||||
|
}
|
||||||
|
}, [idNum, coachDebriefPhase])
|
||||||
|
|
||||||
if (runStartAt == null) return undefined
|
if (runStartAt == null) return undefined
|
||||||
const iv = setInterval(() => setPulse((p) => p + 1), 380)
|
const iv = setInterval(() => setPulse((p) => p + 1), 380)
|
||||||
return () => clearInterval(iv)
|
return () => clearInterval(iv)
|
||||||
|
|
@ -150,8 +263,14 @@ export default function TrainingCoachPage() {
|
||||||
setStep(0)
|
setStep(0)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (coachDebriefPhase) return
|
||||||
setStep((prev) => clampStep(prev, timeline.length))
|
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 =
|
const elapsedMs =
|
||||||
pausedAccumMs + (runStartAt != null ? Date.now() - runStartAt : 0)
|
pausedAccumMs + (runStartAt != null ? Date.now() - runStartAt : 0)
|
||||||
|
|
@ -162,8 +281,17 @@ export default function TrainingCoachPage() {
|
||||||
const nextEntry = timeline[step + 1] || null
|
const nextEntry = timeline[step + 1] || null
|
||||||
const next2Entry = timeline[step + 2] || 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 = () => {
|
const timerStart = () => {
|
||||||
setRunStartAt(Date.now())
|
setRunStartAt(Date.now())
|
||||||
|
setTimerOwningStep(step)
|
||||||
}
|
}
|
||||||
|
|
||||||
const timerPause = () => {
|
const timerPause = () => {
|
||||||
|
|
@ -176,13 +304,18 @@ export default function TrainingCoachPage() {
|
||||||
const timerReset = () => {
|
const timerReset = () => {
|
||||||
setRunStartAt(null)
|
setRunStartAt(null)
|
||||||
setPausedAccumMs(0)
|
setPausedAccumMs(0)
|
||||||
|
setTimerOwningStep(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
const applySuggestedDuration = () => {
|
const applySuggestedDuration = () => {
|
||||||
if (!currentEntry?.item || currentEntry.item.item_type !== 'exercise') return
|
const idx = timerOwningStep != null ? timerOwningStep : step
|
||||||
const key = itemStableKey(currentEntry.item, currentEntry.secOrder, currentEntry.ii)
|
const ent = timeline[idx]
|
||||||
const min = Math.max(1, Math.round(elapsedMs / 60000)) || null
|
const item = ent?.item
|
||||||
if (min != null) mergeDelta(setDeltas, key, { actual_duration_min: min })
|
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()
|
timerPause()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -190,8 +323,27 @@ export default function TrainingCoachPage() {
|
||||||
const goNext = () => setStep((s) => clampStep(s + 1))
|
const goNext = () => setStep((s) => clampStep(s + 1))
|
||||||
|
|
||||||
const markCurrentDoneAdvance = () => {
|
const markCurrentDoneAdvance = () => {
|
||||||
timerPause()
|
const ownerIdx = timerOwningStep != null ? timerOwningStep : step
|
||||||
if (step < timeline.length - 1) setStep((s) => s + 1)
|
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(() => {
|
const durationOverridesForApi = useMemo(() => {
|
||||||
|
|
@ -253,23 +405,27 @@ export default function TrainingCoachPage() {
|
||||||
try {
|
try {
|
||||||
const sectionsPayload = sectionsToPutPayload(unit, durationOverridesForApi)
|
const sectionsPayload = sectionsToPutPayload(unit, durationOverridesForApi)
|
||||||
const tn = trainerAppend.trim()
|
const tn = trainerAppend.trim()
|
||||||
const mergedTrainer = tn
|
const payload = {
|
||||||
? [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' } : {}),
|
|
||||||
sections: sectionsPayload,
|
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()
|
await reloadUnit()
|
||||||
setTrainerAppend('')
|
setTrainerAppend('')
|
||||||
try {
|
try {
|
||||||
sessionStorage.removeItem(storageDeltasKey(idNum))
|
sessionStorage.removeItem(storageDeltasKey(idNum))
|
||||||
|
sessionStorage.removeItem(storageDebriefKey(idNum))
|
||||||
} catch {
|
} catch {
|
||||||
/* ignore */
|
/* ignore */
|
||||||
}
|
}
|
||||||
setDeltas({})
|
setDeltas({})
|
||||||
|
setCoachDebriefPhase(false)
|
||||||
setSaveOk('Gespeichert.')
|
setSaveOk('Gespeichert.')
|
||||||
setDebriefOpen(false)
|
setDebriefOpen(false)
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
@ -341,7 +497,8 @@ export default function TrainingCoachPage() {
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div style={{ fontSize: '0.7rem', color: 'var(--text3)', textTransform: 'uppercase', letterSpacing: '0.06em' }}>
|
<div style={{ fontSize: '0.7rem', color: 'var(--text3)', textTransform: 'uppercase', letterSpacing: '0.06em' }}>
|
||||||
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)}`}
|
||||||
</div>
|
</div>
|
||||||
<h1 style={{ fontSize: '1.1rem', margin: '4px 0 0', lineHeight: 1.28 }}>
|
<h1 style={{ fontSize: '1.1rem', margin: '4px 0 0', lineHeight: 1.28 }}>
|
||||||
{unit.planned_date}
|
{unit.planned_date}
|
||||||
|
|
@ -357,7 +514,7 @@ export default function TrainingCoachPage() {
|
||||||
{timeline.map((ent, ix) => {
|
{timeline.map((ent, ix) => {
|
||||||
const lbl = summarizeTimelineEntry(ent)
|
const lbl = summarizeTimelineEntry(ent)
|
||||||
const secTitle = ent.sec.title || `Abschnitt ${ent.si + 1}`
|
const secTitle = ent.sec.title || `Abschnitt ${ent.si + 1}`
|
||||||
const active = ix === step
|
const active = coachDebriefPhase ? ix === timeline.length - 1 : ix === step
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
key={`${itemStableKey(ent.item, ent.secOrder, ent.ii)}-${ix}`}
|
key={`${itemStableKey(ent.item, ent.secOrder, ent.ii)}-${ix}`}
|
||||||
|
|
@ -369,7 +526,10 @@ export default function TrainingCoachPage() {
|
||||||
opacity: active ? 1 : 0.92,
|
opacity: active ? 1 : 0.92,
|
||||||
fontWeight: active ? 700 : 500
|
fontWeight: active ? 700 : 500
|
||||||
}}
|
}}
|
||||||
onClick={() => setStep(ix)}
|
onClick={() => {
|
||||||
|
setCoachDebriefPhase(false)
|
||||||
|
setStep(ix)
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<span style={{ opacity: 0.75 }}>{secTitle.substring(0, 14)} › </span>
|
<span style={{ opacity: 0.75 }}>{secTitle.substring(0, 14)} › </span>
|
||||||
<span>{lbl}</span>
|
<span>{lbl}</span>
|
||||||
|
|
@ -384,6 +544,84 @@ export default function TrainingCoachPage() {
|
||||||
<p className="card" style={{ padding: '1rem', flexShrink: 0 }}>
|
<p className="card" style={{ padding: '1rem', flexShrink: 0 }}>
|
||||||
Dieser Plan ist leer. <Link to="/planning">Unter Planung ergänzen</Link>.
|
Dieser Plan ist leer. <Link to="/planning">Unter Planung ergänzen</Link>.
|
||||||
</p>
|
</p>
|
||||||
|
) : coachDebriefPhase ? (
|
||||||
|
<div className="training-coach-scroll" style={{ flex: 1, minHeight: 0 }}>
|
||||||
|
<div className="card" style={{ padding: '14px', marginBottom: '12px', borderRadius: '12px', borderLeft: `4px solid var(--accent)` }}>
|
||||||
|
<div style={{ fontSize: '0.88rem', fontWeight: 700, marginBottom: '8px', color: 'var(--accent-dark)' }}>
|
||||||
|
Nachbereitung
|
||||||
|
</div>
|
||||||
|
<p style={{ fontSize: '0.82rem', color: 'var(--text2)', margin: '0 0 14px', lineHeight: 1.5 }}>
|
||||||
|
<strong>Ist‑Minuten</strong> prüfen, Trainer‑Ergänzung anlegen und Einheit sichern (zeigt bestehende Notizen weiter unten an).
|
||||||
|
</p>
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '10px', marginBottom: '12px' }}>
|
||||||
|
{timeline
|
||||||
|
.filter((e) => e.item.item_type === 'exercise')
|
||||||
|
.map((ent) => {
|
||||||
|
const k = itemStableKey(ent.item, ent.secOrder, ent.ii)
|
||||||
|
const val = deltas[k]?.actual_duration_min ?? ent.item.actual_duration_min ?? ''
|
||||||
|
return (
|
||||||
|
<label key={`db-${k}`} style={{ display: 'grid', gridTemplateColumns: '1fr 88px', gap: '10px', alignItems: 'center', fontSize: '0.88rem' }}>
|
||||||
|
<span style={{ wordBreak: 'break-word' }}>{summarizeTimelineEntry(ent)}</span>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
className="form-input"
|
||||||
|
style={{ margin: 0 }}
|
||||||
|
value={val === '' || val == null ? '' : String(val)}
|
||||||
|
placeholder="Min"
|
||||||
|
onChange={(e) => {
|
||||||
|
const raw = e.target.value
|
||||||
|
if (raw === '') mergeDelta(setDeltas, k, { actual_duration_min: null })
|
||||||
|
else mergeDelta(setDeltas, k, { actual_duration_min: parseInt(raw, 10) })
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<label style={{ display: 'block', marginBottom: '10px', fontSize: '0.88rem', color: 'var(--text2)' }}>
|
||||||
|
Ergänzung Trainer (wird angehängt · mit Zeitstempel)
|
||||||
|
<textarea className="form-input" rows={3} value={trainerAppend} onChange={(e) => setTrainerAppend(e.target.value)} style={{ marginTop: '6px' }} />
|
||||||
|
</label>
|
||||||
|
<label style={{ display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '12px', fontSize: '0.88rem' }}>
|
||||||
|
<input type="checkbox" checked={saveMarkDone} onChange={(e) => setSaveMarkDone(e.target.checked)} />
|
||||||
|
Einheit als <strong>durchgeführt</strong> markieren
|
||||||
|
</label>
|
||||||
|
{saveOk && (
|
||||||
|
<p
|
||||||
|
style={{
|
||||||
|
fontSize: '0.88rem',
|
||||||
|
marginBottom: '8px',
|
||||||
|
color: saveOk.startsWith('Fehler') ? 'var(--danger)' : 'var(--accent-dark)'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{saveOk}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', gap: '10px' }}>
|
||||||
|
<button type="button" className="btn btn-primary" style={{ width: '100%', minHeight: '48px' }} disabled={saving} onClick={handleSaveDebrief}>
|
||||||
|
{saving ? 'Speichert…' : 'Speichern'}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-secondary"
|
||||||
|
style={{ width: '100%', minHeight: '44px' }}
|
||||||
|
onClick={() => {
|
||||||
|
setCoachDebriefPhase(false)
|
||||||
|
if (timeline.length > 0) setStep(Math.max(0, timeline.length - 1))
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Zurück zur letzten Übungsposition
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{unit.trainer_notes ? (
|
||||||
|
<div className="card" style={{ marginBottom: '12px', padding: '12px 14px', borderLeft: `4px solid var(--accent)` }}>
|
||||||
|
<div style={{ fontSize: '0.7rem', fontWeight: 700, color: 'var(--text3)', textTransform: 'uppercase' }}>Einheit · Trainernotizen</div>
|
||||||
|
<p style={{ fontSize: '0.88rem', marginTop: '6px', whiteSpace: 'pre-wrap', color: 'var(--text2)' }}>{unit.trainer_notes}</p>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
|
|
@ -412,17 +650,30 @@ export default function TrainingCoachPage() {
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<p style={{ margin: 0, fontSize: '0.88rem', color: 'var(--text2)' }}>
|
<p style={{ margin: 0, fontSize: '0.88rem', color: 'var(--text2)' }}>
|
||||||
Letzter Punkt — unten Zeit speichern / Nachbereitung öffnen.
|
Letzter Punkt — „Nachbereitung & Ist-Zeit“ öffnet die Abschlussseite zum Speichern.
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<CoachStepNavBar
|
<CoachControlsBand
|
||||||
step={step}
|
step={step}
|
||||||
timelineLength={timeline.length}
|
timelineLength={timeline.length}
|
||||||
onPrev={goPrev}
|
onPrev={goPrev}
|
||||||
onNext={goNext}
|
onNext={goNext}
|
||||||
onDone={markCurrentDoneAdvance}
|
onDone={markCurrentDoneAdvance}
|
||||||
|
clockStr={clockStr}
|
||||||
|
runStartAt={runStartAt}
|
||||||
|
pausedAccumMs={pausedAccumMs}
|
||||||
|
onTimerStart={timerStart}
|
||||||
|
onTimerPause={timerPause}
|
||||||
|
onTimerReset={timerReset}
|
||||||
|
onApplyActual={applySuggestedDuration}
|
||||||
|
roundedMinForApply={roundedMinApply}
|
||||||
|
isLastCoachStep={isLastCoachStep}
|
||||||
|
showJumpToTimerOwner={showJumpToTimerOwner}
|
||||||
|
showJumpToTimerOwnerRow
|
||||||
|
onJumpToTimerOwner={() => setStep(timerOwningStep ?? step)}
|
||||||
|
timerOwnerLabelIndex={timerOwningStep ?? 0}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<div className="training-coach-scroll">
|
<div className="training-coach-scroll">
|
||||||
|
|
@ -565,34 +816,26 @@ export default function TrainingCoachPage() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="card training-coach-timer" style={{ flexShrink: 0, padding: '12px', textAlign: 'center', background: 'var(--surface2)', borderRadius: '12px', marginBottom: '8px' }}>
|
<CoachControlsBand
|
||||||
<div style={{ fontSize: '0.7rem', textTransform: 'uppercase', letterSpacing: '0.08em', color: 'var(--text3)', marginBottom: '6px' }}>Zeitnahme</div>
|
step={step}
|
||||||
<div style={{ fontSize: '2.2rem', fontWeight: 800, letterSpacing: '0.06em', fontVariantNumeric: 'tabular-nums', marginBottom: '10px' }}>
|
timelineLength={timeline.length}
|
||||||
{formatClock(tickDisplaySec)}
|
onPrev={goPrev}
|
||||||
</div>
|
onNext={goNext}
|
||||||
<div style={{ display: 'flex', gap: '8px', flexWrap: 'wrap', justifyContent: 'center', marginBottom: '8px' }}>
|
onDone={markCurrentDoneAdvance}
|
||||||
{!runStartAt ? (
|
clockStr={clockStr}
|
||||||
<button type="button" className="btn btn-primary" style={{ minWidth: '100px', minHeight: '46px' }} onClick={timerStart}>
|
runStartAt={runStartAt}
|
||||||
Start
|
pausedAccumMs={pausedAccumMs}
|
||||||
</button>
|
onTimerStart={timerStart}
|
||||||
) : (
|
onTimerPause={timerPause}
|
||||||
<button type="button" className="btn btn-secondary" style={{ minWidth: '100px', minHeight: '46px' }} onClick={timerPause}>
|
onTimerReset={timerReset}
|
||||||
Pause / Stopp
|
onApplyActual={applySuggestedDuration}
|
||||||
</button>
|
roundedMinForApply={roundedMinApply}
|
||||||
)}
|
isLastCoachStep={isLastCoachStep}
|
||||||
<button type="button" className="btn btn-secondary" style={{ minHeight: '46px' }} onClick={() => timerReset()}>
|
showJumpToTimerOwner={showJumpToTimerOwner}
|
||||||
Zurücksetzen
|
showJumpToTimerOwnerRow={false}
|
||||||
</button>
|
onJumpToTimerOwner={() => setStep(timerOwningStep ?? step)}
|
||||||
</div>
|
timerOwnerLabelIndex={timerOwningStep ?? 0}
|
||||||
<p style={{ fontSize: '0.76rem', color: 'var(--text2)', marginBottom: '10px' }}>
|
/>
|
||||||
Übernimmt Ist‑Minuten für diesen Platz (gerundet).
|
|
||||||
</p>
|
|
||||||
<button type="button" className="btn btn-primary" style={{ width: '100%', maxWidth: '400px', minHeight: '46px' }} onClick={applySuggestedDuration}>
|
|
||||||
{`Übernehmen Ist-Zeit (${Math.max(1, Math.round(elapsedMs / 60000))} Min.)`}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<CoachStepNavBar step={step} timelineLength={timeline.length} onPrev={goPrev} onNext={goNext} onDone={markCurrentDoneAdvance} />
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user