feat: Permettre la définition d'une semaine type récurrente pour l'emploi du temps
Les administrateurs devaient recréer manuellement l'emploi du temps chaque semaine. Cette implémentation introduit un système de récurrence hebdomadaire avec gestion des exceptions par occurrence, permettant de modifier ou annuler un cours spécifique sans affecter les autres semaines. Le ScheduleResolver calcule dynamiquement l'EDT réel en combinant les créneaux récurrents, les exceptions ponctuelles et le calendrier scolaire (vacances/fériés).
This commit is contained in:
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Application\Command\CreateScheduleException;
|
||||
|
||||
final readonly class CreateScheduleExceptionCommand
|
||||
{
|
||||
public function __construct(
|
||||
public string $tenantId,
|
||||
public string $slotId,
|
||||
public string $exceptionDate,
|
||||
public string $type,
|
||||
public string $createdBy,
|
||||
public ?string $newStartTime = null,
|
||||
public ?string $newEndTime = null,
|
||||
public ?string $newRoom = null,
|
||||
public ?string $newTeacherId = null,
|
||||
public ?string $reason = null,
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Application\Command\CreateScheduleException;
|
||||
|
||||
use App\Administration\Domain\Model\User\UserId;
|
||||
use App\Scolarite\Domain\Exception\DateExceptionInvalideException;
|
||||
use App\Scolarite\Domain\Model\Schedule\ScheduleException;
|
||||
use App\Scolarite\Domain\Model\Schedule\ScheduleExceptionType;
|
||||
use App\Scolarite\Domain\Model\Schedule\ScheduleSlotId;
|
||||
use App\Scolarite\Domain\Model\Schedule\TimeSlot;
|
||||
use App\Scolarite\Domain\Repository\ScheduleExceptionRepository;
|
||||
use App\Scolarite\Domain\Repository\ScheduleSlotRepository;
|
||||
use App\Shared\Domain\Clock;
|
||||
use App\Shared\Domain\Tenant\TenantId;
|
||||
use DateTimeImmutable;
|
||||
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
|
||||
|
||||
#[AsMessageHandler(bus: 'command.bus')]
|
||||
final readonly class CreateScheduleExceptionHandler
|
||||
{
|
||||
public function __construct(
|
||||
private ScheduleSlotRepository $slotRepository,
|
||||
private ScheduleExceptionRepository $exceptionRepository,
|
||||
private Clock $clock,
|
||||
) {
|
||||
}
|
||||
|
||||
public function __invoke(CreateScheduleExceptionCommand $command): ScheduleException
|
||||
{
|
||||
$tenantId = TenantId::fromString($command->tenantId);
|
||||
$slotId = ScheduleSlotId::fromString($command->slotId);
|
||||
|
||||
$slot = $this->slotRepository->get($slotId, $tenantId);
|
||||
|
||||
$type = ScheduleExceptionType::from($command->type);
|
||||
$exceptionDate = new DateTimeImmutable($command->exceptionDate);
|
||||
|
||||
if (!$slot->isActiveOnDate($exceptionDate)) {
|
||||
throw DateExceptionInvalideException::pourSlotEtDate($slotId, $exceptionDate);
|
||||
}
|
||||
$createdBy = UserId::fromString($command->createdBy);
|
||||
|
||||
$exception = match ($type) {
|
||||
ScheduleExceptionType::CANCELLED => ScheduleException::annuler(
|
||||
tenantId: $tenantId,
|
||||
slotId: $slotId,
|
||||
exceptionDate: $exceptionDate,
|
||||
reason: $command->reason,
|
||||
createdBy: $createdBy,
|
||||
now: $this->clock->now(),
|
||||
),
|
||||
ScheduleExceptionType::MODIFIED => ScheduleException::modifier(
|
||||
tenantId: $tenantId,
|
||||
slotId: $slotId,
|
||||
exceptionDate: $exceptionDate,
|
||||
newTimeSlot: $command->newStartTime !== null && $command->newEndTime !== null
|
||||
? new TimeSlot($command->newStartTime, $command->newEndTime)
|
||||
: null,
|
||||
newRoom: $command->newRoom,
|
||||
newTeacherId: $command->newTeacherId !== null
|
||||
? UserId::fromString($command->newTeacherId)
|
||||
: null,
|
||||
reason: $command->reason,
|
||||
createdBy: $createdBy,
|
||||
now: $this->clock->now(),
|
||||
),
|
||||
};
|
||||
|
||||
$this->exceptionRepository->save($exception);
|
||||
|
||||
return $exception;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Application\Command\TruncateSlotRecurrence;
|
||||
|
||||
final readonly class TruncateSlotRecurrenceCommand
|
||||
{
|
||||
public function __construct(
|
||||
public string $tenantId,
|
||||
public string $slotId,
|
||||
public string $fromDate,
|
||||
public string $updatedBy,
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Application\Command\TruncateSlotRecurrence;
|
||||
|
||||
use App\Scolarite\Domain\Exception\DateExceptionInvalideException;
|
||||
use App\Scolarite\Domain\Model\Schedule\ScheduleSlotId;
|
||||
use App\Scolarite\Domain\Repository\ScheduleSlotRepository;
|
||||
use App\Shared\Domain\Clock;
|
||||
use App\Shared\Domain\Tenant\TenantId;
|
||||
use DateTimeImmutable;
|
||||
|
||||
final readonly class TruncateSlotRecurrenceHandler
|
||||
{
|
||||
public function __construct(
|
||||
private ScheduleSlotRepository $slotRepository,
|
||||
private Clock $clock,
|
||||
) {
|
||||
}
|
||||
|
||||
public function __invoke(TruncateSlotRecurrenceCommand $command): void
|
||||
{
|
||||
$tenantId = TenantId::fromString($command->tenantId);
|
||||
$slotId = ScheduleSlotId::fromString($command->slotId);
|
||||
$slot = $this->slotRepository->get($slotId, $tenantId);
|
||||
$fromDate = new DateTimeImmutable($command->fromDate);
|
||||
|
||||
if (!$slot->isActiveOnDate($fromDate)) {
|
||||
throw DateExceptionInvalideException::pourSlotEtDate($slotId, $fromDate);
|
||||
}
|
||||
|
||||
$now = $this->clock->now();
|
||||
$dayBefore = $fromDate->modify('-1 day');
|
||||
$slot->terminerRecurrenceLe($dayBefore, $now);
|
||||
$this->slotRepository->save($slot);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Application\Command\UpdateRecurringSlot;
|
||||
|
||||
final readonly class UpdateRecurringSlotCommand
|
||||
{
|
||||
/**
|
||||
* @param string $scope 'this_occurrence' | 'all_future'
|
||||
*/
|
||||
public function __construct(
|
||||
public string $tenantId,
|
||||
public string $slotId,
|
||||
public string $occurrenceDate,
|
||||
public string $scope,
|
||||
public string $classId,
|
||||
public string $subjectId,
|
||||
public string $teacherId,
|
||||
public int $dayOfWeek,
|
||||
public string $startTime,
|
||||
public string $endTime,
|
||||
public ?string $room,
|
||||
public string $updatedBy,
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Application\Command\UpdateRecurringSlot;
|
||||
|
||||
use App\Administration\Domain\Model\SchoolClass\ClassId;
|
||||
use App\Administration\Domain\Model\Subject\SubjectId;
|
||||
use App\Administration\Domain\Model\User\UserId;
|
||||
use App\Scolarite\Domain\Exception\DateExceptionInvalideException;
|
||||
use App\Scolarite\Domain\Model\Schedule\DayOfWeek;
|
||||
use App\Scolarite\Domain\Model\Schedule\ScheduleException;
|
||||
use App\Scolarite\Domain\Model\Schedule\ScheduleSlot;
|
||||
use App\Scolarite\Domain\Model\Schedule\ScheduleSlotId;
|
||||
use App\Scolarite\Domain\Model\Schedule\TimeSlot;
|
||||
use App\Scolarite\Domain\Repository\ScheduleExceptionRepository;
|
||||
use App\Scolarite\Domain\Repository\ScheduleSlotRepository;
|
||||
use App\Shared\Domain\Clock;
|
||||
use App\Shared\Domain\Tenant\TenantId;
|
||||
use DateTimeImmutable;
|
||||
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
|
||||
|
||||
#[AsMessageHandler(bus: 'command.bus')]
|
||||
final readonly class UpdateRecurringSlotHandler
|
||||
{
|
||||
public function __construct(
|
||||
private ScheduleSlotRepository $slotRepository,
|
||||
private ScheduleExceptionRepository $exceptionRepository,
|
||||
private Clock $clock,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{exception: ?ScheduleException, newSlot: ?ScheduleSlot}
|
||||
*/
|
||||
public function __invoke(UpdateRecurringSlotCommand $command): array
|
||||
{
|
||||
$tenantId = TenantId::fromString($command->tenantId);
|
||||
$slotId = ScheduleSlotId::fromString($command->slotId);
|
||||
$slot = $this->slotRepository->get($slotId, $tenantId);
|
||||
$now = $this->clock->now();
|
||||
$occurrenceDate = new DateTimeImmutable($command->occurrenceDate);
|
||||
$newTimeSlot = new TimeSlot($command->startTime, $command->endTime);
|
||||
|
||||
if (!$slot->isActiveOnDate($occurrenceDate)) {
|
||||
throw DateExceptionInvalideException::pourSlotEtDate($slotId, $occurrenceDate);
|
||||
}
|
||||
|
||||
if ($command->scope === 'this_occurrence') {
|
||||
return $this->handleThisOccurrence(
|
||||
$tenantId,
|
||||
$slotId,
|
||||
$occurrenceDate,
|
||||
$newTimeSlot,
|
||||
$command->room,
|
||||
UserId::fromString($command->teacherId),
|
||||
UserId::fromString($command->updatedBy),
|
||||
$now,
|
||||
);
|
||||
}
|
||||
|
||||
// all_future: end current recurrence, create new slot
|
||||
return $this->handleAllFuture(
|
||||
$slot,
|
||||
$tenantId,
|
||||
$occurrenceDate,
|
||||
$command,
|
||||
$now,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{exception: ScheduleException, newSlot: null}
|
||||
*/
|
||||
private function handleThisOccurrence(
|
||||
TenantId $tenantId,
|
||||
ScheduleSlotId $slotId,
|
||||
DateTimeImmutable $occurrenceDate,
|
||||
TimeSlot $newTimeSlot,
|
||||
?string $newRoom,
|
||||
UserId $newTeacherId,
|
||||
UserId $createdBy,
|
||||
DateTimeImmutable $now,
|
||||
): array {
|
||||
$exception = ScheduleException::modifier(
|
||||
tenantId: $tenantId,
|
||||
slotId: $slotId,
|
||||
exceptionDate: $occurrenceDate,
|
||||
newTimeSlot: $newTimeSlot,
|
||||
newRoom: $newRoom,
|
||||
newTeacherId: $newTeacherId,
|
||||
reason: null,
|
||||
createdBy: $createdBy,
|
||||
now: $now,
|
||||
);
|
||||
|
||||
$this->exceptionRepository->save($exception);
|
||||
|
||||
return ['exception' => $exception, 'newSlot' => null];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{exception: null, newSlot: ScheduleSlot}
|
||||
*/
|
||||
private function handleAllFuture(
|
||||
ScheduleSlot $slot,
|
||||
TenantId $tenantId,
|
||||
DateTimeImmutable $occurrenceDate,
|
||||
UpdateRecurringSlotCommand $command,
|
||||
DateTimeImmutable $now,
|
||||
): array {
|
||||
// Save original end date before modifying
|
||||
$originalRecurrenceEnd = $slot->recurrenceEnd;
|
||||
|
||||
// End original slot one day before the change
|
||||
$dayBefore = $occurrenceDate->modify('-1 day');
|
||||
$slot->terminerRecurrenceLe($dayBefore, $now);
|
||||
$this->slotRepository->save($slot);
|
||||
|
||||
// Create new slot starting from the occurrence date
|
||||
$newSlot = ScheduleSlot::creer(
|
||||
tenantId: $tenantId,
|
||||
classId: ClassId::fromString($command->classId),
|
||||
subjectId: SubjectId::fromString($command->subjectId),
|
||||
teacherId: UserId::fromString($command->teacherId),
|
||||
dayOfWeek: DayOfWeek::from($command->dayOfWeek),
|
||||
timeSlot: new TimeSlot($command->startTime, $command->endTime),
|
||||
room: $command->room,
|
||||
isRecurring: true,
|
||||
now: $now,
|
||||
recurrenceStart: $occurrenceDate,
|
||||
recurrenceEnd: $originalRecurrenceEnd,
|
||||
);
|
||||
|
||||
$this->slotRepository->save($newSlot);
|
||||
|
||||
return ['exception' => null, 'newSlot' => $newSlot];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user