feat: Permettre à l'enseignant de saisir les notes dans une grille inline
L'enseignant avait besoin d'un moyen rapide de saisir les notes après une évaluation. La grille inline permet de compléter 30 élèves en moins de 3 minutes grâce à la navigation clavier (Tab/Enter/Shift+Tab), la validation temps réel, l'auto-save debounced (500ms) et les raccourcis /abs et /disp pour marquer absents/dispensés. Les notes restent en brouillon jusqu'à publication explicite (avec confirmation modale). Une fois publiées, les élèves les voient immédiatement ; les parents après un délai de 24h (VisibiliteNotesPolicy). Le mode offline stocke les notes en IndexedDB et synchronise automatiquement au retour de la connexion. Chaque modification est auditée dans grade_events via un event subscriber qui écoute NoteSaisie/NoteModifiee sur le bus d'événements.
This commit is contained in:
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Application\Command\PublishGrades;
|
||||
|
||||
final readonly class PublishGradesCommand
|
||||
{
|
||||
public function __construct(
|
||||
public string $tenantId,
|
||||
public string $evaluationId,
|
||||
public string $teacherId,
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Application\Command\PublishGrades;
|
||||
|
||||
use App\Administration\Domain\Model\User\UserId;
|
||||
use App\Scolarite\Domain\Exception\AucuneNoteSaisieException;
|
||||
use App\Scolarite\Domain\Exception\NonProprietaireDeLEvaluationException;
|
||||
use App\Scolarite\Domain\Model\Evaluation\Evaluation;
|
||||
use App\Scolarite\Domain\Model\Evaluation\EvaluationId;
|
||||
use App\Scolarite\Domain\Repository\EvaluationRepository;
|
||||
use App\Scolarite\Domain\Repository\GradeRepository;
|
||||
use App\Shared\Domain\Clock;
|
||||
use App\Shared\Domain\Tenant\TenantId;
|
||||
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
|
||||
|
||||
#[AsMessageHandler(bus: 'command.bus')]
|
||||
final readonly class PublishGradesHandler
|
||||
{
|
||||
public function __construct(
|
||||
private EvaluationRepository $evaluationRepository,
|
||||
private GradeRepository $gradeRepository,
|
||||
private Clock $clock,
|
||||
) {
|
||||
}
|
||||
|
||||
public function __invoke(PublishGradesCommand $command): Evaluation
|
||||
{
|
||||
$tenantId = TenantId::fromString($command->tenantId);
|
||||
$evaluationId = EvaluationId::fromString($command->evaluationId);
|
||||
$teacherId = UserId::fromString($command->teacherId);
|
||||
$now = $this->clock->now();
|
||||
|
||||
$evaluation = $this->evaluationRepository->get($evaluationId, $tenantId);
|
||||
|
||||
if ((string) $evaluation->teacherId !== (string) $teacherId) {
|
||||
throw NonProprietaireDeLEvaluationException::withId($evaluationId);
|
||||
}
|
||||
|
||||
if (!$this->gradeRepository->hasGradesForEvaluation($evaluationId, $tenantId)) {
|
||||
throw AucuneNoteSaisieException::pourEvaluation($evaluationId);
|
||||
}
|
||||
|
||||
$evaluation->publierNotes($now);
|
||||
|
||||
$this->evaluationRepository->save($evaluation);
|
||||
|
||||
return $evaluation;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Application\Command\SaveGrades;
|
||||
|
||||
final readonly class SaveGradesCommand
|
||||
{
|
||||
/**
|
||||
* @param array<array{studentId: string, value: ?float, status: string}> $grades
|
||||
*/
|
||||
public function __construct(
|
||||
public string $tenantId,
|
||||
public string $evaluationId,
|
||||
public string $teacherId,
|
||||
public array $grades,
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Application\Command\SaveGrades;
|
||||
|
||||
use App\Administration\Domain\Model\User\UserId;
|
||||
use App\Scolarite\Domain\Exception\NonProprietaireDeLEvaluationException;
|
||||
use App\Scolarite\Domain\Model\Evaluation\EvaluationId;
|
||||
use App\Scolarite\Domain\Model\Grade\Grade;
|
||||
use App\Scolarite\Domain\Model\Grade\GradeStatus;
|
||||
use App\Scolarite\Domain\Model\Grade\GradeValue;
|
||||
use App\Scolarite\Domain\Repository\EvaluationRepository;
|
||||
use App\Scolarite\Domain\Repository\GradeRepository;
|
||||
use App\Shared\Domain\Clock;
|
||||
use App\Shared\Domain\Tenant\TenantId;
|
||||
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
|
||||
|
||||
#[AsMessageHandler(bus: 'command.bus')]
|
||||
final readonly class SaveGradesHandler
|
||||
{
|
||||
public function __construct(
|
||||
private EvaluationRepository $evaluationRepository,
|
||||
private GradeRepository $gradeRepository,
|
||||
private Clock $clock,
|
||||
) {
|
||||
}
|
||||
|
||||
/** @return array<Grade> */
|
||||
public function __invoke(SaveGradesCommand $command): array
|
||||
{
|
||||
$tenantId = TenantId::fromString($command->tenantId);
|
||||
$evaluationId = EvaluationId::fromString($command->evaluationId);
|
||||
$teacherId = UserId::fromString($command->teacherId);
|
||||
$now = $this->clock->now();
|
||||
|
||||
$evaluation = $this->evaluationRepository->get($evaluationId, $tenantId);
|
||||
|
||||
if ((string) $evaluation->teacherId !== (string) $teacherId) {
|
||||
throw NonProprietaireDeLEvaluationException::withId($evaluationId);
|
||||
}
|
||||
|
||||
$existingGrades = $this->gradeRepository->findByEvaluation($evaluationId, $tenantId);
|
||||
$existingByStudent = [];
|
||||
foreach ($existingGrades as $grade) {
|
||||
$existingByStudent[(string) $grade->studentId] = $grade;
|
||||
}
|
||||
|
||||
$savedGrades = [];
|
||||
|
||||
foreach ($command->grades as $gradeInput) {
|
||||
$studentId = UserId::fromString($gradeInput['studentId']);
|
||||
$status = GradeStatus::from($gradeInput['status']);
|
||||
$value = $gradeInput['value'] !== null ? new GradeValue($gradeInput['value']) : null;
|
||||
|
||||
$existing = $existingByStudent[(string) $studentId] ?? null;
|
||||
|
||||
if ($existing !== null) {
|
||||
$existing->modifier(
|
||||
value: $value,
|
||||
status: $status,
|
||||
gradeScale: $evaluation->gradeScale,
|
||||
modifiedBy: $teacherId,
|
||||
now: $now,
|
||||
);
|
||||
$this->gradeRepository->save($existing);
|
||||
$savedGrades[] = $existing;
|
||||
} else {
|
||||
$grade = Grade::saisir(
|
||||
tenantId: $tenantId,
|
||||
evaluationId: $evaluationId,
|
||||
studentId: $studentId,
|
||||
value: $value,
|
||||
status: $status,
|
||||
gradeScale: $evaluation->gradeScale,
|
||||
createdBy: $teacherId,
|
||||
now: $now,
|
||||
);
|
||||
$this->gradeRepository->save($grade);
|
||||
$savedGrades[] = $grade;
|
||||
}
|
||||
}
|
||||
|
||||
return $savedGrades;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user