Lorsqu'un super-admin crée un établissement via l'interface, le système doit automatiquement créer la base tenant, exécuter les migrations, créer le premier utilisateur admin et envoyer l'invitation — le tout de manière asynchrone pour ne pas bloquer la réponse HTTP. Ce mécanisme rend chaque établissement opérationnel dès sa création sans intervention manuelle sur l'infrastructure.
488 lines
17 KiB
PHP
488 lines
17 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Scolarite\Infrastructure\ReadModel;
|
|
|
|
use App\Scolarite\Application\Port\TeacherStatisticsReader;
|
|
use Doctrine\DBAL\Connection;
|
|
|
|
use function is_numeric;
|
|
use function round;
|
|
|
|
final readonly class DbalTeacherStatisticsReader implements TeacherStatisticsReader
|
|
{
|
|
public function __construct(
|
|
private Connection $connection,
|
|
) {
|
|
}
|
|
|
|
public function teacherClassesSummary(
|
|
string $teacherId,
|
|
string $tenantId,
|
|
string $periodStartDate,
|
|
string $periodEndDate,
|
|
): array {
|
|
$rows = $this->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<string, list<float>> $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;
|
|
}
|
|
}
|