L'admin pouvait attribuer une couleur à chaque matière, mais cette couleur n'était utilisée que dans la vue admin de l'emploi du temps. Les APIs élève et parent ne renvoyaient pas cette information, ce qui donnait un affichage générique (gris/bleu) pour tous les créneaux. L'API renvoie désormais subjectColor dans chaque créneau, et les vues jour/semaine/widget/détails affichent la bordure colorée correspondante. Le marqueur "Prochain cours" conserve sa priorité visuelle via une surcharge CSS variable.
293 lines
11 KiB
PHP
293 lines
11 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Tests\Unit\Scolarite\Application\Query\GetChildrenSchedule;
|
|
|
|
use App\Administration\Domain\Model\SchoolCalendar\SchoolCalendar;
|
|
use App\Administration\Domain\Model\SchoolClass\AcademicYearId;
|
|
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\Port\CurrentCalendarProvider;
|
|
use App\Scolarite\Application\Port\ParentChildrenReader;
|
|
use App\Scolarite\Application\Port\ScheduleDisplayReader;
|
|
use App\Scolarite\Application\Port\StudentClassReader;
|
|
use App\Scolarite\Application\Query\GetChildrenSchedule\ChildScheduleDto;
|
|
use App\Scolarite\Application\Query\GetChildrenSchedule\GetChildrenScheduleHandler;
|
|
use App\Scolarite\Application\Query\GetChildrenSchedule\GetChildrenScheduleQuery;
|
|
use App\Scolarite\Application\Service\ScheduleResolver;
|
|
use App\Scolarite\Domain\Model\Schedule\DayOfWeek;
|
|
use App\Scolarite\Domain\Model\Schedule\ScheduleSlot;
|
|
use App\Scolarite\Domain\Model\Schedule\TimeSlot;
|
|
use App\Scolarite\Infrastructure\Persistence\InMemory\InMemoryScheduleExceptionRepository;
|
|
use App\Scolarite\Infrastructure\Persistence\InMemory\InMemoryScheduleSlotRepository;
|
|
use App\Shared\Domain\Tenant\TenantId;
|
|
use DateTimeImmutable;
|
|
use PHPUnit\Framework\Attributes\Test;
|
|
use PHPUnit\Framework\TestCase;
|
|
|
|
final class GetChildrenScheduleHandlerTest extends TestCase
|
|
{
|
|
private const string TENANT_ID = '550e8400-e29b-41d4-a716-446655440001';
|
|
private const string PARENT_ID = '550e8400-e29b-41d4-a716-446655440099';
|
|
private const string CHILD1_ID = '550e8400-e29b-41d4-a716-446655440050';
|
|
private const string CHILD2_ID = '550e8400-e29b-41d4-a716-446655440051';
|
|
private const string CLASS1_ID = '550e8400-e29b-41d4-a716-446655440020';
|
|
private const string CLASS2_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 $slotRepository;
|
|
private InMemoryScheduleExceptionRepository $exceptionRepository;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
$this->slotRepository = new InMemoryScheduleSlotRepository();
|
|
$this->exceptionRepository = new InMemoryScheduleExceptionRepository();
|
|
}
|
|
|
|
#[Test]
|
|
public function returnsEmptyWhenParentHasNoChildren(): void
|
|
{
|
|
$handler = $this->createHandler(children: []);
|
|
|
|
$result = $handler(new GetChildrenScheduleQuery(
|
|
parentId: self::PARENT_ID,
|
|
tenantId: self::TENANT_ID,
|
|
date: '2026-03-02',
|
|
));
|
|
|
|
self::assertSame([], $result);
|
|
}
|
|
|
|
#[Test]
|
|
public function returnsScheduleForSingleChild(): void
|
|
{
|
|
$this->saveRecurringSlot(self::CLASS1_ID, DayOfWeek::MONDAY, '08:00', '09:00');
|
|
|
|
$handler = $this->createHandler(
|
|
children: [
|
|
['studentId' => self::CHILD1_ID, 'firstName' => 'Alice', 'lastName' => 'Dupont'],
|
|
],
|
|
classMapping: [self::CHILD1_ID => self::CLASS1_ID],
|
|
);
|
|
|
|
$result = $handler(new GetChildrenScheduleQuery(
|
|
parentId: self::PARENT_ID,
|
|
tenantId: self::TENANT_ID,
|
|
date: '2026-03-02', // Monday
|
|
));
|
|
|
|
self::assertCount(1, $result);
|
|
self::assertInstanceOf(ChildScheduleDto::class, $result[0]);
|
|
self::assertSame(self::CHILD1_ID, $result[0]->childId);
|
|
self::assertSame('Alice', $result[0]->firstName);
|
|
self::assertSame('Dupont', $result[0]->lastName);
|
|
self::assertCount(1, $result[0]->slots);
|
|
}
|
|
|
|
#[Test]
|
|
public function returnsScheduleForMultipleChildren(): void
|
|
{
|
|
$this->saveRecurringSlot(self::CLASS1_ID, DayOfWeek::MONDAY, '08:00', '09:00');
|
|
$this->saveRecurringSlot(self::CLASS2_ID, DayOfWeek::MONDAY, '10:00', '11:00');
|
|
$this->saveRecurringSlot(self::CLASS2_ID, DayOfWeek::TUESDAY, '14:00', '15:00');
|
|
|
|
$handler = $this->createHandler(
|
|
children: [
|
|
['studentId' => self::CHILD1_ID, 'firstName' => 'Alice', 'lastName' => 'Dupont'],
|
|
['studentId' => self::CHILD2_ID, 'firstName' => 'Bob', 'lastName' => 'Dupont'],
|
|
],
|
|
classMapping: [
|
|
self::CHILD1_ID => self::CLASS1_ID,
|
|
self::CHILD2_ID => self::CLASS2_ID,
|
|
],
|
|
);
|
|
|
|
$result = $handler(new GetChildrenScheduleQuery(
|
|
parentId: self::PARENT_ID,
|
|
tenantId: self::TENANT_ID,
|
|
date: '2026-03-02',
|
|
));
|
|
|
|
self::assertCount(2, $result);
|
|
self::assertSame(self::CHILD1_ID, $result[0]->childId);
|
|
self::assertCount(1, $result[0]->slots);
|
|
self::assertSame(self::CHILD2_ID, $result[1]->childId);
|
|
self::assertCount(2, $result[1]->slots);
|
|
}
|
|
|
|
#[Test]
|
|
public function returnsEmptySlotsWhenChildHasNoClass(): void
|
|
{
|
|
$handler = $this->createHandler(
|
|
children: [
|
|
['studentId' => self::CHILD1_ID, 'firstName' => 'Alice', 'lastName' => 'Dupont'],
|
|
],
|
|
classMapping: [],
|
|
);
|
|
|
|
$result = $handler(new GetChildrenScheduleQuery(
|
|
parentId: self::PARENT_ID,
|
|
tenantId: self::TENANT_ID,
|
|
date: '2026-03-02',
|
|
));
|
|
|
|
self::assertCount(1, $result);
|
|
self::assertSame(self::CHILD1_ID, $result[0]->childId);
|
|
self::assertSame([], $result[0]->slots);
|
|
}
|
|
|
|
#[Test]
|
|
public function enrichesSlotsWithSubjectAndTeacherNames(): void
|
|
{
|
|
$this->saveRecurringSlot(self::CLASS1_ID, DayOfWeek::MONDAY, '08:00', '09:00');
|
|
|
|
$handler = $this->createHandler(
|
|
children: [
|
|
['studentId' => self::CHILD1_ID, 'firstName' => 'Alice', 'lastName' => 'Dupont'],
|
|
],
|
|
classMapping: [self::CHILD1_ID => self::CLASS1_ID],
|
|
subjectNames: [self::SUBJECT_ID => 'Mathématiques'],
|
|
teacherNames: [self::TEACHER_ID => 'Jean Martin'],
|
|
);
|
|
|
|
$result = $handler(new GetChildrenScheduleQuery(
|
|
parentId: self::PARENT_ID,
|
|
tenantId: self::TENANT_ID,
|
|
date: '2026-03-02',
|
|
));
|
|
|
|
self::assertSame('Mathématiques', $result[0]->slots[0]->subjectName);
|
|
self::assertSame('Jean Martin', $result[0]->slots[0]->teacherName);
|
|
}
|
|
|
|
#[Test]
|
|
public function computesMondayFromAnyDayOfWeek(): void
|
|
{
|
|
$this->saveRecurringSlot(self::CLASS1_ID, DayOfWeek::MONDAY, '08:00', '09:00');
|
|
|
|
$handler = $this->createHandler(
|
|
children: [
|
|
['studentId' => self::CHILD1_ID, 'firstName' => 'Alice', 'lastName' => 'Dupont'],
|
|
],
|
|
classMapping: [self::CHILD1_ID => self::CLASS1_ID],
|
|
);
|
|
|
|
$result = $handler(new GetChildrenScheduleQuery(
|
|
parentId: self::PARENT_ID,
|
|
tenantId: self::TENANT_ID,
|
|
date: '2026-03-04', // Wednesday
|
|
));
|
|
|
|
self::assertCount(1, $result);
|
|
self::assertSame('2026-03-02', $result[0]->slots[0]->date);
|
|
}
|
|
|
|
private function saveRecurringSlot(
|
|
string $classId,
|
|
DayOfWeek $day,
|
|
string $start,
|
|
string $end,
|
|
?string $room = null,
|
|
): void {
|
|
$slot = ScheduleSlot::creer(
|
|
tenantId: TenantId::fromString(self::TENANT_ID),
|
|
classId: ClassId::fromString($classId),
|
|
subjectId: SubjectId::fromString(self::SUBJECT_ID),
|
|
teacherId: UserId::fromString(self::TEACHER_ID),
|
|
dayOfWeek: $day,
|
|
timeSlot: new TimeSlot($start, $end),
|
|
room: $room,
|
|
isRecurring: true,
|
|
now: new DateTimeImmutable('2026-01-01'),
|
|
recurrenceStart: new DateTimeImmutable('2026-01-01'),
|
|
);
|
|
|
|
$this->slotRepository->save($slot);
|
|
}
|
|
|
|
/**
|
|
* @param array<array{studentId: string, firstName: string, lastName: string}> $children
|
|
* @param array<string, string> $classMapping studentId => classId
|
|
* @param array<string, string> $subjectNames
|
|
* @param array<string, string> $teacherNames
|
|
*/
|
|
private function createHandler(
|
|
array $children = [],
|
|
array $classMapping = [],
|
|
array $subjectNames = [],
|
|
array $teacherNames = [],
|
|
): GetChildrenScheduleHandler {
|
|
$tenantId = TenantId::fromString(self::TENANT_ID);
|
|
|
|
$parentChildrenReader = new class($children) implements ParentChildrenReader {
|
|
/** @param array<array{studentId: string, firstName: string, lastName: string}> $children */
|
|
public function __construct(private array $children)
|
|
{
|
|
}
|
|
|
|
public function childrenOf(string $guardianId, TenantId $tenantId): array
|
|
{
|
|
return $this->children;
|
|
}
|
|
};
|
|
|
|
$studentClassReader = new class($classMapping) implements StudentClassReader {
|
|
/** @param array<string, string> $mapping */
|
|
public function __construct(private array $mapping)
|
|
{
|
|
}
|
|
|
|
public function currentClassId(string $studentId, TenantId $tenantId): ?string
|
|
{
|
|
return $this->mapping[$studentId] ?? null;
|
|
}
|
|
};
|
|
|
|
$calendarProvider = new class($tenantId) implements CurrentCalendarProvider {
|
|
public function __construct(private TenantId $tenantId)
|
|
{
|
|
}
|
|
|
|
public function forCurrentYear(TenantId $tenantId): SchoolCalendar
|
|
{
|
|
return SchoolCalendar::initialiser($this->tenantId, AcademicYearId::generate());
|
|
}
|
|
};
|
|
|
|
$displayReader = new class($subjectNames, $teacherNames) implements ScheduleDisplayReader {
|
|
/** @param array<string, string> $subjects @param array<string, string> $teachers */
|
|
public function __construct(
|
|
private array $subjects,
|
|
private array $teachers,
|
|
) {
|
|
}
|
|
|
|
public function subjectDisplay(string $tenantId, string ...$subjectIds): array
|
|
{
|
|
$display = [];
|
|
foreach ($this->subjects as $id => $name) {
|
|
$display[$id] = ['name' => $name, 'color' => null];
|
|
}
|
|
|
|
return $display;
|
|
}
|
|
|
|
public function teacherNames(string $tenantId, string ...$teacherIds): array
|
|
{
|
|
return $this->teachers;
|
|
}
|
|
};
|
|
|
|
return new GetChildrenScheduleHandler(
|
|
$parentChildrenReader,
|
|
$studentClassReader,
|
|
new ScheduleResolver($this->slotRepository, $this->exceptionRepository),
|
|
$calendarProvider,
|
|
$displayReader,
|
|
);
|
|
}
|
|
}
|