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
This commit is contained in:
@@ -0,0 +1,255 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user