feat: Permettre aux élèves de consulter leur emploi du temps

Les élèves n'avaient aucun moyen de voir leur emploi du temps
depuis l'application. Cette fonctionnalité ajoute une page dédiée
avec deux modes de visualisation (jour et semaine), la navigation
temporelle, et le détail des cours au tap.

Le backend résout l'EDT de l'élève en chaînant : affectation classe →
créneaux récurrents + exceptions + calendrier scolaire → enrichissement
des noms (matières/enseignants). Le frontend utilise un cache offline
(Workbox NetworkFirst) pour rester consultable hors connexion.
This commit is contained in:
2026-03-05 16:21:37 +01:00
parent ae640e91ac
commit 36ceefb625
30 changed files with 3526 additions and 30 deletions

View File

@@ -0,0 +1,100 @@
<?php
declare(strict_types=1);
namespace App\Scolarite\Application\Query\GetStudentSchedule;
use App\Administration\Domain\Model\SchoolClass\ClassId;
use App\Scolarite\Application\Port\CurrentCalendarProvider;
use App\Scolarite\Application\Port\ScheduleDisplayReader;
use App\Scolarite\Application\Port\StudentClassReader;
use App\Scolarite\Application\Service\ScheduleResolver;
use App\Scolarite\Domain\Model\Schedule\ResolvedScheduleSlot;
use App\Shared\Domain\Tenant\TenantId;
use function array_map;
use function array_unique;
use function array_values;
use DateTimeImmutable;
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
#[AsMessageHandler(bus: 'query.bus')]
final readonly class GetStudentScheduleHandler
{
public function __construct(
private StudentClassReader $studentClassReader,
private ScheduleResolver $scheduleResolver,
private CurrentCalendarProvider $calendarProvider,
private ScheduleDisplayReader $displayReader,
) {
}
/** @return array<StudentScheduleSlotDto> */
public function __invoke(GetStudentScheduleQuery $query): array
{
$tenantId = TenantId::fromString($query->tenantId);
$classId = $this->studentClassReader->currentClassId($query->studentId, $tenantId);
if ($classId === null) {
return [];
}
$date = new DateTimeImmutable($query->date);
$weekStart = $this->mondayOfWeek($date);
$calendar = $this->calendarProvider->forCurrentYear($tenantId);
$resolved = $this->scheduleResolver->resolveForWeek(
ClassId::fromString($classId),
$weekStart,
$tenantId,
$calendar,
);
return $this->enrichSlots($resolved, $query->tenantId);
}
/**
* @param array<ResolvedScheduleSlot> $slots
*
* @return array<StudentScheduleSlotDto>
*/
private function enrichSlots(array $slots, string $tenantId): array
{
if ($slots === []) {
return [];
}
$subjectIds = array_values(array_unique(
array_map(static fn (ResolvedScheduleSlot $s): string => (string) $s->subjectId, $slots),
));
$teacherIds = array_values(array_unique(
array_map(static fn (ResolvedScheduleSlot $s): string => (string) $s->teacherId, $slots),
));
$subjectNames = $this->displayReader->subjectNames($tenantId, ...$subjectIds);
$teacherNames = $this->displayReader->teacherNames($tenantId, ...$teacherIds);
return array_map(
static fn (ResolvedScheduleSlot $s): StudentScheduleSlotDto => StudentScheduleSlotDto::fromResolved(
$s,
$subjectNames[(string) $s->subjectId] ?? '',
$teacherNames[(string) $s->teacherId] ?? '',
),
$slots,
);
}
private function mondayOfWeek(DateTimeImmutable $date): DateTimeImmutable
{
$dayOfWeek = (int) $date->format('N');
if ($dayOfWeek === 1) {
return $date;
}
return $date->modify('-' . ($dayOfWeek - 1) . ' days');
}
}

View File

@@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
namespace App\Scolarite\Application\Query\GetStudentSchedule;
use DateTimeImmutable;
use InvalidArgumentException;
use function sprintf;
final readonly class GetStudentScheduleQuery
{
public function __construct(
public string $studentId,
public string $tenantId,
public string $date,
) {
if (DateTimeImmutable::createFromFormat('Y-m-d', $date) === false) {
throw new InvalidArgumentException(sprintf('Date invalide : "%s". Format attendu : YYYY-MM-DD.', $date));
}
}
}

View File

@@ -0,0 +1,47 @@
<?php
declare(strict_types=1);
namespace App\Scolarite\Application\Query\GetStudentSchedule;
use App\Scolarite\Domain\Model\Schedule\ResolvedScheduleSlot;
final readonly class StudentScheduleSlotDto
{
public function __construct(
public string $slotId,
public string $date,
public int $dayOfWeek,
public string $startTime,
public string $endTime,
public string $subjectId,
public string $subjectName,
public string $teacherId,
public string $teacherName,
public ?string $room,
public bool $isModified,
public ?string $exceptionId,
) {
}
public static function fromResolved(
ResolvedScheduleSlot $slot,
string $subjectName,
string $teacherName,
): self {
return new self(
slotId: (string) $slot->slotId,
date: $slot->date->format('Y-m-d'),
dayOfWeek: $slot->dayOfWeek->value,
startTime: $slot->timeSlot->startTime,
endTime: $slot->timeSlot->endTime,
subjectId: (string) $slot->subjectId,
subjectName: $subjectName,
teacherId: (string) $slot->teacherId,
teacherName: $teacherName,
room: $slot->room,
isModified: $slot->isModified,
exceptionId: $slot->exceptionId !== null ? (string) $slot->exceptionId : null,
);
}
}