Files
WrestleDesk/docs/superpowers/plans/2026-03-23-dashboard-statistics-implementation.md
T
Andrej Spielmann 3fefc550fe 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
2026-03-26 13:24:57 +01:00

8.6 KiB

Dashboard Statistics 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: Expand Dashboard with comprehensive statistics in Bento Grid layout - attendance by group, homework completion rates, wrestler distribution, and trainer activity.

Architecture: Backend provides single /api/v1/stats/dashboard/ endpoint aggregating all statistics. Frontend displays in Bento Grid with progress bars and simple bar charts using CSS/divs (no external chart library needed).

Tech Stack: Django REST Framework (backend), React/Next.js (frontend), CSS progress bars (no chart library)


File Structure

Backend

  • Create: backend/stats/__init__.py - App init
  • Create: backend/stats/apps.py - App config
  • Create: backend/stats/views.py - DashboardStatsViewSet
  • Modify: backend/wrestleDesk/urls.py - Add stats endpoint
  • Modify: backend/settings.py - Add 'stats' to INSTALLED_APPS

Frontend

  • Modify: frontend/src/lib/api.ts - Add IDashboardStats interface
  • Modify: frontend/src/app/(dashboard)/dashboard/page.tsx - New stat cards and visualizations

Tasks

Task 1: Create Stats Backend App

  • Step 1: Create stats app directory structure

Create backend/stats/ with:

stats/
├── __init__.py
├── apps.py
└── views.py
  • Step 2: Create apps.py
from django.apps import AppConfig

class StatsConfig(AppConfig):
    default_auto_field = 'django.db.models.BigAutoField'
    name = 'stats'
  • Step 3: Add 'stats' to INSTALLED_APPS in backend/settings.py

Add 'stats' to the INSTALLED_APPS list.

  • Step 4: Create DashboardStatsViewSet in views.py
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 Count, Sum
from datetime import datetime, timedelta
from wrestlers.models import Wrestler
from trainers.models import Trainer
from trainings.models import Training, Attendance
from homework.models import TrainingHomeworkAssignment


@api_view(['GET'])
@permission_classes([IsAuthenticated])
def dashboard_stats(request):
    today = datetime.now().date()
    week_start = today - timedelta(days=today.weekday())
    two_weeks_ago = today - timedelta(days=14)
    
    # Wrestlers stats
    total_wrestlers = Wrestler.objects.count()
    wrestlers_this_week = Wrestler.objects.filter(
        created_at__date__gte=week_start
    ).count()
    
    # Trainers stats
    total_trainers = Trainer.objects.count()
    active_trainers = Trainer.objects.filter(is_active=True).count()
    
    # Trainings stats
    total_trainings = Training.objects.count()
    trainings_this_week = Training.objects.filter(
        date__gte=week_start
    ).count()
    
    # Homework stats
    open_homework = TrainingHomeworkAssignment.objects.filter(is_completed=False).count()
    completed_homework = TrainingHomeworkAssignment.objects.filter(is_completed=True).count()
    
    # Attendance by group this week
    trainings_this_week_qs = Training.objects.filter(date__gte=week_start)
    attendance_data = {}
    for group, label in [('kids', 'Kinder'), ('youth', 'Jugend'), ('adults', 'Erwachsene')]:
        group_wrestlers = Wrestler.objects.filter(group=group)
        attended = Attendance.objects.filter(
            training__in=trainings_this_week_qs,
            wrestler__in=group_wrestlers
        ).values('wrestler').distinct().count()
        total = group_wrestlers.count()
        attendance_data[group] = {
            'attended': attended,
            'total': total,
            'percent': int((attended / total * 100) if total > 0 else 0)
        }
    
    # Activity (last 14 days)
    activity = []
    for i in range(14):
        day = today - timedelta(days=13 - i)
        count = Attendance.objects.filter(training__date=day).count()
        activity.append({'date': day.isoformat(), 'count': count})
    
    # Wrestlers by group
    wrestlers_by_group = {
        'kids': Wrestler.objects.filter(group='kids', is_active=True).count(),
        'youth': Wrestler.objects.filter(group='youth', is_active=True).count(),
        'adults': Wrestler.objects.filter(group='adults', is_active=True).count(),
        'inactive': Wrestler.objects.filter(is_active=False).count(),
    }
    
    # Top trainers (by training count)
    trainer_stats = Trainer.objects.annotate(
        training_count=Count('trainings')
    ).order_by('-training_count')[:5]
    top_trainers = [
        {'name': t.first_name + ' ' + t.last_name[0] + '.', 'training_count': t.training_count}
        for t in trainer_stats
    ]
    
    return Response({
        'wrestlers': {'total': total_wrestlers, 'this_week': wrestlers_this_week},
        'trainers': {'total': total_trainers, 'active': active_trainers},
        'trainings': {'total': total_trainings, 'this_week': trainings_this_week},
        'homework': {'open': open_homework, 'completed': completed_homework},
        'attendance': {
            'this_week': attendance_data,
            'average': Attendance.objects.filter(training__date__gte=week_start).values('training').distinct().count(),
            'expected': total_wrestlers
        },
        'activity': activity,
        'wrestlers_by_group': wrestlers_by_group,
        'top_trainers': top_trainers,
    })
  • Step 5: Add stats URL to urls.py

