# 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 1. ✅ Permettre le tri sur toutes les colonnes 2. ✅ Afficher toutes les notes (une colonne par évaluation) 3. ✅ Supprimer les indicateurs de performance (badges Excellent/Bon/Moyen/Insuffisant) ### Tableau Domaines/Compétences 1. ✅ Afficher le nombre de fois qu'ils ont été évalués 2. ✅ Afficher le nombre de points attribués (total obtenu/total possible) 3. ✅ Supprimer les moyennes --- ## 🔧 Modifications Backend ### 1. Nouveaux Schemas (`backend/schemas/class_group.py`) #### Schemas ajoutés **AssessmentScore** ```python 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** ```python 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** ```python 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 ```python # 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 ```python 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**: ```python 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**: 1. 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**: ```python 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**: 1. 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 2. 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): ```python # 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): ```python # 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 ```javascript const sortColumn = ref('name') // Colonne de tri active const sortDirection = ref('asc') // Direction du tri ``` #### Computed ajoutés **assessments** - Extraction des évaluations ```javascript 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 ```javascript 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 ```javascript 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 ```javascript 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 ```javascript function getSortIcon(column) { if (sortColumn.value !== column) return '' return sortDirection.value === 'asc' ? '▲' : '▼' } ``` #### Fonctions supprimées ```javascript // ❌ Supprimé function getPerformanceClass(average) { ... } function getPerformanceLabel(average) { ... } ``` ### 2. Template - Tableau des Élèves #### Avant ```html
Élève Moyenne Performance
{{ student.last_name }} {{ student.first_name }} {{ student.average?.toFixed(2) || '-' }} {{ getPerformanceLabel(student.average) }}
``` #### Après ```html
Élève {{ getSortIcon('name') }} Moyenne {{ getSortIcon('average') }}
{{ assessment.assessment_title }} {{ getSortIcon(`assessment_${assessment.assessment_id}`) }}
{{ student.last_name }} {{ student.first_name }} {{ student.average?.toFixed(2) || '-' }} {{ getAssessmentScore(student, assessment.assessment_id) }}
``` **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 ```html
{{ domain.name }} {{ domain.mean?.toFixed(1) || '-' }}/20

{{ domain.elements_count }} éléments évalués

``` #### Après ```html
{{ domain.name }} {{ domain.total_points_obtained?.toFixed(1) || '0' }}/{{ domain.total_points_possible?.toFixed(0) || '0' }}

{{ domain.evaluation_count }} évaluations

``` **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 ```bash cd backend uv run uvicorn api.main:app --reload ``` ### 2. Lancer le frontend ```bash 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) 1. **`backend/schemas/class_group.py`** - Ajout: AssessmentScore, DomainStudentStats, CompetenceStudentStats - Modification: DomainStats, CompetenceStats, StudentAverage 2. **`backend/domain/services/class_statistics_service.py`** (NOUVEAU) - ClassStatisticsService - calculate_student_statistics() - aggregate_domain_competence_stats() 3. **`backend/api/routes/classes.py`** - get_class_stats() refactorisé - Utilisation de ClassStatisticsService - Import des nouveaux schemas ### Frontend (1 fichier) 4. **`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