Files
WrestleDesk/docs/superpowers/specs/2026-03-24-leistungstest-live-timer-design.md
T
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

8.3 KiB

Leistungstest Live-Timer — Specification

Overview

Add a live-timer mode to the Leistungstest "Zuweisen" tab. Instead of manually entering results for each wrestler, trainers can run through a timed workout session: select a template and multiple wrestlers, then start a live timer that tracks per-wrestler time and reps as they complete exercises in sequence. Results are saved immediately per wrestler.

Architecture

Two Modes in the Zuweisen Tab

The Zuweisen tab has two modes, toggled by a "Modus" switcher above the form:

  1. Form Mode (default, existing behavior): Template + Wrestler selection → manual time/reps entry → save
  2. Timer Mode (new): Template + Wrestler selection → live timer interface → results saved per wrestler automatically

Mode Toggle

const [inputMode, setInputMode] = useState<"form" | "timer">("form")
  • Toggle switch above the form
  • Form Mode: existing inputs for template, wrestlers, time (min+sec), rating, notes
  • Timer Mode: shows "Training starten" button when wrestlers selected

Timer Mode Data Flow

State

interface TimerWrestler {
  wrestler: IWrestler
  status: "pending" | "active" | "done"
  startedAt: number | null      // Date.now() when started
  exercises: TimerExercise[]
  resultId: number | null       // created after first wrestler finishes
}

interface TimerExercise {
  exerciseId: number
  exerciseName: string
  targetReps: number           // from template
  actualReps: string           // user input
  status: "pending" | "done"
  startedAt: number | null
}

interface TimerSession {
  templateId: number
  wrestlers: TimerWrestler[]
  currentWrestlerIndex: number
  totalElapsedSeconds: number
  isRunning: boolean
}

Session Persistence (localStorage)

// Key: "leistungstest_timer_session"
// Saved on every state change
// Restored on page load if session exists and isRunning === false (interrupted)

On page load, if an incomplete session is found in localStorage:

  • Prompt: "Offenes Training gefunden. Fortsetzen?" → Yes restores session, No clears it

UI Layout — Timer Mode

Split View

┌─────────────────────┬──────────────────────────────────────────┐
│  RINGER             │  AKTUELLER RINGER: Anna Schmidt          │
│                     │                                          │
│  ○ Max Mustermann   │  ┌──────────────┐  [Pausieren]           │
│  ● Anna Schmidt     │  │   05:23     │                         │
│  ○ Tom Klein       │  └──────────────┘                         │
│                     │                                          │
│                     │  ÜBUNGEN (2/5)                          │
│                     │  ─────────────────────────────────────── │
│                     │  Liegestütze         Soll: 3x10          │
│                     │  [___ Ist-Reps]  ▶  START               │
│                     │                                          │
│                     │  Kniebeugen          Soll: 3x15          │
│                     │  [___ Ist-Reps]  ✅ ERLEDIGT            │
│                     │                                          │
│                     │  Burpees             Soll: 3x8             │
│                     │  [___ Ist-Reps]  ▶  START               │
│                     │                                          │
│                     │  ─────────────────────────────────────── │
│                     │  [WEITER ZUM NÄCHSTEN RINGER →]       │
│                     │  [TRAINING BEENDEN]                     │
└─────────────────────┴──────────────────────────────────────────┘

Components

Left Panel (wrestler list):

  • Shows all selected wrestlers
  • Status indicator: ○ pending, ● active, done
  • Click on a done wrestler to review/edit their results
  • Stays visible throughout

Right Panel — Top (current wrestler + timer):

  • Wrestler name prominently displayed
  • Large timer display: MM:SS format, updates every second
  • Pause/Resume button
  • Total elapsed time (cumulative across all wrestlers)

Right Panel — Middle (exercise list):

  • List of exercises from template
  • Each row shows: exercise name, target reps ("Soll")
  • Input field for actual reps
  • Start/Done button per exercise
  • When exercise is marked done: saves to that wrestler's result (if result exists) or marks pending

Right Panel — Bottom (actions):

  • "Weiter zum nächsten Ringer" → marks current wrestler done, saves result, advances
  • "Training beenden" → final save, exits timer mode, shows summary

Timer Logic

Starting

  1. User selects template (Sheet) + wrestlers (Sheet)
  2. Clicks "Training starten"
  3. Timer mode activates, first wrestler is set to "active", timer starts

Per-Exercise Flow

  1. Trainer sees current exercise (from template)
  2. Enters actual reps in input field
  3. Clicks "Start" → exercise timer starts
  4. Wrestler completes exercise
  5. Trainer clicks "Done" → exercise marked complete, elapsed time recorded
  6. Next exercise auto-advances to active state

Per-Wrestler Flow

  1. All exercises done for current wrestler
  2. Trainer clicks "Weiter zum nächsten Ringer"
  3. Result is saved immediately via API:
    • POST /leistungstest/results/ with all exercise items
    • total_time_seconds = elapsed time for this wrestler
  4. Next wrestler becomes active, timer continues (does NOT reset)
  5. Repeat until all wrestlers done

Finishing

  1. Trainer clicks "Training beenden"
  2. If current wrestler has started but not all exercises done → prompt: "Nicht alle Übungen gemacht. Trotzdem beenden?"
  3. Confirm → save current wrestler's partial result
  4. Show summary: wrestlers completed, total time, scores

Pause/Resume

  • "Pausieren" stops the timer
  • Timer display shows "PAUSIERT" in orange
  • "Fortsetzen" resumes
  • Paused time is accumulated in totalElapsedSeconds

Backend API

No new endpoints needed. Use existing:

  • POST /leistungstest/results/ — create result for each wrestler
  • GET /leistungstest/results/?template=X — list results

Create Result Payload

{
  "template": 1,
  "wrestler": 5,
  "total_time_seconds": 323,
  "rating": 3,
  "notes": "",
  "items": [
    { "exercise": 3, "target_reps": 30, "actual_reps": 28, "order": 0 },
    { "exercise": 7, "target_reps": 45, "actual_reps": 45, "order": 1 }
  ]
}

Form Mode (Existing)

Unchanged behavior. Shows when inputMode === "form":

  • Template select (Sheet)
  • Wrestler select (Sheet, multi)
  • Minutes + seconds inputs
  • Rating select
  • Notes textarea
  • Submit creates single result

File Structure

frontend/src/app/(dashboard)/leistungstest/page.tsx
- Add inputMode state
- Add TimerMode component (inline or separate)
- TimerMode: TimerSession, TimerWrestler, TimerExercise types
- localStorage persistence with useEffect

New sub-components within page.tsx:
- TimerMode (full-width layout replacing the form)
- WrestlerListPanel (left side)
- TimerPanel (right side: timer + exercises)

Session Persistence

const SESSION_KEY = "leistungstest_timer_session"

useEffect(() => {
  if (inputMode === "timer" && session) {
    localStorage.setItem(SESSION_KEY, JSON.stringify(session))
  }
}, [session, inputMode])

useEffect(() => {
  if (inputMode === "timer") {
    const saved = localStorage.getItem(SESSION_KEY)
    if (saved) {
      const parsed = JSON.parse(saved)
      if (parsed.isRunning === false) {
        // show restore prompt
      }
    }
  }
}, [inputMode])

Implementation Priority

  1. Timer state + basic timer display (MM:SS ticking)
  2. Wrestler list panel with status
  3. Exercise list with reps input + start/done
  4. Per-wrestler save on "Weiter"
  5. localStorage persistence
  6. Pause/Resume
  7. Training beenden + summary
  8. Form mode toggle