Files
notytex/docs/CLASS_DASHBOARD_IMPROVEMENTS.md

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

  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

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:

  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:

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):

# 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)

  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)

  1. 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