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,412 @@
|
||||
# Homework System Redesign - 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:** Redesign homework system to assign exercises directly from training page without templates.
|
||||
|
||||
**Architecture:**
|
||||
- New models: `TrainingHomework`, `TrainingHomeworkExercise`, `TrainingHomeworkAssignment`
|
||||
- Training detail page redesigned with 2-column layout: participants | training homework
|
||||
- Homework button per participant opens modal to select exercises
|
||||
- Homework page shows all assignments grouped by training
|
||||
|
||||
**Tech Stack:** Django + DRF backend, Next.js + React frontend
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
### Backend (Modified)
|
||||
- `backend/homework/models.py` - Add new models
|
||||
- `backend/homework/serializers.py` - Add new serializers
|
||||
- `backend/homework/views.py` - Add new ViewSet
|
||||
- `backend/homework/urls.py` - Add new routes
|
||||
- `backend/homework/admin.py` - Register new models
|
||||
|
||||
### Frontend (Modified)
|
||||
- `frontend/src/lib/api.ts` - Add new types
|
||||
- `frontend/src/app/(dashboard)/trainings/[id]/page.tsx` - Complete redesign
|
||||
- `frontend/src/app/(dashboard)/homework/page.tsx` - Complete rewrite
|
||||
- `frontend/src/app/(dashboard)/dashboard/page.tsx` - Add homework stats
|
||||
|
||||
---
|
||||
|
||||
## Tasks
|
||||
|
||||
### Task 1: Backend - New Models
|
||||
|
||||
**Files:**
|
||||
- Modify: `backend/homework/models.py`
|
||||
|
||||
- [ ] **Step 1: Add new models at end of file**
|
||||
|
||||
```python
|
||||
class TrainingHomework(models.Model):
|
||||
training = models.ForeignKey('trainings.Training', on_delete=models.CASCADE, related_name='homework_assignments')
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
|
||||
class Meta:
|
||||
ordering = ['-created_at']
|
||||
indexes = [
|
||||
models.Index(fields=['training']),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return f"Homework for Training {self.training_id}"
|
||||
|
||||
|
||||
class TrainingHomeworkExercise(models.Model):
|
||||
training_homework = models.ForeignKey(TrainingHomework, on_delete=models.CASCADE, related_name='exercises')
|
||||
exercise = models.ForeignKey('exercises.Exercise', on_delete=models.CASCADE, related_name='training_homework_items')
|
||||
reps = models.PositiveIntegerField(null=True, blank=True)
|
||||
time_minutes = models.PositiveIntegerField(null=True, blank=True)
|
||||
order = models.IntegerField(default=0)
|
||||
|
||||
class Meta:
|
||||
ordering = ['training_homework', 'order']
|
||||
unique_together = ['training_homework', 'exercise']
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.training_homework} - {self.exercise.name}"
|
||||
|
||||
|
||||
class TrainingHomeworkAssignment(models.Model):
|
||||
training_homework = models.ForeignKey(TrainingHomework, on_delete=models.CASCADE, related_name='assignments')
|
||||
wrestler = models.ForeignKey('wrestlers.Wrestler', on_delete=models.CASCADE, related_name='training_homework_assignments')
|
||||
club = models.ForeignKey('clubs.Club', on_delete=models.CASCADE, related_name='training_homework_assignments', null=True, blank=True)
|
||||
notes = models.TextField(blank=True)
|
||||
is_completed = models.BooleanField(default=False)
|
||||
completion_date = models.DateField(null=True, blank=True)
|
||||
created_at = models.DateTimeField(auto_now_add=True)
|
||||
updated_at = models.DateTimeField(auto_now=True)
|
||||
|
||||
class Meta:
|
||||
unique_together = ['training_homework', 'wrestler']
|
||||
ordering = ['-created_at']
|
||||
indexes = [
|
||||
models.Index(fields=['wrestler']),
|
||||
models.Index(fields=['is_completed']),
|
||||
models.Index(fields=['club']),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.wrestler} - Training {self.training_homework.training_id}"
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run makemigrations**
|
||||
|
||||
Run: `cd backend && python manage.py makemigrations homework`
|
||||
Expected: "Migrations for 'homework': homework/migrations/xxx_create_training_homework.py"
|
||||
|
||||
- [ ] **Step 3: Run migrate**
|
||||
|
||||
Run: `cd backend && python manage.py migrate`
|
||||
Expected: "Applying homework.xxx_create_training_homework... OK"
|
||||
|
||||
---
|
||||
|
||||
### Task 2: Backend - Serializers
|
||||
|
||||
**Files:**
|
||||
- Modify: `backend/homework/serializers.py`
|
||||
|
||||
- [ ] **Step 1: Add new serializers**
|
||||
|
||||
Add at end of file:
|
||||
|
||||
```python
|
||||
class TrainingHomeworkExerciseSerializer(serializers.ModelSerializer):
|
||||
exercise_name = serializers.CharField(source='exercise.name', read_only=True)
|
||||
exercise_category = serializers.CharField(source='exercise.category', read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = TrainingHomeworkExercise
|
||||
fields = ['id', 'exercise', 'exercise_name', 'exercise_category', 'reps', 'time_minutes', 'order']
|
||||
|
||||
|
||||
class TrainingHomeworkSerializer(serializers.ModelSerializer):
|
||||
exercises = TrainingHomeworkExerciseSerializer(many=True, read_only=True)
|
||||
training_date = serializers.DateField(source='training.date', read_only=True)
|
||||
training_group = serializers.CharField(source='training.group', read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = TrainingHomework
|
||||
fields = ['id', 'training', 'training_date', 'training_group', 'exercises', 'created_at']
|
||||
|
||||
|
||||
class TrainingHomeworkAssignmentSerializer(serializers.ModelSerializer):
|
||||
training_homework_detail = TrainingHomeworkSerializer(source='training_homework', read_only=True)
|
||||
wrestler_name = serializers.SerializerMethodField()
|
||||
wrestler_group = serializers.CharField(source='wrestler.group', read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = TrainingHomeworkAssignment
|
||||
fields = ['id', 'training_homework', 'training_homework_detail', 'wrestler', 'wrestler_name', 'wrestler_group', 'notes', 'is_completed', 'completion_date', 'created_at']
|
||||
|
||||
def get_wrestler_name(self, obj):
|
||||
return f"{obj.wrestler.first_name} {obj.wrestler.last_name}"
|
||||
|
||||
|
||||
class TrainingHomeworkAssignmentCreateSerializer(serializers.Serializer):
|
||||
training = serializers.IntegerField()
|
||||
wrestler = serializers.IntegerField()
|
||||
exercises = serializers.ListField(
|
||||
child=serializers.DictField(child=serializers.IntegerField(allow_null=True))
|
||||
)
|
||||
notes = serializers.CharField(required=False, allow_blank=True, default='')
|
||||
|
||||
def create(self, validated_data):
|
||||
from clubs.utils import get_user_club
|
||||
from exercises.models import Exercise
|
||||
from trainings.models import Training
|
||||
from wrestlers.models import Wrestler
|
||||
|
||||
user = self.context['request'].user
|
||||
club = get_user_club(user)
|
||||
|
||||
training_id = validated_data['training']
|
||||
wrestler_id = validated_data['wrestler']
|
||||
exercises_data = validated_data['exercises']
|
||||
notes = validated_data.get('notes', '')
|
||||
|
||||
# Create or get TrainingHomework for this training
|
||||
training_obj = Training.objects.get(id=training_id)
|
||||
training_homework, _ = TrainingHomework.objects.get_or_create(training=training_obj)
|
||||
|
||||
# Create assignment
|
||||
assignment = TrainingHomeworkAssignment.objects.create(
|
||||
training_homework=training_homework,
|
||||
wrestler_id=wrestler_id,
|
||||
club=club,
|
||||
notes=notes
|
||||
)
|
||||
|
||||
# Add exercises
|
||||
for i, ex in enumerate(exercises_data):
|
||||
TrainingHomeworkExercise.objects.create(
|
||||
training_homework=training_homework,
|
||||
exercise_id=ex['exercise'],
|
||||
reps=ex.get('reps'),
|
||||
time_minutes=ex.get('time_minutes'),
|
||||
order=i
|
||||
)
|
||||
|
||||
return assignment
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3: Backend - Views
|
||||
|
||||
**Files:**
|
||||
- Modify: `backend/homework/views.py`
|
||||
|
||||
- [ ] **Step 1: Add new ViewSet**
|
||||
|
||||
Add at end of file:
|
||||
|
||||
```python
|
||||
class TrainingHomeworkAssignmentViewSet(viewsets.ModelViewSet):
|
||||
permission_classes = [IsAuthenticated, ClubLevelPermission]
|
||||
filter_backends = [ClubFilterBackend, filters.SearchFilter, filters.OrderingFilter]
|
||||
serializer_class = TrainingHomeworkAssignmentSerializer
|
||||
filterset_fields = ['is_completed']
|
||||
search_fields = ['wrestler__first_name', 'wrestler__last_name']
|
||||
ordering_fields = ['created_at', 'is_completed']
|
||||
http_method_names = ['get', 'post', 'patch', 'delete']
|
||||
|
||||
def get_queryset(self):
|
||||
from clubs.utils import get_user_club
|
||||
club = get_user_club(self.request.user)
|
||||
return TrainingHomeworkAssignment.objects.filter(club=club).select_related(
|
||||
'training_homework', 'training_homework__training', 'wrestler'
|
||||
)
|
||||
|
||||
def get_serializer_class(self):
|
||||
if self.action == 'create':
|
||||
return TrainingHomeworkAssignmentCreateSerializer
|
||||
return TrainingHomeworkAssignmentSerializer
|
||||
|
||||
@action(detail=True, methods=['post'])
|
||||
def complete(self, request, pk=None):
|
||||
assignment = self.get_object()
|
||||
assignment.is_completed = True
|
||||
assignment.completion_date = timezone.now().date()
|
||||
assignment.save()
|
||||
serializer = self.get_serializer(assignment)
|
||||
return Response(serializer.data)
|
||||
|
||||
@action(detail=True, methods=['post'])
|
||||
def uncomplete(self, request, pk=None):
|
||||
assignment = self.get_object()
|
||||
assignment.is_completed = False
|
||||
assignment.completion_date = None
|
||||
assignment.save()
|
||||
serializer = self.get_serializer(assignment)
|
||||
return Response(serializer.data)
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Import new serializers at top of file**
|
||||
|
||||
Add to imports:
|
||||
```python
|
||||
from .serializers import (
|
||||
# ... existing imports ...
|
||||
TrainingHomeworkAssignmentSerializer,
|
||||
TrainingHomeworkAssignmentCreateSerializer,
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 4: Backend - URLs
|
||||
|
||||
**Files:**
|
||||
- Modify: `backend/homework/urls.py`
|
||||
|
||||
- [ ] **Step 1: Add new route**
|
||||
|
||||
Add to urlpatterns:
|
||||
```python
|
||||
router.register(r'training-assignments', views.TrainingHomeworkAssignmentViewSet, basename='training-assignment')
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 5: Backend - Admin
|
||||
|
||||
**Files:**
|
||||
- Modify: `backend/homework/admin.py`
|
||||
|
||||
- [ ] **Step 1: Register new models**
|
||||
|
||||
Add at end of file:
|
||||
```python
|
||||
@admin.register(TrainingHomework)
|
||||
class TrainingHomeworkAdmin(admin.ModelAdmin):
|
||||
list_display = ['id', 'training', 'created_at']
|
||||
list_select_related = ['training']
|
||||
|
||||
|
||||
@admin.register(TrainingHomeworkAssignment)
|
||||
class TrainingHomeworkAssignmentAdmin(admin.ModelAdmin):
|
||||
list_display = ['id', 'wrestler', 'training_homework', 'is_completed', 'created_at']
|
||||
list_filter = ['is_completed', 'created_at']
|
||||
list_select_related = ['wrestler', 'training_homework__training']
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 6: Frontend - Types
|
||||
|
||||
**Files:**
|
||||
- Modify: `frontend/src/lib/api.ts`
|
||||
|
||||
- [ ] **Step 1: Add new interfaces**
|
||||
|
||||
Add after existing homework interfaces:
|
||||
|
||||
```typescript
|
||||
export interface ITrainingHomeworkExercise {
|
||||
id: number
|
||||
exercise: number
|
||||
exercise_name: string
|
||||
exercise_category: string
|
||||
reps: number | null
|
||||
time_minutes: number | null
|
||||
order: number
|
||||
}
|
||||
|
||||
export interface ITrainingHomework {
|
||||
id: number
|
||||
training: number
|
||||
training_date: string
|
||||
training_group: string
|
||||
exercises: ITrainingHomeworkExercise[]
|
||||
created_at: string
|
||||
}
|
||||
|
||||
export interface ITrainingHomeworkAssignment {
|
||||
id: number
|
||||
training_homework: number
|
||||
training_homework_detail: ITrainingHomework
|
||||
wrestler: number
|
||||
wrestler_name: string
|
||||
wrestler_group: string
|
||||
notes: string
|
||||
is_completed: boolean
|
||||
completion_date: string | null
|
||||
created_at: string
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 7: Frontend - Training Page Redesign
|
||||
|
||||
**Files:**
|
||||
- Modify: `frontend/src/app/(dashboard)/trainings/[id]/page.tsx`
|
||||
|
||||
This is a complete rewrite. Key changes:
|
||||
- 2-column layout: left = participants, right = training homework for this session
|
||||
- Each participant has a homework button (BookOpen icon)
|
||||
- Click opens modal to select exercises and assign
|
||||
|
||||
- [ ] **Step 1: Read current file** (already done above)
|
||||
|
||||
- [ ] **Step 2: Replace the entire file content**
|
||||
|
||||
Write new content with:
|
||||
1. State for homework modal
|
||||
2. State for selected exercises (with reps/time)
|
||||
3. Fetch exercises for the modal
|
||||
4. POST to `/homework/training-assignments/` on submit
|
||||
5. 2-column layout below details section
|
||||
|
||||
---
|
||||
|
||||
### Task 8: Frontend - Homework Page
|
||||
|
||||
**Files:**
|
||||
- Modify: `frontend/src/app/(dashboard)/homework/page.tsx`
|
||||
|
||||
Complete rewrite:
|
||||
- Fetch from `/homework/training-assignments/`
|
||||
- Group by training date
|
||||
- Filter: All / Open / Completed
|
||||
- Search by wrestler name
|
||||
- Each assignment shows wrestler, exercises, status
|
||||
- Toggle complete/uncomplete via API
|
||||
|
||||
---
|
||||
|
||||
### Task 9: Frontend - Dashboard Stats
|
||||
|
||||
**Files:**
|
||||
- Modify: `frontend/src/app/(dashboard)/dashboard/page.tsx`
|
||||
|
||||
- [ ] **Step 1: Add homework count to stats**
|
||||
|
||||
Add a new stat card showing count of incomplete assignments from `/homework/training-assignments/?is_completed=false`
|
||||
|
||||
---
|
||||
|
||||
### Task 10: Build & Test
|
||||
|
||||
- [ ] **Step 1: Run backend migrate**
|
||||
|
||||
Run: `cd backend && python manage.py migrate`
|
||||
|
||||
- [ ] **Step 2: Run frontend build**
|
||||
|
||||
Run: `cd frontend && npm run build`
|
||||
|
||||
- [ ] **Step 3: Test manually**
|
||||
|
||||
1. Go to training detail page
|
||||
2. Add a participant
|
||||
3. Click homework button on participant
|
||||
4. Select exercises and assign
|
||||
5. Go to homework page and verify assignment appears
|
||||
6. Toggle completion status
|
||||
7. Check dashboard for homework count
|
||||
Reference in New Issue
Block a user