feat: Provisionner automatiquement un nouvel établissement
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.
This commit is contained in:
@@ -0,0 +1,487 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user