feat: Permettre l'import d'élèves via fichier CSV ou XLSX

L'import manuel élève par élève est fastidieux pour les établissements
qui gèrent des centaines d'élèves. Un wizard d'import en 4 étapes
(upload → mapping → preview → confirmation) permet de traiter un
fichier complet en une seule opération, avec détection automatique
du format (Pronote, École Directe) et validation avant import.

L'import est traité de manière asynchrone via Messenger pour ne pas
bloquer l'interface, avec suivi de progression en temps réel et
réutilisation des mappings entre imports successifs.
This commit is contained in:
2026-02-25 16:51:13 +01:00
parent 560b941821
commit 2420e35492
62 changed files with 7510 additions and 86 deletions

View File

@@ -0,0 +1,129 @@
<?php
declare(strict_types=1);
namespace App\Tests\Unit\Administration\Domain\Model\Import;
use App\Administration\Domain\Exception\MappingIncompletException;
use App\Administration\Domain\Model\Import\ColumnMapping;
use App\Administration\Domain\Model\Import\KnownImportFormat;
use App\Administration\Domain\Model\Import\StudentImportField;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
final class ColumnMappingTest extends TestCase
{
#[Test]
public function creerWithAllRequiredFieldsSucceeds(): void
{
$mapping = ColumnMapping::creer(
[
'Nom' => StudentImportField::LAST_NAME,
'Prénom' => StudentImportField::FIRST_NAME,
'Classe' => StudentImportField::CLASS_NAME,
],
KnownImportFormat::CUSTOM,
);
self::assertCount(3, $mapping->colonnesSources());
self::assertSame(KnownImportFormat::CUSTOM, $mapping->format);
}
#[Test]
public function creerWithOptionalFieldsSucceeds(): void
{
$mapping = ColumnMapping::creer(
[
'Nom' => StudentImportField::LAST_NAME,
'Prénom' => StudentImportField::FIRST_NAME,
'Classe' => StudentImportField::CLASS_NAME,
'Email' => StudentImportField::EMAIL,
'Naissance' => StudentImportField::BIRTH_DATE,
],
KnownImportFormat::PRONOTE,
);
self::assertCount(5, $mapping->colonnesSources());
}
#[Test]
public function creerSansNomLeveException(): void
{
$this->expectException(MappingIncompletException::class);
ColumnMapping::creer(
[
'Prénom' => StudentImportField::FIRST_NAME,
'Classe' => StudentImportField::CLASS_NAME,
],
KnownImportFormat::CUSTOM,
);
}
#[Test]
public function creerSansPrenomLeveException(): void
{
$this->expectException(MappingIncompletException::class);
ColumnMapping::creer(
[
'Nom' => StudentImportField::LAST_NAME,
'Classe' => StudentImportField::CLASS_NAME,
],
KnownImportFormat::CUSTOM,
);
}
#[Test]
public function creerSansClasseLeveException(): void
{
$this->expectException(MappingIncompletException::class);
ColumnMapping::creer(
[
'Nom' => StudentImportField::LAST_NAME,
'Prénom' => StudentImportField::FIRST_NAME,
],
KnownImportFormat::CUSTOM,
);
}
#[Test]
public function champPourReturnsMappedField(): void
{
$mapping = ColumnMapping::creer(
[
'Nom' => StudentImportField::LAST_NAME,
'Prénom' => StudentImportField::FIRST_NAME,
'Classe' => StudentImportField::CLASS_NAME,
],
KnownImportFormat::CUSTOM,
);
self::assertSame(StudentImportField::LAST_NAME, $mapping->champPour('Nom'));
self::assertSame(StudentImportField::FIRST_NAME, $mapping->champPour('Prénom'));
self::assertNull($mapping->champPour('Inconnu'));
}
#[Test]
public function equalsComparesCorrectly(): void
{
$mapping1 = ColumnMapping::creer(
['Nom' => StudentImportField::LAST_NAME, 'Prénom' => StudentImportField::FIRST_NAME, 'Classe' => StudentImportField::CLASS_NAME],
KnownImportFormat::CUSTOM,
);
$mapping2 = ColumnMapping::creer(
['Nom' => StudentImportField::LAST_NAME, 'Prénom' => StudentImportField::FIRST_NAME, 'Classe' => StudentImportField::CLASS_NAME],
KnownImportFormat::CUSTOM,
);
$mapping3 = ColumnMapping::creer(
['Nom' => StudentImportField::LAST_NAME, 'Prénom' => StudentImportField::FIRST_NAME, 'Classe' => StudentImportField::CLASS_NAME],
KnownImportFormat::PRONOTE,
);
self::assertTrue($mapping1->equals($mapping2));
self::assertFalse($mapping1->equals($mapping3));
}
}