feat: Permettre la création et modification de l'emploi du temps des classes
Some checks failed
CI / Backend Tests (push) Has been cancelled
CI / Frontend Tests (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
CI / Naming Conventions (push) Has been cancelled
CI / Build Check (push) Has been cancelled

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:
2026-03-03 13:54:53 +01:00
parent 1db8a7a0b2
commit d103b34023
53 changed files with 6382 additions and 1 deletions

View File

@@ -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;
}
}

View File

@@ -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;
}
}