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:
Andrej Spielmann
2026-03-26 13:24:57 +01:00
commit 3fefc550fe
256 changed files with 38295 additions and 0 deletions
@@ -0,0 +1,154 @@
# Homework System Integration - Design Spec
## Overview
The backend has a complete homework system with exercises, assignments, and completion tracking. The frontend currently only supports basic CRUD. This spec covers integrating all backend features into the frontend.
---
## Backend Models
1. **Homework** - Template with title, description, due_date, exercises
2. **HomeworkExerciseItem** - Links exercises to homework with reps/time
3. **HomeworkAssignment** - Assigns homework to wrestlers
4. **HomeworkAssignmentItem** - Tracks completion of each exercise
5. **HomeworkStatus** - Overall completion status
---
## Frontend Integration
### 1. Update Types (lib/api.ts)
Add missing types:
```typescript
interface IHomeworkExerciseItem {
id: number
exercise: number
exercise_name: string
reps: number | null
time_minutes: number | null
order: number
}
interface IHomework {
id: number
title: string
description: string
club: number
club_name: string
due_date: string
is_active: boolean
exercise_items: IHomeworkExerciseItem[]
exercise_count: number
created_at: string
updated_at: string
}
interface IHomeworkAssignment {
id: number
homework: number
homework_title: string
wrestler: number
wrestler_name: string
club: number
club_name: string
due_date: string
notes: string
is_completed: boolean
completion_date: string | null
items: IHomeworkAssignmentItem[]
created_at: string
}
interface IHomeworkAssignmentItem {
id: number
exercise: number
exercise_name: string
is_completed: boolean
completion_date: string | null
}
```
### 2. Homework Page - Template Management
**Exercise Selection:**
- When creating/editing homework, allow selecting exercises from the exercise list
- Each exercise can have reps (for rep-based) or time_minutes (for time-based)
- Drag to reorder exercises
**API Integration:**
- `GET /homework/{id}/exercise-items/` - Get exercises for homework
- `POST /homework/{id}/exercise-items/` - Add exercise to homework
- `DELETE /homework/{id}/exercise-items/{item_id}/` - Remove exercise
### 3. Assignments Page - View Assigned Homework
Create a new tab/section for viewing assignments:
- `GET /homework/assignments/` - List all assignments
- Filter by: homework, wrestler, completion status
- Show completion progress (e.g., "3/5 exercises completed")
**Assignment Detail:**
- View all exercises in the assignment
- Mark individual exercises as complete
- `POST /homework/assignments/{id}/complete-item/` - Mark exercise complete
### 4. Update Distribute Flow
Current: Select wrestlers, assign homework
New:
1. Select wrestlers (filtered by club + groups)
2. Due date is set per assignment
3. Creates HomeworkAssignment with all exercise items
---
## UI Components
### Homework Templates Tab
- List all homework templates
- Create/Edit with exercise selection
- Delete template
### My Homework Tab (for wrestlers)
- View assigned homework
- Track completion per exercise
- Due dates
### Assignment Management Tab (for trainers)
- View all assignments
- Filter by status (pending/completed/overdue)
- Track which wrestlers completed
---
## Data Flow
1. Trainer creates Homework template with exercises
2. Trainer assigns homework to wrestlers (filtered by group)
3. System creates HomeworkAssignment for each wrestler
4. Wrestlers see assignments and mark exercises complete
5. Trainers can track completion rates
---
## Technical Approach
**Frontend Pages:**
- `/homework` - Templates list
- `/homework/assignments` - Assignments list (new)
- `/homework/[id]/edit` - Edit template with exercises
**Key Changes:**
- Update IHomework type with exercise_items
- Add exercise selection UI to homework form
- Create assignments list view
- Add completion tracking
**API Endpoints Used:**
- `GET/POST /homework/` - CRUD templates
- `GET/POST /homework/{id}/exercise-items/` - Manage exercises
- `POST /homework/{id}/assign/` - Assign to wrestlers
- `GET /homework/assignments/` - List assignments
- `POST /homework/assignments/{id}/complete-item/` - Mark complete
@@ -0,0 +1,303 @@
# Homework System Redesign - Spezifikation
## Überblick
Komplette Neugestaltung des Homework-Systems ohne Vorlagen. Homework wird direkt von der Training-Seite aus Assignees zugewiesen.
## Konzept
**Workflow:**
1. Trainer ist auf Training-Detail-Seite
2. Bei jedem Teilnehmer gibt es einen Homework-Button (Icon)
3. Klick öffnet Modal: Exercises auswählen (mit reps/time)
4. Homework wird dem Wrestler für dieses Training zugewiesen
5. Auf der Homework-Seite sieht man alle Zuweisungen gruppiert nach Training
**Keine Templates** - Exercises werden direkt zugewiesen, keine wiederverwendbaren Vorlagen.
---
## Datenmodell
### Neues Model: `TrainingHomework`
Verknüpft ein Training mit Exercises.
| Feld | Typ | Beschreibung |
|------|-----|--------------|
| `id` | AutoField | Primary Key |
| `training` | FK → Training | Das Training |
| `created_at` | DateTime | Auto |
### Neues Model: `TrainingHomeworkAssignment`
Welcher Wrestler welche Homework eines Trainings bekommen hat.
| Feld | Typ | Beschreibung |
|------|-----|--------------|
| `id` | AutoField | Primary Key |
| `training_homework` | FK → TrainingHomework | Die Homework |
| `wrestler` | FK → Wrestler | Der Wrestler |
| `club` | FK → Club | Club (für Filter) |
| `notes` | TextField | Optionale Notizen |
| `created_at` | DateTime | Auto |
### Junction Table: `TrainingHomeworkExercise`
| Feld | Typ | Beschreibung |
|------|-----|--------------|
| `id` | AutoField | Primary Key |
| `training_homework` | FK → TrainingHomework | Die Homework |
| `exercise` | FK → Exercise | Die Übung |
| `reps` | PositiveInteger | Wiederholungen |
| `time_minutes` | PositiveInteger | Zeit in Minuten |
| `order` | Integer | Sortierung |
---
## API Endpoints
### Backend
```
GET /homework/training-assignments/ - Alle Zuweisungen für Club (gefiltert)
POST /homework/training-assignments/ - Neue Zuweisung erstellen
GET /homework/training-assignments/{id}/ - Detail
PATCH /homework/training-assignments/{id}/ - Aktualisieren (notizen, etc.)
DELETE /homework/training-assignments/{id}/ - Löschen
POST /homework/training-assignments/{id}/complete/ - Als erledigt markieren
POST /homework/training-assignments/{id}/uncomplete/ - Als nicht erledigt markieren
```
### Frontend Types
```typescript
interface ITrainingHomework {
id: number
training: number
training_date: string
training_group: string
exercises: ITrainingHomeworkExercise[]
created_at: string
}
interface ITrainingHomeworkExercise {
id: number
exercise: number
exercise_name: string
exercise_category: string
reps: number | null
time_minutes: number | null
order: number
}
interface ITrainingHomeworkAssignment {
id: number
training_homework: number
training_homework_detail: ITrainingHomework
wrestler: number
wrestler_name: string
wrestler_group: string
is_completed: boolean
completion_date: string | null
notes: string
created_at: string
}
```
---
## UI: Training Detail Seite
### Layout (oben nach unten)
```
┌─────────────────────────────────────────────────────────────┐
│ [← Zurück] Training vom 22.03.2026 [Kinder Badge]│
├─────────────────────────────────────────────────────────────┤
│ Datum: Montag, 22. März 2026 │
│ Zeit: 17:00 - 18:30 │
│ Ort: Sporthalle │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────┐ ┌─────────────────────┐ │
│ │ TEILNEHMER (8) │ │ TRAININGSHOMEWORK │ │
│ │ │ │ │ │
│ │ ┌─────────────────┐ │ │ Hier kommen die │ │
│ │ │ 🧒 Max M. 📚 │ │ │ Homework-Übungen │ │
│ │ │ Club A │ │ │ für dieses Training │ │
│ │ └─────────────────┘ │ │ rein wenn jemand │ │
│ │ ┌─────────────────┐ │ │ Homework hat. │ │
│ │ │ 🧒 Anna S. 📚 │ │ │ │ │
│ │ │ Club B │ │ │ │ │
│ │ └─────────────────┘ │ │ │ │
│ │ │ │ │ │
│ │ [+ Ringer hinzu] │ │ │ │
│ └─────────────────────┘ └─────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
```
### Teilnehmer-Karte
- Liste der Teilnehmer mit Avatar
- Bei jedem Teilnehmer rechts:
- **Homework-Button** (Buch-Icon) → öffnet Modal
- **X-Button** → entfernen
- "Ringer hinzufügen" Button unter der Liste
### Homework-Button Modal
```
┌─────────────────────────────────────────────────────────────┐
│ 📚 Hausaufgabe zuweisen - Max Mustermann │
├─────────────────────────────────────────────────────────────┤
│ │
│ Übungen auswählen: │
│ ┌─────────────────────────────────────────────────────────┐│
│ │ [🔍] Übungen durchsuchen... ││
│ ├─────────────────────────────────────────────────────────┤│
│ │ ○ Liegestütze (Kraft) [10] Reps ││
│ │ ○ Sit-ups (Kraft) [30] Sek ││
│ │ ○ Ausfallschritte (Technik) [ ] Reps [60] Sek ││
│ └─────────────────────────────────────────────────────────┘│
│ │
│ Ausgewählte Übungen: │
│ ┌─────────────────────────────────────────────────────────┐│
│ │ 1. Liegestütze - 10 Wiederholungen [✕] ││
│ │ 2. Sit-ups - 30 Sekunden [✕] ││
│ └─────────────────────────────────────────────────────────┘│
│ │
│ Notizen (optional): │
│ ┌─────────────────────────────────────────────────────────┐│
│ │ ││
│ └─────────────────────────────────────────────────────────┘│
│ │
├─────────────────────────────────────────────────────────────┤
│ [Abbrechen] [Zuweisen] │
└─────────────────────────────────────────────────────────────┘
```
---
## UI: Hausaufgaben Seite
### Layout
```
┌─────────────────────────────────────────────────────────────┐
│ 📚 Hausaufgaben │
├─────────────────────────────────────────────────────────────┤
│ │
│ Filter: [Alle] [Offen] [Erledigt] [🔍 Suche...] │
│ │
│ Training: 22.03.2026 (Kinder) │
│ ┌─────────────────────────────────────────────────────────┐│
│ │ ┌─────────────────────────────────────────────────────┐ ││
│ │ │ 🧒 Max Mustermann [Erledigt ✓] │ ││
│ │ │ Liegestütze (10), Sit-ups (30s), ... │ ││
│ │ │ Zugewiesen: vor 2 Tagen │ ││
│ │ └─────────────────────────────────────────────────────┘ ││
│ │ ┌─────────────────────────────────────────────────────┐ ││
│ │ │ 🧒 Anna Schmidt [Offen] │ ││
│ │ │ Liegestütze (10), Sit-ups (30s), ... │ ││
│ │ │ Zugewiesen: vor 2 Tagen │ ││
│ │ └─────────────────────────────────────────────────────┘ ││
│ └─────────────────────────────────────────────────────────┘│
│ │
│ Training: 20.03.2026 (Jugend) │
│ ... │
│ │
└─────────────────────────────────────────────────────────────┘
```
### Assignment Card
- Wrestler Name + Avatar
- Status Badge: "Offen" (orange) oder "Erledigt" (grün)
- Liste der Exercises mit reps/time
- "Erledigt" Button zum Markieren
- Click auf Card → expandiert Details
---
## UI: Dashboard
Zeigt offene Homework-Anzahl:
```
┌─────────────────────────────────────────────────────────────┐
│ 📊 Dashboard │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ 🧒 24 │ │ 👨‍🏫 8 │ │ 📅 12 │ │ 📚 5 │ │
│ │ Ringer │ │ Trainer │ │ Training │ │ Offene │ │
│ │ │ │ │ │ diese Wo │ │ Homework│ │
│ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
```
---
## Farben & Styling
- Status "Offen": `bg-warning/10 text-warning`
- Status "Erledigt": `bg-success/10 text-success`
- Homework Icon: `BookOpen` von Lucide
---
## Technische Umsetzung
### Backend
1. **Neue Models** in `backend/homework/models.py`:
- `TrainingHomework`
- `TrainingHomeworkExercise` (Junction)
- `TrainingHomeworkAssignment`
2. **Serializers** in `backend/homework/serializers.py`:
- `TrainingHomeworkSerializer`
- `TrainingHomeworkExerciseSerializer`
- `TrainingHomeworkAssignmentSerializer`
3. **Views** in `backend/homework/views.py`:
- `TrainingHomeworkAssignmentViewSet` mit `/training-assignments/` endpoint
- `complete` und `uncomplete` actions
4. **URLs** in `backend/homework/urls.py`:
- `training-assignments/` routes
5. **Admin** in `backend/homework/admin.py`:
- Neue Models registrieren
### Frontend
1. **Types** in `frontend/src/lib/api.ts`:
- `ITrainingHomework`
- `ITrainingHomeworkExercise`
- `ITrainingHomeworkAssignment`
2. **Training Detail Page** `frontend/src/app/(dashboard)/trainings/[id]/page.tsx`:
- Layout: Details oben, dann 2-Spalten (Teilnehmer | TrainingHomework)
- Teilnehmer-Karte mit Homework-Button
- Modal für Homework-Zuweisung
3. **Homework Page** `frontend/src/app/(dashboard)/homework/page.tsx`:
- Komplett neu schreiben
- Gruppiert nach Training
- Status-Filter
- Erledigt/Undone togglen
4. **Dashboard** `frontend/src/app/(dashboard)/dashboard/page.tsx`:
- Homework-Count hinzufügen
---
## Zu löschende alte Components
- Alte `Homework`, `HomeworkExerciseItem`, `HomeworkAssignment`, `HomeworkAssignmentItem`, `HomeworkStatus` Models optional behalten (für Migration) aber nicht mehr nutzen
- Alte Frontend-Homework-Logik entfernen
@@ -0,0 +1,123 @@
# Trainings Calendar View - Design Spec
## Overview
Add a calendar month view to the Trainings page as an alternative to the existing grid/list views. Users can toggle between three views: **Grid**, **List**, and **Calendar**.
---
## Design
### View Toggle
Location: Top-right of the page header (already exists as grid/list toggle)
```
[+ Training hinzufügen] [Grid] [List] [Calendar]
```
Three icon buttons with active state highlighting. Same pattern as existing grid/list toggle.
---
### Calendar Month View
**Library:** `react-big-calendar` with custom styling to match Shadcn design
**Layout:**
- Full-width month grid
- Weekdays: Mo Di Mi Do Fr Sa So (German)
- Month/Year header with `<` `>` navigation arrows
- "Today" button to jump to current month
**Day Cells:**
- Show up to 3 training chips per day (colored by group)
- "+N more" indicator if more trainings exist
- Today highlighted with accent ring
- Past days slightly muted
**Training Chips:**
- Small colored pills showing time + group badge
- Colors match existing groupConfig:
- kids: primary
- youth: secondary
- adults: accent
**Click on Day:**
- Opens a popover/dropdown showing all trainings for that day
- Each training shows: time, group, location, attendance count
- Click training to open detail page or edit modal
**Click on Training Chip:**
- Opens training detail modal directly
---
## Data Loading
- Fetch trainings for the displayed month (with buffer for partial weeks)
- `date_from` / `date_to` params sent to API to get relevant trainings
- Cache fetched month data to avoid re-fetching on day navigation within same month
---
## Component Structure
```
TrainingsPage/
├── TrainingsContent (main container)
│ ├── Header with toggle buttons
│ ├── FilterBar (existing)
│ ├── ViewContainer
│ │ ├── GridView (existing)
│ │ ├── ListView (existing)
│ │ └── CalendarView (NEW)
│ │ ├── CalendarHeader (month nav)
│ │ ├── CalendarGrid
│ │ └── DayPopover (trainings for selected day)
│ └── Pagination (shown in grid/list, hidden in calendar)
```
---
## Technical Approach
**Dependencies:**
- `react-big-calendar` - Calendar component
- `date-fns` - Date manipulation (already available via project or to be added)
**Styling:**
- Custom CSS matching Shadcn theme colors
- Override default react-big-calendar styles
**State:**
- `viewMode: "grid" | "list" | "calendar"` (extend existing)
- `calendarMonth: Date` - currently displayed month
- `selectedDay: Date | null` - for day popover
**API:**
- Keep existing `/trainings/?date_from=X&date_to=Y` to fetch trainings
- Calculate date range from calendar month view
---
## Interactions
| Action | Result |
|--------|--------|
| Click calendar icon in toggle | Switch to calendar view |
| Click `<` / `>` arrows | Navigate months |
| Click "Today" | Jump to current month |
| Click on day cell | Show popover with day's trainings |
| Click on training chip | Open training detail modal |
| Switch away from calendar | Preserve last viewed month |
| Create/edit training | Refresh calendar data |
---
## Polish
- Smooth transitions when switching views
- Loading skeleton for calendar while fetching
- Empty state for days with no trainings
- Responsive: on mobile, calendar shows in portrait mode with smaller cells
@@ -0,0 +1,156 @@
# Dashboard Statistics Design
## Date: 2026-03-23
## Status: Approved
## Overview
Expand the Dashboard with comprehensive statistics displayed in a Bento Grid layout. Replace the current simple count cards with rich stat cards and add new visualization cards for attendance, homework completion, wrestler distribution, and trainer activity.
## Design
### Layout Structure
```
┌─────────────────────────────────────────────────────────────────┐
│ Dashboard Willkommen, [User] │
├─────────────────────────────────────────────────────────────────┤
│ │
│ [Stat Cards Row - 4 cards] │
│ ┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐ │
│ │ Ringer │ │Trainer │ │Training│ │Hausauf.│ │
│ │ 127 │ │ 12 │ │ 45 │ │ 23 │ │
│ │+5/woche│ │Aktiv:10│ │diese: 8│ │offen │ │
│ └────────┘ └────────┘ └────────┘ └────────┘ │
│ │
│ [Middle Row - 2 cards] │
│ ┌─────────────────────────┐ ┌─────────────────────────┐ │
│ │ Teilnahme diese Woche │ │ Training Aktivität │ │
│ │ ● Kinder ████████░░ 85% │ │ ▁▂▃▅▇██▇▅▃▂▁▂▃▅▇ │ │
│ │ ● Jugend ██████░░░░ 62% │ │ Letzte 14 Tage │ │
│ │ ● Erwachs.████████ 100% │ └─────────────────────────┘ │
│ │ Ø: 24/30 │ │
│ └─────────────────────────┘ │
│ │
│ [Full Width Card] │
│ ┌─────────────────────────────────────────────────────────────┐│
│ │ ✅ Hausaufgaben Erledigung ││
│ │ ████████████░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ 38% ││
│ └─────────────────────────────────────────────────────────────┘│
│ │
│ [Bottom Row - 2 cards] │
│ ┌─────────────────────────┐ ┌─────────────────────────┐ │
│ │ Ringer nach Gruppe │ │ Top Trainer │ │
│ │ ●Kinder ████████ 45│ │ 1. Max M. 12 │ │
│ │ ●Jugend ██████░░ 35│ │ 2. Anna S. 10 │ │
│ │ ●Erwachs. █████░░░ 25│ │ 3. Tom K. 9 │ │
│ │ ●Inaktiv ███░░░░░ 22│ │ │ │
│ └─────────────────────────┘ └─────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
```
### Components
#### 1. Stat Card (existing, enhanced)
- Icon + Label (top-left)
- Main number (large, bold)
- Sub-info (small, muted)
- Hover: subtle lift effect
#### 2. Attendance Card
- Title: "Teilnahme diese Woche"
- 3 rows (Kids, Youth, Adults) with progress bars
- Progress bar: colored background with filled portion
- Percentage and absolute numbers (e.g., "24/30")
- Average attendance line at bottom
#### 3. Training Activity Card
- Title: "Training Aktivität"
- Simple bar chart visualization using divs
- 14 bars representing last 14 days
- Bar height proportional to attendance
- Label: "Letzte 14 Tage"
#### 4. Homework Completion Card (full width)
- Title: "Hausaufgaben Erledigung"
- Single horizontal progress bar
- Green fill for completed, gray for open
- Percentage and absolute numbers on right
- Trend indicator: "+5 diese Woche"
#### 5. Wrestlers by Group Card
- Title: "Ringer nach Gruppe"
- 4 rows with progress bars
- Groups: Kinder, Jugend, Erwachsene, Inaktiv
- Each row: colored dot, label, progress bar, count
#### 6. Top Trainers Card
- Title: "Top Trainer"
- List of 3-5 trainers with:
- Rank number
- Trainer name
- Training count
- Simple list, no avatars needed
### Data Requirements
**Backend API Endpoints needed:**
- `GET /api/v1/stats/dashboard/` - Returns all dashboard statistics
**Response shape:**
```json
{
"wrestlers": { "total": 127, "this_week": 5 },
"trainers": { "total": 12, "active": 10 },
"trainings": { "total": 45, "this_week": 8 },
"homework": { "open": 23, "completed": 38 },
"attendance": {
"this_week": {
"kids": { "attended": 17, "total": 20, "percent": 85 },
"youth": { "attended": 12, "total": 20, "percent": 62 },
"adults": { "attended": 20, "total": 20, "percent": 100 }
},
"average": 24,
"expected": 30
},
"activity": [
{ "date": "2026-03-09", "count": 18 },
{ "date": "2026-03-10", "count": 22 },
...
],
"wrestlers_by_group": {
"kids": 45,
"youth": 35,
"adults": 25,
"inactive": 22
},
"top_trainers": [
{ "name": "Max M.", "training_count": 12 },
{ "name": "Anna S.", "training_count": 10 },
{ "name": "Tom K.", "training_count": 9 }
]
}
```
### Implementation Steps
1. **Backend**: Create stats endpoint with all aggregations
2. **Frontend**: Update dashboard page with new stat cards
3. **Frontend**: Add new visualization cards
4. **Styling**: Match existing color scheme (primary, secondary, accent)
### Color Scheme
- Primary: `#1B1A55` (navy) - for main elements
- Secondary: `#535C91` (blue) - for secondary elements
- Accent: `#9290C3` (lavender) - for highlights
- Kids: Blue
- Youth: Purple
- Adults: Orange
- Success/Completed: Green (`#22c55e`)
- Inactive: Gray
## Notes
- All statistics should load asynchronously
- Show skeleton loaders during loading
- Error handling: show "Fehler beim Laden" message if API fails
@@ -0,0 +1,208 @@
# 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
@@ -0,0 +1,220 @@
# Leistungstest & UI Modernisierung - Design
## Status
- **Draft** - Needs user review
- **Date**: 2026-03-23
---
## Teil 1: Backend Fixes
### Problem 1: Template-Erstellung
**Aktuell**: Template wird erstellt, aber Übungen werden in separaten POST-Requests gesendet → Inkonsistenz
**Lösung**: Die `create()` Methode im ViewSet sollte Übungen direkt mit dem Template speichern
```python
def create(self, request, *args, **kwargs):
# 1. Template erstellen
# 2. Übungen direkt in derselben Transaktion speichern
# 3. Response mit allen Daten zurückgeben
```
### Problem 2: IDs statt Namen
**Ursache**: Frontend zeigt `SelectItem key={id} value={String(id)}` - der `value` ist die ID, aber `SelectValue` zeigt `children` nicht korrekt
**Lösung**: Nach dem Speichern der Übungen muss das Template mit `prefetch_related` neu geladen werden, damit `exercise_name` verfügbar ist
---
## Teil 2: UI Modernisierung (Ganze App)
### Design Philosophy: "Elegant & Clean"
- Sanfte, subtile Animationen
- Professionelles Erscheinungsbild
- Fokus auf Lesbarkeit und Benutzerfreundlichkeit
- Keine übertriebenen Effekte
### Aktuelle Probleme
1. **Keine Page Transitions** - Seitenwechsel sind abrupt
2. **Statische Hover-Effekte** - Keine sanften Übergänge
3. **Cards ohne Tiefe** - Keine subtilen Schatten oder Glows
4. **Monochrome Icons** - Keine Farbvariation
5. **FadeIn nur auf Hauptelementen** - Inkonsistente Animationen
### Geplante Verbesserungen
#### 2.1 Global Page Transitions
```tsx
// app/(dashboard)/layout.tsx
import { motion, AnimatePresence } from "framer-motion"
// Page-Übergänge mit smooth fade + slide
<AnimatePresence mode="wait">
<motion.div
initial={{ opacity: 0, y: 10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
transition={{ duration: 0.2, ease: "easeOut" }}
>
{children}
</motion.div>
</AnimatePresence>
```
#### 2.2 Subtile Card-Hover-Effekte
```tsx
// Aktuell
<Card>
// Neu mit motion
<motion.div whileHover={{ y: -2, boxShadow: "0 10px 40px -10px rgba(0,0,0,0.1)" }}>
<Card>
</motion.div>
```
#### 2.3 Button Micro-Interactions
```tsx
// Scale + subtle glow on hover
<motion.button whileHover={{ scale: 1.02 }} whileTap={{ scale: 0.98 }}>
```
#### 2.4 Staggered List Animations
```tsx
// Für Listen und Cards mit verzögertem Erscheinen
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ staggerChildren: 0.05 }}
>
{items.map(item => (
<motion.div key={item.id} variants={itemVariant} />
))}
</motion.div>
```
#### 2.5 Smooth Scrollbar Styling
```css
/* Modern scrollbar für Webkit-Browser */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: rgba(100, 100, 100, 0.3);
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: rgba(100, 100, 100, 0.5);
}
```
#### 2.6 Icon Color Transitions
```tsx
// Icons mit Farbwechsel bei Hover
<motion.div whileHover={{ color: "#1B1A55" }}>
<Icon className="transition-colors" />
</motion.div>
```
---
## Teil 3: Component-Spezifische Änderungen
### 3.1 Sidebar
- Hover-State mit sanftem Slide + Farbwechsel
- Active-State mit subtle glow/underline
- Smooth expand/collapse für Submenüs
### 3.2 Cards (Stat Cards, Data Cards)
- Subtle lift bei Hover
- Border-color transition
- Sanfte Schatten-Verstärkung
### 3.3 Tables
- Row hover mit background shift
- Smooth row appearance (staggered)
- Subtle column highlight
### 3.4 Forms
- Input focus mit border-color + subtle glow
- Label animation (float label style optional)
- Button loading state mit spinner
### 3.5 Tabs
- Underline indicator mit smooth slide
- Active tab mit subtle scale
---
## Teil 4: Animations-Token
```typescript
// lib/animations.ts (neu oder erweitern)
export const transitions = {
fast: "150ms",
normal: "250ms",
slow: "400ms",
}
export const easing = {
default: "easeOut",
smooth: [0.4, 0, 0.2, 1], // cubic-bezier
bounce: [0.68, -0.55, 0.265, 1.55],
}
export const stagger = {
fast: 0.05,
normal: 0.1,
slow: 0.15,
}
```
---
## Implementation Reihenfolge
1. **Backend Fix** (10 min)
- Template create mit embedded exercises
- Verify exercise_name wird korrekt zurückgegeben
2. **Animation Library Setup** (5 min)
- Framer Motion installieren/verifizieren
- Animation tokens definieren
3. **Global Layout** (15 min)
- Page transitions in layout.tsx
- Smooth scrolling CSS
4. **Sidebar Enhancement** (15 min)
- Hover/active animations
- Icon transitions
5. **Card Components** (20 min)
- Alle Cards mit hover effects
- Stat cards auf Dashboard
6. **List/Table Enhancements** (20 min)
- Staggered animations
- Row hover effects
7. **Form Enhancements** (15 min)
- Input focus states
- Button micro-interactions
8. **Tab Enhancements** (10 min)
- Slide underline indicator
---
## Erfolgskriterien
- [ ] Template-Erstellung funktioniert vollständig mit Übungen
- [ ] Dropdowns zeigen Namen statt IDs
- [ ] Seitenwechsel sind smooth (fade + slide)
- [ ] Hover-Effekte auf allen interaktiven Elementen
- [ ] Listen appear mit staggered animation
- [ ] Keine "jarring" Übergänge
- [ ] Performance bleibt gut (nicht zu viele Animationen)
@@ -0,0 +1,188 @@
# Training Log & Progress Analysis Design
## Date: 2026-03-23
## Status: Approved
## Overview
Create a "Training Log" page for recording actual exercises performed by wrestlers during training sessions. The system tracks detailed exercise data (reps, sets, time, weight, rating) and provides analysis features including wrestler comparison and progress tracking over time.
## Design
### Layout
Single page with 3 tabs:
- **Log** (📋) — Quick entry form for new exercise records
- **Historie** (📜) — Filterable list of all entries
- **Analyse** (📊) — Summary stats, progress tracking, and wrestler comparison
### Tab 1: Log
**Entry Form:**
- Ringer dropdown (required)
- Training dropdown (optional — links to specific training session)
- Übung dropdown (required — from exercises)
- reps input (number, required)
- sets input (number, default 1)
- zeit input (minutes, optional)
- Gewicht input (kg, optional)
- Bewertung — 5-star rating selector
- Notizen textarea (optional)
- Speichern button
### Tab 2: Historie
**Filter Bar:**
- Ringer dropdown (filter by wrestler)
- Datum dropdown (date range)
- Übung dropdown (filter by exercise)
- Suchen button
**Entry List:**
- Table showing: Datum, Ringer, Übung, reps×sets, Zeit, Gewicht, Bewertung
- Clickable rows for potential editing
- Sorted by date (newest first)
### Tab 3: Analyse
**Wrestler Selector:**
- Dropdown to select specific wrestler
- OR "Alle vergleichen" option
**Summary Card:**
- Gesamt: total entries
- Verschiedene Übungen: count of unique exercises
- Wiederholungen: total reps
- Ø Sätze: average sets per entry
- Ø Bewertung: average rating
- Diese Woche: entries this week
**Top Übungen Card:**
- List of most performed exercises with counts
- Bar visualization
**Fortschritt Card:**
- Shows improvement over time for selected wrestler
- Progress bars with percentage change
- Only exercises with measurable progress
**Übungsvergleich Card:**
- Side-by-side comparison of two wrestlers
- Bar chart showing reps for same exercise types
- Only shows exercises both wrestlers have done
## Data Models
### Backend Model: TrainingLogEntry
```python
class TrainingLogEntry(models.Model):
wrestler = ForeignKey('wrestlers.Wrestler')
training = ForeignKey('trainings.Training', null=True, blank=True)
exercise = ForeignKey('exercises.Exercise')
reps = PositiveIntegerField()
sets = PositiveIntegerField(default=1)
time_minutes = PositiveIntegerField(null=True, blank=True)
weight_kg = DecimalField(null=True, blank=True, max_digits=5, decimal_places=2)
rating = PositiveIntegerField(choices=[(1,1),(2,2),(3,3),(4,4),(5,5)])
notes = TextField(blank=True)
logged_at = DateTimeField(default=timezone.now)
created_at = DateTimeField(auto_now_add=True)
updated_at = DateTimeField(auto_now=True)
class Meta:
ordering = ['-logged_at']
indexes = [
Index(fields=['wrestler']),
Index(fields=['exercise']),
Index(fields=['logged_at']),
]
```
## API Endpoints
```
GET /api/v1/training-log/ — List entries (filterable)
POST /api/v1/training-log/ — Create entry
GET /api/v1/training-log/{id}/ — Get single entry
PATCH /api/v1/training-log/{id}/ — Update entry
DELETE /api/v1/training-log/{id}/ — Delete entry
GET /api/v1/training-log/stats/ — Analysis stats
GET /api/v1/training-log/compare/ — Comparison data
```
## Response Shapes
### List Response (GET /training-log/)
```json
{
"count": 156,
"results": [
{
"id": 1,
"wrestler": 1,
"wrestler_name": "Max M.",
"training": 5,
"training_date": "2026-03-23",
"exercise": 1,
"exercise_name": "Pushups",
"reps": 50,
"sets": 3,
"time_minutes": 2,
"weight_kg": 10.0,
"rating": 4,
"notes": "",
"logged_at": "2026-03-23T15:30:00Z"
}
]
}
```
### Stats Response (GET /training-log/stats/?wrestler=1)
```json
{
"total_entries": 156,
"unique_exercises": 12,
"total_reps": 4230,
"avg_sets": 3.2,
"avg_rating": 3.8,
"this_week": 23,
"top_exercises": [
{"name": "Pushups", "count": 45},
{"name": "Klimmzüge", "count": 32}
],
"progress": {
"Pushups": {"before": 40, "after": 50, "change_percent": 25},
"Klimmzüge": {"before": 8, "after": 10, "change_percent": 40}
}
}
```
### Compare Response (GET /training-log/compare/?wrestler1=1&wrestler2=2)
```json
{
"wrestler1": {"id": 1, "name": "Max M."},
"wrestler2": {"id": 2, "name": "Anna S."},
"exercises": [
{
"exercise": "Pushups",
"wrestler1_avg": 50,
"wrestler2_avg": 30
}
]
}
```
## Implementation Steps
1. **Backend Model** — Create TrainingLogEntry model in homework app (or new training_log app)
2. **Backend ViewSet** — CRUD endpoints + stats endpoint + compare endpoint
3. **Frontend API Types** — Add ITrainingLogEntry interface
4. **Frontend Page** — Create /training-log page with 3 tabs
## Notes
- Training dropdown should only show trainings from the past (can't log future)
- Weight should accept decimals (e.g., 10.5 kg)
- Rating is 1-5 stars
- All timestamps stored in UTC, displayed in local time
@@ -0,0 +1,91 @@
# Leistungstest Ergebnisse — Neue Ansicht
## Overview
Der "ergebnisse" Tab im Leistungstest zeigt ab sofort Ergebnisse in einer modernen Karten-Ansicht mit Toggle-Option zwischen Karten und Tabelle.
## Design
### Karten-Ansicht (Default)
- Pro Ergebnis eine übersichtliche Karte
- Header: Ringer-Avatar (initialbasiert), Name, Datum, Vorlage
- Header-Right: Score (%) in groß (farbcodiert: grün ≥100%, gelb ≥80%, rot <80%), Gesamtzeit
- Body: Liste aller Übungen mit Soll/Reps, elapsed_seconds, Done-Check
- Footer: Bewertung (Sterne), Bearbeiten/Löschen Buttons
### Toggle
- Toggle-Button-Leiste oben rechts: "⊞ Tabelle" | "📋 Karten"
- Aktive Ansicht ist hervorgehoben (weißer Hintergrund, Schatten)
- Auswahl wird in User-Preferences gespeichert (optional, falls bereits Preferences-System existiert)
### Farbcodierung
- Score ≥100%: Grün (`#16a34a`)
- Score ≥80%: Gelb (`#ca8a04`)
- Score <80%: Rot (`#dc2626`)
### Filter
- Vorlage-Dropdown (alle Vorlagen oder spezifische)
- Ringer-Dropdown (optional)
- Sortierung: Neueste zuerst (default)
### Detail-Ansicht pro Ergebnis
- Alle Übungen mit:
- Übungsname
- Soll: `targetReps` (z.B. "3×10")
- Zeit: `elapsedSeconds` formatiert als `m:ss`
- Status: ✓ wenn done
## Komponenten
### ResultsCardsView
- Rendering aller Ergebnis-Karten
- Filter-Logik
- Toggle-State
### ResultCard
- Einzelne Ergebniskarte
- Avatar mit Initial des Ringers
- Farbcodierter Score
- Übungsliste mit Zeiten
- Aktions-Buttons
## API
Ergebnisse werden via `apiFetch("/leistungstest/results/")` geladen (existierendes API).
Response-Shape:
```typescript
{
id: number
template: number
template_name: string
wrestler: number
wrestler_name: string
total_time_seconds: number
rating: number
score_percent: number
completed_at: string
items: Array<{
id: number
exercise: number
exercise_name: string
target_reps: number
actual_reps: number
elapsed_seconds: number
order: number
}>
}
```
## Backend
Keine Änderungen notwendig — bestehendes API wird verwendet.
## Implementierung
1. Neuer State `resultViewMode: "cards" | "table"` (default: "cards")
2. `ResultsCardsView` Component mit Karten-Logic
3. Toggle-Buttons im CardHeader
4. Ergebnis-Karten mit allen Details
5. Filter funktionieren wie bisher
6. Pagination bleibt bestehen
@@ -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
@@ -0,0 +1,293 @@
# 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)
```typescript
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
```typescript
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
```typescript
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:
```typescript
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
```typescript
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
@@ -0,0 +1,154 @@
# Leistungstest Statistiken — Design
## Overview
Erweiterung des Leistungstest-Bereichs um Statistiken und Ranglisten auf zwei Ebenen: **Vorlagen** und **Übungen**. Filterbar nach Zeitraum (Monat / Alle Zeiten).
## Features
### 1. Ranglisten pro Vorlage
- Rangliste aller Wrestler nach `score_percent` (absteigend)
- Optional: Sortierung nach `total_time_seconds` (aufsteigend = schnellste Zeit)
- Zeitraum-Filter: "Alle Zeiten" | "Dieser Monat" | "Letzte 3 Monate" | "Dieses Jahr"
- Anzeige: Rank, Wrestler-Name, Score %, Gesamtzeit, Datum
### 2. Ranglisten pro Übung
- Für jede Übung: Rangliste nach `elapsed_seconds` (schnellste Zeit)
- Berechnung: Beste Zeit (niedrigste elapsed_seconds) pro Wrestler pro Übung
- Zeitraum-Filter wie oben
- Anzeige: Rank, Wrestler-Name, Übungsname, Bestzeit, Datum
### 3. Ringer-Statistik (optional)
- Pro Wrestler: Durchschnitts-Score, Anzahl Tests, Trend (besser/schlechter)
## Datenmodell
Keine neuen Modelle — Berechnung erfolgt on-the-fly aus `LeistungstestResult` und `LeistungstestResultItem`.
## API Endpoints (Backend)
### `GET /api/v1/leistungstest/stats/leaderboard/`
Query-Parameter:
- `type`: `"template"` | `"exercise"`
- `template_id`: number (für template-Typ)
- `period`: `"all"` | `"month"` | `"3months"` | `"year"`
- `limit`: number (default: 10)
Response für `type=template`:
```json
{
"template_id": 1,
"template_name": "Krafttest Februar",
"period": "all",
"results": [
{
"rank": 1,
"wrestler_id": 1,
"wrestler_name": "Max Mustermann",
"score_percent": 100.0,
"total_time_seconds": 342,
"completed_at": "2026-03-24"
}
]
}
```
Response für `type=exercise`:
```json
{
"exercise_id": 1,
"exercise_name": "Liegestütze",
"period": "all",
"results": [
{
"rank": 1,
"wrestler_id": 1,
"wrestler_name": "Max Mustermann",
"best_time_seconds": 75,
"completed_at": "2026-03-24"
}
]
}
```
### `GET /api/v1/leistungstest/stats/exercises/`
Liste aller Übungen die jemals in einem Leistungstest verwendet wurden (für Dropdown).
## Frontend
### Neuer Tab: "📊 Statistiken"
Tabs im Leistungstest:
1. Vorlagen (existiert)
2. Zuweisen (existiert)
3. Ergebnisse (existiert, jetzt Karten-Ansicht)
4. **📊 Statistiken** (NEU)
### Statistiken-Tab Layout:
```
[Tab: Vorlagen | Zuweisen | Ergebnisse | 📊 Statistiken]
📊 Statistiken
[Dropdowns: Vorlage auswählen | Zeitraum: Alle Zeiten ▾]
-tabs-
[⊞ Nach Vorlage] [📋 Nach Übung]
--- Nach Vorlage ---
Rangliste: Krafttest Februar
1. 🥇 Max Mustermann — 100% (5:42)
2. 🥈 Anna Schmidt — 87% (6:20)
3. 🥉 Tom Klein — 65% (8:15)
--- Nach Übung ---
Rangliste: Liegestütze
1. 🥇 Max Mustermann — 1:15
2. 🥈 Anna Schmidt — 1:30
3. 🥉 Tom Klein — 2:00
Rangliste: Kniebeugen
1. 🥇 Max Mustermann — 2:30
...
```
## Backend-Implementierung
### Neue Datei: `leistungstest/stats.py`
```python
def get_template_leaderboard(template_id, period="all", limit=10):
# Filter results by template and period
# Order by score_percent DESC, total_time_seconds ASC
# Return top N with rank
def get_exercise_leaderboard(exercise_id, period="all", limit=10):
# For each wrestler, find their BEST (lowest) elapsed_seconds
# Filter by period
# Order by best_time ASC
# Return top N
```
### Neue URL: `leistungstest/stats.py` ViewSet
- `GET /leaderboard/` — Template oder Exercise Leaderboard
- `GET /exercises/` — Liste verwendeter Übungen
## Zeitraum-Filter Logik
```python
def get_date_range(period):
today = date.today()
if period == "month":
return today.replace(day=1)
elif period == "3months":
return today - timedelta(days=90)
elif period == "year":
return today.replace(month=1, day=1)
return None # "all"
```
## Implementierungs-Reihenfolge
1. Backend: `stats.py` mit ViewSet + URLs
2. Frontend: Neuer "Statistiken" Tab
3. Toggle zwischen Vorlagen/Übungs-Ansicht
4. Zeitraum-Filter
5. Ranglisten-Anzeige mit Medaillen-Icons