17 KiB
Améliorations du Dashboard de Classe
Date: 3 décembre 2025
Version: 2.0
Fichiers modifiés: 4 fichiers (3 backend, 1 frontend)
📋 Objectifs des Modifications
Tableau des Élèves
- ✅ Permettre le tri sur toutes les colonnes
- ✅ Afficher toutes les notes (une colonne par évaluation)
- ✅ Supprimer les indicateurs de performance (badges Excellent/Bon/Moyen/Insuffisant)
Tableau Domaines/Compétences
- ✅ Afficher le nombre de fois qu'ils ont été évalués
- ✅ Afficher le nombre de points attribués (total obtenu/total possible)
- ✅ Supprimer les moyennes
🔧 Modifications Backend
1. Nouveaux Schemas (backend/schemas/class_group.py)
Schemas ajoutés
AssessmentScore
class AssessmentScore(BaseSchema):
"""Score d'un élève pour une évaluation."""
assessment_id: int
assessment_title: str
score: Optional[float] = None # Score brut (ex: 15.5)
max_points: float = 0.0 # Points maximum (ex: 20)
score_on_20: Optional[float] = None # Score ramené sur 20
DomainStudentStats
class DomainStudentStats(BaseSchema):
"""Statistiques d'un élève pour un domaine."""
domain_id: int
evaluation_count: int = 0 # Nombre de fois évalué sur ce domaine
total_points_obtained: float = 0.0 # Total des points obtenus
total_points_possible: float = 0.0 # Total des points possibles
CompetenceStudentStats
class CompetenceStudentStats(BaseSchema):
"""Statistiques d'un élève pour une compétence."""
competence_id: int
evaluation_count: int = 0
total_points_obtained: float = 0.0
total_points_possible: float = 0.0
Schemas modifiés
DomainStats - Avant vs Après
# AVANT
class DomainStats(BaseSchema):
id: int
name: str
color: str
mean: Optional[float] = None # ❌ Supprimé
elements_count: int = 0
# APRÈS
class DomainStats(BaseSchema):
id: int
name: str
color: str
evaluation_count: int = 0 # ✅ Nombre d'évaluations
total_points_obtained: float = 0.0 # ✅ Points obtenus
total_points_possible: float = 0.0 # ✅ Points possibles
CompetenceStats - Même structure que DomainStats
StudentAverage - Enrichi avec 3 nouveaux champs
class StudentAverage(BaseSchema):
student_id: int
first_name: str
last_name: str
full_name: str
average: Optional[float] = None
assessment_count: int = 0
# ✅ NOUVEAUX CHAMPS
assessment_scores: Dict[int, AssessmentScore] = {} # Scores par évaluation
domain_stats: Dict[int, DomainStudentStats] = {} # Stats par domaine
competence_stats: Dict[int, CompetenceStudentStats] = {} # Stats par compétence
2. Nouveau Service (backend/domain/services/class_statistics_service.py)
ClassStatisticsService - Service de calcul des statistiques de classe
Méthode 1: calculate_student_statistics()
Signature:
async def calculate_student_statistics(
students: List[Student],
assessments: List[Assessment],
grades_by_student_assessment: Dict[Tuple[int, int], List[Tuple[Grade, GradingElement]]],
domains: List[Domain],
competences: List[Competence],
) -> List[StudentAverage]
Rôle: Calcule toutes les statistiques pour chaque élève
- Score par évaluation (brut + sur 20)
- Moyenne pondérée par coefficient
- Statistiques par domaine (nombre d'évaluations + points)
- Statistiques par compétence (via
element.skill)
Logique:
- Pour chaque élève:
- Initialiser les dictionnaires de stats par domaine/compétence
- Pour chaque évaluation:
- Calculer le score total et max_points
- Ramener sur 20 pour la moyenne pondérée
- Pour chaque note:
- Mettre à jour les stats du domaine associé
- Mettre à jour les stats de la compétence associée (via skill)
Méthode 2: aggregate_domain_competence_stats()
Signature:
def aggregate_domain_competence_stats(
student_averages: List[StudentAverage],
domains: List[Domain],
competences: List[Competence],
) -> Tuple[List[DomainStats], List[CompetenceStats]]
Rôle: Agrège les statistiques de tous les élèves par domaine/compétence
Logique:
- Pour chaque domaine:
- Sommer evaluation_count de tous les élèves
- Sommer total_points_obtained de tous les élèves
- Sommer total_points_possible de tous les élèves
- Même chose pour les compétences
3. Endpoint Refactorisé (backend/api/routes/classes.py)
GET /classes/{class_id}/stats?trimester={1|2|3}
Modifications principales
Avant (ancien code):
# Calculer les moyennes de chaque élève
calculator = GradingCalculator()
student_averages = []
for student in students:
# ... calcul simple de la moyenne
student_averages.append(StudentAverage(
student_id=student.id,
average=average,
assessment_count=assessment_count
))
# Statistiques domaines/compétences simplifiées (vides)
domains_stats = []
for domain in domains:
domains_stats.append(DomainStats(
id=domain.id,
name=domain.name,
color=domain.color,
mean=None, # ❌ Pas calculé
elements_count=0 # ❌ Pas calculé
))
Après (nouveau code):
# Récupérer toutes les notes en une passe
grades_by_student_assessment = {}
for student in students:
for assessment in assessments:
grades_query = (...)
grades_by_student_assessment[(student.id, assessment.id)] = grades_result.all()
# Utiliser le service pour calculer les statistiques
stats_service = ClassStatisticsService()
student_averages = await stats_service.calculate_student_statistics(
students=students,
assessments=assessments,
grades_by_student_assessment=grades_by_student_assessment,
domains=domains,
competences=competences,
)
# Agréger les statistiques domaines/compétences
domains_stats, competences_stats = stats_service.aggregate_domain_competence_stats(
student_averages=student_averages,
domains=domains,
competences=competences,
)
Avantages
- ✅ Code modulaire et testable
- ✅ Séparation des responsabilités (service vs controller)
- ✅ Statistiques complètes calculées automatiquement
- ✅ Données enrichies retournées au frontend
🎨 Modifications Frontend
1. Script Vue.js (frontend/src/views/ClassDashboardView.vue)
Variables ajoutées
const sortColumn = ref('name') // Colonne de tri active
const sortDirection = ref('asc') // Direction du tri
Computed ajoutés
assessments - Extraction des évaluations
const assessments = computed(() => {
if (!stats.value?.student_averages?.length) return []
const firstStudent = stats.value.student_averages[0]
if (!firstStudent?.assessment_scores) return []
// Extraire et trier les évaluations par ID
return Object.values(firstStudent.assessment_scores)
.sort((a, b) => a.assessment_id - b.assessment_id)
})
sortedStudents - Tri dynamique des élèves
const sortedStudents = computed(() => {
if (!stats.value?.student_averages) return []
const students = [...stats.value.student_averages]
students.sort((a, b) => {
let valA, valB
if (sortColumn.value === 'name') {
valA = `${a.last_name} ${a.first_name}`.toLowerCase()
valB = `${b.last_name} ${b.first_name}`.toLowerCase()
} else if (sortColumn.value === 'average') {
valA = a.average ?? -1
valB = b.average ?? -1
} else if (sortColumn.value.startsWith('assessment_')) {
const assessmentId = parseInt(sortColumn.value.split('_')[1])
valA = a.assessment_scores?.[assessmentId]?.score ?? -1
valB = b.assessment_scores?.[assessmentId]?.score ?? -1
}
const comparison = valA > valB ? 1 : valA < valB ? -1 : 0
return sortDirection.value === 'asc' ? comparison : -comparison
})
return students
})
Fonctions ajoutées
sortBy(column) - Gestion du tri
function sortBy(column) {
if (sortColumn.value === column) {
// Inverser la direction si même colonne
sortDirection.value = sortDirection.value === 'asc' ? 'desc' : 'asc'
} else {
// Nouvelle colonne : tri ascendant
sortColumn.value = column
sortDirection.value = 'asc'
}
}
getAssessmentScore(student, assessmentId) - Formatage des notes
function getAssessmentScore(student, assessmentId) {
const score = student.assessment_scores?.[assessmentId]
if (!score || score.score === null) return '-'
return `${score.score.toFixed(1)}/${score.max_points.toFixed(0)}`
}
getSortIcon(column) - Indicateur visuel
function getSortIcon(column) {
if (sortColumn.value !== column) return ''
return sortDirection.value === 'asc' ? '▲' : '▼'
}
Fonctions supprimées
// ❌ Supprimé
function getPerformanceClass(average) { ... }
function getPerformanceLabel(average) { ... }
2. Template - Tableau des Élèves
Avant
<table>
<thead>
<tr>
<th>Élève</th>
<th>Moyenne</th>
<th>Performance</th> <!-- ❌ Supprimé -->
</tr>
</thead>
<tbody>
<tr v-for="student in stats.student_averages">
<td>{{ student.last_name }} {{ student.first_name }}</td>
<td>{{ student.average?.toFixed(2) || '-' }}</td>
<td>
<span :class="getPerformanceClass(student.average)">
{{ getPerformanceLabel(student.average) }}
</span>
</td>
</tr>
</tbody>
</table>
Après
<table>
<thead>
<tr>
<!-- Colonne Nom (triable) -->
<th @click="sortBy('name')" class="cursor-pointer hover:bg-gray-100">
Élève {{ getSortIcon('name') }}
</th>
<!-- Colonne Moyenne (triable) -->
<th @click="sortBy('average')" class="cursor-pointer hover:bg-gray-100">
Moyenne {{ getSortIcon('average') }}
</th>
<!-- Colonnes dynamiques pour chaque évaluation (triables) -->
<th
v-for="assessment in assessments"
:key="assessment.assessment_id"
@click="sortBy(`assessment_${assessment.assessment_id}`)"
class="cursor-pointer hover:bg-gray-100"
:title="assessment.assessment_title"
>
<div class="flex flex-col items-center">
<span class="truncate max-w-[120px]">
{{ assessment.assessment_title }}
</span>
<span class="text-[10px]">
{{ getSortIcon(`assessment_${assessment.assessment_id}`) }}
</span>
</div>
</th>
</tr>
</thead>
<tbody>
<tr v-for="student in sortedStudents" :key="student.student_id">
<td>{{ student.last_name }} {{ student.first_name }}</td>
<td class="font-bold text-blue-600">
{{ student.average?.toFixed(2) || '-' }}
</td>
<!-- Notes pour chaque évaluation -->
<td v-for="assessment in assessments" :key="assessment.assessment_id">
{{ getAssessmentScore(student, assessment.assessment_id) }}
</td>
</tr>
</tbody>
</table>
Changements:
- ✅ Colonnes dynamiques générées depuis
assessments - ✅ Tri sur toutes les colonnes (clic sur en-tête)
- ✅ Indicateur visuel de tri (▲/▼)
- ✅ Hover sur en-têtes
- ✅ Titre complet en tooltip (
:title) - ❌ Suppression colonne Performance
3. Template - Domaines/Compétences
Avant
<div v-for="domain in stats.domains_stats" :key="domain.id">
<div class="flex justify-between">
<span>{{ domain.name }}</span>
<span>{{ domain.mean?.toFixed(1) || '-' }}/20</span> <!-- ❌ -->
</div>
<div class="progress-bar">
<div :style="{ width: `${(domain.mean / 20) * 100}%` }"></div>
</div>
<p>{{ domain.elements_count }} éléments évalués</p> <!-- ❌ -->
</div>
Après
<div v-for="domain in stats.domains_stats" :key="domain.id">
<div class="flex justify-between">
<span>{{ domain.name }}</span>
<!-- ✅ Affichage points obtenus / points possibles -->
<span>
{{ domain.total_points_obtained?.toFixed(1) || '0' }}/{{ domain.total_points_possible?.toFixed(0) || '0' }}
</span>
</div>
<!-- ✅ Barre calculée sur les points réels -->
<div class="progress-bar">
<div :style="{
width: `${domain.total_points_possible > 0
? (domain.total_points_obtained / domain.total_points_possible) * 100
: 0}%`
}"></div>
</div>
<!-- ✅ Nombre d'évaluations -->
<p>{{ domain.evaluation_count }} évaluations</p>
</div>
Changements:
- ✅ Affichage
total_points_obtained / total_points_possible - ✅ Texte "X évaluations" au lieu de "X éléments"
- ✅ Barre calculée sur les points réels
- ❌ Suppression de
mean
📊 Flux de Données Complet
1. Chargement Initial
Frontend (ClassDashboardView.vue)
→ fetchData()
→ classesStore.fetchClassStats(classId, trimester)
→ GET /api/classes/{id}/stats?trimester={t}
Backend (classes.py)
→ get_class_stats()
→ Récupérer students, assessments, domains, competences
→ Charger toutes les notes (grades_by_student_assessment)
→ ClassStatisticsService.calculate_student_statistics()
→ Pour chaque élève:
→ Calculer scores par évaluation
→ Calculer stats par domaine
→ Calculer stats par compétence
→ Retourner StudentAverage enrichi
→ ClassStatisticsService.aggregate_domain_competence_stats()
→ Agréger tous les élèves
→ Retourner DomainStats et CompetenceStats
→ Calculer statistiques globales (mean, median, std_dev)
→ Retourner ClassDashboardStats complet
Frontend (ClassDashboardView.vue)
→ stats.value = résultat API
→ assessments computed → extrait évaluations
→ sortedStudents computed → tri initial
→ Affichage tableau
2. Tri Utilisateur
Frontend
→ Utilisateur clique sur en-tête de colonne
→ sortBy(column) appelée
→ Met à jour sortColumn, sortDirection
→ sortedStudents computed se recalcule automatiquement
→ Vue.js re-rend le tableau
3. Changement de Trimestre
Frontend
→ Utilisateur clique sur "Trimestre 2"
→ selectTrimester(2) appelée
→ Nouvelle requête API avec trimester=2
→ stats.value mis à jour
→ assessments computed se recalcule
→ sortedStudents computed se recalcule
→ Tableau re-rendu avec nouvelles données
🎯 Résultat Final
Tableau des Élèves - Fonctionnalités
| Fonctionnalité | Avant | Après |
|---|---|---|
| Tri sur colonnes | ❌ | ✅ Toutes colonnes |
| Affichage notes évaluations | ❌ | ✅ Toutes visibles |
| Indicateurs visuels | ✅ Badges | ❌ Supprimés |
| UX | Statique | ✅ Interactive |
Tableau Domaines/Compétences - Données
| Donnée | Avant | Après |
|---|---|---|
| Moyenne | ✅ X/20 ou X/3 | ❌ Supprimée |
| Points obtenus/possibles | ❌ | ✅ XX.X/YY |
| Nombre d'évaluations | ❌ "X éléments" | ✅ "X évaluations" |
| Barre de progression | Basée sur moyenne | ✅ Basée sur points |
🚀 Pour Tester
1. Lancer le backend
cd backend
uv run uvicorn api.main:app --reload
2. Lancer le frontend
cd frontend
npm run dev
3. Accéder à une classe
http://localhost:5173/classes/{id}
4. Vérifier
- ✅ Tableau élèves affiche toutes les colonnes d'évaluations
- ✅ Clic sur en-tête trie la colonne (nom, moyenne, évaluations)
- ✅ Indicateur ▲/▼ s'affiche
- ✅ Domaines/Compétences affichent points et nombre d'évaluations
- ✅ Pas de badges de performance
📁 Fichiers Modifiés
Backend (3 fichiers)
-
backend/schemas/class_group.py- Ajout: AssessmentScore, DomainStudentStats, CompetenceStudentStats
- Modification: DomainStats, CompetenceStats, StudentAverage
-
backend/domain/services/class_statistics_service.py(NOUVEAU)- ClassStatisticsService
- calculate_student_statistics()
- aggregate_domain_competence_stats()
-
backend/api/routes/classes.py- get_class_stats() refactorisé
- Utilisation de ClassStatisticsService
- Import des nouveaux schemas
Frontend (1 fichier)
frontend/src/views/ClassDashboardView.vue- Script: ajout tri, computed, fonctions
- Template: refonte tableau élèves + domaines/compétences
- Suppression: fonctions de performance
🎓 Points Clés Techniques
Architecture Backend
- Service Layer: Logique métier isolée dans ClassStatisticsService
- Schema Evolution: Schemas enrichis pour supporter données complexes
- Performance: Une requête par élève/évaluation (optimisable avec joinedload)
Architecture Frontend
- Reactive Computing: Tri géré par computed (pas de setState manuel)
- Dynamic Columns: Colonnes générées depuis les données backend
- UX: Hover, cursors, indicateurs visuels pour meilleure expérience
Coordination
- Contract-First: Schemas Pydantic garantissent le contrat API
- Type Safety: Dict[int, Schema] pour accès rapide côté frontend
- Consistency: Même structure pour domaines et compétences