/**
* Trainingsablauf anzeigen, drucken und lokal auf der Matte abhaken (Fortschritt im Browser gespeichert).
* Phasen: Ganzgruppe vs. Split (planLoc); Druck mit optional getrennten Breakout-Seiten.
*/
import React, { useCallback, useEffect, useMemo, useState } from 'react'
import { Link, useNavigate, useParams } from 'react-router-dom'
import api from '../utils/api'
import ExercisePeekModal from '../components/ExercisePeekModal'
import CombinationPlanBracket from '../components/CombinationPlanBracket'
import {
buildPlanRunViewModelFromSections,
itemStableKey,
sectionsWithPlanLocForDisplay,
sortedItems,
} from '../utils/trainingPlanUtils'
import { effectiveComboMethodProfile } from '../utils/comboPlanningMethodProfile'
function storageKey(unitId) {
return `sj_training_run_checked_${unitId}`
}
function formatMin(m) {
if (m === null || m === undefined || m === '') return null
const n = Number(m)
if (!Number.isFinite(n)) return null
return `${n} Min.`
}
function statusLabel(s) {
if (s === 'completed') return 'Durchgeführt'
if (s === 'cancelled') return 'Abgesagt'
return 'Geplant'
}
export default function TrainingUnitRunPage() {
const { unitId } = useParams()
const navigate = useNavigate()
const idNum = unitId ? parseInt(unitId, 10) : NaN
const [unit, setUnit] = useState(null)
const [loadError, setLoadError] = useState(null)
const [loading, setLoading] = useState(true)
const [checked, setChecked] = useState(() => new Set())
const [peekCtx, setPeekCtx] = useState(null)
/** null | printStreamId z. B. p0-s1 — nur dieser Split-Stream in @media print */
const [printOnlyStreamId, setPrintOnlyStreamId] = useState(null)
const loadChecked = useCallback((uid) => {
try {
const raw = sessionStorage.getItem(storageKey(uid))
if (!raw) return new Set()
const arr = JSON.parse(raw)
if (!Array.isArray(arr)) return new Set()
return new Set(arr.map(String))
} catch {
return new Set()
}
}, [])
useEffect(() => {
if (!unitId || Number.isNaN(idNum)) {
setLoadError('Ungültige Trainingseinheit')
setLoading(false)
return
}
let cancelled = false
;(async () => {
setLoading(true)
setLoadError(null)
try {
const u = await api.getTrainingUnit(idNum)
if (!cancelled) {
setUnit(u)
setChecked(loadChecked(idNum))
}
} catch (e) {
if (!cancelled) setLoadError(e.message || 'Laden fehlgeschlagen')
} finally {
if (!cancelled) setLoading(false)
}
})()
return () => {
cancelled = true
}
}, [unitId, idNum, loadChecked])
const persistChecked = useCallback(
(next) => {
setChecked(next)
try {
sessionStorage.setItem(storageKey(idNum), JSON.stringify([...next]))
} catch {
/* ignore quota */
}
},
[idNum]
)
const toggle = useCallback(
(key) => {
const next = new Set(checked)
if (next.has(key)) next.delete(key)
else next.add(key)
persistChecked(next)
},
[checked, persistChecked]
)
const clearProgress = useCallback(() => {
persistChecked(new Set())
try {
sessionStorage.removeItem(storageKey(idNum))
} catch {
/* ignore */
}
}, [idNum, persistChecked])
const sections = useMemo(() => sectionsWithPlanLocForDisplay(unit), [unit])
const planModel = useMemo(() => buildPlanRunViewModelFromSections(sections), [sections])
const printStreamOptions = useMemo(() => {
const opts = []
for (const run of planModel.runs) {
if (run.kind !== 'parallel' || !run.streams) continue
for (const st of run.streams) {
opts.push({
id: st.printStreamId,
label: `${run.phaseTitle ? String(run.phaseTitle).trim().slice(0, 28) : `Phase ${run.phaseOrderIndex}`} · ${st.streamTitle ? String(st.streamTitle).trim() : `Gruppe ${st.streamOrder + 1}`}`,
})
}
}
return opts
}, [planModel.runs])
const totalPlannedMin = planModel.totalMin
const showWholeGroupInView = !printOnlyStreamId
const showStreamColumn = (streamPrintId) => !printOnlyStreamId || streamPrintId === printOnlyStreamId
const renderSectionCard = (sec, siInUnit) => {
const secOrder = sec.order_index ?? siInUnit
const items = sortedItems(sec)
return (
{sec.guidance_notes}
{sec.title || `Abschnitt ${siInUnit + 1}`}
{sec.guidance_notes && (
{items.map((it, ii) => {
const ck = itemStableKey(it, secOrder, ii)
const done = checked.has(ck)
if (it.item_type === 'note') {
return (
Plan wird geladen…
{loadError || 'Trainingseinheit nicht gefunden.'}
| Block | Art | Min | ∑ bis hier |
|---|---|---|---|
| {label} | {run.kind === 'parallel' ? 'Split' : run.kind === 'legacy' ? '—' : 'Ganzgruppe'} | {run.minutes || '—'} | {cum} |
Hinweis: Startzeit oben im Kopf; Minuten sind aus den Übungseinplanungen — ohne Pausen und ohne automatische Uhrzeitliste.
)}Noch keine Abschnitte in diesem Plan. Unter Planung bearbeiten.
) : ({unit.trainer_notes}