feat: Permettre la création et modification de l'emploi du temps des classes
L'administration a besoin de construire et maintenir les emplois du temps hebdomadaires pour chaque classe, en s'assurant que les enseignants ne sont pas en conflit (même créneau, classes différentes) et que les affectations enseignant-matière-classe sont respectées. Cette implémentation couvre le CRUD complet des créneaux (ScheduleSlot), la détection de conflits (classe, enseignant, salle) avec possibilité de forcer, la validation des affectations côté serveur (AC2), l'intégration calendrier pour les jours bloqués, une vue mobile-first avec onglets jour par jour, et le drag-and-drop pour réorganiser les créneaux sur desktop.
This commit is contained in:
@@ -0,0 +1,116 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Infrastructure\Api\Processor;
|
||||
|
||||
use ApiPlatform\Metadata\Operation;
|
||||
use ApiPlatform\State\ProcessorInterface;
|
||||
use App\Scolarite\Application\Command\UpdateScheduleSlot\UpdateScheduleSlotCommand;
|
||||
use App\Scolarite\Application\Command\UpdateScheduleSlot\UpdateScheduleSlotHandler;
|
||||
use App\Scolarite\Domain\Exception\CreneauHoraireInvalideException;
|
||||
use App\Scolarite\Domain\Exception\EnseignantNonAffecteException;
|
||||
use App\Scolarite\Domain\Exception\ScheduleSlotNotFoundException;
|
||||
use App\Scolarite\Domain\Service\ScheduleConflict;
|
||||
use App\Scolarite\Infrastructure\Api\Resource\ScheduleSlotResource;
|
||||
use App\Scolarite\Infrastructure\Security\ScheduleSlotVoter;
|
||||
use App\Shared\Infrastructure\Tenant\TenantContext;
|
||||
|
||||
use function array_map;
|
||||
|
||||
use Override;
|
||||
use Ramsey\Uuid\Exception\InvalidUuidStringException;
|
||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException;
|
||||
use Symfony\Component\Messenger\MessageBusInterface;
|
||||
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
|
||||
use ValueError;
|
||||
|
||||
/**
|
||||
* Processor API Platform pour modifier un créneau d'emploi du temps.
|
||||
*
|
||||
* @implements ProcessorInterface<ScheduleSlotResource, ScheduleSlotResource>
|
||||
*/
|
||||
final readonly class UpdateScheduleSlotProcessor implements ProcessorInterface
|
||||
{
|
||||
public function __construct(
|
||||
private UpdateScheduleSlotHandler $handler,
|
||||
private TenantContext $tenantContext,
|
||||
private MessageBusInterface $eventBus,
|
||||
private AuthorizationCheckerInterface $authorizationChecker,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @param ScheduleSlotResource $data
|
||||
*/
|
||||
#[Override]
|
||||
public function process(mixed $data, Operation $operation, array $uriVariables = [], array $context = []): ScheduleSlotResource
|
||||
{
|
||||
if (!$this->authorizationChecker->isGranted(ScheduleSlotVoter::EDIT)) {
|
||||
throw new AccessDeniedHttpException("Vous n'êtes pas autorisé à modifier l'emploi du temps.");
|
||||
}
|
||||
|
||||
if (!$this->tenantContext->hasTenant()) {
|
||||
throw new UnauthorizedHttpException('Bearer', 'Tenant non défini.');
|
||||
}
|
||||
|
||||
/** @var string|null $slotId */
|
||||
$slotId = $uriVariables['id'] ?? null;
|
||||
if ($slotId === null) {
|
||||
throw new NotFoundHttpException('Créneau non trouvé.');
|
||||
}
|
||||
|
||||
$tenantId = (string) $this->tenantContext->getCurrentTenantId();
|
||||
|
||||
try {
|
||||
$command = new UpdateScheduleSlotCommand(
|
||||
tenantId: $tenantId,
|
||||
slotId: $slotId,
|
||||
classId: $data->classId ?? '',
|
||||
subjectId: $data->subjectId ?? '',
|
||||
teacherId: $data->teacherId ?? '',
|
||||
dayOfWeek: $data->dayOfWeek ?? 1,
|
||||
startTime: $data->startTime ?? '',
|
||||
endTime: $data->endTime ?? '',
|
||||
room: $data->room,
|
||||
forceConflicts: $data->forceConflicts ?? false,
|
||||
);
|
||||
|
||||
$result = ($this->handler)($command);
|
||||
$slot = $result['slot'];
|
||||
/** @var array<ScheduleConflict> $conflicts */
|
||||
$conflicts = $result['conflicts'];
|
||||
|
||||
if ($conflicts === [] || $command->forceConflicts) {
|
||||
foreach ($slot->pullDomainEvents() as $event) {
|
||||
$this->eventBus->dispatch($event);
|
||||
}
|
||||
}
|
||||
|
||||
$resource = ScheduleSlotResource::fromDomain($slot);
|
||||
|
||||
if ($conflicts !== []) {
|
||||
$resource->conflicts = array_map(
|
||||
static fn (ScheduleConflict $c) => [
|
||||
'type' => $c->type,
|
||||
'description' => $c->description,
|
||||
'slotId' => (string) $c->conflictingSlot->id,
|
||||
],
|
||||
$conflicts,
|
||||
);
|
||||
}
|
||||
|
||||
return $resource;
|
||||
} catch (EnseignantNonAffecteException $e) {
|
||||
throw new UnprocessableEntityHttpException($e->getMessage());
|
||||
} catch (ScheduleSlotNotFoundException|InvalidUuidStringException) {
|
||||
throw new NotFoundHttpException('Créneau non trouvé.');
|
||||
} catch (CreneauHoraireInvalideException|ValueError $e) {
|
||||
throw new BadRequestHttpException($e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user