feat: Permettre au parent de consulter les devoirs de ses enfants
Les parents avaient accès à l'emploi du temps de leurs enfants mais pas à leurs devoirs. Sans cette visibilité, ils ne pouvaient pas accompagner efficacement le travail scolaire à la maison, notamment identifier les devoirs urgents ou contacter l'enseignant en cas de besoin. Le parent dispose désormais d'une vue consolidée multi-enfants avec filtrage par enfant et par matière, badges d'urgence différenciés (en retard / aujourd'hui / pour demain), lien de contact enseignant pré-rempli, et cache offline scopé par utilisateur.
This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Application\Query\GetChildrenHomework;
|
||||
|
||||
use App\Scolarite\Application\Query\GetStudentHomework\StudentHomeworkDto;
|
||||
|
||||
final readonly class ChildHomeworkDto
|
||||
{
|
||||
/**
|
||||
* @param array<StudentHomeworkDto> $homework
|
||||
*/
|
||||
public function __construct(
|
||||
public string $childId,
|
||||
public string $firstName,
|
||||
public string $lastName,
|
||||
public array $homework,
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Application\Query\GetChildrenHomework;
|
||||
|
||||
use App\Scolarite\Application\Port\ParentChildrenReader;
|
||||
use App\Scolarite\Application\Port\ScheduleDisplayReader;
|
||||
use App\Scolarite\Application\Port\StudentClassReader;
|
||||
use App\Scolarite\Application\Query\GetStudentHomework\StudentHomeworkDetailDto;
|
||||
use App\Scolarite\Domain\Model\Homework\HomeworkId;
|
||||
use App\Scolarite\Domain\Repository\HomeworkAttachmentRepository;
|
||||
use App\Scolarite\Domain\Repository\HomeworkRepository;
|
||||
use App\Shared\Domain\Tenant\TenantId;
|
||||
|
||||
final readonly class GetChildrenHomeworkDetailHandler
|
||||
{
|
||||
public function __construct(
|
||||
private ParentChildrenReader $parentChildrenReader,
|
||||
private StudentClassReader $studentClassReader,
|
||||
private HomeworkRepository $homeworkRepository,
|
||||
private HomeworkAttachmentRepository $attachmentRepository,
|
||||
private ScheduleDisplayReader $displayReader,
|
||||
) {
|
||||
}
|
||||
|
||||
public function __invoke(string $parentId, string $tenantId, string $homeworkId): ?StudentHomeworkDetailDto
|
||||
{
|
||||
$tid = TenantId::fromString($tenantId);
|
||||
|
||||
$homework = $this->homeworkRepository->findById(HomeworkId::fromString($homeworkId), $tid);
|
||||
|
||||
if ($homework === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!$this->parentHasChildInClass($parentId, $tid, (string) $homework->classId)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$attachments = $this->attachmentRepository->findByHomeworkId($homework->id);
|
||||
|
||||
$subjects = $this->displayReader->subjectDisplay($tenantId, (string) $homework->subjectId);
|
||||
$teacherNames = $this->displayReader->teacherNames($tenantId, (string) $homework->teacherId);
|
||||
|
||||
return StudentHomeworkDetailDto::fromDomain(
|
||||
$homework,
|
||||
$subjects[(string) $homework->subjectId]['name'] ?? '',
|
||||
$subjects[(string) $homework->subjectId]['color'] ?? null,
|
||||
$teacherNames[(string) $homework->teacherId] ?? '',
|
||||
$attachments,
|
||||
);
|
||||
}
|
||||
|
||||
private function parentHasChildInClass(string $parentId, TenantId $tenantId, string $homeworkClassId): bool
|
||||
{
|
||||
$children = $this->parentChildrenReader->childrenOf($parentId, $tenantId);
|
||||
|
||||
foreach ($children as $child) {
|
||||
$classId = $this->studentClassReader->currentClassId($child['studentId'], $tenantId);
|
||||
|
||||
if ($classId === $homeworkClassId) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Application\Query\GetChildrenHomework;
|
||||
|
||||
use App\Administration\Domain\Model\SchoolClass\ClassId;
|
||||
use App\Scolarite\Application\Port\ParentChildrenReader;
|
||||
use App\Scolarite\Application\Port\ScheduleDisplayReader;
|
||||
use App\Scolarite\Application\Port\StudentClassReader;
|
||||
use App\Scolarite\Application\Query\GetStudentHomework\StudentHomeworkDto;
|
||||
use App\Scolarite\Domain\Model\Homework\Homework;
|
||||
use App\Scolarite\Domain\Model\Homework\HomeworkId;
|
||||
use App\Scolarite\Domain\Repository\HomeworkAttachmentRepository;
|
||||
use App\Scolarite\Domain\Repository\HomeworkRepository;
|
||||
use App\Shared\Domain\Tenant\TenantId;
|
||||
|
||||
use function array_filter;
|
||||
use function array_map;
|
||||
use function array_unique;
|
||||
use function array_values;
|
||||
|
||||
use DateTimeImmutable;
|
||||
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
|
||||
|
||||
use function usort;
|
||||
|
||||
#[AsMessageHandler(bus: 'query.bus')]
|
||||
final readonly class GetChildrenHomeworkHandler
|
||||
{
|
||||
public function __construct(
|
||||
private ParentChildrenReader $parentChildrenReader,
|
||||
private StudentClassReader $studentClassReader,
|
||||
private HomeworkRepository $homeworkRepository,
|
||||
private HomeworkAttachmentRepository $attachmentRepository,
|
||||
private ScheduleDisplayReader $displayReader,
|
||||
) {
|
||||
}
|
||||
|
||||
/** @return array<ChildHomeworkDto> */
|
||||
public function __invoke(GetChildrenHomeworkQuery $query): array
|
||||
{
|
||||
$tenantId = TenantId::fromString($query->tenantId);
|
||||
$allChildren = $this->parentChildrenReader->childrenOf($query->parentId, $tenantId);
|
||||
|
||||
if ($allChildren === []) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$children = $query->childId !== null
|
||||
? array_values(array_filter($allChildren, static fn (array $c): bool => $c['studentId'] === $query->childId))
|
||||
: $allChildren;
|
||||
|
||||
if ($children === []) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$result = [];
|
||||
|
||||
foreach ($children as $child) {
|
||||
$classId = $this->studentClassReader->currentClassId($child['studentId'], $tenantId);
|
||||
|
||||
if ($classId === null) {
|
||||
$result[] = new ChildHomeworkDto(
|
||||
childId: $child['studentId'],
|
||||
firstName: $child['firstName'],
|
||||
lastName: $child['lastName'],
|
||||
homework: [],
|
||||
);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$homeworks = $this->homeworkRepository->findByClass(ClassId::fromString($classId), $tenantId);
|
||||
|
||||
$today = new DateTimeImmutable('today');
|
||||
$homeworks = array_values(array_filter(
|
||||
$homeworks,
|
||||
static fn (Homework $h): bool => $h->dueDate >= $today,
|
||||
));
|
||||
|
||||
if ($query->subjectId !== null) {
|
||||
$filterSubjectId = $query->subjectId;
|
||||
$homeworks = array_values(array_filter(
|
||||
$homeworks,
|
||||
static fn (Homework $h): bool => (string) $h->subjectId === $filterSubjectId,
|
||||
));
|
||||
}
|
||||
|
||||
usort($homeworks, static fn (Homework $a, Homework $b): int => $a->dueDate <=> $b->dueDate);
|
||||
|
||||
$enriched = $this->enrichHomeworks($homeworks, $query->tenantId);
|
||||
|
||||
$result[] = new ChildHomeworkDto(
|
||||
childId: $child['studentId'],
|
||||
firstName: $child['firstName'],
|
||||
lastName: $child['lastName'],
|
||||
homework: $enriched,
|
||||
);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<Homework> $homeworks
|
||||
*
|
||||
* @return array<StudentHomeworkDto>
|
||||
*/
|
||||
private function enrichHomeworks(array $homeworks, string $tenantId): array
|
||||
{
|
||||
if ($homeworks === []) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$subjectIds = array_values(array_unique(
|
||||
array_map(static fn (Homework $h): string => (string) $h->subjectId, $homeworks),
|
||||
));
|
||||
$teacherIds = array_values(array_unique(
|
||||
array_map(static fn (Homework $h): string => (string) $h->teacherId, $homeworks),
|
||||
));
|
||||
|
||||
$subjects = $this->displayReader->subjectDisplay($tenantId, ...$subjectIds);
|
||||
$teacherNames = $this->displayReader->teacherNames($tenantId, ...$teacherIds);
|
||||
|
||||
$homeworkIds = array_map(static fn (Homework $h): HomeworkId => $h->id, $homeworks);
|
||||
$attachmentMap = $this->attachmentRepository->hasAttachments(...$homeworkIds);
|
||||
|
||||
return array_map(
|
||||
static fn (Homework $h): StudentHomeworkDto => StudentHomeworkDto::fromDomain(
|
||||
$h,
|
||||
$subjects[(string) $h->subjectId]['name'] ?? '',
|
||||
$subjects[(string) $h->subjectId]['color'] ?? null,
|
||||
$teacherNames[(string) $h->teacherId] ?? '',
|
||||
$attachmentMap[(string) $h->id] ?? false,
|
||||
),
|
||||
$homeworks,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Application\Query\GetChildrenHomework;
|
||||
|
||||
final readonly class GetChildrenHomeworkQuery
|
||||
{
|
||||
public function __construct(
|
||||
public string $parentId,
|
||||
public string $tenantId,
|
||||
public ?string $childId = null,
|
||||
public ?string $subjectId = null,
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,232 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Infrastructure\Api\Controller;
|
||||
|
||||
use App\Administration\Infrastructure\Security\SecurityUser;
|
||||
use App\Scolarite\Application\Query\GetChildrenHomework\ChildHomeworkDto;
|
||||
use App\Scolarite\Application\Query\GetChildrenHomework\GetChildrenHomeworkDetailHandler;
|
||||
use App\Scolarite\Application\Query\GetChildrenHomework\GetChildrenHomeworkHandler;
|
||||
use App\Scolarite\Application\Query\GetChildrenHomework\GetChildrenHomeworkQuery;
|
||||
use App\Scolarite\Application\Query\GetStudentHomework\StudentHomeworkDetailDto;
|
||||
use App\Scolarite\Application\Query\GetStudentHomework\StudentHomeworkDto;
|
||||
use App\Scolarite\Domain\Model\Homework\HomeworkId;
|
||||
use App\Scolarite\Domain\Repository\HomeworkAttachmentRepository;
|
||||
use App\Scolarite\Domain\Repository\HomeworkRepository;
|
||||
use App\Scolarite\Infrastructure\Security\HomeworkParentVoter;
|
||||
use App\Shared\Domain\Tenant\TenantId;
|
||||
|
||||
use function array_map;
|
||||
use function is_string;
|
||||
use function realpath;
|
||||
use function str_starts_with;
|
||||
|
||||
use Symfony\Bundle\SecurityBundle\Security;
|
||||
use Symfony\Component\DependencyInjection\Attribute\Autowire;
|
||||
use Symfony\Component\HttpFoundation\BinaryFileResponse;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\ResponseHeaderBag;
|
||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
|
||||
/**
|
||||
* Endpoints de consultation des devoirs des enfants pour le parent connecté.
|
||||
*/
|
||||
#[IsGranted(HomeworkParentVoter::VIEW)]
|
||||
final readonly class ParentHomeworkController
|
||||
{
|
||||
public function __construct(
|
||||
private Security $security,
|
||||
private GetChildrenHomeworkHandler $handler,
|
||||
private GetChildrenHomeworkDetailHandler $detailHandler,
|
||||
private HomeworkRepository $homeworkRepository,
|
||||
private HomeworkAttachmentRepository $attachmentRepository,
|
||||
#[Autowire('%kernel.project_dir%/var/uploads')]
|
||||
private string $uploadsDir,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Devoirs d'un enfant spécifique.
|
||||
*/
|
||||
#[Route('/api/me/children/{childId}/homework', name: 'api_parent_child_homework', methods: ['GET'])]
|
||||
public function childHomework(string $childId, Request $request): JsonResponse
|
||||
{
|
||||
$user = $this->getSecurityUser();
|
||||
$subjectId = $request->query->get('subjectId');
|
||||
|
||||
$children = ($this->handler)(new GetChildrenHomeworkQuery(
|
||||
parentId: $user->userId(),
|
||||
tenantId: $user->tenantId(),
|
||||
childId: $childId,
|
||||
subjectId: is_string($subjectId) && $subjectId !== '' ? $subjectId : null,
|
||||
));
|
||||
|
||||
if ($children === []) {
|
||||
throw new NotFoundHttpException('Enfant non trouvé ou non lié à ce parent.');
|
||||
}
|
||||
|
||||
return new JsonResponse([
|
||||
'data' => $this->serializeChild($children[0]),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Vue consolidée des devoirs de tous les enfants.
|
||||
*/
|
||||
#[Route('/api/me/children/homework', name: 'api_parent_children_homework', methods: ['GET'])]
|
||||
public function allChildrenHomework(Request $request): JsonResponse
|
||||
{
|
||||
$user = $this->getSecurityUser();
|
||||
$subjectId = $request->query->get('subjectId');
|
||||
|
||||
$children = ($this->handler)(new GetChildrenHomeworkQuery(
|
||||
parentId: $user->userId(),
|
||||
tenantId: $user->tenantId(),
|
||||
subjectId: is_string($subjectId) && $subjectId !== '' ? $subjectId : null,
|
||||
));
|
||||
|
||||
return new JsonResponse([
|
||||
'data' => array_map($this->serializeChild(...), $children),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Détail d'un devoir (accessible si l'enfant du parent est dans la classe du devoir).
|
||||
*/
|
||||
#[Route('/api/me/children/homework/{id}', name: 'api_parent_child_homework_detail', methods: ['GET'])]
|
||||
public function homeworkDetail(string $id): JsonResponse
|
||||
{
|
||||
$user = $this->getSecurityUser();
|
||||
|
||||
$detail = ($this->detailHandler)($user->userId(), $user->tenantId(), $id);
|
||||
|
||||
if ($detail === null) {
|
||||
throw new NotFoundHttpException('Devoir non trouvé.');
|
||||
}
|
||||
|
||||
return new JsonResponse(['data' => $this->serializeDetail($detail)]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Téléchargement d'une pièce jointe (parent).
|
||||
*/
|
||||
#[Route('/api/me/children/homework/{homeworkId}/attachments/{attachmentId}', name: 'api_parent_child_homework_attachment', methods: ['GET'])]
|
||||
public function downloadAttachment(string $homeworkId, string $attachmentId): BinaryFileResponse
|
||||
{
|
||||
$user = $this->getSecurityUser();
|
||||
$tenantId = TenantId::fromString($user->tenantId());
|
||||
|
||||
$homework = $this->homeworkRepository->findById(HomeworkId::fromString($homeworkId), $tenantId);
|
||||
|
||||
if ($homework === null) {
|
||||
throw new NotFoundHttpException('Devoir non trouvé.');
|
||||
}
|
||||
|
||||
// Verify parent has a child in the homework's class
|
||||
$detail = ($this->detailHandler)($user->userId(), $user->tenantId(), $homeworkId);
|
||||
|
||||
if ($detail === null) {
|
||||
throw new NotFoundHttpException('Devoir non trouvé.');
|
||||
}
|
||||
|
||||
$attachments = $this->attachmentRepository->findByHomeworkId($homework->id);
|
||||
|
||||
foreach ($attachments as $attachment) {
|
||||
if ((string) $attachment->id === $attachmentId) {
|
||||
$realPath = realpath($attachment->filePath);
|
||||
$realUploadsDir = realpath($this->uploadsDir);
|
||||
|
||||
if ($realPath === false || $realUploadsDir === false || !str_starts_with($realPath, $realUploadsDir)) {
|
||||
throw new NotFoundHttpException('Pièce jointe non trouvée.');
|
||||
}
|
||||
|
||||
$response = new BinaryFileResponse($realPath);
|
||||
$response->setContentDisposition(
|
||||
ResponseHeaderBag::DISPOSITION_INLINE,
|
||||
$attachment->filename,
|
||||
);
|
||||
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
|
||||
throw new NotFoundHttpException('Pièce jointe non trouvée.');
|
||||
}
|
||||
|
||||
private function getSecurityUser(): SecurityUser
|
||||
{
|
||||
$user = $this->security->getUser();
|
||||
|
||||
if (!$user instanceof SecurityUser) {
|
||||
throw new AccessDeniedHttpException('Authentification requise.');
|
||||
}
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function serializeChild(ChildHomeworkDto $child): array
|
||||
{
|
||||
return [
|
||||
'childId' => $child->childId,
|
||||
'firstName' => $child->firstName,
|
||||
'lastName' => $child->lastName,
|
||||
'homework' => array_map($this->serializeHomework(...), $child->homework),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function serializeDetail(StudentHomeworkDetailDto $dto): array
|
||||
{
|
||||
return [
|
||||
'id' => $dto->id,
|
||||
'subjectId' => $dto->subjectId,
|
||||
'subjectName' => $dto->subjectName,
|
||||
'subjectColor' => $dto->subjectColor,
|
||||
'teacherId' => $dto->teacherId,
|
||||
'teacherName' => $dto->teacherName,
|
||||
'title' => $dto->title,
|
||||
'description' => $dto->description,
|
||||
'dueDate' => $dto->dueDate,
|
||||
'createdAt' => $dto->createdAt,
|
||||
'attachments' => array_map(
|
||||
static fn ($a): array => [
|
||||
'id' => $a->id,
|
||||
'filename' => $a->filename,
|
||||
'fileSize' => $a->fileSize,
|
||||
'mimeType' => $a->mimeType,
|
||||
],
|
||||
$dto->attachments,
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function serializeHomework(StudentHomeworkDto $dto): array
|
||||
{
|
||||
return [
|
||||
'id' => $dto->id,
|
||||
'subjectId' => $dto->subjectId,
|
||||
'subjectName' => $dto->subjectName,
|
||||
'subjectColor' => $dto->subjectColor,
|
||||
'teacherId' => $dto->teacherId,
|
||||
'teacherName' => $dto->teacherName,
|
||||
'title' => $dto->title,
|
||||
'description' => $dto->description,
|
||||
'dueDate' => $dto->dueDate,
|
||||
'createdAt' => $dto->createdAt,
|
||||
'hasAttachments' => $dto->hasAttachments,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Infrastructure\Security;
|
||||
|
||||
use App\Administration\Domain\Model\User\Role;
|
||||
use App\Administration\Infrastructure\Security\SecurityUser;
|
||||
|
||||
use function in_array;
|
||||
|
||||
use Override;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||
use Symfony\Component\Security\Core\Authorization\Voter\Vote;
|
||||
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
|
||||
|
||||
/**
|
||||
* Voter pour la consultation des devoirs des enfants par le parent.
|
||||
*
|
||||
* @extends Voter<string, null>
|
||||
*/
|
||||
final class HomeworkParentVoter extends Voter
|
||||
{
|
||||
public const string VIEW = 'HOMEWORK_PARENT_VIEW';
|
||||
|
||||
#[Override]
|
||||
protected function supports(string $attribute, mixed $subject): bool
|
||||
{
|
||||
return $attribute === self::VIEW;
|
||||
}
|
||||
|
||||
#[Override]
|
||||
protected function voteOnAttribute(string $attribute, mixed $subject, TokenInterface $token, ?Vote $vote = null): bool
|
||||
{
|
||||
$user = $token->getUser();
|
||||
|
||||
if (!$user instanceof SecurityUser) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return in_array(Role::PARENT->value, $user->getRoles(), true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,338 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Unit\Scolarite\Application\Query\GetChildrenHomework;
|
||||
|
||||
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\ParentChildrenReader;
|
||||
use App\Scolarite\Application\Port\ScheduleDisplayReader;
|
||||
use App\Scolarite\Application\Port\StudentClassReader;
|
||||
use App\Scolarite\Application\Query\GetChildrenHomework\GetChildrenHomeworkHandler;
|
||||
use App\Scolarite\Application\Query\GetChildrenHomework\GetChildrenHomeworkQuery;
|
||||
use App\Scolarite\Domain\Model\Homework\Homework;
|
||||
use App\Scolarite\Infrastructure\Persistence\InMemory\InMemoryHomeworkAttachmentRepository;
|
||||
use App\Scolarite\Infrastructure\Persistence\InMemory\InMemoryHomeworkRepository;
|
||||
use App\Shared\Domain\Tenant\TenantId;
|
||||
use DateTimeImmutable;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
final class GetChildrenHomeworkHandlerTest extends TestCase
|
||||
{
|
||||
private const string TENANT_ID = '550e8400-e29b-41d4-a716-446655440001';
|
||||
private const string PARENT_ID = '550e8400-e29b-41d4-a716-446655440060';
|
||||
private const string CHILD_A_ID = '550e8400-e29b-41d4-a716-446655440050';
|
||||
private const string CHILD_B_ID = '550e8400-e29b-41d4-a716-446655440051';
|
||||
private const string CLASS_A_ID = '550e8400-e29b-41d4-a716-446655440020';
|
||||
private const string CLASS_B_ID = '550e8400-e29b-41d4-a716-446655440021';
|
||||
private const string SUBJECT_MATH_ID = '550e8400-e29b-41d4-a716-446655440030';
|
||||
private const string SUBJECT_FRENCH_ID = '550e8400-e29b-41d4-a716-446655440031';
|
||||
private const string TEACHER_ID = '550e8400-e29b-41d4-a716-446655440010';
|
||||
|
||||
private InMemoryHomeworkRepository $homeworkRepository;
|
||||
private InMemoryHomeworkAttachmentRepository $attachmentRepository;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->homeworkRepository = new InMemoryHomeworkRepository();
|
||||
$this->attachmentRepository = new InMemoryHomeworkAttachmentRepository();
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function itReturnsEmptyWhenParentHasNoChildren(): void
|
||||
{
|
||||
$handler = $this->createHandler(children: []);
|
||||
|
||||
$result = $handler(new GetChildrenHomeworkQuery(
|
||||
parentId: self::PARENT_ID,
|
||||
tenantId: self::TENANT_ID,
|
||||
));
|
||||
|
||||
self::assertSame([], $result);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function itReturnsHomeworkForSingleChild(): void
|
||||
{
|
||||
$this->givenHomework(title: 'Exercices chapitre 5', dueDate: '2026-04-15', classId: self::CLASS_A_ID);
|
||||
|
||||
$handler = $this->createHandler(
|
||||
children: [
|
||||
['studentId' => self::CHILD_A_ID, 'firstName' => 'Emma', 'lastName' => 'Dupont'],
|
||||
],
|
||||
classMap: [self::CHILD_A_ID => self::CLASS_A_ID],
|
||||
);
|
||||
|
||||
$result = $handler(new GetChildrenHomeworkQuery(
|
||||
parentId: self::PARENT_ID,
|
||||
tenantId: self::TENANT_ID,
|
||||
));
|
||||
|
||||
self::assertCount(1, $result);
|
||||
self::assertSame(self::CHILD_A_ID, $result[0]->childId);
|
||||
self::assertSame('Emma', $result[0]->firstName);
|
||||
self::assertSame('Dupont', $result[0]->lastName);
|
||||
self::assertCount(1, $result[0]->homework);
|
||||
self::assertSame('Exercices chapitre 5', $result[0]->homework[0]->title);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function itReturnsHomeworkForMultipleChildren(): void
|
||||
{
|
||||
$this->givenHomework(title: 'Maths 6A', dueDate: '2026-04-15', classId: self::CLASS_A_ID);
|
||||
$this->givenHomework(title: 'Maths 6B', dueDate: '2026-04-16', classId: self::CLASS_B_ID);
|
||||
|
||||
$handler = $this->createHandler(
|
||||
children: [
|
||||
['studentId' => self::CHILD_A_ID, 'firstName' => 'Emma', 'lastName' => 'Dupont'],
|
||||
['studentId' => self::CHILD_B_ID, 'firstName' => 'Lucas', 'lastName' => 'Dupont'],
|
||||
],
|
||||
classMap: [
|
||||
self::CHILD_A_ID => self::CLASS_A_ID,
|
||||
self::CHILD_B_ID => self::CLASS_B_ID,
|
||||
],
|
||||
);
|
||||
|
||||
$result = $handler(new GetChildrenHomeworkQuery(
|
||||
parentId: self::PARENT_ID,
|
||||
tenantId: self::TENANT_ID,
|
||||
));
|
||||
|
||||
self::assertCount(2, $result);
|
||||
self::assertSame('Emma', $result[0]->firstName);
|
||||
self::assertCount(1, $result[0]->homework);
|
||||
self::assertSame('Maths 6A', $result[0]->homework[0]->title);
|
||||
self::assertSame('Lucas', $result[1]->firstName);
|
||||
self::assertCount(1, $result[1]->homework);
|
||||
self::assertSame('Maths 6B', $result[1]->homework[0]->title);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function itFiltersToSpecificChildWhenChildIdProvided(): void
|
||||
{
|
||||
$this->givenHomework(title: 'Maths 6A', dueDate: '2026-04-15', classId: self::CLASS_A_ID);
|
||||
$this->givenHomework(title: 'Maths 6B', dueDate: '2026-04-16', classId: self::CLASS_B_ID);
|
||||
|
||||
$handler = $this->createHandler(
|
||||
children: [
|
||||
['studentId' => self::CHILD_A_ID, 'firstName' => 'Emma', 'lastName' => 'Dupont'],
|
||||
['studentId' => self::CHILD_B_ID, 'firstName' => 'Lucas', 'lastName' => 'Dupont'],
|
||||
],
|
||||
classMap: [
|
||||
self::CHILD_A_ID => self::CLASS_A_ID,
|
||||
self::CHILD_B_ID => self::CLASS_B_ID,
|
||||
],
|
||||
);
|
||||
|
||||
$result = $handler(new GetChildrenHomeworkQuery(
|
||||
parentId: self::PARENT_ID,
|
||||
tenantId: self::TENANT_ID,
|
||||
childId: self::CHILD_B_ID,
|
||||
));
|
||||
|
||||
self::assertCount(1, $result);
|
||||
self::assertSame('Lucas', $result[0]->firstName);
|
||||
self::assertCount(1, $result[0]->homework);
|
||||
self::assertSame('Maths 6B', $result[0]->homework[0]->title);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function itReturnsEmptyHomeworkWhenChildHasNoClass(): void
|
||||
{
|
||||
$handler = $this->createHandler(
|
||||
children: [
|
||||
['studentId' => self::CHILD_A_ID, 'firstName' => 'Emma', 'lastName' => 'Dupont'],
|
||||
],
|
||||
classMap: [],
|
||||
);
|
||||
|
||||
$result = $handler(new GetChildrenHomeworkQuery(
|
||||
parentId: self::PARENT_ID,
|
||||
tenantId: self::TENANT_ID,
|
||||
));
|
||||
|
||||
self::assertCount(1, $result);
|
||||
self::assertSame('Emma', $result[0]->firstName);
|
||||
self::assertSame([], $result[0]->homework);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function itReturnsEmptyWhenChildIdDoesNotMatchAnyChild(): void
|
||||
{
|
||||
$handler = $this->createHandler(
|
||||
children: [
|
||||
['studentId' => self::CHILD_A_ID, 'firstName' => 'Emma', 'lastName' => 'Dupont'],
|
||||
],
|
||||
classMap: [self::CHILD_A_ID => self::CLASS_A_ID],
|
||||
);
|
||||
|
||||
$result = $handler(new GetChildrenHomeworkQuery(
|
||||
parentId: self::PARENT_ID,
|
||||
tenantId: self::TENANT_ID,
|
||||
childId: '550e8400-e29b-41d4-a716-446655449999',
|
||||
));
|
||||
|
||||
self::assertSame([], $result);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function itExcludesPastHomework(): void
|
||||
{
|
||||
$this->givenHomework(title: 'Passé', dueDate: '2026-01-01', classId: self::CLASS_A_ID);
|
||||
$this->givenHomework(title: 'Futur', dueDate: '2026-12-15', classId: self::CLASS_A_ID);
|
||||
|
||||
$handler = $this->createHandler(
|
||||
children: [
|
||||
['studentId' => self::CHILD_A_ID, 'firstName' => 'Emma', 'lastName' => 'Dupont'],
|
||||
],
|
||||
classMap: [self::CHILD_A_ID => self::CLASS_A_ID],
|
||||
);
|
||||
|
||||
$result = $handler(new GetChildrenHomeworkQuery(
|
||||
parentId: self::PARENT_ID,
|
||||
tenantId: self::TENANT_ID,
|
||||
));
|
||||
|
||||
self::assertCount(1, $result);
|
||||
self::assertCount(1, $result[0]->homework);
|
||||
self::assertSame('Futur', $result[0]->homework[0]->title);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function itFiltersBySubjectWhenProvided(): void
|
||||
{
|
||||
$this->givenHomework(title: 'Maths', dueDate: '2026-04-15', classId: self::CLASS_A_ID, subjectId: self::SUBJECT_MATH_ID);
|
||||
$this->givenHomework(title: 'Français', dueDate: '2026-04-16', classId: self::CLASS_A_ID, subjectId: self::SUBJECT_FRENCH_ID);
|
||||
|
||||
$handler = $this->createHandler(
|
||||
children: [
|
||||
['studentId' => self::CHILD_A_ID, 'firstName' => 'Emma', 'lastName' => 'Dupont'],
|
||||
],
|
||||
classMap: [self::CHILD_A_ID => self::CLASS_A_ID],
|
||||
);
|
||||
|
||||
$result = $handler(new GetChildrenHomeworkQuery(
|
||||
parentId: self::PARENT_ID,
|
||||
tenantId: self::TENANT_ID,
|
||||
subjectId: self::SUBJECT_MATH_ID,
|
||||
));
|
||||
|
||||
self::assertCount(1, $result);
|
||||
self::assertCount(1, $result[0]->homework);
|
||||
self::assertSame('Maths', $result[0]->homework[0]->title);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function itSortsHomeworkByDueDateAscending(): void
|
||||
{
|
||||
$this->givenHomework(title: 'Lointain', dueDate: '2026-05-20', classId: self::CLASS_A_ID);
|
||||
$this->givenHomework(title: 'Proche', dueDate: '2026-04-10', classId: self::CLASS_A_ID);
|
||||
$this->givenHomework(title: 'Milieu', dueDate: '2026-04-25', classId: self::CLASS_A_ID);
|
||||
|
||||
$handler = $this->createHandler(
|
||||
children: [
|
||||
['studentId' => self::CHILD_A_ID, 'firstName' => 'Emma', 'lastName' => 'Dupont'],
|
||||
],
|
||||
classMap: [self::CHILD_A_ID => self::CLASS_A_ID],
|
||||
);
|
||||
|
||||
$result = $handler(new GetChildrenHomeworkQuery(
|
||||
parentId: self::PARENT_ID,
|
||||
tenantId: self::TENANT_ID,
|
||||
));
|
||||
|
||||
self::assertCount(1, $result);
|
||||
$titles = array_map(static fn ($hw) => $hw->title, $result[0]->homework);
|
||||
self::assertSame(['Proche', 'Milieu', 'Lointain'], $titles);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<array{studentId: string, firstName: string, lastName: string}> $children
|
||||
* @param array<string, string> $classMap studentId => classId
|
||||
*/
|
||||
private function createHandler(
|
||||
array $children = [],
|
||||
array $classMap = [],
|
||||
): GetChildrenHomeworkHandler {
|
||||
$parentChildrenReader = new class($children) implements ParentChildrenReader {
|
||||
/** @param array<array{studentId: string, firstName: string, lastName: string}> $children */
|
||||
public function __construct(private readonly array $children)
|
||||
{
|
||||
}
|
||||
|
||||
public function childrenOf(string $guardianId, TenantId $tenantId): array
|
||||
{
|
||||
return $this->children;
|
||||
}
|
||||
};
|
||||
|
||||
$studentClassReader = new class($classMap) implements StudentClassReader {
|
||||
/** @param array<string, string> $classMap */
|
||||
public function __construct(private readonly array $classMap)
|
||||
{
|
||||
}
|
||||
|
||||
public function currentClassId(string $studentId, TenantId $tenantId): ?string
|
||||
{
|
||||
return $this->classMap[$studentId] ?? null;
|
||||
}
|
||||
};
|
||||
|
||||
$displayReader = new class implements ScheduleDisplayReader {
|
||||
public function subjectDisplay(string $tenantId, string ...$subjectIds): array
|
||||
{
|
||||
$map = [];
|
||||
|
||||
foreach ($subjectIds as $id) {
|
||||
$map[$id] = ['name' => 'Mathématiques', 'color' => '#3b82f6'];
|
||||
}
|
||||
|
||||
return $map;
|
||||
}
|
||||
|
||||
public function teacherNames(string $tenantId, string ...$teacherIds): array
|
||||
{
|
||||
$map = [];
|
||||
|
||||
foreach ($teacherIds as $id) {
|
||||
$map[$id] = 'Jean Dupont';
|
||||
}
|
||||
|
||||
return $map;
|
||||
}
|
||||
};
|
||||
|
||||
return new GetChildrenHomeworkHandler(
|
||||
$parentChildrenReader,
|
||||
$studentClassReader,
|
||||
$this->homeworkRepository,
|
||||
$this->attachmentRepository,
|
||||
$displayReader,
|
||||
);
|
||||
}
|
||||
|
||||
private function givenHomework(
|
||||
string $title,
|
||||
string $dueDate,
|
||||
string $classId = self::CLASS_A_ID,
|
||||
string $subjectId = self::SUBJECT_MATH_ID,
|
||||
): Homework {
|
||||
$homework = Homework::creer(
|
||||
tenantId: TenantId::fromString(self::TENANT_ID),
|
||||
classId: ClassId::fromString($classId),
|
||||
subjectId: SubjectId::fromString($subjectId),
|
||||
teacherId: UserId::fromString(self::TEACHER_ID),
|
||||
title: $title,
|
||||
description: null,
|
||||
dueDate: new DateTimeImmutable($dueDate),
|
||||
now: new DateTimeImmutable('2026-03-12 10:00:00'),
|
||||
);
|
||||
|
||||
$this->homeworkRepository->save($homework);
|
||||
|
||||
return $homework;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user