feat: Permettre aux enseignants de dupliquer un devoir vers plusieurs classes
Some checks failed
CI / Backend Tests (push) Has been cancelled
CI / Frontend Tests (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
CI / Naming Conventions (push) Has been cancelled
CI / Build Check (push) Has been cancelled

Un enseignant qui donne le même travail à plusieurs classes devait
jusqu'ici recréer manuellement chaque devoir. La duplication permet
de sélectionner les classes cibles, d'ajuster les dates d'échéance
par classe, et de créer tous les devoirs en une seule opération
atomique (transaction).

La validation s'effectue par classe (affectation enseignant, date
d'échéance) avec un rapport d'erreurs détaillé. L'infrastructure
de warnings est prête pour les règles de timing de la Story 5.3.
Le filtrage par classe dans la liste des devoirs passe côté serveur
pour rester compatible avec la pagination.
This commit is contained in:
2026-03-15 14:20:48 +01:00
parent e9efb90f59
commit 68179a929f
18 changed files with 1831 additions and 2 deletions

View File

@@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace App\Scolarite\Application\Command\DuplicateHomework;
final readonly class DuplicateHomeworkCommand
{
/**
* @param array<string> $targetClassIds
* @param array<string, string> $dueDates Clé = classId, valeur = date (Y-m-d)
*/
public function __construct(
public string $tenantId,
public string $homeworkId,
public string $teacherId,
public array $targetClassIds,
public array $dueDates = [],
) {
}
}

View File

@@ -0,0 +1,166 @@
<?php
declare(strict_types=1);
namespace App\Scolarite\Application\Command\DuplicateHomework;
use App\Administration\Domain\Model\SchoolClass\ClassId;
use App\Administration\Domain\Model\User\UserId;
use App\Scolarite\Application\Port\CurrentCalendarProvider;
use App\Scolarite\Application\Port\EnseignantAffectationChecker;
use App\Scolarite\Domain\Exception\DateEcheanceInvalideException;
use App\Scolarite\Domain\Exception\DuplicationValidationException;
use App\Scolarite\Domain\Exception\NonProprietaireDuDevoirException;
use App\Scolarite\Domain\Model\Homework\HomeworkAttachment;
use App\Scolarite\Domain\Model\Homework\HomeworkAttachmentId;
use App\Scolarite\Domain\Model\Homework\HomeworkId;
use App\Scolarite\Domain\Repository\HomeworkAttachmentRepository;
use App\Scolarite\Domain\Repository\HomeworkRepository;
use App\Scolarite\Domain\Service\DueDateValidator;
use App\Scolarite\Domain\Service\HomeworkDuplicator;
use App\Scolarite\Domain\ValueObject\ClassValidationResult;
use App\Shared\Domain\Clock;
use App\Shared\Domain\Tenant\TenantId;
use function array_filter;
use function array_unique;
use function array_values;
use DateTimeImmutable;
use Doctrine\DBAL\Connection;
use Exception;
use function in_array;
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
use Throwable;
#[AsMessageHandler(bus: 'command.bus')]
final readonly class DuplicateHomeworkHandler
{
public function __construct(
private HomeworkRepository $homeworkRepository,
private HomeworkAttachmentRepository $attachmentRepository,
private EnseignantAffectationChecker $affectationChecker,
private CurrentCalendarProvider $calendarProvider,
private DueDateValidator $dueDateValidator,
private HomeworkDuplicator $duplicator,
private Clock $clock,
private Connection $connection,
) {
}
public function __invoke(DuplicateHomeworkCommand $command): DuplicationResult
{
$tenantId = TenantId::fromString($command->tenantId);
$homeworkId = HomeworkId::fromString($command->homeworkId);
$teacherId = UserId::fromString($command->teacherId);
$now = $this->clock->now();
$source = $this->homeworkRepository->get($homeworkId, $tenantId);
if ((string) $source->teacherId !== (string) $teacherId) {
throw NonProprietaireDuDevoirException::withId($homeworkId);
}
$calendar = $this->calendarProvider->forCurrentYear($tenantId);
// Dédupliquer et exclure la classe source
$uniqueClassIds = array_values(array_unique($command->targetClassIds));
$sourceClassIdStr = (string) $source->classId;
$targetClassIds = [];
$dueDates = [];
$validationResults = [];
$seen = [];
foreach ($uniqueClassIds as $classIdStr) {
if ($classIdStr === $sourceClassIdStr) {
continue;
}
if (in_array($classIdStr, $seen, true)) {
continue;
}
$seen[] = $classIdStr;
$classId = ClassId::fromString($classIdStr);
try {
$dueDate = isset($command->dueDates[$classIdStr])
? new DateTimeImmutable($command->dueDates[$classIdStr])
: $source->dueDate;
} catch (Exception) {
$validationResults[] = ClassValidationResult::failure(
$classId,
"Date d'échéance invalide : {$command->dueDates[$classIdStr]}.",
);
continue;
}
if (!$this->affectationChecker->estAffecte($teacherId, $classId, $source->subjectId, $tenantId)) {
$validationResults[] = ClassValidationResult::failure(
$classId,
"L'enseignant n'est pas affecté à cette classe pour cette matière.",
);
continue;
}
try {
$this->dueDateValidator->valider($dueDate, $now, $calendar);
$validationResults[] = ClassValidationResult::success($classId);
$targetClassIds[] = $classId;
$dueDates[] = $dueDate;
} catch (DateEcheanceInvalideException $e) {
$validationResults[] = ClassValidationResult::failure($classId, $e->getMessage());
}
}
$failures = array_filter($validationResults, static fn (ClassValidationResult $r): bool => !$r->valid);
if ($failures !== []) {
throw DuplicationValidationException::withResults($validationResults);
}
$duplicates = $this->duplicator->dupliquer($source, $targetClassIds, $dueDates, $now);
$sourceAttachments = $this->attachmentRepository->findByHomeworkId($source->id);
$this->connection->beginTransaction();
try {
foreach ($duplicates as $duplicate) {
$this->homeworkRepository->save($duplicate);
foreach ($sourceAttachments as $attachment) {
$this->attachmentRepository->save(
$duplicate->id,
new HomeworkAttachment(
id: HomeworkAttachmentId::generate(),
filename: $attachment->filename,
filePath: $attachment->filePath,
fileSize: $attachment->fileSize,
mimeType: $attachment->mimeType,
uploadedAt: $now,
),
);
}
}
$this->connection->commit();
} catch (Throwable $e) {
$this->connection->rollBack();
throw $e;
}
$warnings = array_values(array_filter(
$validationResults,
static fn (ClassValidationResult $r): bool => $r->valid && $r->warning !== null,
));
return new DuplicationResult($duplicates, $warnings);
}
}

View File

@@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace App\Scolarite\Application\Command\DuplicateHomework;
use App\Scolarite\Domain\Model\Homework\Homework;
use App\Scolarite\Domain\ValueObject\ClassValidationResult;
final readonly class DuplicationResult
{
/**
* @param array<Homework> $homeworks
* @param array<ClassValidationResult> $warnings
*/
public function __construct(
public array $homeworks,
public array $warnings,
) {
}
}