- Django backend with DRF (clubs, wrestlers, trainers, exercises, templates, trainings, homework, locations, leistungstest) - Next.js 16 frontend with React, Shadcn UI, Tailwind - JWT authentication - Full CRUD for all entities - Calendar view for trainings - Homework management system - Leistungstest tracking
10 KiB
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)
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/withtemplate,wrestler,total_time_seconds(cumulative),rating: 3,notes: "",itemsarray
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
const [inputMode, setInputMode] = useState<"form" | "timer">("form")
const [session, setSession] = useState<TrainingSession | null>(null)
const [globalTimerInterval, setGlobalTimerInterval] = useState<NodeJS.Timeout | null>(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
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:
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
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.30or just stores as-is)- Backend serializer accepts
target_repsas 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
itemsarray (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
- Page reload during training → localStorage restore, resume/discard prompt
- Trainer closes browser → session persisted, can resume
- All exercises done before "Training beenden" → auto-trigger summary
- Only some wrestlers finish → partial results saved, can resume later
- Pause → global timer stops, all active exercises pause (no per-exercise timer, just global)
- 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