feat: Désignation de remplaçants temporaires avec corrections sécurité
Permet aux administrateurs de désigner un enseignant remplaçant pour un autre enseignant absent, sur des classes et matières précises, pour une période donnée. Le dashboard enseignant affiche les remplacements actifs avec les noms de classes/matières au lieu des identifiants bruts. Inclut les corrections de la code review : - Requête findActiveByTenant qui excluait les remplacements en cours mais incluait les futurs (manquait start_date <= :at) - Validation tenant et rôle enseignant dans le handler de désignation pour empêcher l'affectation cross-tenant ou de non-enseignants - Validation structurée du payload classes (Assert\Collection + UUID) pour éviter les erreurs serveur sur payloads malformés - API replaced-classes enrichie avec les noms classe/matière
This commit is contained in:
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Application\Query\GetActiveReplacements;
|
||||
|
||||
use App\Scolarite\Domain\Repository\TeacherReplacementRepository;
|
||||
use App\Shared\Domain\Clock;
|
||||
use App\Shared\Domain\Tenant\TenantId;
|
||||
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
|
||||
|
||||
#[AsMessageHandler(bus: 'query.bus')]
|
||||
final readonly class GetActiveReplacementsHandler
|
||||
{
|
||||
public function __construct(
|
||||
private TeacherReplacementRepository $replacementRepository,
|
||||
private Clock $clock,
|
||||
) {
|
||||
}
|
||||
|
||||
/** @return array<ReplacementDto> */
|
||||
public function __invoke(GetActiveReplacementsQuery $query): array
|
||||
{
|
||||
$replacements = $this->replacementRepository->findActiveByTenant(
|
||||
TenantId::fromString($query->tenantId),
|
||||
$this->clock->now(),
|
||||
);
|
||||
|
||||
return array_map(
|
||||
static fn ($r) => ReplacementDto::fromDomain($r),
|
||||
$replacements,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Application\Query\GetActiveReplacements;
|
||||
|
||||
final readonly class GetActiveReplacementsQuery
|
||||
{
|
||||
public function __construct(
|
||||
public string $tenantId,
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Application\Query\GetActiveReplacements;
|
||||
|
||||
use App\Scolarite\Domain\Model\TeacherReplacement\TeacherReplacement;
|
||||
use DateTimeImmutable;
|
||||
|
||||
final readonly class ReplacementDto
|
||||
{
|
||||
/** @param array<array{classId: string, subjectId: string}> $classes */
|
||||
public function __construct(
|
||||
public string $id,
|
||||
public string $replacedTeacherId,
|
||||
public string $replacementTeacherId,
|
||||
public DateTimeImmutable $startDate,
|
||||
public DateTimeImmutable $endDate,
|
||||
public string $status,
|
||||
public array $classes,
|
||||
public ?string $reason,
|
||||
) {
|
||||
}
|
||||
|
||||
public static function fromDomain(TeacherReplacement $replacement): self
|
||||
{
|
||||
return new self(
|
||||
id: (string) $replacement->id,
|
||||
replacedTeacherId: (string) $replacement->replacedTeacherId,
|
||||
replacementTeacherId: (string) $replacement->replacementTeacherId,
|
||||
startDate: $replacement->startDate,
|
||||
endDate: $replacement->endDate,
|
||||
status: $replacement->status->value,
|
||||
classes: array_map(
|
||||
static fn ($pair) => [
|
||||
'classId' => (string) $pair->classId,
|
||||
'subjectId' => (string) $pair->subjectId,
|
||||
],
|
||||
$replacement->classes,
|
||||
),
|
||||
reason: $replacement->reason,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Application\Query\GetReplacedClassesForTeacher;
|
||||
|
||||
use App\Administration\Domain\Model\User\UserId;
|
||||
use App\Administration\Domain\Repository\ClassRepository;
|
||||
use App\Administration\Domain\Repository\SubjectRepository;
|
||||
use App\Scolarite\Domain\Repository\TeacherReplacementRepository;
|
||||
use App\Shared\Domain\Clock;
|
||||
use App\Shared\Domain\Tenant\TenantId;
|
||||
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
|
||||
|
||||
#[AsMessageHandler(bus: 'query.bus')]
|
||||
final readonly class GetReplacedClassesForTeacherHandler
|
||||
{
|
||||
public function __construct(
|
||||
private TeacherReplacementRepository $replacementRepository,
|
||||
private ClassRepository $classRepository,
|
||||
private SubjectRepository $subjectRepository,
|
||||
private Clock $clock,
|
||||
) {
|
||||
}
|
||||
|
||||
/** @return array<ReplacedClassDto> */
|
||||
public function __invoke(GetReplacedClassesForTeacherQuery $query): array
|
||||
{
|
||||
$tenantId = TenantId::fromString($query->tenantId);
|
||||
|
||||
$replacements = $this->replacementRepository->findActiveByReplacementTeacher(
|
||||
UserId::fromString($query->replacementTeacherId),
|
||||
$tenantId,
|
||||
$this->clock->now(),
|
||||
);
|
||||
|
||||
if ($replacements === []) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$classNames = [];
|
||||
foreach ($this->classRepository->findAllActiveByTenant($tenantId) as $class) {
|
||||
$classNames[(string) $class->id] = (string) $class->name;
|
||||
}
|
||||
|
||||
$subjectNames = [];
|
||||
foreach ($this->subjectRepository->findAllActiveByTenant($tenantId) as $subject) {
|
||||
$subjectNames[(string) $subject->id] = (string) $subject->name;
|
||||
}
|
||||
|
||||
$result = [];
|
||||
|
||||
foreach ($replacements as $replacement) {
|
||||
foreach ($replacement->classes as $pair) {
|
||||
$classId = (string) $pair->classId;
|
||||
$subjectId = (string) $pair->subjectId;
|
||||
|
||||
$result[] = new ReplacedClassDto(
|
||||
replacementId: (string) $replacement->id,
|
||||
replacedTeacherId: (string) $replacement->replacedTeacherId,
|
||||
classId: $classId,
|
||||
subjectId: $subjectId,
|
||||
className: $classNames[$classId] ?? $classId,
|
||||
subjectName: $subjectNames[$subjectId] ?? $subjectId,
|
||||
startDate: $replacement->startDate,
|
||||
endDate: $replacement->endDate,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Application\Query\GetReplacedClassesForTeacher;
|
||||
|
||||
final readonly class GetReplacedClassesForTeacherQuery
|
||||
{
|
||||
public function __construct(
|
||||
public string $replacementTeacherId,
|
||||
public string $tenantId,
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Application\Query\GetReplacedClassesForTeacher;
|
||||
|
||||
use DateTimeImmutable;
|
||||
|
||||
final readonly class ReplacedClassDto
|
||||
{
|
||||
public function __construct(
|
||||
public string $replacementId,
|
||||
public string $replacedTeacherId,
|
||||
public string $classId,
|
||||
public string $subjectId,
|
||||
public string $className,
|
||||
public string $subjectName,
|
||||
public DateTimeImmutable $startDate,
|
||||
public DateTimeImmutable $endDate,
|
||||
) {
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user