feat: Provisionner automatiquement un nouvel établissement
Some checks failed
CI / Backend Tests (push) Has been cancelled
CI / Frontend Tests (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
CI / Naming Conventions (push) Has been cancelled
CI / Build Check (push) Has been cancelled

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:
2026-04-08 13:55:41 +02:00
parent bec211ebf0
commit 18c54e6d67
106 changed files with 9586 additions and 380 deletions

View File

@@ -0,0 +1,154 @@
<?php
declare(strict_types=1);
namespace App\Scolarite\Domain\Service;
use function abs;
use function array_fill;
use function array_values;
use function count;
use function floor;
use function max;
use function min;
use function round;
final class TeacherStatisticsCalculator
{
/**
* Répartition des notes en 8 bins de largeur 2.5 points (sur /20).
* Bins : [02.5[, [2.55[, [57.5[, [7.510[, [1012.5[, [12.515[, [1517.5[, [17.520].
*
* @param list<float> $normalizedValues Notes normalisées sur /20
*
* @return list<int> 8 éléments
*/
public function calculateDistribution(array $normalizedValues): array
{
/** @var list<int> $bins */
$bins = array_fill(0, 8, 0);
foreach ($normalizedValues as $value) {
$binIndex = min(7, max(0, (int) floor($value / 2.5)));
++$bins[$binIndex];
}
return array_values($bins);
}
/**
* Taux de réussite : pourcentage de notes >= seuil.
*
* @param list<float> $normalizedValues Notes normalisées sur /20
* @param float $threshold Seuil de réussite (défaut : 10.0)
*/
public function calculateSuccessRate(array $normalizedValues, float $threshold = 10.0): float
{
if ($normalizedValues === []) {
return 0.0;
}
$count = count($normalizedValues);
$above = 0;
foreach ($normalizedValues as $value) {
if ($value >= $threshold) {
++$above;
}
}
return round($above * 100.0 / $count, 1);
}
/**
* Régression linéaire simple (moindres carrés) pour la ligne de tendance.
*
* @param list<array{0: int|float, 1: float}> $points [[x, y], ...]
*/
public function calculateTrendLine(array $points): ?TrendResult
{
$n = count($points);
if ($n < 2) {
return null;
}
$sumX = 0.0;
$sumY = 0.0;
$sumXY = 0.0;
$sumX2 = 0.0;
foreach ($points as [$x, $y]) {
$sumX += $x;
$sumY += $y;
$sumXY += $x * $y;
$sumX2 += $x * $x;
}
$denominator = $n * $sumX2 - $sumX * $sumX;
if (abs($denominator) < 1e-10) {
return new TrendResult(slope: 0.0, intercept: $sumY / $n);
}
$slope = ($n * $sumXY - $sumX * $sumY) / $denominator;
$intercept = ($sumY - $slope * $sumX) / $n;
return new TrendResult(
slope: round($slope, 4),
intercept: round($intercept, 4),
);
}
/**
* Détecte la tendance à partir de moyennes périodiques (trimestres).
* Compare la dernière moyenne à la première avec un seuil de 1 point.
*
* @param list<float> $periodicAverages Moyennes par période chronologique
*
* @return 'improving'|'stable'|'declining'
*/
public function detectTrend(array $periodicAverages): string
{
if (count($periodicAverages) < 2) {
return 'stable';
}
$first = $periodicAverages[0];
$last = $periodicAverages[count($periodicAverages) - 1];
$diff = $last - $first;
if ($diff > 1.0) {
return 'improving';
}
if ($diff < -1.0) {
return 'declining';
}
return 'stable';
}
/**
* Calcule le percentile d'une valeur parmi un ensemble d'autres valeurs.
* Indique le % de valeurs inférieures à la valeur donnée.
*
* @param list<float> $otherValues
*/
public function calculatePercentile(float $value, array $otherValues): float
{
if ($otherValues === []) {
return 100.0;
}
$below = 0;
foreach ($otherValues as $other) {
if ($other < $value) {
++$below;
}
}
return round($below * 100.0 / count($otherValues), 1);
}
}