feat: Permettre la création manuelle d'élèves et leur affectation aux classes
Les administrateurs et secrétaires avaient besoin de pouvoir inscrire un élève en cours d'année sans passer par un import CSV. Cette fonctionnalité pose aussi les fondations du modèle élève↔classe (ClassAssignment) qui sera réutilisé par l'import CSV en masse (Story 3.1). L'email est désormais optionnel pour les élèves : si fourni, une invitation est envoyée (User::inviter) ; sinon l'élève est créé avec le statut INSCRIT sans accès compte (User::inscrire). La création de l'utilisateur et l'affectation à la classe sont atomiques (transaction DBAL). Côté frontend, la page /admin/students offre liste paginée, recherche, filtrage par classe, création via modale (avec détection de doublons côté serveur), et changement de classe avec optimistic update.
This commit is contained in:
@@ -0,0 +1,104 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Administration\Infrastructure\Api\Processor;
|
||||
|
||||
use ApiPlatform\Metadata\Operation;
|
||||
use ApiPlatform\State\ProcessorInterface;
|
||||
use App\Administration\Application\Command\CreateStudent\CreateStudentCommand;
|
||||
use App\Administration\Application\Command\CreateStudent\CreateStudentHandler;
|
||||
use App\Administration\Domain\Exception\ClasseNotFoundException;
|
||||
use App\Administration\Domain\Exception\EmailDejaUtiliseeException;
|
||||
use App\Administration\Domain\Model\SchoolClass\ClassId;
|
||||
use App\Administration\Domain\Repository\ClassRepository;
|
||||
use App\Administration\Infrastructure\Api\Resource\StudentResource;
|
||||
use App\Administration\Infrastructure\Security\StudentVoter;
|
||||
use App\Administration\Infrastructure\Service\CurrentAcademicYearResolver;
|
||||
use App\Shared\Infrastructure\Tenant\TenantContext;
|
||||
use Override;
|
||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\ConflictHttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException;
|
||||
use Symfony\Component\Messenger\MessageBusInterface;
|
||||
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
|
||||
|
||||
/**
|
||||
* @implements ProcessorInterface<StudentResource, StudentResource>
|
||||
*/
|
||||
final readonly class CreateStudentProcessor implements ProcessorInterface
|
||||
{
|
||||
public function __construct(
|
||||
private CreateStudentHandler $handler,
|
||||
private ClassRepository $classRepository,
|
||||
private TenantContext $tenantContext,
|
||||
private MessageBusInterface $eventBus,
|
||||
private AuthorizationCheckerInterface $authorizationChecker,
|
||||
private CurrentAcademicYearResolver $academicYearResolver,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param StudentResource $data
|
||||
*/
|
||||
#[Override]
|
||||
public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): StudentResource
|
||||
{
|
||||
if (!$this->authorizationChecker->isGranted(StudentVoter::CREATE)) {
|
||||
throw new AccessDeniedHttpException('Vous n\'êtes pas autorisé à créer un élève.');
|
||||
}
|
||||
|
||||
if (!$this->tenantContext->hasTenant()) {
|
||||
throw new UnauthorizedHttpException('Bearer', 'Tenant non défini.');
|
||||
}
|
||||
|
||||
$tenantId = (string) $this->tenantContext->getCurrentTenantId();
|
||||
$tenantConfig = $this->tenantContext->getCurrentTenantConfig();
|
||||
$academicYearId = $this->academicYearResolver->resolve('current')
|
||||
?? throw new BadRequestHttpException('Impossible de résoudre l\'année scolaire courante.');
|
||||
|
||||
try {
|
||||
$command = new CreateStudentCommand(
|
||||
tenantId: $tenantId,
|
||||
schoolName: $tenantConfig->subdomain,
|
||||
firstName: $data->firstName ?? '',
|
||||
lastName: $data->lastName ?? '',
|
||||
classId: $data->classId ?? '',
|
||||
academicYearId: $academicYearId,
|
||||
email: $data->email,
|
||||
dateNaissance: $data->dateNaissance,
|
||||
studentNumber: $data->studentNumber,
|
||||
);
|
||||
|
||||
$user = ($this->handler)($command);
|
||||
|
||||
// Dispatch domain events
|
||||
foreach ($user->pullDomainEvents() as $event) {
|
||||
$this->eventBus->dispatch($event);
|
||||
}
|
||||
|
||||
// Build the response from the created user and class info
|
||||
$class = $this->classRepository->get(ClassId::fromString($data->classId ?? ''));
|
||||
|
||||
$resource = new StudentResource();
|
||||
$resource->id = (string) $user->id;
|
||||
$resource->firstName = $user->firstName;
|
||||
$resource->lastName = $user->lastName;
|
||||
$resource->email = $user->email !== null ? (string) $user->email : null;
|
||||
$resource->statut = $user->statut->value;
|
||||
$resource->classId = $data->classId;
|
||||
$resource->className = (string) $class->name;
|
||||
$resource->classLevel = $class->level?->value;
|
||||
$resource->studentNumber = $user->studentNumber;
|
||||
$resource->dateNaissance = $user->dateNaissance?->format('Y-m-d');
|
||||
|
||||
return $resource;
|
||||
} catch (EmailDejaUtiliseeException $e) {
|
||||
throw new ConflictHttpException($e->getMessage());
|
||||
} catch (ClasseNotFoundException $e) {
|
||||
throw new NotFoundHttpException($e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user