diff --git a/backend/src/Administration/Application/Command/CreateStudent/CreateStudentCommand.php b/backend/src/Administration/Application/Command/CreateStudent/CreateStudentCommand.php index 2f8b30a..bcc561b 100644 --- a/backend/src/Administration/Application/Command/CreateStudent/CreateStudentCommand.php +++ b/backend/src/Administration/Application/Command/CreateStudent/CreateStudentCommand.php @@ -16,6 +16,8 @@ final readonly class CreateStudentCommand public ?string $email = null, public ?string $dateNaissance = null, public ?string $studentNumber = null, + public bool $parentalConsent = false, + public ?string $consentRecordedBy = null, ) { } } diff --git a/backend/src/Administration/Application/Command/CreateStudent/CreateStudentHandler.php b/backend/src/Administration/Application/Command/CreateStudent/CreateStudentHandler.php index 18cad3c..7a28d52 100644 --- a/backend/src/Administration/Application/Command/CreateStudent/CreateStudentHandler.php +++ b/backend/src/Administration/Application/Command/CreateStudent/CreateStudentHandler.php @@ -7,11 +7,13 @@ namespace App\Administration\Application\Command\CreateStudent; use App\Administration\Domain\Exception\ClasseNotFoundException; use App\Administration\Domain\Exception\EmailDejaUtiliseeException; use App\Administration\Domain\Model\ClassAssignment\ClassAssignment; +use App\Administration\Domain\Model\ConsentementParental\ConsentementParental; use App\Administration\Domain\Model\SchoolClass\AcademicYearId; use App\Administration\Domain\Model\SchoolClass\ClassId; use App\Administration\Domain\Model\User\Email; use App\Administration\Domain\Model\User\Role; use App\Administration\Domain\Model\User\User; +use App\Administration\Domain\Policy\ConsentementParentalPolicy; use App\Administration\Domain\Repository\ClassAssignmentRepository; use App\Administration\Domain\Repository\ClassRepository; use App\Administration\Domain\Repository\UserRepository; @@ -31,6 +33,7 @@ final readonly class CreateStudentHandler private ClassRepository $classRepository, private Connection $connection, private Clock $clock, + private ConsentementParentalPolicy $consentementPolicy, ) { } @@ -94,6 +97,24 @@ final readonly class CreateStudentHandler studentNumber: $command->studentNumber, ); + // Enregistrer le consentement parental si fourni par l'admin + if ($command->parentalConsent && $command->consentRecordedBy !== null) { + $dateNaissance = $command->dateNaissance !== null + ? new DateTimeImmutable($command->dateNaissance) + : null; + + if ($this->consentementPolicy->estRequis($dateNaissance)) { + $user->enregistrerConsentementParental( + ConsentementParental::accorder( + parentId: $command->consentRecordedBy, + eleveId: (string) $user->id, + at: $now, + ipAddress: 'admin-creation', + ), + ); + } + } + $this->userRepository->save($user); // Affecter à la classe diff --git a/backend/src/Administration/Infrastructure/Api/Processor/CreateStudentProcessor.php b/backend/src/Administration/Infrastructure/Api/Processor/CreateStudentProcessor.php index 7ef87cb..3cf5333 100644 --- a/backend/src/Administration/Infrastructure/Api/Processor/CreateStudentProcessor.php +++ b/backend/src/Administration/Infrastructure/Api/Processor/CreateStudentProcessor.php @@ -13,10 +13,12 @@ use App\Administration\Domain\Exception\EmailDejaUtiliseeException; use App\Administration\Domain\Model\SchoolClass\ClassId; use App\Administration\Domain\Repository\ClassRepository; use App\Administration\Infrastructure\Api\Resource\StudentResource; +use App\Administration\Infrastructure\Security\SecurityUser; use App\Administration\Infrastructure\Security\StudentVoter; use App\Administration\Infrastructure\Service\CurrentAcademicYearResolver; use App\Shared\Infrastructure\Tenant\TenantContext; use Override; +use Symfony\Bundle\SecurityBundle\Security; use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; use Symfony\Component\HttpKernel\Exception\ConflictHttpException; @@ -37,6 +39,7 @@ final readonly class CreateStudentProcessor implements ProcessorInterface private MessageBusInterface $eventBus, private AuthorizationCheckerInterface $authorizationChecker, private CurrentAcademicYearResolver $academicYearResolver, + private Security $security, ) { } @@ -60,6 +63,13 @@ final readonly class CreateStudentProcessor implements ProcessorInterface ?? throw new BadRequestHttpException('Impossible de résoudre l\'année scolaire courante.'); try { + $adminUserId = null; + $securityUser = $this->security->getUser(); + + if ($securityUser instanceof SecurityUser) { + $adminUserId = $securityUser->userId(); + } + $command = new CreateStudentCommand( tenantId: $tenantId, schoolName: $tenantConfig->subdomain, @@ -70,6 +80,8 @@ final readonly class CreateStudentProcessor implements ProcessorInterface email: $data->email, dateNaissance: $data->dateNaissance, studentNumber: $data->studentNumber, + parentalConsent: $data->parentalConsent, + consentRecordedBy: $adminUserId, ); $user = ($this->handler)($command); diff --git a/backend/src/Administration/Infrastructure/Api/Resource/StudentResource.php b/backend/src/Administration/Infrastructure/Api/Resource/StudentResource.php index b6c3dc0..15a4553 100644 --- a/backend/src/Administration/Infrastructure/Api/Resource/StudentResource.php +++ b/backend/src/Administration/Infrastructure/Api/Resource/StudentResource.php @@ -72,6 +72,8 @@ final class StudentResource #[Assert\Regex(pattern: '/^[A-Za-z0-9]{11}$/', message: 'L\'INE doit contenir exactement 11 caractères alphanumériques.')] public ?string $studentNumber = null; + public bool $parentalConsent = false; + public static function fromDto(StudentWithClassDto $dto): self { $resource = new self(); diff --git a/backend/tests/Unit/Administration/Application/Command/CreateStudent/CreateStudentHandlerTest.php b/backend/tests/Unit/Administration/Application/Command/CreateStudent/CreateStudentHandlerTest.php index 5fe7ff4..16cadda 100644 --- a/backend/tests/Unit/Administration/Application/Command/CreateStudent/CreateStudentHandlerTest.php +++ b/backend/tests/Unit/Administration/Application/Command/CreateStudent/CreateStudentHandlerTest.php @@ -189,6 +189,7 @@ final class CreateStudentHandlerTest extends TestCase $this->classRepository, $connection, $this->clock, + new \App\Administration\Domain\Policy\ConsentementParentalPolicy($this->clock), ); $command = $this->createCommand(); @@ -278,6 +279,7 @@ final class CreateStudentHandlerTest extends TestCase $this->classRepository, $this->connection, $this->clock, + new \App\Administration\Domain\Policy\ConsentementParentalPolicy($this->clock), ); } diff --git a/frontend/src/lib/features/students/api/students.ts b/frontend/src/lib/features/students/api/students.ts index 0e4d382..09ff7fc 100644 --- a/frontend/src/lib/features/students/api/students.ts +++ b/frontend/src/lib/features/students/api/students.ts @@ -40,6 +40,7 @@ export interface CreateStudentData { email?: string | undefined; dateNaissance?: string | undefined; studentNumber?: string | undefined; + parentalConsent?: boolean | undefined; } /** @@ -93,7 +94,7 @@ export async function fetchClasses(): Promise { */ export async function createStudent(studentData: CreateStudentData): Promise { const apiUrl = getApiBaseUrl(); - const body: Record = { + const body: Record = { firstName: studentData.firstName, lastName: studentData.lastName, classId: studentData.classId @@ -101,6 +102,7 @@ export async function createStudent(studentData: CreateStudentData): Promise(null); let duplicateConfirmed = $state(false); let createAnother = $state(false); + let needsParentalConsent = $derived(() => { + if (!newDateNaissance) return false; + const birth = new Date(newDateNaissance + 'T00:00:00'); + const now = new Date(); + let age = now.getFullYear() - birth.getFullYear(); + const monthDiff = now.getMonth() - birth.getMonth(); + if (monthDiff < 0 || (monthDiff === 0 && now.getDate() < birth.getDate())) { + age--; + } + return age < 15; + }); + // Change class modal state let showChangeClassModal = $state(false); let changeClassTarget = $state(null); @@ -176,6 +189,7 @@ newClassId = ''; newDateNaissance = ''; newStudentNumber = ''; + newParentalConsent = false; duplicateWarning = null; duplicateConfirmed = false; createAnother = false; @@ -224,6 +238,7 @@ async function handleCreateStudent() { if (!newFirstName.trim() || !newLastName.trim() || !newClassId) return; + if (needsParentalConsent() && !newParentalConsent) return; if (await checkDuplicate()) return; @@ -236,7 +251,8 @@ classId: newClassId, email: newEmail.trim() ? newEmail.trim().toLowerCase() : undefined, dateNaissance: newDateNaissance || undefined, - studentNumber: newStudentNumber.trim() || undefined + studentNumber: newStudentNumber.trim() || undefined, + parentalConsent: newParentalConsent || undefined }); // Optimistic update: only add to list if matching current filters @@ -375,7 +391,8 @@ newLastName.trim() !== '' && newClassId !== '' && !isSubmitting && - (!newStudentNumber.trim() || isValidINE(newStudentNumber.trim())) + (!newStudentNumber.trim() || isValidINE(newStudentNumber.trim())) && + (!needsParentalConsent() || newParentalConsent) ); @@ -609,6 +626,19 @@ + {#if needsParentalConsent()} + + {/if} + {#if duplicateWarning}

{duplicateWarning}

@@ -1160,6 +1190,33 @@ color: #9ca3af; } + .consent-notice { + padding: 0.75rem; + background: #fef3c7; + border: 1px solid #fcd34d; + border-radius: 0.5rem; + } + + .consent-label { + display: flex; + align-items: center; + gap: 0.5rem; + font-weight: 500; + color: #92400e; + cursor: pointer; + } + + .consent-label input[type='checkbox'] { + width: 1rem; + height: 1rem; + accent-color: #f59e0b; + } + + .consent-notice .field-hint { + margin-left: 1.5rem; + color: #92400e; + } + .field-error { display: block; margin-top: 0.25rem;