Files
WrestleDesk/docs/superpowers/specs/2026-03-24-leistungstest-live-timer-v2-design.md
Andrej Spielmann 3fefc550fe Initial commit: WrestleDesk full project
- 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
2026-03-26 13:24:57 +01:00

10 KiB
Raw Permalink Blame History

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/ 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

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. 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