3fefc550fe
- 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
413 lines
13 KiB
Markdown
413 lines
13 KiB
Markdown
# 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
|