fix: Permettre l'affectation de classe pour les élèves sans affectation existante
Le handler ChangeStudentClass exigeait une affectation existante pour l'année scolaire en cours avant de pouvoir changer la classe. Un élève créé sans ClassAssignment (import direct, année précédente) provoquait une erreur "Élève non trouvé" au lieu d'être simplement affecté. Le handler crée désormais une nouvelle affectation quand aucune n'existe, et l'erreur de changement de classe s'affiche dans la modale au lieu de la page principale.
This commit is contained in:
@@ -4,7 +4,6 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Administration\Application\Command\ChangeStudentClass;
|
namespace App\Administration\Application\Command\ChangeStudentClass;
|
||||||
|
|
||||||
use App\Administration\Domain\Exception\AffectationEleveNonTrouveeException;
|
|
||||||
use App\Administration\Domain\Exception\ClasseNotFoundException;
|
use App\Administration\Domain\Exception\ClasseNotFoundException;
|
||||||
use App\Administration\Domain\Model\ClassAssignment\ClassAssignment;
|
use App\Administration\Domain\Model\ClassAssignment\ClassAssignment;
|
||||||
use App\Administration\Domain\Model\SchoolClass\AcademicYearId;
|
use App\Administration\Domain\Model\SchoolClass\AcademicYearId;
|
||||||
@@ -44,15 +43,23 @@ final readonly class ChangeStudentClassHandler
|
|||||||
throw ClasseNotFoundException::withId($newClassId);
|
throw ClasseNotFoundException::withId($newClassId);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Trouver l'affectation existante
|
$now = $this->clock->now();
|
||||||
|
|
||||||
|
// Trouver l'affectation existante ou en créer une nouvelle
|
||||||
$assignment = $this->classAssignmentRepository->findByStudent($studentId, $academicYearId, $tenantId);
|
$assignment = $this->classAssignmentRepository->findByStudent($studentId, $academicYearId, $tenantId);
|
||||||
|
|
||||||
if ($assignment === null) {
|
if ($assignment !== null) {
|
||||||
throw AffectationEleveNonTrouveeException::pourEleve($studentId);
|
$assignment->changerClasse($newClassId, $now);
|
||||||
|
} else {
|
||||||
|
$assignment = ClassAssignment::affecter(
|
||||||
|
tenantId: $tenantId,
|
||||||
|
studentId: $studentId,
|
||||||
|
classId: $newClassId,
|
||||||
|
academicYearId: $academicYearId,
|
||||||
|
assignedAt: $now,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
$assignment->changerClasse($newClassId, $this->clock->now());
|
|
||||||
|
|
||||||
$this->classAssignmentRepository->save($assignment);
|
$this->classAssignmentRepository->save($assignment);
|
||||||
|
|
||||||
return $assignment;
|
return $assignment;
|
||||||
|
|||||||
@@ -1,21 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace App\Administration\Domain\Exception;
|
|
||||||
|
|
||||||
use App\Administration\Domain\Model\User\UserId;
|
|
||||||
use DomainException;
|
|
||||||
|
|
||||||
use function sprintf;
|
|
||||||
|
|
||||||
final class AffectationEleveNonTrouveeException extends DomainException
|
|
||||||
{
|
|
||||||
public static function pourEleve(UserId $studentId): self
|
|
||||||
{
|
|
||||||
return new self(sprintf(
|
|
||||||
'Aucune affectation trouvée pour l\'élève "%s" cette année scolaire.',
|
|
||||||
$studentId,
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -8,7 +8,6 @@ use ApiPlatform\Metadata\Operation;
|
|||||||
use ApiPlatform\State\ProcessorInterface;
|
use ApiPlatform\State\ProcessorInterface;
|
||||||
use App\Administration\Application\Command\ChangeStudentClass\ChangeStudentClassCommand;
|
use App\Administration\Application\Command\ChangeStudentClass\ChangeStudentClassCommand;
|
||||||
use App\Administration\Application\Command\ChangeStudentClass\ChangeStudentClassHandler;
|
use App\Administration\Application\Command\ChangeStudentClass\ChangeStudentClassHandler;
|
||||||
use App\Administration\Domain\Exception\AffectationEleveNonTrouveeException;
|
|
||||||
use App\Administration\Domain\Exception\ClasseNotFoundException;
|
use App\Administration\Domain\Exception\ClasseNotFoundException;
|
||||||
use App\Administration\Domain\Repository\ClassRepository;
|
use App\Administration\Domain\Repository\ClassRepository;
|
||||||
use App\Administration\Infrastructure\Api\Resource\StudentResource;
|
use App\Administration\Infrastructure\Api\Resource\StudentResource;
|
||||||
@@ -80,8 +79,6 @@ final readonly class ChangeStudentClassProcessor implements ProcessorInterface
|
|||||||
$data->classLevel = $newClass->level?->value;
|
$data->classLevel = $newClass->level?->value;
|
||||||
|
|
||||||
return $data;
|
return $data;
|
||||||
} catch (AffectationEleveNonTrouveeException) {
|
|
||||||
throw new NotFoundHttpException('Élève non trouvé.');
|
|
||||||
} catch (ClasseNotFoundException $e) {
|
} catch (ClasseNotFoundException $e) {
|
||||||
throw new NotFoundHttpException($e->getMessage());
|
throw new NotFoundHttpException($e->getMessage());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ namespace App\Tests\Unit\Administration\Application\Command\ChangeStudentClass;
|
|||||||
|
|
||||||
use App\Administration\Application\Command\ChangeStudentClass\ChangeStudentClassCommand;
|
use App\Administration\Application\Command\ChangeStudentClass\ChangeStudentClassCommand;
|
||||||
use App\Administration\Application\Command\ChangeStudentClass\ChangeStudentClassHandler;
|
use App\Administration\Application\Command\ChangeStudentClass\ChangeStudentClassHandler;
|
||||||
use App\Administration\Domain\Exception\AffectationEleveNonTrouveeException;
|
|
||||||
use App\Administration\Domain\Exception\ClasseNotFoundException;
|
use App\Administration\Domain\Exception\ClasseNotFoundException;
|
||||||
use App\Administration\Domain\Model\ClassAssignment\ClassAssignment;
|
use App\Administration\Domain\Model\ClassAssignment\ClassAssignment;
|
||||||
use App\Administration\Domain\Model\SchoolClass\AcademicYearId;
|
use App\Administration\Domain\Model\SchoolClass\AcademicYearId;
|
||||||
@@ -74,13 +73,17 @@ final class ChangeStudentClassHandlerTest extends TestCase
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[Test]
|
#[Test]
|
||||||
public function itThrowsWhenAssignmentNotFound(): void
|
public function itCreatesAssignmentWhenNoneExists(): void
|
||||||
{
|
{
|
||||||
$handler = $this->createHandler();
|
$handler = $this->createHandler();
|
||||||
$command = $this->createCommand(studentId: '550e8400-e29b-41d4-a716-446655440070');
|
$newStudentId = '550e8400-e29b-41d4-a716-446655440070';
|
||||||
|
$command = $this->createCommand(studentId: $newStudentId);
|
||||||
|
|
||||||
$this->expectException(AffectationEleveNonTrouveeException::class);
|
$assignment = $handler($command);
|
||||||
$handler($command);
|
|
||||||
|
self::assertTrue($assignment->studentId->equals(UserId::fromString($newStudentId)));
|
||||||
|
self::assertTrue($assignment->classId->equals(ClassId::fromString(self::NEW_CLASS_ID)));
|
||||||
|
self::assertTrue($assignment->academicYearId->equals(AcademicYearId::fromString(self::ACADEMIC_YEAR_ID)));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[Test]
|
#[Test]
|
||||||
|
|||||||
@@ -62,6 +62,7 @@
|
|||||||
let changeClassTarget = $state<Student | null>(null);
|
let changeClassTarget = $state<Student | null>(null);
|
||||||
let newClassForChange = $state('');
|
let newClassForChange = $state('');
|
||||||
let isChangingClass = $state(false);
|
let isChangingClass = $state(false);
|
||||||
|
let changeClassError = $state<string | null>(null);
|
||||||
|
|
||||||
// Classes grouped by level for optgroup
|
// Classes grouped by level for optgroup
|
||||||
let classesByLevel = $derived.by(() => {
|
let classesByLevel = $derived.by(() => {
|
||||||
@@ -300,7 +301,7 @@
|
|||||||
changeClassTarget = student;
|
changeClassTarget = student;
|
||||||
newClassForChange = '';
|
newClassForChange = '';
|
||||||
showChangeClassModal = true;
|
showChangeClassModal = true;
|
||||||
error = null;
|
changeClassError = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeChangeClassModal() {
|
function closeChangeClassModal() {
|
||||||
@@ -338,7 +339,7 @@
|
|||||||
successMessage = `${changeClassTarget.firstName} ${changeClassTarget.lastName} a été transféré vers ${targetClass?.name ?? 'la nouvelle classe'}.`;
|
successMessage = `${changeClassTarget.firstName} ${changeClassTarget.lastName} a été transféré vers ${targetClass?.name ?? 'la nouvelle classe'}.`;
|
||||||
closeChangeClassModal();
|
closeChangeClassModal();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
error = e instanceof Error ? e.message : 'Erreur lors du changement de classe';
|
changeClassError = e instanceof Error ? e.message : 'Erreur lors du changement de classe';
|
||||||
} finally {
|
} finally {
|
||||||
isChangingClass = false;
|
isChangingClass = false;
|
||||||
}
|
}
|
||||||
@@ -720,6 +721,13 @@
|
|||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{#if changeClassError}
|
||||||
|
<div class="alert alert-error modal-error">
|
||||||
|
{changeClassError}
|
||||||
|
<button class="alert-close" onclick={() => (changeClassError = null)}>×</button>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
{#if newClassForChange}
|
{#if newClassForChange}
|
||||||
{@const targetClass = classes.find((c) => c.id === newClassForChange)}
|
{@const targetClass = classes.find((c) => c.id === newClassForChange)}
|
||||||
<div class="change-confirm-info">
|
<div class="change-confirm-info">
|
||||||
@@ -1244,6 +1252,10 @@
|
|||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.modal-error {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.change-confirm-info {
|
.change-confirm-info {
|
||||||
padding: 0.75rem 1rem;
|
padding: 0.75rem 1rem;
|
||||||
background: #eff6ff;
|
background: #eff6ff;
|
||||||
|
|||||||
Reference in New Issue
Block a user