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,221 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Unit\Scolarite\Application\Command\UpdateScheduleSlot;
|
||||
|
||||
use App\Administration\Domain\Model\SchoolClass\ClassId;
|
||||
use App\Administration\Domain\Model\Subject\SubjectId;
|
||||
use App\Administration\Domain\Model\User\UserId;
|
||||
use App\Scolarite\Application\Command\UpdateScheduleSlot\UpdateScheduleSlotCommand;
|
||||
use App\Scolarite\Application\Command\UpdateScheduleSlot\UpdateScheduleSlotHandler;
|
||||
use App\Scolarite\Application\Port\EnseignantAffectationChecker;
|
||||
use App\Scolarite\Domain\Exception\EnseignantNonAffecteException;
|
||||
use App\Scolarite\Domain\Model\Schedule\DayOfWeek;
|
||||
use App\Scolarite\Domain\Model\Schedule\ScheduleSlot;
|
||||
use App\Scolarite\Domain\Model\Schedule\TimeSlot;
|
||||
use App\Scolarite\Domain\Service\ScheduleConflictDetector;
|
||||
use App\Scolarite\Infrastructure\Persistence\InMemory\InMemoryScheduleSlotRepository;
|
||||
use App\Shared\Domain\Clock;
|
||||
use App\Shared\Domain\Tenant\TenantId;
|
||||
use DateTimeImmutable;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
final class UpdateScheduleSlotHandlerTest extends TestCase
|
||||
{
|
||||
private const string TENANT_ID = '550e8400-e29b-41d4-a716-446655440001';
|
||||
private const string CLASS_ID = '550e8400-e29b-41d4-a716-446655440020';
|
||||
private const string OTHER_CLASS_ID = '550e8400-e29b-41d4-a716-446655440021';
|
||||
private const string SUBJECT_ID = '550e8400-e29b-41d4-a716-446655440030';
|
||||
private const string TEACHER_ID = '550e8400-e29b-41d4-a716-446655440010';
|
||||
|
||||
private InMemoryScheduleSlotRepository $repository;
|
||||
private UpdateScheduleSlotHandler $handler;
|
||||
private Clock $clock;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->repository = new InMemoryScheduleSlotRepository();
|
||||
$this->clock = new class implements Clock {
|
||||
public function now(): DateTimeImmutable
|
||||
{
|
||||
return new DateTimeImmutable('2026-03-02 15:00:00');
|
||||
}
|
||||
};
|
||||
|
||||
$this->handler = new UpdateScheduleSlotHandler(
|
||||
$this->repository,
|
||||
new ScheduleConflictDetector($this->repository),
|
||||
$this->createAlwaysAssignedChecker(),
|
||||
$this->clock,
|
||||
);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function updatesSlotProperties(): void
|
||||
{
|
||||
$slot = $this->createAndSaveSlot();
|
||||
$newSubjectId = '550e8400-e29b-41d4-a716-446655440031';
|
||||
|
||||
$result = ($this->handler)(new UpdateScheduleSlotCommand(
|
||||
tenantId: self::TENANT_ID,
|
||||
slotId: (string) $slot->id,
|
||||
classId: self::CLASS_ID,
|
||||
subjectId: $newSubjectId,
|
||||
teacherId: self::TEACHER_ID,
|
||||
dayOfWeek: DayOfWeek::WEDNESDAY->value,
|
||||
startTime: '10:00',
|
||||
endTime: '11:00',
|
||||
room: 'Salle 301',
|
||||
));
|
||||
|
||||
/** @var ScheduleSlot $updated */
|
||||
$updated = $result['slot'];
|
||||
|
||||
self::assertTrue($updated->subjectId->equals(SubjectId::fromString($newSubjectId)));
|
||||
self::assertSame(DayOfWeek::WEDNESDAY, $updated->dayOfWeek);
|
||||
self::assertSame('10:00', $updated->timeSlot->startTime);
|
||||
self::assertSame('Salle 301', $updated->room);
|
||||
self::assertEmpty($result['conflicts']);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function updatesClassId(): void
|
||||
{
|
||||
$slot = $this->createAndSaveSlot();
|
||||
|
||||
$result = ($this->handler)(new UpdateScheduleSlotCommand(
|
||||
tenantId: self::TENANT_ID,
|
||||
slotId: (string) $slot->id,
|
||||
classId: self::OTHER_CLASS_ID,
|
||||
subjectId: self::SUBJECT_ID,
|
||||
teacherId: self::TEACHER_ID,
|
||||
dayOfWeek: DayOfWeek::MONDAY->value,
|
||||
startTime: '08:00',
|
||||
endTime: '09:00',
|
||||
room: null,
|
||||
));
|
||||
|
||||
/** @var ScheduleSlot $updated */
|
||||
$updated = $result['slot'];
|
||||
|
||||
self::assertTrue($updated->classId->equals(ClassId::fromString(self::OTHER_CLASS_ID)));
|
||||
self::assertFalse($updated->classId->equals(ClassId::fromString(self::CLASS_ID)));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function persistsClassIdChange(): void
|
||||
{
|
||||
$slot = $this->createAndSaveSlot();
|
||||
|
||||
($this->handler)(new UpdateScheduleSlotCommand(
|
||||
tenantId: self::TENANT_ID,
|
||||
slotId: (string) $slot->id,
|
||||
classId: self::OTHER_CLASS_ID,
|
||||
subjectId: self::SUBJECT_ID,
|
||||
teacherId: self::TEACHER_ID,
|
||||
dayOfWeek: DayOfWeek::MONDAY->value,
|
||||
startTime: '08:00',
|
||||
endTime: '09:00',
|
||||
room: null,
|
||||
));
|
||||
|
||||
$persisted = $this->repository->get($slot->id, TenantId::fromString(self::TENANT_ID));
|
||||
|
||||
self::assertTrue($persisted->classId->equals(ClassId::fromString(self::OTHER_CLASS_ID)));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function excludesSelfFromConflictDetection(): void
|
||||
{
|
||||
$slot = $this->createAndSaveSlot();
|
||||
|
||||
// Modifier le slot sans changer le créneau → pas de conflit avec lui-même
|
||||
$result = ($this->handler)(new UpdateScheduleSlotCommand(
|
||||
tenantId: self::TENANT_ID,
|
||||
slotId: (string) $slot->id,
|
||||
classId: self::CLASS_ID,
|
||||
subjectId: self::SUBJECT_ID,
|
||||
teacherId: self::TEACHER_ID,
|
||||
dayOfWeek: DayOfWeek::MONDAY->value,
|
||||
startTime: '08:00',
|
||||
endTime: '09:00',
|
||||
room: 'Salle 101',
|
||||
));
|
||||
|
||||
self::assertEmpty($result['conflicts']);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function rejectsUnassignedTeacher(): void
|
||||
{
|
||||
$slot = $this->createAndSaveSlot();
|
||||
|
||||
$handler = new UpdateScheduleSlotHandler(
|
||||
$this->repository,
|
||||
new ScheduleConflictDetector($this->repository),
|
||||
$this->createNeverAssignedChecker(),
|
||||
$this->clock,
|
||||
);
|
||||
|
||||
$this->expectException(EnseignantNonAffecteException::class);
|
||||
($handler)(new UpdateScheduleSlotCommand(
|
||||
tenantId: self::TENANT_ID,
|
||||
slotId: (string) $slot->id,
|
||||
classId: self::CLASS_ID,
|
||||
subjectId: self::SUBJECT_ID,
|
||||
teacherId: self::TEACHER_ID,
|
||||
dayOfWeek: DayOfWeek::MONDAY->value,
|
||||
startTime: '08:00',
|
||||
endTime: '09:00',
|
||||
room: null,
|
||||
));
|
||||
}
|
||||
|
||||
private function createAndSaveSlot(): ScheduleSlot
|
||||
{
|
||||
$slot = ScheduleSlot::creer(
|
||||
tenantId: TenantId::fromString(self::TENANT_ID),
|
||||
classId: ClassId::fromString(self::CLASS_ID),
|
||||
subjectId: SubjectId::fromString(self::SUBJECT_ID),
|
||||
teacherId: UserId::fromString(self::TEACHER_ID),
|
||||
dayOfWeek: DayOfWeek::MONDAY,
|
||||
timeSlot: new TimeSlot('08:00', '09:00'),
|
||||
room: null,
|
||||
isRecurring: true,
|
||||
now: new DateTimeImmutable('2026-03-01 10:00:00'),
|
||||
);
|
||||
$this->repository->save($slot);
|
||||
|
||||
return $slot;
|
||||
}
|
||||
|
||||
private function createAlwaysAssignedChecker(): EnseignantAffectationChecker
|
||||
{
|
||||
return new class implements EnseignantAffectationChecker {
|
||||
public function estAffecte(
|
||||
UserId $teacherId,
|
||||
ClassId $classId,
|
||||
SubjectId $subjectId,
|
||||
TenantId $tenantId,
|
||||
): bool {
|
||||
return true;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private function createNeverAssignedChecker(): EnseignantAffectationChecker
|
||||
{
|
||||
return new class implements EnseignantAffectationChecker {
|
||||
public function estAffecte(
|
||||
UserId $teacherId,
|
||||
ClassId $classId,
|
||||
SubjectId $subjectId,
|
||||
TenantId $tenantId,
|
||||
): bool {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user