# Leistungstest Statistiken 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:** Add statistics and leaderboards to Leistungstest with rankings by template and by exercise, filterable by time period. **Architecture:** - Backend: New ViewSet with leaderboard endpoints in `leistungstest/stats.py` and `leistungstest/views.py` - Frontend: New "📊 Statistiken" tab in Leistungstest page with template/exercise toggle and period filter **Tech Stack:** Django REST Framework, React/Next.js, Tailwind CSS --- ## File Structure ### Backend - Create: `backend/leistungstest/stats.py` — Leaderboard calculation logic - Modify: `backend/leistungstest/views.py` — Add stats ViewSet - Modify: `backend/leistungstest/urls.py` — Add stats URLs - Modify: `frontend/src/lib/api.ts` — Add TypeScript interfaces ### Frontend - Modify: `frontend/src/app/(dashboard)/leistungstest/page.tsx` — Add Statistiken tab --- ## Backend Tasks ### Task 1: Create stats calculation module **Files:** - Create: `backend/leistungstest/stats.py` ```python from datetime import date, timedelta from django.db.models import Min, Avg, Count def get_date_range(period): """Return start date for period filter, or None for 'all'.""" 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 def get_template_leaderboard(template_id, period="all", limit=10): """Return top wrestlers by score_percent for a template.""" from .models import LeistungstestResult qs = LeistungstestResult.objects.filter(template_id=template_id) start_date = get_date_range(period) if start_date: qs = qs.filter(completed_at__date__gte=start_date) qs = qs.select_related('wrestler').order_by('-score_percent', 'total_time_seconds') results = [] for rank, result in enumerate(qs[:limit], 1): results.append({ 'rank': rank, 'wrestler_id': result.wrestler_id, 'wrestler_name': str(result.wrestler), 'score_percent': result.score_percent, 'total_time_seconds': result.total_time_seconds, 'completed_at': result.completed_at.date().isoformat() if result.completed_at else None, }) return results def get_exercise_leaderboard(exercise_id, period="all", limit=10): """Return top wrestlers by best time for an exercise.""" from .models import LeistungstestResultItem, LeistungstestResult from django.db.models import Min start_date = get_date_range(period) qs = LeistungstestResultItem.objects.filter(exercise_id=exercise_id) if start_date: qs = qs.filter(result__completed_at__date__gte=start_date) # Get best time per wrestler best_times = qs.values('result__wrestler__id', 'result__wrestler__first_name', 'result__wrestler__last_name', 'result__completed_at__date')\ .annotate(best_time=Min('elapsed_seconds'))\ .order_by('best_time') results = [] for rank, item in enumerate(best_times[:limit], 1): wrestler_name = f"{item['result__wrestler__first_name']} {item['result__wrestler__last_name']}" results.append({ 'rank': rank, 'wrestler_id': item['result__wrestler__id'], 'wrestler_name': wrestler_name.strip(), 'best_time_seconds': item['best_time'], 'completed_at': item['result__completed_at__date'].isoformat() if item['result__completed_at__date'] else None, }) return results def get_used_exercises(): """Return all exercises that have been used in any Leistungstest result.""" from .models import LeistungstestResultItem from exercises.models import Exercise exercise_ids = LeistungstestResultItem.objects.values_list('exercise_id', flat=True).distinct() return Exercise.objects.filter(id__in=exercise_ids).order_by('name') ``` - [ ] **Step 1: Create stats.py with functions** - [ ] **Step 2: Test stats functions in Django shell** - [ ] **Step 3: Commit** --- ### Task 2: Add stats ViewSet and URLs **Files:** - Modify: `backend/leistungstest/views.py` — Add `LeistungstestStatsViewSet` - Modify: `backend/leistungstest/urls.py` — Add stats routes ```python # In views.py, add: class LeistungstestStatsViewSet(viewsets.ViewSet): permission_classes = [IsAuthenticated] def leaderboard(self, request): lb_type = request.query_params.get('type', 'template') template_id = request.query_params.get('template_id') exercise_id = request.query_params.get('exercise_id') period = request.query_params.get('period', 'all') limit = int(request.query_params.get('limit', 10)) if lb_type == 'template' and template_id: results = get_template_leaderboard(int(template_id), period, limit) template = LeistungstestTemplate.objects.get(pk=template_id) return Response({ 'template_id': template_id, 'template_name': template.name, 'period': period, 'results': results, }) elif lb_type == 'exercise' and exercise_id: results = get_exercise_leaderboard(int(exercise_id), period, limit) exercise = Exercise.objects.get(pk=exercise_id) return Response({ 'exercise_id': exercise_id, 'exercise_name': exercise.name, 'period': period, 'results': results, }) return Response({'error': 'Invalid parameters'}, status=400) def exercises(self, request): exercises = get_used_exercises() return Response([{'id': e.id, 'name': e.name} for e in exercises]) ``` ```python # In urls.py, add: from .views import LeistungstestStatsViewSet router.register(r'stats', LeistungstestStatsViewSet, basename='leistungstest-stats') ``` - [ ] **Step 1: Add ViewSet to views.py** - [ ] **Step 2: Add URLs to urls.py** - [ ] **Step 3: Test endpoint with curl:** ```bash curl -H "Authorization: Bearer $TOKEN" "http://localhost:8000/api/v1/leistungstest/stats/leaderboard/?type=template&template_id=1" ``` - [ ] **Step 4: Commit** --- ## Frontend Tasks ### Task 3: Add TypeScript interfaces **Files:** - Modify: `frontend/src/lib/api.ts` — Add interfaces ```typescript export interface ILeaderboardEntry { rank: number wrestler_id: number wrestler_name: string score_percent?: number total_time_seconds?: number best_time_seconds?: number completed_at: string | null } export interface ITemplateLeaderboard { template_id: number template_name: string period: string results: ILeaderboardEntry[] } export interface IExerciseLeaderboard { exercise_id: number exercise_name: string period: string results: ILeaderboardEntry[] } export interface IExerciseOption { id: number name: string } ``` - [ ] **Step 1: Add interfaces to api.ts** - [ ] **Step 2: Commit** --- ### Task 4: Add Statistiken tab to Leistungstest page **Files:** - Modify: `frontend/src/app/(dashboard)/leistungstest/page.tsx` Add new tab type: ```typescript type TabType = "vorlagen" | "zuweisen" | "ergebnisse" | "statistiken" ``` Add state for stats: ```typescript const [statsViewMode, setStatsViewMode] = useState<"template" | "exercise">("template") const [statsPeriod, setStatsPeriod] = useState<"all" | "month" | "3months" | "year">("all") const [statsTemplateId, setStatsTemplateId] = useState("") const [statsExerciseId, setStatsExerciseId] = useState("") const [templateLeaderboard, setTemplateLeaderboard] = useState(null) const [exerciseLeaderboard, setExerciseLeaderboard] = useState(null) const [exerciseOptions, setExerciseOptions] = useState([]) ``` Add fetch function: ```typescript const fetchTemplateLeaderboard = async () => { if (!statsTemplateId) return try { const data = await apiFetch( `/leistungstest/stats/leaderboard/?type=template&template_id=${statsTemplateId}&period=${statsPeriod}`, { token: token! } ) setTemplateLeaderboard(data) } catch (err) { console.error("Failed to fetch leaderboard:", err) } } const fetchExerciseLeaderboard = async () => { if (!statsExerciseId) return try { const data = await apiFetch( `/leistungstest/stats/leaderboard/?type=exercise&exercise_id=${statsExerciseId}&period=${statsPeriod}`, { token: token! } ) setExerciseLeaderboard(data) } catch (err) { console.error("Failed to fetch leaderboard:", err) } } const fetchExerciseOptions = async () => { try { const data = await apiFetch(`/leistungstest/stats/exercises/`, { token: token! }) setExerciseOptions(data) } catch (err) { console.error("Failed to fetch exercises:", err) } } ``` Add useEffect: ```typescript useEffect(() => { if (activeTab === "statistiken") { fetchExerciseOptions() } }, [activeTab]) useEffect(() => { if (activeTab === "statistiken" && statsViewMode === "template" && statsTemplateId) { fetchTemplateLeaderboard() } }, [activeTab, statsViewMode, statsTemplateId, statsPeriod]) useEffect(() => { if (activeTab === "statistiken" && statsViewMode === "exercise" && statsExerciseId) { fetchExerciseLeaderboard() } }, [activeTab, statsViewMode, statsExerciseId, statsPeriod]) ``` Add tab button in header: ```tsx ``` Add Statistiken tab content (new card after ergebnisse tab): ```tsx {activeTab === "statistiken" && (
📊 Statistiken
{statsViewMode === "template" ? ( ) : ( )}
{statsViewMode === "template" ? ( !statsTemplateId ? (

Wähle eine Vorlage aus

) : templateLeaderboard?.results.length === 0 ? (

Keine Ergebnisse für diese Vorlage

) : (

{templateLeaderboard?.template_name}

{templateLeaderboard?.results.map(entry => (
{entry.rank <= 3 ? ["🥇","🥈","🥉"][entry.rank-1] : entry.rank}
{entry.wrestler_name}
{entry.completed_at}
{entry.score_percent}%
{entry.total_time_seconds != null ? `${Math.floor(entry.total_time_seconds / 60)}:${String(entry.total_time_seconds % 60).padStart(2, "0")}` : "-"}
))}
) ) : ( !statsExerciseId ? (

Wähle eine Übung aus

) : exerciseLeaderboard?.results.length === 0 ? (

Keine Ergebnisse für diese Übung

) : (

{exerciseLeaderboard?.exercise_name}

{exerciseLeaderboard?.results.map(entry => (
{entry.rank <= 3 ? ["🥇","🥈","🥉"][entry.rank-1] : entry.rank}
{entry.wrestler_name}
{entry.completed_at}
{entry.best_time_seconds != null ? formatTime(entry.best_time_seconds) : "-"}
beste Zeit
))}
) )}
)} ``` - [ ] **Step 1: Add tab type and state** - [ ] **Step 2: Add fetch functions** - [ ] **Step 3: Add tab button** - [ ] **Step 4: Add Statistiken tab content** - [ ] **Step 5: Test in browser** - [ ] **Step 6: Build to verify** - [ ] **Step 7: Commit** --- ## Verification 1. Backend test: ```bash cd backend && python3 manage.py runserver curl -H "Authorization: Bearer $TOKEN" "http://localhost:8000/api/v1/leistungstest/stats/exercises/" curl -H "Authorization: Bearer $TOKEN" "http://localhost:8000/api/v1/leistungstest/stats/leaderboard/?type=template&template_id=1&period=all" ``` 2. Frontend test: ```bash cd frontend && npm run build ```