# Leistungstest Live-Timer — Design Specification ## Overview **What:** A live-timer mode for the Leistungstest "Zuweisen" tab where a trainer runs through a timed workout with multiple wrestlers simultaneously. All wrestlers start the same exercise at the same time. The trainer clicks "Erledigt" per wrestler when each finishes — the next exercise starts automatically. All wrestlers visible in a table-like layout. **Why:** Replaces the current per-wrestler, per-exercise form entry with a real-time group training experience. Trainers can see all wrestlers at once and track live progress. --- ## Data Model ### TrainingSession (React state + localStorage) ```typescript interface TrainingSession { templateId: number templateName: string wrestlers: WrestlerSession[] globalElapsedSeconds: number // shared across all wrestlers isRunning: boolean } interface WrestlerSession { wrestler: IWrestler currentExerciseIndex: number exercises: ExerciseSession[] } interface ExerciseSession { exerciseId: number exerciseName: string targetReps: string // e.g. "3×10" — from template elapsedSeconds: number // time when Erledigt was clicked status: "pending" | "active" | "done" } ``` ### Backend API Uses existing endpoint: `POST /leistungstest/results/` When a wrestler's exercise is marked "Erledigt": - `POST /leistungstest/results/` with `template`, `wrestler`, `total_time_seconds` (cumulative), `rating: 3`, `notes: ""`, `items` array When all exercises of a wrestler are done: - The full result is already saved incrementally — no final save step needed No new backend endpoints required. --- ## User Flow ### Step 1: Setup (Form Mode) - Trainer is in "Zuweisen" tab - Selects a **Template** (has exercises + target reps pre-defined) - Selects **Wrestlers** (multiple via Sheet) - Clicks **"⏱ Training starten"** → switches to timer mode ### Step 2: Training Starts - All wrestlers begin **Exercise 1** simultaneously - Global timer starts (00:00 → 00:01 → ...) - All wrestlers visible in table-like layout, each with their own exercise cards ### Step 3: Tracking - Trainer watches live elapsed time per wrestler/exercise - When a wrestler finishes an exercise → clicks **"✓ Erledigt"** - Elapsed time for that exercise is saved - Next exercise for that wrestler starts immediately (auto-start) - Other wrestlers continue their current exercise ### Step 4: Completion - When all exercises of all wrestlers are done (or trainer clicks "Training beenden"): - Summary modal shows: X/Y wrestlers completed, total time - All results already saved to backend via incremental POSTs - localStorage session cleared - Trainer returns to form mode ### Step 5: Post-Edit - Results visible in "Ergebnisse" tab - Can be edited (notes, rating, individual exercise times) via existing edit form --- ## UI Layout ### Header Banner (always visible during timer) ``` ┌──────────────────────────────────────────────────────────────┐ │ GEMEINSAME ZEIT 05:23 [⏸ Pausieren] [■ Ende] │ └──────────────────────────────────────────────────────────────┘ ``` - Dark navy background (#1B1A55) - Large monospace timer (MM:SS) - "Pausieren" toggles to "▶ Fortsetzen" when paused - "Training beenden" opens confirmation dialog ### Wrestler Rows (scrollable list) Each wrestler = one row, stacked vertically. Like a table but each row is independent. ``` ┌──────────────────────────────────────────────────────────────┐ │ ● Max Mustermann 3/3 ✓ [grün] │ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │ │Liegestütze │ │Kniebeugen │ │Burpees │ │ │ │Soll: 3×10 │ │Soll: 3×15 │ │Soll: 3×20 │ │ │ │✓ 0:28 │ │✓ 0:44 │ │✓ 0:19 │ │ │ │[grün] │ │[grün] │ │[grün] │ │ │ └─────────────┘ └─────────────┘ └─────────────┘ │ └──────────────────────────────────────────────────────────────┘ ``` Three states per exercise card: | State | Background | Border | Badge | Button | |-------|-----------|--------|-------|--------| | **pending** | gray-50 | gray-200 | — | ▶ Start (disabled) | | **active** | yellow-50 | yellow-400 | ▶ Aktiv | ✓ Erledigt | | **done** | green-50 | green-400 | ✓ MM:SS | — | When a wrestler finishes all exercises → row header turns green with "✓ Alle erledigt". ### Exercise Card (within wrestler row) ``` ┌──────────────────────────┐ │ ▶ Übungsname │ │ Soll: 3×10 │ │ MM:SS │ │ [✓ Erledigt] │ └──────────────────────────┘ ``` - Exercise name with status icon - Target reps from template (e.g. "Soll: 3×10") — **reps, not time** - Live elapsed seconds (MM:SS) updating every second when active - Button: "✓ Erledigt" for active exercises --- ## State Management ### React State ```typescript const [inputMode, setInputMode] = useState<"form" | "timer">("form") const [session, setSession] = useState(null) const [globalTimerInterval, setGlobalTimerInterval] = useState(null) const [restoreModalOpen, setRestoreModalOpen] = useState(false) const [endModalOpen, setEndModalOpen] = useState(false) const [summary, setSummary] = useState<{ completed: number; total: number; totalTime: number } | null>(null) ``` ### Timer Tick Effect ```typescript useEffect(() => { if (session?.isRunning) { const interval = setInterval(() => { setSession(prev => prev ? { ...prev, globalElapsedSeconds: prev.globalElapsedSeconds + 1 } : prev) }, 1000) setGlobalTimerInterval(interval) return () => { clearInterval(interval); setGlobalTimerInterval(null) } } }, [session?.isRunning]) ``` ### localStorage Persistence Key: `"leistungstest_timer_session"` Saved on every session change. Restored session shows resume/discard modal on next load. --- ## Session Initialization (Start Training) When "⏱ Training starten" is clicked: ```typescript const timerWrestlers: WrestlerSession[] = wrestlers .filter(w => selectedWrestlerIds.includes(w.id)) .map(w => ({ wrestler: w, currentExerciseIndex: 0, exercises: template.exercises.map(e => ({ exerciseId: e.exercise, exerciseName: e.exercise_name || String(e.exercise), targetReps: e.target_reps, // e.g. "3×10" from template elapsedSeconds: 0, status: "active" as const, // all start active (first exercise) })), })) setSession({ templateId: template.id, templateName: template.name, wrestlers: timerWrestlers, globalElapsedSeconds: 0, isRunning: true, }) setInputMode("timer") ``` All wrestlers start with their first exercise in "active" state simultaneously. --- ## Marking Exercise Done ```typescript const markDone = async (wrestlerIdx: number, exerciseIdx: number) => { const wrestler = session.wrestlers[wrestlerIdx] const exercise = wrestler.exercises[exerciseIdx] const elapsed = exercise.elapsedSeconds // Save to backend await apiFetch("/leistungstest/results/", { method: "POST", token, body: JSON.stringify({ template: session.templateId, wrestler: wrestler.wrestler.id, total_time_seconds: elapsed, rating: 3, notes: "", items: [{ exercise: exercise.exerciseId, target_reps: parseReps(exercise.targetReps), actual_reps: parseReps(exercise.targetReps), order: exerciseIdx, }], }), }) // Update state: mark done, auto-start next const updated = [...session.wrestlers] updated[wrestlerIdx] = { ...wrestler, exercises: wrestler.exercises.map((e, i) => i === exerciseIdx ? { ...e, status: "done" as const, elapsedSeconds: elapsed } : e ), } // Auto-start next exercise if exists const nextIdx = exerciseIdx + 1 if (nextIdx < wrestler.exercises.length) { updated[wrestlerIdx].exercises[nextIdx].status = "active" updated[wrestlerIdx].currentExerciseIndex = nextIdx } setSession({ ...session, wrestlers: updated }) } ``` --- ## Backend Compatibility ### Template stores target_reps as string (e.g. "3×10") - `parseReps("3×10")` → extracts number for API (e.g. `30` or just stores as-is) - Backend serializer accepts `target_reps` as string or int — needs verification ### LeistungstestResult stores `total_time_seconds` (int) - Each "Erledigt" click saves the cumulative elapsed time for that wrestler - Individual exercise times stored in `items` array (one item per exercise) ### If backend needs `items` on every save or only at end - Currently: save on every "Erledigt" click with just that exercise - Alternative: save incrementally, last item completes the result - **Decision needed:** Does the backend create one result per exercise click, or update an existing result? --- ## Edge Cases 1. **Page reload during training** → localStorage restore, resume/discard prompt 2. **Trainer closes browser** → session persisted, can resume 3. **All exercises done before "Training beenden"** → auto-trigger summary 4. **Only some wrestlers finish** → partial results saved, can resume later 5. **Pause** → global timer stops, all active exercises pause (no per-exercise timer, just global) 6. **Empty template** → disable "Training starten" if template has no exercises --- ## Out of Scope (for this implementation) - Real-time sync across multiple trainers (future) - Per-exercise individual timers (only global timer) - Audio alerts - Export/print - Changing exercise order mid-training