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
209 lines
5.8 KiB
Markdown
209 lines
5.8 KiB
Markdown
# Leistungstest Design
|
|
|
|
## Date: 2026-03-23
|
|
|
|
## Status: Approved
|
|
|
|
## Overview
|
|
|
|
Create a "Leistungstest" (Performance Test) page for creating fitness test templates and assigning them to wrestlers. Each test records exercise results, total time, and rating. Results are tracked over time with progress visualization and leaderboards.
|
|
|
|
## Design
|
|
|
|
### Layout
|
|
|
|
Single page with 4 tabs:
|
|
- **Vorlagen** (📋) — Create/edit/delete test templates
|
|
- **Zuweisen** (📝) — Assign template to wrestler, record results
|
|
- **Ergebnisse** (📊) — View results with progress tracking
|
|
- **Leaderboard** (🏆) — Rankings by template
|
|
|
|
Plus: Sidebar navigation item "Leistungstest"
|
|
|
|
### Tab 1: Vorlagen
|
|
|
|
**Template List:**
|
|
- Card for each template showing name, exercise list, usage count
|
|
- Delete button on each card
|
|
|
|
**Create Template Form:**
|
|
- Name input
|
|
- Dynamic list of exercises with target reps
|
|
- Add/remove exercise buttons
|
|
- Save button
|
|
|
|
### Tab 2: Zuweisen
|
|
|
|
**Selection:**
|
|
- Wrestler dropdown (shows names, not IDs)
|
|
- Template dropdown (shows names, not IDs)
|
|
|
|
**Test Form (when both selected):**
|
|
- List of exercises from template
|
|
- For each exercise: target reps input + actual result input
|
|
- Total time input (minutes)
|
|
- Overall rating (5 stars)
|
|
- Notes textarea
|
|
- Submit button
|
|
|
|
### Tab 3: Ergebnisse
|
|
|
|
**Filters:**
|
|
- Wrestler dropdown
|
|
- Template dropdown
|
|
|
|
**Results Table:**
|
|
- Columns: Date, Wrestler, Template, Score (%), Rating, Time
|
|
- Sorted by date (newest first)
|
|
|
|
**Progress Section (when one wrestler + one template selected):**
|
|
- Shows improvement over time for each exercise
|
|
- Progress bars with percentage change
|
|
|
|
### Tab 4: Leaderboard
|
|
|
|
**Selection:**
|
|
- Template dropdown
|
|
|
|
**Rankings Table:**
|
|
- Columns: Rank, Wrestler, Score %, Rating, Time
|
|
- Sorted by score (highest first)
|
|
- Medal icons for top 3 (🥇🥈🥉)
|
|
|
|
## Data Models
|
|
|
|
### Backend Model: LeistungstestTemplate
|
|
|
|
```python
|
|
class LeistungstestTemplate(models.Model):
|
|
name = CharField(max_length=200)
|
|
created_at = DateTimeField(auto_now_add=True)
|
|
updated_at = DateTimeField(auto_now=True)
|
|
|
|
class Meta:
|
|
ordering = ['-created_at']
|
|
```
|
|
|
|
### Backend Model: LeistungstestTemplateExercise
|
|
|
|
```python
|
|
class LeistungstestTemplateExercise(models.Model):
|
|
template = ForeignKey(LeistungstestTemplate, related_name='exercises')
|
|
exercise = ForeignKey('exercises.Exercise')
|
|
target_reps = PositiveIntegerField()
|
|
order = IntegerField(default=0)
|
|
|
|
class Meta:
|
|
ordering = ['template', 'order']
|
|
unique_together = ['template', 'exercise']
|
|
```
|
|
|
|
### Backend Model: LeistungstestResult
|
|
|
|
```python
|
|
class LeistungstestResult(models.Model):
|
|
template = ForeignKey(LeistungstestTemplate)
|
|
wrestler = ForeignKey('wrestlers.Wrestler')
|
|
total_time_minutes = PositiveIntegerField(null=True, blank=True)
|
|
rating = PositiveSmallIntegerField(choices=[(1,1),(2,2),(3,3),(4,4),(5,5)], default=3)
|
|
notes = TextField(blank=True)
|
|
completed_at = DateTimeField(default=timezone.now)
|
|
created_at = DateTimeField(auto_now_add=True)
|
|
|
|
class Meta:
|
|
ordering = ['-completed_at']
|
|
indexes = [
|
|
Index(fields=['wrestler']),
|
|
Index(fields=['template']),
|
|
Index(fields=['completed_at']),
|
|
]
|
|
```
|
|
|
|
### Backend Model: LeistungstestResultItem
|
|
|
|
```python
|
|
class LeistungstestResultItem(models.Model):
|
|
result = ForeignKey(LeistungstestResult, related_name='items')
|
|
exercise = ForeignKey('exercises.Exercise')
|
|
target_reps = PositiveIntegerField()
|
|
actual_reps = PositiveIntegerField()
|
|
order = IntegerField(default=0)
|
|
|
|
class Meta:
|
|
ordering = ['result', 'order']
|
|
```
|
|
|
|
## API Endpoints
|
|
|
|
```
|
|
# Templates
|
|
GET /api/v1/leistungstest/templates/ — List templates
|
|
POST /api/v1/leistungstest/templates/ — Create template
|
|
GET /api/v1/leistungstest/templates/{id}/ — Get template
|
|
PATCH /api/v1/leistungstest/templates/{id}/ — Update template
|
|
DELETE /api/v1/leistungstest/templates/{id}/ — Delete template
|
|
|
|
# Template Exercises
|
|
POST /api/v1/leistungstest/template-exercises/ — Add exercise to template
|
|
DELETE /api/v1/leistungstest/template-exercises/{id}/ — Remove exercise
|
|
|
|
# Results
|
|
GET /api/v1/leistungstest/results/ — List results (filterable)
|
|
POST /api/v1/leistungstest/results/ — Create result
|
|
GET /api/v1/leistungstest/results/{id}/ — Get result
|
|
DELETE /api/v1/leistungstest/results/{id}/ — Delete result
|
|
|
|
# Leaderboard
|
|
GET /api/v1/leistungstest/leaderboard/ — Get rankings by template
|
|
```
|
|
|
|
## Response Shapes
|
|
|
|
### Template Response
|
|
```json
|
|
{
|
|
"id": 1,
|
|
"name": "Kraft-Test",
|
|
"exercises": [
|
|
{"id": 1, "exercise": 1, "exercise_name": "Klimmzüge", "target_reps": 20, "order": 0},
|
|
{"id": 2, "exercise": 2, "exercise_name": "Liegestütze", "target_reps": 50, "order": 1}
|
|
],
|
|
"usage_count": 12,
|
|
"created_at": "2026-03-20T10:00:00Z"
|
|
}
|
|
```
|
|
|
|
### Create Result Request
|
|
```json
|
|
{
|
|
"template": 1,
|
|
"wrestler": 1,
|
|
"total_time_minutes": 12,
|
|
"rating": 4,
|
|
"notes": "Gute Leistung",
|
|
"items": [
|
|
{"exercise": 1, "target_reps": 20, "actual_reps": 20},
|
|
{"exercise": 2, "target_reps": 50, "actual_reps": 48}
|
|
]
|
|
}
|
|
```
|
|
|
|
### Leaderboard Response
|
|
```json
|
|
{
|
|
"template": {"id": 1, "name": "Kraft-Test"},
|
|
"rankings": [
|
|
{"rank": 1, "wrestler": {"id": 2, "name": "Anna S."}, "score_percent": 100, "rating": 5, "time_minutes": 10},
|
|
{"rank": 2, "wrestler": {"id": 1, "name": "Max M."}, "score_percent": 96, "rating": 4, "time_minutes": 12}
|
|
]
|
|
}
|
|
```
|
|
|
|
## Implementation Notes
|
|
|
|
- Wrestler and template dropdowns show names, not IDs (use SelectValue with find)
|
|
- Score = (sum of actual_reps / sum of target_reps) * 100
|
|
- Results table shows score as percentage with progress bar
|
|
- Leaderboard only shows wrestlers who have done the specific template
|
|
- Progress tracking shows change in score between first and latest result for same wrestler+template
|