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:
@@ -6,22 +6,26 @@ namespace App\Scolarite\Application\Query\GetBlockedDates;
|
||||
|
||||
use App\Administration\Domain\Model\SchoolCalendar\SchoolCalendarRepository;
|
||||
use App\Administration\Domain\Model\SchoolClass\AcademicYearId;
|
||||
use App\Scolarite\Application\Port\HomeworkRulesChecker;
|
||||
use App\Shared\Domain\Clock;
|
||||
use App\Shared\Domain\Tenant\TenantId;
|
||||
use DateInterval;
|
||||
use DateTimeImmutable;
|
||||
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
|
||||
|
||||
/**
|
||||
* Retourne les dates bloquées (jours fériés, vacances, journées pédagogiques, weekends)
|
||||
* pour une plage de dates donnée.
|
||||
* Retourne les dates bloquées (jours fériés, vacances, journées pédagogiques, weekends,
|
||||
* et dates non conformes aux règles de devoirs) pour une plage de dates donnée.
|
||||
*
|
||||
* Utilisé par le frontend pour griser les jours non modifiables dans la grille EDT.
|
||||
* Utilisé par le frontend pour griser les jours non disponibles dans le calendrier.
|
||||
*/
|
||||
#[AsMessageHandler(bus: 'query.bus')]
|
||||
final readonly class GetBlockedDatesHandler
|
||||
{
|
||||
public function __construct(
|
||||
private SchoolCalendarRepository $calendarRepository,
|
||||
private HomeworkRulesChecker $rulesChecker,
|
||||
private Clock $clock,
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -37,6 +41,7 @@ final readonly class GetBlockedDatesHandler
|
||||
$endDate = new DateTimeImmutable($query->endDate);
|
||||
$oneDay = new DateInterval('P1D');
|
||||
|
||||
$now = $this->clock->now();
|
||||
$blockedDates = [];
|
||||
$current = $startDate;
|
||||
|
||||
@@ -50,14 +55,21 @@ final readonly class GetBlockedDatesHandler
|
||||
reason: $dayOfWeek === 6 ? 'Samedi' : 'Dimanche',
|
||||
type: 'weekend',
|
||||
);
|
||||
} elseif ($calendar !== null) {
|
||||
$entry = $calendar->trouverEntreePourDate($current);
|
||||
} elseif ($calendar !== null && ($entry = $calendar->trouverEntreePourDate($current)) !== null) {
|
||||
$blockedDates[] = new BlockedDateDto(
|
||||
date: $dateStr,
|
||||
reason: $entry->label,
|
||||
type: $entry->type->value,
|
||||
);
|
||||
} else {
|
||||
$dueDate = new DateTimeImmutable($dateStr);
|
||||
$result = $this->rulesChecker->verifier($tenantId, $dueDate, $now);
|
||||
|
||||
if ($entry !== null) {
|
||||
if (!$result->estValide()) {
|
||||
$blockedDates[] = new BlockedDateDto(
|
||||
date: $dateStr,
|
||||
reason: $entry->label,
|
||||
type: $entry->type->value,
|
||||
reason: $result->messages()[0] ?? 'Règle de devoirs',
|
||||
type: $result->estBloquant() ? 'rule_hard' : 'rule_soft',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Application\Query\GetClassStatisticsDetail;
|
||||
|
||||
final readonly class ClassStatisticsDetailDto
|
||||
{
|
||||
/**
|
||||
* @param list<int> $distribution
|
||||
* @param list<array{month: string, average: float}> $evolution
|
||||
* @param list<StudentAverageDto> $students
|
||||
*/
|
||||
public function __construct(
|
||||
public ?float $average,
|
||||
public float $successRate,
|
||||
public array $distribution,
|
||||
public array $evolution,
|
||||
public array $students,
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Application\Query\GetClassStatisticsDetail;
|
||||
|
||||
use App\Scolarite\Application\Port\PeriodFinder;
|
||||
use App\Scolarite\Application\Port\TeacherStatisticsReader;
|
||||
use App\Scolarite\Domain\Service\AverageCalculator;
|
||||
use App\Scolarite\Domain\Service\TeacherStatisticsCalculator;
|
||||
use App\Shared\Domain\Tenant\TenantId;
|
||||
|
||||
use function array_map;
|
||||
use function count;
|
||||
|
||||
use DateTimeImmutable;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
|
||||
|
||||
#[AsMessageHandler(bus: 'query.bus')]
|
||||
final readonly class GetClassStatisticsDetailHandler
|
||||
{
|
||||
public function __construct(
|
||||
private TeacherStatisticsReader $reader,
|
||||
private PeriodFinder $periodFinder,
|
||||
private TeacherStatisticsCalculator $statisticsCalculator,
|
||||
private AverageCalculator $averageCalculator,
|
||||
) {
|
||||
}
|
||||
|
||||
public function __invoke(GetClassStatisticsDetailQuery $query): ClassStatisticsDetailDto
|
||||
{
|
||||
$tenantId = TenantId::fromString($query->tenantId);
|
||||
$period = $this->periodFinder->findForDate(new DateTimeImmutable(), $tenantId);
|
||||
|
||||
if ($period === null) {
|
||||
return new ClassStatisticsDetailDto(
|
||||
average: null,
|
||||
successRate: 0.0,
|
||||
distribution: [0, 0, 0, 0, 0, 0, 0, 0],
|
||||
evolution: [],
|
||||
students: [],
|
||||
);
|
||||
}
|
||||
|
||||
$normalizedGrades = $this->reader->classGradesNormalized(
|
||||
$query->teacherId,
|
||||
$query->classId,
|
||||
$query->subjectId,
|
||||
$query->tenantId,
|
||||
$period->startDate->format('Y-m-d'),
|
||||
$period->endDate->format('Y-m-d'),
|
||||
);
|
||||
|
||||
$classStats = $this->averageCalculator->calculateClassStatistics($normalizedGrades);
|
||||
$distribution = $this->statisticsCalculator->calculateDistribution($normalizedGrades);
|
||||
$successRate = $this->statisticsCalculator->calculateSuccessRate($normalizedGrades);
|
||||
|
||||
$now = new DateTimeImmutable();
|
||||
$month = (int) $now->format('n');
|
||||
$yearStart = $month >= 9 ? (int) $now->format('Y') : (int) $now->format('Y') - 1;
|
||||
$academicYearStart = sprintf('%d-09-01', $yearStart);
|
||||
$academicYearEnd = sprintf('%d-08-31', $yearStart + 1);
|
||||
|
||||
$evolution = $this->reader->classMonthlyAverages(
|
||||
$query->teacherId,
|
||||
$query->classId,
|
||||
$query->subjectId,
|
||||
$query->tenantId,
|
||||
$academicYearStart,
|
||||
$academicYearEnd,
|
||||
);
|
||||
|
||||
$studentAverages = $this->reader->studentAveragesForClass(
|
||||
$query->teacherId,
|
||||
$query->classId,
|
||||
$query->subjectId,
|
||||
$period->periodId,
|
||||
$query->tenantId,
|
||||
);
|
||||
|
||||
$studentTrends = $this->reader->studentMonthlyAveragesForClass(
|
||||
$query->classId,
|
||||
$query->subjectId,
|
||||
$query->tenantId,
|
||||
$academicYearStart,
|
||||
$academicYearEnd,
|
||||
);
|
||||
|
||||
$students = array_map(
|
||||
fn (array $s) => new StudentAverageDto(
|
||||
studentId: $s['studentId'],
|
||||
studentName: $s['studentName'],
|
||||
average: $s['average'],
|
||||
inDifficulty: $s['average'] !== null && $s['average'] < $query->threshold,
|
||||
trend: isset($studentTrends[$s['studentId']]) && count($studentTrends[$s['studentId']]) >= 2
|
||||
? $this->statisticsCalculator->detectTrend($studentTrends[$s['studentId']])
|
||||
: 'stable',
|
||||
),
|
||||
$studentAverages,
|
||||
);
|
||||
|
||||
return new ClassStatisticsDetailDto(
|
||||
average: $classStats->average,
|
||||
successRate: $successRate,
|
||||
distribution: $distribution,
|
||||
evolution: $evolution,
|
||||
students: $students,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Application\Query\GetClassStatisticsDetail;
|
||||
|
||||
final readonly class GetClassStatisticsDetailQuery
|
||||
{
|
||||
public function __construct(
|
||||
public string $teacherId,
|
||||
public string $classId,
|
||||
public string $subjectId,
|
||||
public string $tenantId,
|
||||
public float $threshold = 8.0,
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Application\Query\GetClassStatisticsDetail;
|
||||
|
||||
final readonly class StudentAverageDto
|
||||
{
|
||||
public function __construct(
|
||||
public string $studentId,
|
||||
public string $studentName,
|
||||
public ?float $average,
|
||||
public bool $inDifficulty,
|
||||
public string $trend,
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Application\Query\GetEvaluationDifficulty;
|
||||
|
||||
final readonly class EvaluationDifficultyDto
|
||||
{
|
||||
public function __construct(
|
||||
public string $evaluationId,
|
||||
public string $title,
|
||||
public string $classId,
|
||||
public string $className,
|
||||
public string $subjectId,
|
||||
public string $subjectName,
|
||||
public string $date,
|
||||
public ?float $average,
|
||||
public int $gradedCount,
|
||||
public ?float $subjectAverage,
|
||||
public ?float $percentile,
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Application\Query\GetEvaluationDifficulty;
|
||||
|
||||
use App\Scolarite\Application\Port\TeacherStatisticsReader;
|
||||
use App\Scolarite\Domain\Service\TeacherStatisticsCalculator;
|
||||
|
||||
use function array_sum;
|
||||
use function count;
|
||||
use function round;
|
||||
|
||||
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
|
||||
|
||||
#[AsMessageHandler(bus: 'query.bus')]
|
||||
final readonly class GetEvaluationDifficultyHandler
|
||||
{
|
||||
public function __construct(
|
||||
private TeacherStatisticsReader $reader,
|
||||
private TeacherStatisticsCalculator $calculator,
|
||||
) {
|
||||
}
|
||||
|
||||
/** @return list<EvaluationDifficultyDto> */
|
||||
public function __invoke(GetEvaluationDifficultyQuery $query): array
|
||||
{
|
||||
$evaluations = $this->reader->teacherEvaluationDifficulties(
|
||||
$query->teacherId,
|
||||
$query->tenantId,
|
||||
);
|
||||
|
||||
/** @var array<string, list<float>> $otherAveragesCache */
|
||||
$otherAveragesCache = [];
|
||||
|
||||
$results = [];
|
||||
|
||||
foreach ($evaluations as $eval) {
|
||||
/** @var string $subjectId */
|
||||
$subjectId = $eval['subjectId'];
|
||||
|
||||
if (!isset($otherAveragesCache[$subjectId])) {
|
||||
$otherAveragesCache[$subjectId] = $this->reader->subjectAveragesForOtherTeachers(
|
||||
$query->teacherId,
|
||||
$subjectId,
|
||||
$query->tenantId,
|
||||
);
|
||||
}
|
||||
|
||||
$otherAvgs = $otherAveragesCache[$subjectId];
|
||||
$subjectAverage = $otherAvgs !== [] ? round(array_sum($otherAvgs) / count($otherAvgs), 2) : null;
|
||||
$percentile = $eval['average'] !== null && $otherAvgs !== []
|
||||
? $this->calculator->calculatePercentile($eval['average'], $otherAvgs)
|
||||
: null;
|
||||
|
||||
$results[] = new EvaluationDifficultyDto(
|
||||
evaluationId: $eval['evaluationId'],
|
||||
title: $eval['title'],
|
||||
classId: $eval['classId'],
|
||||
className: $eval['className'],
|
||||
subjectId: $subjectId,
|
||||
subjectName: $eval['subjectName'],
|
||||
date: $eval['date'],
|
||||
average: $eval['average'],
|
||||
gradedCount: $eval['gradedCount'],
|
||||
subjectAverage: $subjectAverage,
|
||||
percentile: $percentile,
|
||||
);
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Application\Query\GetEvaluationDifficulty;
|
||||
|
||||
final readonly class GetEvaluationDifficultyQuery
|
||||
{
|
||||
public function __construct(
|
||||
public string $teacherId,
|
||||
public string $tenantId,
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Application\Query\GetStudentProgression;
|
||||
|
||||
use App\Scolarite\Application\Port\TeacherStatisticsReader;
|
||||
use App\Scolarite\Domain\Service\TeacherStatisticsCalculator;
|
||||
|
||||
use function array_map;
|
||||
use function array_values;
|
||||
|
||||
use DateTimeImmutable;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
|
||||
|
||||
#[AsMessageHandler(bus: 'query.bus')]
|
||||
final readonly class GetStudentProgressionHandler
|
||||
{
|
||||
public function __construct(
|
||||
private TeacherStatisticsReader $reader,
|
||||
private TeacherStatisticsCalculator $calculator,
|
||||
) {
|
||||
}
|
||||
|
||||
public function __invoke(GetStudentProgressionQuery $query): StudentProgressionDto
|
||||
{
|
||||
$now = new DateTimeImmutable();
|
||||
$month = (int) $now->format('n');
|
||||
$yearStart = $month >= 9 ? (int) $now->format('Y') : (int) $now->format('Y') - 1;
|
||||
$academicYearStart = sprintf('%d-09-01', $yearStart);
|
||||
$academicYearEnd = sprintf('%d-08-31', $yearStart + 1);
|
||||
|
||||
$history = $this->reader->studentGradeHistory(
|
||||
$query->studentId,
|
||||
$query->subjectId,
|
||||
$query->classId,
|
||||
$query->teacherId,
|
||||
$query->tenantId,
|
||||
$academicYearStart,
|
||||
$academicYearEnd,
|
||||
);
|
||||
|
||||
$grades = array_map(
|
||||
static fn (array $row) => new GradePointDto(
|
||||
date: $row['date'],
|
||||
value: $row['value'],
|
||||
evaluationTitle: $row['evaluationTitle'],
|
||||
),
|
||||
$history,
|
||||
);
|
||||
|
||||
$points = array_values(array_map(
|
||||
static fn (int $i, GradePointDto $g) => [$i + 1, $g->value],
|
||||
array_keys($grades),
|
||||
$grades,
|
||||
));
|
||||
|
||||
$trendLine = $this->calculator->calculateTrendLine($points);
|
||||
|
||||
return new StudentProgressionDto(
|
||||
grades: $grades,
|
||||
trendLine: $trendLine,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Application\Query\GetStudentProgression;
|
||||
|
||||
final readonly class GetStudentProgressionQuery
|
||||
{
|
||||
public function __construct(
|
||||
public string $studentId,
|
||||
public string $subjectId,
|
||||
public string $classId,
|
||||
public string $teacherId,
|
||||
public string $tenantId,
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Application\Query\GetStudentProgression;
|
||||
|
||||
final readonly class GradePointDto
|
||||
{
|
||||
public function __construct(
|
||||
public string $date,
|
||||
public float $value,
|
||||
public string $evaluationTitle,
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Application\Query\GetStudentProgression;
|
||||
|
||||
use App\Scolarite\Domain\Service\TrendResult;
|
||||
|
||||
final readonly class StudentProgressionDto
|
||||
{
|
||||
/**
|
||||
* @param list<GradePointDto> $grades
|
||||
*/
|
||||
public function __construct(
|
||||
public array $grades,
|
||||
public ?TrendResult $trendLine,
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Application\Query\GetTeacherStatisticsOverview;
|
||||
|
||||
final readonly class ClassOverviewDto
|
||||
{
|
||||
public function __construct(
|
||||
public string $classId,
|
||||
public string $className,
|
||||
public string $subjectId,
|
||||
public string $subjectName,
|
||||
public int $evaluationCount,
|
||||
public int $studentCount,
|
||||
public ?float $average,
|
||||
public ?float $successRate,
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Application\Query\GetTeacherStatisticsOverview;
|
||||
|
||||
use App\Scolarite\Application\Port\PeriodFinder;
|
||||
use App\Scolarite\Application\Port\TeacherStatisticsReader;
|
||||
use App\Shared\Domain\Tenant\TenantId;
|
||||
|
||||
use function array_map;
|
||||
|
||||
use DateTimeImmutable;
|
||||
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
|
||||
|
||||
#[AsMessageHandler(bus: 'query.bus')]
|
||||
final readonly class GetTeacherStatisticsOverviewHandler
|
||||
{
|
||||
public function __construct(
|
||||
private TeacherStatisticsReader $reader,
|
||||
private PeriodFinder $periodFinder,
|
||||
) {
|
||||
}
|
||||
|
||||
/** @return list<ClassOverviewDto> */
|
||||
public function __invoke(GetTeacherStatisticsOverviewQuery $query): array
|
||||
{
|
||||
$period = $this->periodFinder->findForDate(
|
||||
new DateTimeImmutable(),
|
||||
TenantId::fromString($query->tenantId),
|
||||
);
|
||||
|
||||
if ($period === null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$rows = $this->reader->teacherClassesSummary(
|
||||
$query->teacherId,
|
||||
$query->tenantId,
|
||||
$period->startDate->format('Y-m-d'),
|
||||
$period->endDate->format('Y-m-d'),
|
||||
);
|
||||
|
||||
return array_map(
|
||||
static fn (array $row) => new ClassOverviewDto(
|
||||
classId: $row['classId'],
|
||||
className: $row['className'],
|
||||
subjectId: $row['subjectId'],
|
||||
subjectName: $row['subjectName'],
|
||||
evaluationCount: $row['evaluationCount'],
|
||||
studentCount: $row['studentCount'],
|
||||
average: $row['average'],
|
||||
successRate: $row['successRate'],
|
||||
),
|
||||
$rows,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Application\Query\GetTeacherStatisticsOverview;
|
||||
|
||||
final readonly class GetTeacherStatisticsOverviewQuery
|
||||
{
|
||||
public function __construct(
|
||||
public string $teacherId,
|
||||
public string $tenantId,
|
||||
) {
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user