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:
@@ -0,0 +1,18 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Application\Port;
|
||||
|
||||
use App\Administration\Domain\Model\SchoolCalendar\SchoolCalendar;
|
||||
use App\Shared\Domain\Tenant\TenantId;
|
||||
|
||||
/**
|
||||
* Port pour obtenir le calendrier scolaire de l'année courante.
|
||||
*
|
||||
* L'implémentation résout l'année académique courante de façon transparente.
|
||||
*/
|
||||
interface CurrentCalendarProvider
|
||||
{
|
||||
public function forCurrentYear(TenantId $tenantId): SchoolCalendar;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Application\Port;
|
||||
|
||||
/**
|
||||
* Port pour lire les noms d'affichage des matières et enseignants.
|
||||
*
|
||||
* Permet à la couche Application de résoudre les noms sans dépendre
|
||||
* directement des repositories du bounded context Administration.
|
||||
*/
|
||||
interface ScheduleDisplayReader
|
||||
{
|
||||
/**
|
||||
* @param string ...$subjectIds Identifiants des matières
|
||||
*
|
||||
* @return array<string, string> Map subjectId => nom de la matière
|
||||
*/
|
||||
public function subjectNames(string $tenantId, string ...$subjectIds): array;
|
||||
|
||||
/**
|
||||
* @param string ...$teacherIds Identifiants des enseignants
|
||||
*
|
||||
* @return array<string, string> Map teacherId => "Prénom Nom"
|
||||
*/
|
||||
public function teacherNames(string $tenantId, string ...$teacherIds): array;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Application\Port;
|
||||
|
||||
use App\Shared\Domain\Tenant\TenantId;
|
||||
|
||||
/**
|
||||
* Port pour résoudre la classe d'un élève pour l'année scolaire courante.
|
||||
*
|
||||
* L'implémentation résout l'année académique courante de façon transparente.
|
||||
*/
|
||||
interface StudentClassReader
|
||||
{
|
||||
/**
|
||||
* @return string|null L'identifiant de la classe, ou null si l'élève n'est affecté à aucune classe
|
||||
*/
|
||||
public function currentClassId(string $studentId, TenantId $tenantId): ?string;
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user