Add to backend/wrestleDesk/urls.py:

from stats.views import dashboard_stats

path('api/v1/stats/dashboard/', dashboard_stats, name='dashboard-stats'),

Task 2: Update Frontend API Types

  • Step 1: Add IDashboardStats interface to frontend/src/lib/api.ts

Add after existing interfaces:

export interface IDashboardStats {
  wrestlers: { total: number; this_week: number }
  trainers: { total: number; active: number }
  trainings: { total: number; this_week: number }
  homework: { open: number; completed: number }
  attendance: {
    this_week: {
      kids: { attended: number; total: number; percent: number }
      youth: { attended: number; total: number; percent: number }
      adults: { attended: number; total: number; percent: number }
    }
    average: number
    expected: number
  }
  activity: { date: string; count: number }[]
  wrestlers_by_group: {
    kids: number
    youth: number
    adults: number
    inactive: number
  }
  top_trainers: { name: string; training_count: number }[]
}

Task 3: Update Dashboard Page

  • Step 1: Update imports in frontend/src/app/(dashboard)/dashboard/page.tsx

Add Progress component and IDashboardStats:

import { apiFetch, IDashboardStats } from "@/lib/api"
import { Progress } from "@/components/ui/progress"
  • Step 2: Replace Stats interface and statCards with new implementation

Replace the existing interface and statCards with:

const groupColors = {
  kids: "bg-blue-500",
  youth: "bg-purple-500",
  adults: "bg-orange-500",
}

const groupLabels = {
  kids: "Kinder",
  youth: "Jugend",
  adults: "Erwachsene",
  inactive: "Inaktiv",
}
  • Step 3: Replace useEffect to fetch from stats endpoint

Replace fetchStats with:

useEffect(() => {
  if (!token) return
  const fetchStats = async () => {
    setIsLoading(true)
    try {
      const data = await apiFetch<IDashboardStats>('/stats/dashboard/', { token })
      setStats(data)
    } catch (error) {
      console.error("Failed to fetch stats:", error)
    } finally {
      setIsLoading(false)
    }
  }
  fetchStats()
}, [token])
  • Step 4: Replace the dashboard content

Replace the entire return section with the Bento Grid layout including:

  • 4 stat cards (enhanced)
  • Attendance by group card with progress bars
  • Training activity card with bar chart
  • Homework completion card (full width)
  • Wrestlers by group card
  • Top trainers card

Each card uses FadeIn with appropriate delay props.


Testing

Backend

  • Run: cd backend && python manage.py check
  • Test endpoint: curl -H "Authorization: Bearer <token>" http://localhost:8000/api/v1/stats/dashboard/

Frontend


Notes

  • Progress bars use Tailwind bg-* classes with calculated widths
  • Bar chart uses flexbox with varying heights
  • All data loaded asynchronously with loading state
  • Error handling: console.error on failure, UI continues to show zeros