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:
@@ -0,0 +1,278 @@
|
||||
# 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**
|
||||
|
||||
```python
|
||||
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**
|
||||
|
||||
```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 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`:
|
||||
```python
|
||||
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:
|
||||
```typescript
|
||||
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`:
|
||||
```typescript
|
||||
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:
|
||||
```typescript
|
||||
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:
|
||||
```typescript
|
||||
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
|
||||
- Run: `cd frontend && npm run lint`
|
||||
- Run: `npm run typecheck`
|
||||
- Visit: http://localhost:3000/dashboard
|
||||
|
||||
---
|
||||
|
||||
## 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
|
||||
Reference in New Issue
Block a user