feat: Affectation des enseignants aux classes et matières
Permet aux administrateurs d'associer un enseignant à une classe pour une matière donnée au sein d'une année scolaire. Cette brique est nécessaire pour construire les emplois du temps et les carnets de notes par la suite. Le modèle impose l'unicité du triplet enseignant × classe × matière par année scolaire, avec réactivation automatique d'une affectation retirée plutôt que duplication. L'isolation multi-tenant est garantie au niveau du repository (findById/get filtrent par tenant_id).
This commit is contained in:
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Administration\Application\Command\AssignTeacher;
|
||||
|
||||
final readonly class AssignTeacherCommand
|
||||
{
|
||||
public function __construct(
|
||||
public string $tenantId,
|
||||
public string $teacherId,
|
||||
public string $classId,
|
||||
public string $subjectId,
|
||||
public string $academicYearId,
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Administration\Application\Command\AssignTeacher;
|
||||
|
||||
use App\Administration\Domain\Exception\AffectationDejaExistanteException;
|
||||
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\TeacherAssignment\TeacherAssignment;
|
||||
use App\Administration\Domain\Model\User\UserId;
|
||||
use App\Administration\Domain\Repository\ClassRepository;
|
||||
use App\Administration\Domain\Repository\SubjectRepository;
|
||||
use App\Administration\Domain\Repository\TeacherAssignmentRepository;
|
||||
use App\Administration\Domain\Repository\UserRepository;
|
||||
use App\Shared\Domain\Clock;
|
||||
use App\Shared\Domain\Tenant\TenantId;
|
||||
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
|
||||
|
||||
#[AsMessageHandler(bus: 'command.bus')]
|
||||
final readonly class AssignTeacherHandler
|
||||
{
|
||||
public function __construct(
|
||||
private TeacherAssignmentRepository $assignmentRepository,
|
||||
private UserRepository $userRepository,
|
||||
private ClassRepository $classRepository,
|
||||
private SubjectRepository $subjectRepository,
|
||||
private Clock $clock,
|
||||
) {
|
||||
}
|
||||
|
||||
public function __invoke(AssignTeacherCommand $command): TeacherAssignment
|
||||
{
|
||||
$tenantId = TenantId::fromString($command->tenantId);
|
||||
$teacherId = UserId::fromString($command->teacherId);
|
||||
$classId = ClassId::fromString($command->classId);
|
||||
$subjectId = SubjectId::fromString($command->subjectId);
|
||||
$academicYearId = AcademicYearId::fromString($command->academicYearId);
|
||||
|
||||
// Valider l'existence des entités référencées (throws NotFoundException)
|
||||
$this->userRepository->get($teacherId);
|
||||
$this->classRepository->get($classId);
|
||||
$this->subjectRepository->get($subjectId);
|
||||
|
||||
// Vérifier l'unicité du triplet enseignant × classe × matière
|
||||
$existing = $this->assignmentRepository->findByTeacherClassSubject(
|
||||
$teacherId,
|
||||
$classId,
|
||||
$subjectId,
|
||||
$academicYearId,
|
||||
$tenantId,
|
||||
);
|
||||
|
||||
if ($existing !== null) {
|
||||
throw AffectationDejaExistanteException::pourTriple($teacherId, $classId, $subjectId);
|
||||
}
|
||||
|
||||
// Vérifier si une affectation retirée existe pour ce même triplet.
|
||||
// Si oui, la réactiver au lieu d'en créer une nouvelle (évite la violation
|
||||
// de la contrainte UNIQUE qui couvre tous les statuts).
|
||||
$removed = $this->assignmentRepository->findRemovedByTeacherClassSubject(
|
||||
$teacherId,
|
||||
$classId,
|
||||
$subjectId,
|
||||
$academicYearId,
|
||||
$tenantId,
|
||||
);
|
||||
|
||||
if ($removed !== null) {
|
||||
$removed->reactiver($this->clock->now());
|
||||
$this->assignmentRepository->save($removed);
|
||||
|
||||
return $removed;
|
||||
}
|
||||
|
||||
$assignment = TeacherAssignment::creer(
|
||||
tenantId: $tenantId,
|
||||
teacherId: $teacherId,
|
||||
classId: $classId,
|
||||
subjectId: $subjectId,
|
||||
academicYearId: $academicYearId,
|
||||
createdAt: $this->clock->now(),
|
||||
);
|
||||
|
||||
$this->assignmentRepository->save($assignment);
|
||||
|
||||
return $assignment;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Administration\Application\Command\RemoveAssignment;
|
||||
|
||||
final readonly class RemoveAssignmentCommand
|
||||
{
|
||||
public function __construct(
|
||||
public string $assignmentId,
|
||||
public string $tenantId,
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Administration\Application\Command\RemoveAssignment;
|
||||
|
||||
use App\Administration\Domain\Model\TeacherAssignment\TeacherAssignment;
|
||||
use App\Administration\Domain\Model\TeacherAssignment\TeacherAssignmentId;
|
||||
use App\Administration\Domain\Repository\TeacherAssignmentRepository;
|
||||
use App\Shared\Domain\Clock;
|
||||
use App\Shared\Domain\Tenant\TenantId;
|
||||
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
|
||||
|
||||
#[AsMessageHandler(bus: 'command.bus')]
|
||||
final readonly class RemoveAssignmentHandler
|
||||
{
|
||||
public function __construct(
|
||||
private TeacherAssignmentRepository $assignmentRepository,
|
||||
private Clock $clock,
|
||||
) {
|
||||
}
|
||||
|
||||
public function __invoke(RemoveAssignmentCommand $command): TeacherAssignment
|
||||
{
|
||||
$assignmentId = TeacherAssignmentId::fromString($command->assignmentId);
|
||||
$tenantId = TenantId::fromString($command->tenantId);
|
||||
|
||||
$assignment = $this->assignmentRepository->get($assignmentId, $tenantId);
|
||||
|
||||
$assignment->retirer($this->clock->now());
|
||||
|
||||
$this->assignmentRepository->save($assignment);
|
||||
|
||||
return $assignment;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Administration\Application\Port;
|
||||
|
||||
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\Shared\Domain\Tenant\TenantId;
|
||||
|
||||
/**
|
||||
* Port pour vérifier si un enseignant est affecté à une classe/matière.
|
||||
*
|
||||
* Utilisé par les Voters (GradeVoter, HomeworkVoter) pour autoriser
|
||||
* la saisie de notes ou devoirs uniquement aux enseignants affectés.
|
||||
*
|
||||
* @see AC2: Peut saisir notes pour cette matière dans cette classe
|
||||
* @see AC4: Bloquer saisie si affectation retirée
|
||||
*/
|
||||
interface TeacherAssignmentChecker
|
||||
{
|
||||
public function estAffecte(
|
||||
UserId $teacherId,
|
||||
ClassId $classId,
|
||||
SubjectId $subjectId,
|
||||
AcademicYearId $academicYearId,
|
||||
TenantId $tenantId,
|
||||
): bool;
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Administration\Application\Query\GetAssignment;
|
||||
|
||||
use App\Administration\Application\Query\GetAssignmentsForTeacher\TeacherAssignmentDto;
|
||||
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\Administration\Domain\Repository\TeacherAssignmentRepository;
|
||||
use App\Shared\Domain\Tenant\TenantId;
|
||||
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
|
||||
|
||||
#[AsMessageHandler(bus: 'query.bus')]
|
||||
final readonly class GetAssignmentHandler
|
||||
{
|
||||
public function __construct(
|
||||
private TeacherAssignmentRepository $assignmentRepository,
|
||||
) {
|
||||
}
|
||||
|
||||
public function __invoke(GetAssignmentQuery $query): ?TeacherAssignmentDto
|
||||
{
|
||||
$assignment = $this->assignmentRepository->findByTeacherClassSubject(
|
||||
UserId::fromString($query->teacherId),
|
||||
ClassId::fromString($query->classId),
|
||||
SubjectId::fromString($query->subjectId),
|
||||
AcademicYearId::fromString($query->academicYearId),
|
||||
TenantId::fromString($query->tenantId),
|
||||
);
|
||||
|
||||
if ($assignment === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return TeacherAssignmentDto::fromDomain($assignment);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Administration\Application\Query\GetAssignment;
|
||||
|
||||
final readonly class GetAssignmentQuery
|
||||
{
|
||||
public function __construct(
|
||||
public string $teacherId,
|
||||
public string $classId,
|
||||
public string $subjectId,
|
||||
public string $academicYearId,
|
||||
public string $tenantId,
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Administration\Application\Query\GetAssignmentsForTeacher;
|
||||
|
||||
use App\Administration\Domain\Model\User\UserId;
|
||||
use App\Administration\Domain\Repository\TeacherAssignmentRepository;
|
||||
use App\Shared\Domain\Tenant\TenantId;
|
||||
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
|
||||
|
||||
#[AsMessageHandler(bus: 'query.bus')]
|
||||
final readonly class GetAssignmentsForTeacherHandler
|
||||
{
|
||||
public function __construct(
|
||||
private TeacherAssignmentRepository $assignmentRepository,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @return TeacherAssignmentDto[]
|
||||
*/
|
||||
public function __invoke(GetAssignmentsForTeacherQuery $query): array
|
||||
{
|
||||
$teacherId = UserId::fromString($query->teacherId);
|
||||
$tenantId = TenantId::fromString($query->tenantId);
|
||||
|
||||
$assignments = $this->assignmentRepository->findActiveByTeacher($teacherId, $tenantId);
|
||||
|
||||
return array_map(TeacherAssignmentDto::fromDomain(...), $assignments);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Administration\Application\Query\GetAssignmentsForTeacher;
|
||||
|
||||
final readonly class GetAssignmentsForTeacherQuery
|
||||
{
|
||||
public function __construct(
|
||||
public string $teacherId,
|
||||
public string $tenantId,
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Administration\Application\Query\GetAssignmentsForTeacher;
|
||||
|
||||
use App\Administration\Domain\Model\TeacherAssignment\TeacherAssignment;
|
||||
use DateTimeImmutable;
|
||||
|
||||
final readonly class TeacherAssignmentDto
|
||||
{
|
||||
public function __construct(
|
||||
public string $id,
|
||||
public string $teacherId,
|
||||
public string $classId,
|
||||
public string $subjectId,
|
||||
public string $academicYearId,
|
||||
public string $status,
|
||||
public DateTimeImmutable $startDate,
|
||||
public ?DateTimeImmutable $endDate,
|
||||
public DateTimeImmutable $createdAt,
|
||||
) {
|
||||
}
|
||||
|
||||
public static function fromDomain(TeacherAssignment $assignment): self
|
||||
{
|
||||
return new self(
|
||||
id: (string) $assignment->id,
|
||||
teacherId: (string) $assignment->teacherId,
|
||||
classId: (string) $assignment->classId,
|
||||
subjectId: (string) $assignment->subjectId,
|
||||
academicYearId: (string) $assignment->academicYearId,
|
||||
status: $assignment->status->value,
|
||||
startDate: $assignment->startDate,
|
||||
endDate: $assignment->endDate,
|
||||
createdAt: $assignment->createdAt,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Administration\Application\Query\GetTeachersForClass;
|
||||
|
||||
use App\Administration\Application\Query\GetAssignmentsForTeacher\TeacherAssignmentDto;
|
||||
use App\Administration\Domain\Model\SchoolClass\ClassId;
|
||||
use App\Administration\Domain\Repository\TeacherAssignmentRepository;
|
||||
use App\Shared\Domain\Tenant\TenantId;
|
||||
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
|
||||
|
||||
#[AsMessageHandler(bus: 'query.bus')]
|
||||
final readonly class GetTeachersForClassHandler
|
||||
{
|
||||
public function __construct(
|
||||
private TeacherAssignmentRepository $assignmentRepository,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* @return TeacherAssignmentDto[]
|
||||
*/
|
||||
public function __invoke(GetTeachersForClassQuery $query): array
|
||||
{
|
||||
$classId = ClassId::fromString($query->classId);
|
||||
$tenantId = TenantId::fromString($query->tenantId);
|
||||
|
||||
$assignments = $this->assignmentRepository->findActiveByClass($classId, $tenantId);
|
||||
|
||||
return array_map(TeacherAssignmentDto::fromDomain(...), $assignments);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Administration\Application\Query\GetTeachersForClass;
|
||||
|
||||
final readonly class GetTeachersForClassQuery
|
||||
{
|
||||
public function __construct(
|
||||
public string $classId,
|
||||
public string $tenantId,
|
||||
) {
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user