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,105 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Infrastructure\Api\Processor;
|
||||
|
||||
use ApiPlatform\Metadata\Operation;
|
||||
use ApiPlatform\State\ProcessorInterface;
|
||||
use App\Scolarite\Application\Command\CreateScheduleSlot\CreateScheduleSlotCommand;
|
||||
use App\Scolarite\Application\Command\CreateScheduleSlot\CreateScheduleSlotHandler;
|
||||
use App\Scolarite\Domain\Exception\CreneauHoraireInvalideException;
|
||||
use App\Scolarite\Domain\Exception\EnseignantNonAffecteException;
|
||||
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 Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||
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 créer un créneau d'emploi du temps.
|
||||
*
|
||||
* @implements ProcessorInterface<ScheduleSlotResource, ScheduleSlotResource>
|
||||
*/
|
||||
final readonly class CreateScheduleSlotProcessor implements ProcessorInterface
|
||||
{
|
||||
public function __construct(
|
||||
private CreateScheduleSlotHandler $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::CREATE)) {
|
||||
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.');
|
||||
}
|
||||
|
||||
$tenantId = (string) $this->tenantContext->getCurrentTenantId();
|
||||
|
||||
try {
|
||||
$command = new CreateScheduleSlotCommand(
|
||||
tenantId: $tenantId,
|
||||
classId: $data->classId ?? '',
|
||||
subjectId: $data->subjectId ?? '',
|
||||
teacherId: $data->teacherId ?? '',
|
||||
dayOfWeek: $data->dayOfWeek ?? 1,
|
||||
startTime: $data->startTime ?? '',
|
||||
endTime: $data->endTime ?? '',
|
||||
room: $data->room,
|
||||
isRecurring: $data->isRecurring ?? true,
|
||||
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 (CreneauHoraireInvalideException|ValueError $e) {
|
||||
throw new BadRequestHttpException($e->getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Infrastructure\Api\Processor;
|
||||
|
||||
use ApiPlatform\Metadata\Operation;
|
||||
use ApiPlatform\State\ProcessorInterface;
|
||||
use App\Scolarite\Application\Command\DeleteScheduleSlot\DeleteScheduleSlotCommand;
|
||||
use App\Scolarite\Application\Command\DeleteScheduleSlot\DeleteScheduleSlotHandler;
|
||||
use App\Scolarite\Domain\Exception\ScheduleSlotNotFoundException;
|
||||
use App\Scolarite\Infrastructure\Api\Resource\ScheduleSlotResource;
|
||||
use App\Scolarite\Infrastructure\Security\ScheduleSlotVoter;
|
||||
use App\Shared\Infrastructure\Tenant\TenantContext;
|
||||
use Override;
|
||||
use Ramsey\Uuid\Exception\InvalidUuidStringException;
|
||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException;
|
||||
use Symfony\Component\Messenger\MessageBusInterface;
|
||||
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
|
||||
|
||||
/**
|
||||
* Processor API Platform pour supprimer un créneau d'emploi du temps.
|
||||
*
|
||||
* @implements ProcessorInterface<ScheduleSlotResource, null>
|
||||
*/
|
||||
final readonly class DeleteScheduleSlotProcessor implements ProcessorInterface
|
||||
{
|
||||
public function __construct(
|
||||
private DeleteScheduleSlotHandler $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 = []): null
|
||||
{
|
||||
if (!$this->authorizationChecker->isGranted(ScheduleSlotVoter::DELETE)) {
|
||||
throw new AccessDeniedHttpException("Vous n'êtes pas autorisé à supprimer des créneaux.");
|
||||
}
|
||||
|
||||
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 DeleteScheduleSlotCommand(
|
||||
tenantId: $tenantId,
|
||||
slotId: $slotId,
|
||||
);
|
||||
|
||||
$slot = ($this->handler)($command);
|
||||
|
||||
foreach ($slot->pullDomainEvents() as $event) {
|
||||
$this->eventBus->dispatch($event);
|
||||
}
|
||||
|
||||
return null;
|
||||
} catch (ScheduleSlotNotFoundException|InvalidUuidStringException) {
|
||||
throw new NotFoundHttpException('Créneau non trouvé.');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Infrastructure\Api\Provider;
|
||||
|
||||
use ApiPlatform\Metadata\Operation;
|
||||
use ApiPlatform\State\ProviderInterface;
|
||||
use App\Administration\Infrastructure\Service\CurrentAcademicYearResolver;
|
||||
use App\Scolarite\Application\Query\GetBlockedDates\GetBlockedDatesHandler;
|
||||
use App\Scolarite\Application\Query\GetBlockedDates\GetBlockedDatesQuery;
|
||||
use App\Scolarite\Infrastructure\Api\Resource\BlockedDateResource;
|
||||
use App\Scolarite\Infrastructure\Security\ScheduleSlotVoter;
|
||||
use App\Shared\Infrastructure\Tenant\TenantContext;
|
||||
|
||||
use function array_map;
|
||||
|
||||
use Override;
|
||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException;
|
||||
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
|
||||
|
||||
/**
|
||||
* State Provider pour récupérer les dates bloquées (jours fériés, vacances, etc.).
|
||||
*
|
||||
* @implements ProviderInterface<BlockedDateResource>
|
||||
*/
|
||||
final readonly class BlockedDateCollectionProvider implements ProviderInterface
|
||||
{
|
||||
public function __construct(
|
||||
private GetBlockedDatesHandler $handler,
|
||||
private TenantContext $tenantContext,
|
||||
private AuthorizationCheckerInterface $authorizationChecker,
|
||||
private CurrentAcademicYearResolver $academicYearResolver,
|
||||
) {
|
||||
}
|
||||
|
||||
/** @return array<BlockedDateResource> */
|
||||
#[Override]
|
||||
public function provide(Operation $operation, array $uriVariables = [], array $context = []): array
|
||||
{
|
||||
if (!$this->authorizationChecker->isGranted(ScheduleSlotVoter::VIEW)) {
|
||||
throw new AccessDeniedHttpException("Vous n'êtes pas autorisé à consulter les dates bloquées.");
|
||||
}
|
||||
|
||||
if (!$this->tenantContext->hasTenant()) {
|
||||
throw new UnauthorizedHttpException('Bearer', 'Tenant non défini.');
|
||||
}
|
||||
|
||||
/** @var array<string, string> $filters */
|
||||
$filters = $context['filters'] ?? [];
|
||||
|
||||
if (!isset($filters['startDate'], $filters['endDate'])) {
|
||||
throw new BadRequestHttpException('Les paramètres startDate et endDate sont requis.');
|
||||
}
|
||||
|
||||
$tenantId = (string) $this->tenantContext->getCurrentTenantId();
|
||||
$academicYearId = $this->academicYearResolver->resolve('current');
|
||||
|
||||
if ($academicYearId === null) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$query = new GetBlockedDatesQuery(
|
||||
tenantId: $tenantId,
|
||||
academicYearId: $academicYearId,
|
||||
startDate: (string) $filters['startDate'],
|
||||
endDate: (string) $filters['endDate'],
|
||||
);
|
||||
|
||||
$dtos = ($this->handler)($query);
|
||||
|
||||
return array_map(BlockedDateResource::fromDto(...), $dtos);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Infrastructure\Api\Provider;
|
||||
|
||||
use ApiPlatform\Metadata\Operation;
|
||||
use ApiPlatform\State\ProviderInterface;
|
||||
use App\Scolarite\Application\Query\GetScheduleSlots\GetScheduleSlotsHandler;
|
||||
use App\Scolarite\Application\Query\GetScheduleSlots\GetScheduleSlotsQuery;
|
||||
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 Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException;
|
||||
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
|
||||
|
||||
/**
|
||||
* State Provider pour récupérer l'emploi du temps avec filtrage par classe ou enseignant.
|
||||
*
|
||||
* @implements ProviderInterface<ScheduleSlotResource>
|
||||
*/
|
||||
final readonly class ScheduleSlotCollectionProvider implements ProviderInterface
|
||||
{
|
||||
public function __construct(
|
||||
private GetScheduleSlotsHandler $handler,
|
||||
private TenantContext $tenantContext,
|
||||
private AuthorizationCheckerInterface $authorizationChecker,
|
||||
) {
|
||||
}
|
||||
|
||||
/** @return array<ScheduleSlotResource> */
|
||||
#[Override]
|
||||
public function provide(Operation $operation, array $uriVariables = [], array $context = []): array
|
||||
{
|
||||
if (!$this->authorizationChecker->isGranted(ScheduleSlotVoter::VIEW)) {
|
||||
throw new AccessDeniedHttpException("Vous n'êtes pas autorisé à consulter l'emploi du temps.");
|
||||
}
|
||||
|
||||
if (!$this->tenantContext->hasTenant()) {
|
||||
throw new UnauthorizedHttpException('Bearer', 'Tenant non défini.');
|
||||
}
|
||||
|
||||
$tenantId = (string) $this->tenantContext->getCurrentTenantId();
|
||||
/** @var array<string, string> $filters */
|
||||
$filters = $context['filters'] ?? [];
|
||||
|
||||
$query = new GetScheduleSlotsQuery(
|
||||
tenantId: $tenantId,
|
||||
classId: isset($filters['classId']) ? (string) $filters['classId'] : null,
|
||||
teacherId: isset($filters['teacherId']) ? (string) $filters['teacherId'] : null,
|
||||
);
|
||||
|
||||
$dtos = ($this->handler)($query);
|
||||
|
||||
return array_map(ScheduleSlotResource::fromDto(...), $dtos);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Infrastructure\Api\Provider;
|
||||
|
||||
use ApiPlatform\Metadata\Operation;
|
||||
use ApiPlatform\State\ProviderInterface;
|
||||
use App\Scolarite\Domain\Exception\ScheduleSlotNotFoundException;
|
||||
use App\Scolarite\Domain\Model\Schedule\ScheduleSlotId;
|
||||
use App\Scolarite\Domain\Repository\ScheduleSlotRepository;
|
||||
use App\Scolarite\Infrastructure\Api\Resource\ScheduleSlotResource;
|
||||
use App\Scolarite\Infrastructure\Security\ScheduleSlotVoter;
|
||||
use App\Shared\Infrastructure\Tenant\TenantContext;
|
||||
use Override;
|
||||
use Ramsey\Uuid\Exception\InvalidUuidStringException;
|
||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException;
|
||||
use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
|
||||
|
||||
/**
|
||||
* State Provider pour récupérer un créneau d'emploi du temps par son ID.
|
||||
*
|
||||
* @implements ProviderInterface<ScheduleSlotResource>
|
||||
*/
|
||||
final readonly class ScheduleSlotItemProvider implements ProviderInterface
|
||||
{
|
||||
public function __construct(
|
||||
private ScheduleSlotRepository $repository,
|
||||
private TenantContext $tenantContext,
|
||||
private AuthorizationCheckerInterface $authorizationChecker,
|
||||
) {
|
||||
}
|
||||
|
||||
#[Override]
|
||||
public function provide(Operation $operation, array $uriVariables = [], array $context = []): ScheduleSlotResource
|
||||
{
|
||||
if (!$this->tenantContext->hasTenant()) {
|
||||
throw new UnauthorizedHttpException('Bearer', 'Tenant non défini.');
|
||||
}
|
||||
|
||||
if (!$this->authorizationChecker->isGranted(ScheduleSlotVoter::VIEW)) {
|
||||
throw new AccessDeniedHttpException("Vous n'êtes pas autorisé à consulter l'emploi du temps.");
|
||||
}
|
||||
|
||||
/** @var string|null $slotId */
|
||||
$slotId = $uriVariables['id'] ?? null;
|
||||
if ($slotId === null) {
|
||||
throw new NotFoundHttpException('Créneau non trouvé.');
|
||||
}
|
||||
|
||||
$tenantId = $this->tenantContext->getCurrentTenantId();
|
||||
|
||||
try {
|
||||
$slot = $this->repository->get(ScheduleSlotId::fromString($slotId), $tenantId);
|
||||
} catch (ScheduleSlotNotFoundException|InvalidUuidStringException) {
|
||||
throw new NotFoundHttpException('Créneau non trouvé.');
|
||||
}
|
||||
|
||||
return ScheduleSlotResource::fromDomain($slot);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Infrastructure\Api\Resource;
|
||||
|
||||
use ApiPlatform\Metadata\ApiProperty;
|
||||
use ApiPlatform\Metadata\ApiResource;
|
||||
use ApiPlatform\Metadata\GetCollection;
|
||||
use App\Scolarite\Application\Query\GetBlockedDates\BlockedDateDto;
|
||||
use App\Scolarite\Infrastructure\Api\Provider\BlockedDateCollectionProvider;
|
||||
|
||||
#[ApiResource(
|
||||
shortName: 'BlockedDate',
|
||||
operations: [
|
||||
new GetCollection(
|
||||
uriTemplate: '/schedule/blocked-dates',
|
||||
provider: BlockedDateCollectionProvider::class,
|
||||
name: 'get_blocked_dates',
|
||||
),
|
||||
],
|
||||
)]
|
||||
final class BlockedDateResource
|
||||
{
|
||||
#[ApiProperty(identifier: true)]
|
||||
public ?string $date = null;
|
||||
|
||||
public ?string $reason = null;
|
||||
|
||||
public ?string $type = null;
|
||||
|
||||
public static function fromDto(BlockedDateDto $dto): self
|
||||
{
|
||||
$resource = new self();
|
||||
$resource->date = $dto->date;
|
||||
$resource->reason = $dto->reason;
|
||||
$resource->type = $dto->type;
|
||||
|
||||
return $resource;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Infrastructure\Api\Resource;
|
||||
|
||||
use ApiPlatform\Metadata\ApiProperty;
|
||||
use ApiPlatform\Metadata\ApiResource;
|
||||
use ApiPlatform\Metadata\Delete;
|
||||
use ApiPlatform\Metadata\Get;
|
||||
use ApiPlatform\Metadata\GetCollection;
|
||||
use ApiPlatform\Metadata\Patch;
|
||||
use ApiPlatform\Metadata\Post;
|
||||
use App\Scolarite\Application\Query\GetScheduleSlots\ScheduleSlotDto;
|
||||
use App\Scolarite\Domain\Model\Schedule\ScheduleSlot;
|
||||
use App\Scolarite\Infrastructure\Api\Processor\CreateScheduleSlotProcessor;
|
||||
use App\Scolarite\Infrastructure\Api\Processor\DeleteScheduleSlotProcessor;
|
||||
use App\Scolarite\Infrastructure\Api\Processor\UpdateScheduleSlotProcessor;
|
||||
use App\Scolarite\Infrastructure\Api\Provider\ScheduleSlotCollectionProvider;
|
||||
use App\Scolarite\Infrastructure\Api\Provider\ScheduleSlotItemProvider;
|
||||
use DateTimeImmutable;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
/**
|
||||
* API Resource pour la gestion de l'emploi du temps.
|
||||
*
|
||||
* @see Story 4.1 - Création et Modification de l'Emploi du Temps
|
||||
* @see FR26 - Créer et modifier l'emploi du temps des classes
|
||||
*/
|
||||
#[ApiResource(
|
||||
shortName: 'ScheduleSlot',
|
||||
operations: [
|
||||
new GetCollection(
|
||||
uriTemplate: '/schedule/slots',
|
||||
provider: ScheduleSlotCollectionProvider::class,
|
||||
name: 'get_schedule_slots',
|
||||
),
|
||||
new Get(
|
||||
uriTemplate: '/schedule/slots/{id}',
|
||||
provider: ScheduleSlotItemProvider::class,
|
||||
name: 'get_schedule_slot',
|
||||
),
|
||||
new Post(
|
||||
uriTemplate: '/schedule/slots',
|
||||
processor: CreateScheduleSlotProcessor::class,
|
||||
validationContext: ['groups' => ['Default', 'create']],
|
||||
name: 'create_schedule_slot',
|
||||
),
|
||||
new Patch(
|
||||
uriTemplate: '/schedule/slots/{id}',
|
||||
provider: ScheduleSlotItemProvider::class,
|
||||
processor: UpdateScheduleSlotProcessor::class,
|
||||
validationContext: ['groups' => ['Default', 'update']],
|
||||
name: 'update_schedule_slot',
|
||||
),
|
||||
new Delete(
|
||||
uriTemplate: '/schedule/slots/{id}',
|
||||
provider: ScheduleSlotItemProvider::class,
|
||||
processor: DeleteScheduleSlotProcessor::class,
|
||||
name: 'delete_schedule_slot',
|
||||
),
|
||||
],
|
||||
)]
|
||||
final class ScheduleSlotResource
|
||||
{
|
||||
#[ApiProperty(identifier: true)]
|
||||
public ?string $id = null;
|
||||
|
||||
#[Assert\NotBlank(message: 'La classe est requise.', groups: ['create'])]
|
||||
public ?string $classId = null;
|
||||
|
||||
#[Assert\NotBlank(message: 'La matière est requise.', groups: ['create'])]
|
||||
public ?string $subjectId = null;
|
||||
|
||||
#[Assert\NotBlank(message: "L'enseignant est requis.", groups: ['create'])]
|
||||
public ?string $teacherId = null;
|
||||
|
||||
#[Assert\NotNull(message: 'Le jour de la semaine est requis.', groups: ['create'])]
|
||||
#[Assert\Range(min: 1, max: 7, notInRangeMessage: 'Le jour doit être compris entre 1 (lundi) et 7 (dimanche).')]
|
||||
public ?int $dayOfWeek = null;
|
||||
|
||||
#[Assert\NotBlank(message: "L'heure de début est requise.", groups: ['create'])]
|
||||
#[Assert\Regex(pattern: '/^([01]\d|2[0-3]):[0-5]\d$/', message: "L'heure doit être au format HH:MM.")]
|
||||
public ?string $startTime = null;
|
||||
|
||||
#[Assert\NotBlank(message: "L'heure de fin est requise.", groups: ['create'])]
|
||||
#[Assert\Regex(pattern: '/^([01]\d|2[0-3]):[0-5]\d$/', message: "L'heure doit être au format HH:MM.")]
|
||||
public ?string $endTime = null;
|
||||
|
||||
#[Assert\Length(max: 50, maxMessage: 'Le nom de la salle ne peut pas dépasser {{ limit }} caractères.')]
|
||||
public ?string $room = null;
|
||||
|
||||
public ?bool $isRecurring = null;
|
||||
|
||||
/**
|
||||
* Si true, forcer la création/modification malgré les conflits.
|
||||
*/
|
||||
#[ApiProperty(readable: false)]
|
||||
public ?bool $forceConflicts = null;
|
||||
|
||||
/**
|
||||
* Conflits détectés lors de la création/modification.
|
||||
* Renvoyé en réponse si des conflits existent.
|
||||
*
|
||||
* @var array<array{type: string, description: string, slotId: string}>|null
|
||||
*/
|
||||
#[ApiProperty(readable: true, writable: false)]
|
||||
public ?array $conflicts = null;
|
||||
|
||||
public ?DateTimeImmutable $createdAt = null;
|
||||
|
||||
public ?DateTimeImmutable $updatedAt = null;
|
||||
|
||||
public static function fromDomain(ScheduleSlot $slot): self
|
||||
{
|
||||
$resource = new self();
|
||||
$resource->id = (string) $slot->id;
|
||||
$resource->classId = (string) $slot->classId;
|
||||
$resource->subjectId = (string) $slot->subjectId;
|
||||
$resource->teacherId = (string) $slot->teacherId;
|
||||
$resource->dayOfWeek = $slot->dayOfWeek->value;
|
||||
$resource->startTime = $slot->timeSlot->startTime;
|
||||
$resource->endTime = $slot->timeSlot->endTime;
|
||||
$resource->room = $slot->room;
|
||||
$resource->isRecurring = $slot->isRecurring;
|
||||
$resource->createdAt = $slot->createdAt;
|
||||
$resource->updatedAt = $slot->updatedAt;
|
||||
|
||||
return $resource;
|
||||
}
|
||||
|
||||
public static function fromDto(ScheduleSlotDto $dto): self
|
||||
{
|
||||
$resource = new self();
|
||||
$resource->id = $dto->id;
|
||||
$resource->classId = $dto->classId;
|
||||
$resource->subjectId = $dto->subjectId;
|
||||
$resource->teacherId = $dto->teacherId;
|
||||
$resource->dayOfWeek = $dto->dayOfWeek;
|
||||
$resource->startTime = $dto->startTime;
|
||||
$resource->endTime = $dto->endTime;
|
||||
$resource->room = $dto->room;
|
||||
$resource->isRecurring = $dto->isRecurring;
|
||||
$resource->createdAt = $dto->createdAt;
|
||||
$resource->updatedAt = $dto->updatedAt;
|
||||
|
||||
return $resource;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user