connection->fetchAllAssociative( <<<'SQL' SELECT ta.school_class_id AS class_id, sc.name AS class_name, ta.subject_id, s.name AS subject_name, COUNT(DISTINCT e.id) AS evaluation_count, COUNT(DISTINCT g.student_id) AS student_count, CASE WHEN COUNT(g.id) FILTER (WHERE g.status = 'graded') > 0 THEN ROUND(AVG(g.value * 20.0 / NULLIF(e.grade_scale, 0)) FILTER (WHERE g.status = 'graded'), 2) ELSE NULL END AS average, CASE WHEN COUNT(g.id) FILTER (WHERE g.status = 'graded') > 0 THEN ROUND( COUNT(g.id) FILTER (WHERE g.status = 'graded' AND g.value * 20.0 / NULLIF(e.grade_scale, 0) >= 10) * 100.0 / COUNT(g.id) FILTER (WHERE g.status = 'graded'), 1 ) ELSE NULL END AS success_rate FROM teacher_assignments ta JOIN school_classes sc ON sc.id = ta.school_class_id JOIN subjects s ON s.id = ta.subject_id LEFT JOIN evaluations e ON e.class_id = ta.school_class_id AND e.subject_id = ta.subject_id AND e.teacher_id = ta.teacher_id AND e.tenant_id = ta.tenant_id AND e.status != 'deleted' AND e.grades_published_at IS NOT NULL AND e.evaluation_date BETWEEN :period_start AND :period_end LEFT JOIN grades g ON g.evaluation_id = e.id AND g.tenant_id = ta.tenant_id WHERE ta.teacher_id = :teacher_id AND ta.tenant_id = :tenant_id AND ta.status = 'active' GROUP BY ta.school_class_id, sc.name, ta.subject_id, s.name ORDER BY sc.name, s.name SQL, [ 'teacher_id' => $teacherId, 'tenant_id' => $tenantId, 'period_start' => $periodStartDate, 'period_end' => $periodEndDate, ], ); $result = []; foreach ($rows as $row) { /** @var string $classId */ $classId = $row['class_id']; /** @var string $className */ $className = $row['class_name']; /** @var string $subjectId */ $subjectId = $row['subject_id']; /** @var string $subjectName */ $subjectName = $row['subject_name']; /** @var string|int $evaluationCount */ $evaluationCount = $row['evaluation_count']; /** @var string|int $studentCount */ $studentCount = $row['student_count']; /** @var string|float|null $average */ $average = $row['average']; /** @var string|float|null $successRate */ $successRate = $row['success_rate']; $result[] = [ 'classId' => $classId, 'className' => $className, 'subjectId' => $subjectId, 'subjectName' => $subjectName, 'evaluationCount' => (int) $evaluationCount, 'studentCount' => (int) $studentCount, 'average' => $average !== null ? (float) $average : null, 'successRate' => $successRate !== null ? (float) $successRate : null, ]; } return $result; } public function classGradesNormalized( string $teacherId, string $classId, string $subjectId, string $tenantId, string $periodStartDate, string $periodEndDate, ): array { $rows = $this->connection->fetchFirstColumn( <<<'SQL' SELECT ROUND(g.value * 20.0 / NULLIF(e.grade_scale, 0), 2) FROM grades g JOIN evaluations e ON e.id = g.evaluation_id WHERE e.teacher_id = :teacher_id AND e.class_id = :class_id AND e.subject_id = :subject_id AND e.tenant_id = :tenant_id AND e.status != 'deleted' AND e.grades_published_at IS NOT NULL AND e.evaluation_date BETWEEN :period_start AND :period_end AND g.status = 'graded' AND g.tenant_id = :tenant_id ORDER BY g.value SQL, [ 'teacher_id' => $teacherId, 'class_id' => $classId, 'subject_id' => $subjectId, 'tenant_id' => $tenantId, 'period_start' => $periodStartDate, 'period_end' => $periodEndDate, ], ); $result = []; foreach ($rows as $v) { if (is_numeric($v)) { $result[] = (float) $v; } } return $result; } public function classMonthlyAverages( string $teacherId, string $classId, string $subjectId, string $tenantId, string $academicYearStart, string $academicYearEnd, ): array { $rows = $this->connection->fetchAllAssociative( <<<'SQL' SELECT TO_CHAR(e.evaluation_date, 'YYYY-MM') AS month, ROUND(AVG(g.value * 20.0 / NULLIF(e.grade_scale, 0)), 2) AS average FROM grades g JOIN evaluations e ON e.id = g.evaluation_id WHERE e.teacher_id = :teacher_id AND e.class_id = :class_id AND e.subject_id = :subject_id AND e.tenant_id = :tenant_id AND e.status != 'deleted' AND e.grades_published_at IS NOT NULL AND g.status = 'graded' AND g.tenant_id = :tenant_id AND e.evaluation_date BETWEEN :year_start AND :year_end GROUP BY TO_CHAR(e.evaluation_date, 'YYYY-MM') ORDER BY month SQL, [ 'teacher_id' => $teacherId, 'class_id' => $classId, 'subject_id' => $subjectId, 'tenant_id' => $tenantId, 'year_start' => $academicYearStart, 'year_end' => $academicYearEnd, ], ); $result = []; foreach ($rows as $row) { /** @var string $month */ $month = $row['month']; /** @var string|float $average */ $average = $row['average']; $result[] = [ 'month' => $month, 'average' => (float) $average, ]; } return $result; } /** * Les moyennes lues depuis student_averages incluent toutes les évaluations * de la matière (tous enseignants confondus) — c'est le même agrégat que * celui du bulletin scolaire. Le JOIN teacher_assignments contrôle l'accès * (seul un enseignant affecté à la classe peut appeler cette méthode). */ public function studentAveragesForClass( string $teacherId, string $classId, string $subjectId, string $periodId, string $tenantId, ): array { $rows = $this->connection->fetchAllAssociative( <<<'SQL' SELECT ca.user_id AS student_id, CONCAT(u.first_name, ' ', u.last_name) AS student_name, sa.average FROM class_assignments ca JOIN users u ON u.id = ca.user_id JOIN teacher_assignments ta ON ta.school_class_id = ca.school_class_id AND ta.subject_id = :subject_id AND ta.teacher_id = :teacher_id AND ta.tenant_id = :tenant_id AND ta.status = 'active' LEFT JOIN student_averages sa ON sa.student_id = ca.user_id AND sa.subject_id = :subject_id AND sa.period_id = :period_id AND sa.tenant_id = :tenant_id WHERE ca.school_class_id = :class_id AND ca.tenant_id = :tenant_id ORDER BY u.last_name, u.first_name SQL, [ 'teacher_id' => $teacherId, 'class_id' => $classId, 'subject_id' => $subjectId, 'period_id' => $periodId, 'tenant_id' => $tenantId, ], ); $result = []; foreach ($rows as $row) { /** @var string $studentId */ $studentId = $row['student_id']; /** @var string $studentName */ $studentName = $row['student_name']; /** @var string|float|null $studentAvg */ $studentAvg = $row['average']; $result[] = [ 'studentId' => $studentId, 'studentName' => $studentName, 'average' => $studentAvg !== null ? (float) $studentAvg : null, ]; } return $result; } public function studentMonthlyAveragesForClass( string $classId, string $subjectId, string $tenantId, string $academicYearStart, string $academicYearEnd, ): array { $rows = $this->connection->fetchAllAssociative( <<<'SQL' SELECT g.student_id, TO_CHAR(e.evaluation_date, 'YYYY-MM') AS month, ROUND(AVG(g.value * 20.0 / NULLIF(e.grade_scale, 0)), 2) AS average FROM grades g JOIN evaluations e ON e.id = g.evaluation_id WHERE e.class_id = :class_id AND e.subject_id = :subject_id AND e.tenant_id = :tenant_id AND e.status != 'deleted' AND e.grades_published_at IS NOT NULL AND g.status = 'graded' AND g.tenant_id = :tenant_id AND e.evaluation_date BETWEEN :year_start AND :year_end GROUP BY g.student_id, TO_CHAR(e.evaluation_date, 'YYYY-MM') ORDER BY g.student_id, month SQL, [ 'class_id' => $classId, 'subject_id' => $subjectId, 'tenant_id' => $tenantId, 'year_start' => $academicYearStart, 'year_end' => $academicYearEnd, ], ); /** @var array> $result */ $result = []; foreach ($rows as $row) { /** @var string $sid */ $sid = $row['student_id']; /** @var string|float $avg */ $avg = $row['average']; $result[$sid][] = (float) $avg; } return $result; } public function studentGradeHistory( string $studentId, string $subjectId, string $classId, string $teacherId, string $tenantId, string $academicYearStart, string $academicYearEnd, ): array { $rows = $this->connection->fetchAllAssociative( <<<'SQL' SELECT e.evaluation_date::date AS date, ROUND(g.value * 20.0 / NULLIF(e.grade_scale, 0), 2) AS value, e.title AS evaluation_title FROM grades g JOIN evaluations e ON e.id = g.evaluation_id JOIN teacher_assignments ta ON ta.school_class_id = e.class_id AND ta.subject_id = e.subject_id AND ta.teacher_id = :teacher_id AND ta.tenant_id = :tenant_id AND ta.status = 'active' WHERE g.student_id = :student_id AND e.subject_id = :subject_id AND e.class_id = :class_id AND e.tenant_id = :tenant_id AND e.status != 'deleted' AND e.grades_published_at IS NOT NULL AND g.status = 'graded' AND g.tenant_id = :tenant_id AND e.evaluation_date BETWEEN :year_start AND :year_end ORDER BY e.evaluation_date SQL, [ 'student_id' => $studentId, 'subject_id' => $subjectId, 'class_id' => $classId, 'teacher_id' => $teacherId, 'tenant_id' => $tenantId, 'year_start' => $academicYearStart, 'year_end' => $academicYearEnd, ], ); $result = []; foreach ($rows as $row) { /** @var string $date */ $date = $row['date']; /** @var string|float $value */ $value = $row['value']; /** @var string $evaluationTitle */ $evaluationTitle = $row['evaluation_title']; $result[] = [ 'date' => $date, 'value' => (float) $value, 'evaluationTitle' => $evaluationTitle, ]; } return $result; } public function teacherEvaluationDifficulties(string $teacherId, string $tenantId): array { $rows = $this->connection->fetchAllAssociative( <<<'SQL' SELECT e.id AS evaluation_id, e.title, e.class_id, sc.name AS class_name, e.subject_id, s.name AS subject_name, e.evaluation_date::date AS date, es.average, COALESCE(es.graded_count, 0) AS graded_count FROM evaluations e JOIN school_classes sc ON sc.id = e.class_id JOIN subjects s ON s.id = e.subject_id LEFT JOIN evaluation_statistics es ON es.evaluation_id = e.id WHERE e.teacher_id = :teacher_id AND e.tenant_id = :tenant_id AND e.status != 'deleted' AND e.grades_published_at IS NOT NULL ORDER BY e.evaluation_date DESC SQL, [ 'teacher_id' => $teacherId, 'tenant_id' => $tenantId, ], ); $result = []; foreach ($rows as $row) { /** @var string $evaluationId */ $evaluationId = $row['evaluation_id']; /** @var string $title */ $title = $row['title']; /** @var string $rowClassId */ $rowClassId = $row['class_id']; /** @var string $className */ $className = $row['class_name']; /** @var string $rowSubjectId */ $rowSubjectId = $row['subject_id']; /** @var string $subjectName */ $subjectName = $row['subject_name']; /** @var string $date */ $date = $row['date']; /** @var string|int $gradedCount */ $gradedCount = $row['graded_count']; /** @var string|float|null $evalAvg */ $evalAvg = $row['average']; $result[] = [ 'evaluationId' => $evaluationId, 'title' => $title, 'classId' => $rowClassId, 'className' => $className, 'subjectId' => $rowSubjectId, 'subjectName' => $subjectName, 'date' => $date, 'average' => $evalAvg !== null ? round((float) $evalAvg, 2) : null, 'gradedCount' => (int) $gradedCount, ]; } return $result; } public function subjectAveragesForOtherTeachers( string $teacherId, string $subjectId, string $tenantId, ): array { $rows = $this->connection->fetchFirstColumn( <<<'SQL' SELECT ROUND(AVG(es.average), 2) FROM evaluations e JOIN evaluation_statistics es ON es.evaluation_id = e.id WHERE e.subject_id = :subject_id AND e.tenant_id = :tenant_id AND e.teacher_id != :teacher_id AND e.status != 'deleted' AND e.grades_published_at IS NOT NULL AND es.average IS NOT NULL GROUP BY e.teacher_id SQL, [ 'teacher_id' => $teacherId, 'subject_id' => $subjectId, 'tenant_id' => $tenantId, ], ); $result = []; foreach ($rows as $v) { if (is_numeric($v)) { $result[] = (float) $v; } } return $result; } }