3fefc550fe
- 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
256 lines
8.3 KiB
Markdown
256 lines
8.3 KiB
Markdown
# 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
|
|
|
|
```tsx
|
|
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
|
|
|
|
```typescript
|
|
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)
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
```json
|
|
{
|
|
"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
|
|
|
|
```typescript
|
|
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
|