# Leistungstest Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** Create Leistungstest (Performance Test) system with templates, assignment, results tracking, and leaderboard. **Architecture:** Backend provides CRUD for templates and results + leaderboard endpoint. Frontend has 4 tabs: Vorlagen, Zuweisen, Ergebnisse, Leaderboard. **Tech Stack:** Django REST Framework (backend), React/Next.js (frontend) --- ## File Structure ### Backend - **Create:** `backend/leistungstest/__init__.py` - **Create:** `backend/leistungstest/apps.py` - **Create:** `backend/leistungstest/models.py` - **Create:** `backend/leistungstest/serializers.py` - **Create:** `backend/leistungstest/views.py` - **Create:** `backend/leistungstest/urls.py` - **Modify:** `backend/wrestleDesk/settings.py` — add 'leistungstest' - **Modify:** `backend/wrestleDesk/urls.py` — include leistungstest URLs ### Frontend - **Create:** `frontend/src/app/(dashboard)/leistungstest/page.tsx` - **Modify:** `frontend/src/components/layout/sidebar.tsx` — add Leistungstest nav item - **Modify:** `frontend/src/lib/api.ts` — add interfaces --- ## Tasks ### Task 1: Create Leistungstest Backend App - [ ] **Step 1: Create directory and files** Create `backend/leistungstest/` with `__init__.py` and `apps.py` - [ ] **Step 2: Add to INSTALLED_APPS** Add `'leistungstest'` to `INSTALLED_APPS` in `backend/wrestleDesk/settings.py` - [ ] **Step 3: Create models** ```python from django.db import models from django.utils import timezone class LeistungstestTemplate(models.Model): name = models.CharField(max_length=200) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) class Meta: ordering = ['-created_at'] def __str__(self): return self.name @property def usage_count(self): return self.results.count() class LeistungstestTemplateExercise(models.Model): template = models.ForeignKey(LeistungstestTemplate, on_delete=models.CASCADE, related_name='exercises') exercise = models.ForeignKey('exercises.Exercise', on_delete=models.CASCADE) target_reps = models.PositiveIntegerField() order = models.IntegerField(default=0) class Meta: ordering = ['template', 'order'] unique_together = ['template', 'exercise'] def __str__(self): return f"{self.template.name} - {self.exercise.name}" class LeistungstestResult(models.Model): template = models.ForeignKey(LeistungstestTemplate, on_delete=models.CASCADE, related_name='results') wrestler = models.ForeignKey('wrestlers.Wrestler', on_delete=models.CASCADE, related_name='leistungstest_results') total_time_minutes = models.PositiveIntegerField(null=True, blank=True) rating = models.PositiveSmallIntegerField(choices=[(1,1),(2,2),(3,3),(4,4),(5,5)], default=3) notes = models.TextField(blank=True) completed_at = models.DateTimeField(default=timezone.now) created_at = models.DateTimeField(auto_now_add=True) class Meta: ordering = ['-completed_at'] indexes = [ models.Index(fields=['wrestler']), models.Index(fields=['template']), models.Index(fields=['completed_at']), ] def __str__(self): return f"{self.wrestler} - {self.template.name}" @property def score_percent(self): items = self.items.all() if not items.exists(): return 0 total_target = sum(item.target_reps for item in items) total_actual = sum(item.actual_reps for item in items) if total_target == 0: return 0 return round((total_actual / total_target) * 100, 1) class LeistungstestResultItem(models.Model): result = models.ForeignKey(LeistungstestResult, on_delete=models.CASCADE, related_name='items') exercise = models.ForeignKey('exercises.Exercise', on_delete=models.CASCADE) target_reps = models.PositiveIntegerField() actual_reps = models.PositiveIntegerField() order = models.IntegerField(default=0) class Meta: ordering = ['result', 'order'] def __str__(self): return f"{self.result} - {self.exercise.name}: {self.actual_reps}/{self.target_reps}" ``` - [ ] **Step 4: Create serializers** ```python from rest_framework import serializers from .models import LeistungstestTemplate, LeistungstestTemplateExercise, LeistungstestResult, LeistungstestResultItem class LeistungstestTemplateExerciseSerializer(serializers.ModelSerializer): exercise_name = serializers.CharField(source='exercise.name', read_only=True) class Meta: model = LeistungstestTemplateExercise fields = ['id', 'exercise', 'exercise_name', 'target_reps', 'order'] class LeistungstestTemplateSerializer(serializers.ModelSerializer): exercises = LeistungstestTemplateExerciseSerializer(many=True, read_only=True) usage_count = serializers.IntegerField(read_only=True) class Meta: model = LeistungstestTemplate fields = ['id', 'name', 'exercises', 'usage_count', 'created_at'] class LeistungstestResultItemSerializer(serializers.ModelSerializer): exercise_name = serializers.CharField(source='exercise.name', read_only=True) class Meta: model = LeistungstestResultItem fields = ['id', 'exercise', 'exercise_name', 'target_reps', 'actual_reps', 'order'] class LeistungstestResultSerializer(serializers.ModelSerializer): items = LeistungstestResultItemSerializer(many=True, read_only=True) template_name = serializers.CharField(source='template.name', read_only=True) wrestler_name = serializers.CharField(source='wrestler.__str__', read_only=True) score_percent = serializers.FloatField(read_only=True) class Meta: model = LeistungstestResult fields = ['id', 'template', 'template_name', 'wrestler', 'wrestler_name', 'total_time_minutes', 'rating', 'notes', 'completed_at', 'score_percent', 'items', 'created_at'] class LeaderboardEntrySerializer(serializers.Serializer): rank = serializers.IntegerField() wrestler = serializers.DictField() score_percent = serializers.FloatField() rating = serializers.IntegerField() time_minutes = serializers.IntegerField(allow_null=True) ``` - [ ] **Step 5: Create views** ```python from rest_framework import viewsets from rest_framework.decorators import api_view, permission_classes from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from django.db.models import Max from .models import LeistungstestTemplate, LeistungstestTemplateExercise, LeistungstestResult, LeistungstestResultItem from .serializers import ( LeistungstestTemplateSerializer, LeistungstestResultSerializer ) class LeistungstestTemplateViewSet(viewsets.ModelViewSet): permission_classes = [IsAuthenticated] queryset = LeistungstestTemplate.objects.all() serializer_class = LeistungstestTemplateSerializer def perform_create(self, serializer): template = serializer.save() exercises_data = self.request.data.get('exercises', []) for i, ex in enumerate(exercises_data): LeistungstestTemplateExercise.objects.create( template=template, exercise_id=ex['exercise'], target_reps=ex['target_reps'], order=i ) class LeistungstestTemplateExerciseViewSet(viewsets.ModelViewSet): permission_classes = [IsAuthenticated] queryset = LeistungstestTemplateExercise.objects.all() serializer_class = None class LeistungstestResultViewSet(viewsets.ModelViewSet): permission_classes = [IsAuthenticated] serializer_class = LeistungstestResultSerializer def get_queryset(self): queryset = LeistungstestResult.objects.all() wrestler = self.request.query_params.get('wrestler') template = self.request.query_params.get('template') if wrestler: queryset = queryset.filter(wrestler=wrestler) if template: queryset = queryset.filter(template=template) return queryset.prefetch_related('items') def perform_create(self, serializer): result = serializer.save() items_data = self.request.data.get('items', []) for i, item in enumerate(items_data): LeistungstestResultItem.objects.create( result=result, exercise_id=item['exercise'], target_reps=item['target_reps'], actual_reps=item['actual_reps'], order=i ) @api_view(['GET']) @permission_classes([IsAuthenticated]) def leaderboard(request): template_id = request.query_params.get('template') if not template_id: return Response({'error': 'template is required'}, status=400) template = LeistungstestTemplate.objects.get(id=template_id) latest_results = LeistungstestResult.objects.filter( template=template ).values('wrestler').annotate( latest_date=Max('completed_at') ) rankings = [] for entry in latest_results: result = LeistungstestResult.objects.get( template=template, wrestler_id=entry['wrestler'], completed_at=entry['latest_date'] ) wrestler = result.wrestler rankings.append({ 'rank': 0, 'wrestler': {'id': wrestler.id, 'name': str(wrestler)}, 'score_percent': result.score_percent, 'rating': result.rating, 'time_minutes': result.total_time_minutes }) rankings.sort(key=lambda x: (-x['score_percent'], x['time_minutes'] or 999)) for i, r in enumerate(rankings): r['rank'] = i + 1 return Response({ 'template': {'id': template.id, 'name': template.name}, 'rankings': rankings }) ``` - [ ] **Step 6: Create urls.py** ```python from django.urls import path, include from rest_framework.routers import DefaultRouter from .views import LeistungstestTemplateViewSet, LeistungstestResultViewSet, leaderboard router = DefaultRouter() router.register(r'templates', LeistungstestTemplateViewSet, basename='leistungstest-template') router.register(r'results', LeistungstestResultViewSet, basename='leistungstest-result') urlpatterns = [ path('leaderboard/', leaderboard, name='leistungstest-leaderboard'), path('', include(router.urls)), ] ``` - [ ] **Step 7: Add URL to main urls.py** ```python path('api/v1/leistungstest/', include('leistungstest.urls')), ``` - [ ] **Step 8: Create migration** Run: `cd backend && python manage.py makemigrations leistungstest` --- ### Task 2: Update Frontend API Types - [ ] **Step 1: Add interfaces to frontend/src/lib/api.ts** ```typescript export interface ILeistungstestTemplateExercise { id: number exercise: number exercise_name: string target_reps: number order: number } export interface ILeistungstestTemplate { id: number name: string exercises: ILeistungstestTemplateExercise[] usage_count: number created_at: string } export interface ILeistungstestResultItem { id: number exercise: number exercise_name: string target_reps: number actual_reps: number order: number } export interface ILeistungstestResult { id: number template: number template_name: string wrestler: number wrestler_name: string total_time_minutes: number | null rating: number notes: string completed_at: string score_percent: number items: ILeistungstestResultItem[] created_at: string } export interface ILeaderboardEntry { rank: number wrestler: { id: number; name: string } score_percent: number rating: number time_minutes: number | null } export interface ILeaderboard { template: { id: number; name: string } rankings: ILeaderboardEntry[] } ``` --- ### Task 3: Create Leistungstest Page - [ ] **Step 1: Create page with 4 tabs** Create `frontend/src/app/(dashboard)/leistungstest/page.tsx` with: - Tab state (vorlagen | zuweisen | ergebnisse | leaderboard) - Vorlagen tab: Template list + create form - Zuweisen tab: Select wrestler/template, record results - Ergebnisse tab: Results table with filters + progress - Leaderboard tab: Rankings by template - [ ] **Step 2: Add sidebar navigation** Add to `sidebar.tsx`: ```typescript { name: "Leistungstest", href: "/leistungstest", icon: Trophy }, ``` Import Trophy icon from lucide-react. --- ## Testing ### Backend - Run: `cd backend && python manage.py check` - Run: `python manage.py migrate` - Test endpoint: `curl -H "Authorization: Bearer " http://localhost:8000/api/v1/leistungstest/templates/` ### Frontend - Run: `cd frontend && npm run lint` - Visit: http://localhost:3000/leistungstest --- ## Notes - Wrestler/Template dropdowns use SelectValue with find() to show names not IDs - Score = (sum actual_reps / sum target_reps) * 100 - Leaderboard shows latest result per wrestler for selected template - Results table sorted by date (newest first) - Star rating component with clickable stars (1-5)