feat: Permettre à l'élève de consulter ses notes et moyennes
L'élève avait accès à ses compétences mais pas à ses notes numériques. Cette fonctionnalité lui donne une vue complète de sa progression scolaire avec moyennes par matière, détail par évaluation, statistiques de classe, et un mode "découverte" pour révéler ses notes à son rythme (FR14, FR15). Les notes ne sont visibles qu'après publication par l'enseignant, ce qui garantit que l'élève les découvre avant ses parents (délai 24h story 6.7).
This commit is contained in:
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Application\Query\GetChildrenGrades;
|
||||
|
||||
final readonly class ChildGradesDto
|
||||
{
|
||||
/**
|
||||
* @param array<ParentGradeDto> $grades
|
||||
*/
|
||||
public function __construct(
|
||||
public string $childId,
|
||||
public string $firstName,
|
||||
public string $lastName,
|
||||
public array $grades,
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Application\Query\GetChildrenGrades;
|
||||
|
||||
final readonly class ChildGradesSummaryDto
|
||||
{
|
||||
/**
|
||||
* @param list<array{subjectId: string, subjectName: string|null, average: float, gradeCount: int}> $subjectAverages
|
||||
*/
|
||||
public function __construct(
|
||||
public string $childId,
|
||||
public string $firstName,
|
||||
public string $lastName,
|
||||
public ?string $periodId,
|
||||
public array $subjectAverages,
|
||||
public ?float $generalAverage,
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Application\Query\GetChildrenGrades;
|
||||
|
||||
use App\Administration\Domain\Model\User\UserId;
|
||||
use App\Scolarite\Application\Port\ParentChildrenReader;
|
||||
use App\Scolarite\Application\Port\ParentGradeDelayReader;
|
||||
use App\Scolarite\Application\Port\ScheduleDisplayReader;
|
||||
use App\Scolarite\Domain\Model\Evaluation\Evaluation;
|
||||
use App\Scolarite\Domain\Model\Evaluation\EvaluationId;
|
||||
use App\Scolarite\Domain\Model\Grade\Grade;
|
||||
use App\Scolarite\Domain\Policy\VisibiliteNotesPolicy;
|
||||
use App\Scolarite\Domain\Repository\EvaluationRepository;
|
||||
use App\Scolarite\Domain\Repository\EvaluationStatisticsRepository;
|
||||
use App\Scolarite\Domain\Repository\GradeRepository;
|
||||
use App\Shared\Domain\Tenant\TenantId;
|
||||
|
||||
use function array_filter;
|
||||
use function array_map;
|
||||
use function array_unique;
|
||||
use function array_values;
|
||||
use function usort;
|
||||
|
||||
final readonly class GetChildrenGradesHandler
|
||||
{
|
||||
public function __construct(
|
||||
private ParentChildrenReader $parentChildrenReader,
|
||||
private EvaluationRepository $evaluationRepository,
|
||||
private GradeRepository $gradeRepository,
|
||||
private EvaluationStatisticsRepository $statisticsRepository,
|
||||
private ScheduleDisplayReader $displayReader,
|
||||
private VisibiliteNotesPolicy $visibiliteNotesPolicy,
|
||||
private ParentGradeDelayReader $delayReader,
|
||||
) {
|
||||
}
|
||||
|
||||
/** @return array<ChildGradesDto> */
|
||||
public function __invoke(GetChildrenGradesQuery $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 [];
|
||||
}
|
||||
|
||||
$delaiHeures = $this->delayReader->delayHoursForTenant($tenantId);
|
||||
|
||||
$result = [];
|
||||
|
||||
foreach ($children as $child) {
|
||||
$studentId = UserId::fromString($child['studentId']);
|
||||
|
||||
// Get all grades for this student (not filtered by class)
|
||||
$studentGrades = $this->gradeRepository->findByStudent($studentId, $tenantId);
|
||||
|
||||
if ($studentGrades === []) {
|
||||
$result[] = new ChildGradesDto(
|
||||
childId: $child['studentId'],
|
||||
firstName: $child['firstName'],
|
||||
lastName: $child['lastName'],
|
||||
grades: [],
|
||||
);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// Collect unique evaluation IDs and load evaluations
|
||||
$evaluationIds = array_values(array_unique(
|
||||
array_map(static fn (Grade $g): string => (string) $g->evaluationId, $studentGrades),
|
||||
));
|
||||
|
||||
$evaluationsById = [];
|
||||
|
||||
foreach ($evaluationIds as $evalIdStr) {
|
||||
$evaluation = $this->evaluationRepository->findById(
|
||||
EvaluationId::fromString($evalIdStr),
|
||||
$tenantId,
|
||||
);
|
||||
|
||||
if ($evaluation !== null) {
|
||||
$evaluationsById[$evalIdStr] = $evaluation;
|
||||
}
|
||||
}
|
||||
|
||||
// Filter evaluations visible to parents (published + delay elapsed)
|
||||
$visibleEvaluationIds = [];
|
||||
|
||||
foreach ($evaluationsById as $evalIdStr => $evaluation) {
|
||||
if ($this->visibiliteNotesPolicy->visiblePourParent($evaluation, $delaiHeures)) {
|
||||
$visibleEvaluationIds[$evalIdStr] = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Filter by subject if requested
|
||||
if ($query->subjectId !== null) {
|
||||
$filterSubjectId = $query->subjectId;
|
||||
|
||||
foreach ($visibleEvaluationIds as $evalIdStr => $_) {
|
||||
$evaluation = $evaluationsById[$evalIdStr];
|
||||
|
||||
if ((string) $evaluation->subjectId !== $filterSubjectId) {
|
||||
unset($visibleEvaluationIds[$evalIdStr]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($visibleEvaluationIds === []) {
|
||||
$result[] = new ChildGradesDto(
|
||||
childId: $child['studentId'],
|
||||
firstName: $child['firstName'],
|
||||
lastName: $child['lastName'],
|
||||
grades: [],
|
||||
);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// Resolve display names
|
||||
$visibleEvaluations = array_values(array_filter(
|
||||
$evaluationsById,
|
||||
static fn (Evaluation $e): bool => isset($visibleEvaluationIds[(string) $e->id]),
|
||||
));
|
||||
$subjectIds = array_values(array_unique(
|
||||
array_map(static fn (Evaluation $e): string => (string) $e->subjectId, $visibleEvaluations),
|
||||
));
|
||||
$subjects = $this->displayReader->subjectDisplay($query->tenantId, ...$subjectIds);
|
||||
|
||||
// Build grade DTOs
|
||||
$childGrades = [];
|
||||
|
||||
foreach ($studentGrades as $grade) {
|
||||
$evalIdStr = (string) $grade->evaluationId;
|
||||
|
||||
if (!isset($visibleEvaluationIds[$evalIdStr])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$evaluation = $evaluationsById[$evalIdStr];
|
||||
$subjectInfo = $subjects[(string) $evaluation->subjectId] ?? ['name' => null, 'color' => null];
|
||||
$statistics = $this->statisticsRepository->findByEvaluation($evaluation->id);
|
||||
|
||||
$childGrades[] = new ParentGradeDto(
|
||||
id: (string) $grade->id,
|
||||
evaluationId: $evalIdStr,
|
||||
evaluationTitle: $evaluation->title,
|
||||
evaluationDate: $evaluation->evaluationDate->format('Y-m-d'),
|
||||
gradeScale: $evaluation->gradeScale->maxValue,
|
||||
coefficient: $evaluation->coefficient->value,
|
||||
subjectId: (string) $evaluation->subjectId,
|
||||
subjectName: $subjectInfo['name'],
|
||||
subjectColor: $subjectInfo['color'],
|
||||
value: $grade->value?->value,
|
||||
status: $grade->status->value,
|
||||
appreciation: $grade->appreciation,
|
||||
publishedAt: $evaluation->gradesPublishedAt?->format('Y-m-d\TH:i:sP') ?? '',
|
||||
classAverage: $statistics?->average,
|
||||
classMin: $statistics?->min,
|
||||
classMax: $statistics?->max,
|
||||
);
|
||||
}
|
||||
|
||||
// Sort by evaluation date descending
|
||||
usort($childGrades, static fn (ParentGradeDto $a, ParentGradeDto $b): int => $b->evaluationDate <=> $a->evaluationDate);
|
||||
|
||||
$result[] = new ChildGradesDto(
|
||||
childId: $child['studentId'],
|
||||
firstName: $child['firstName'],
|
||||
lastName: $child['lastName'],
|
||||
grades: $childGrades,
|
||||
);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Application\Query\GetChildrenGrades;
|
||||
|
||||
final readonly class GetChildrenGradesQuery
|
||||
{
|
||||
public function __construct(
|
||||
public string $parentId,
|
||||
public string $tenantId,
|
||||
public ?string $childId = null,
|
||||
public ?string $subjectId = null,
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Application\Query\GetChildrenGrades;
|
||||
|
||||
use function array_sum;
|
||||
use function count;
|
||||
use function round;
|
||||
|
||||
/**
|
||||
* Calcule les moyennes par matière et la moyenne générale
|
||||
* à partir des notes visibles (respectant le délai parent).
|
||||
*
|
||||
* Ne lit PAS les agrégats pré-calculés (student_averages) car ceux-ci
|
||||
* incluent des notes encore dans la période de délai.
|
||||
*/
|
||||
final readonly class GetChildrenGradesSummaryHandler
|
||||
{
|
||||
public function __construct(
|
||||
private GetChildrenGradesHandler $gradesHandler,
|
||||
) {
|
||||
}
|
||||
|
||||
/** @return array<ChildGradesSummaryDto> */
|
||||
public function __invoke(GetChildrenGradesSummaryQuery $query): array
|
||||
{
|
||||
// Réutilise le handler notes qui applique le délai de visibilité
|
||||
$childrenGrades = ($this->gradesHandler)(new GetChildrenGradesQuery(
|
||||
parentId: $query->parentId,
|
||||
tenantId: $query->tenantId,
|
||||
));
|
||||
|
||||
$result = [];
|
||||
|
||||
foreach ($childrenGrades as $child) {
|
||||
// Grouper par matière et calculer les moyennes pondérées
|
||||
$subjectData = [];
|
||||
|
||||
foreach ($child->grades as $grade) {
|
||||
if ($grade->value === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$key = $grade->subjectId;
|
||||
|
||||
if (!isset($subjectData[$key])) {
|
||||
$subjectData[$key] = [
|
||||
'subjectId' => $grade->subjectId,
|
||||
'subjectName' => $grade->subjectName,
|
||||
'weightedSum' => 0.0,
|
||||
'coefficientSum' => 0.0,
|
||||
'gradeCount' => 0,
|
||||
];
|
||||
}
|
||||
|
||||
$subjectData[$key]['weightedSum'] += $grade->value * $grade->coefficient;
|
||||
$subjectData[$key]['coefficientSum'] += $grade->coefficient;
|
||||
++$subjectData[$key]['gradeCount'];
|
||||
}
|
||||
|
||||
$subjectAverages = [];
|
||||
|
||||
foreach ($subjectData as $data) {
|
||||
if ($data['coefficientSum'] > 0) {
|
||||
$subjectAverages[] = [
|
||||
'subjectId' => $data['subjectId'],
|
||||
'subjectName' => $data['subjectName'],
|
||||
'average' => round($data['weightedSum'] / $data['coefficientSum'], 2),
|
||||
'gradeCount' => $data['gradeCount'],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
$generalAverage = null;
|
||||
|
||||
if ($subjectAverages !== []) {
|
||||
$averages = array_map(static fn (array $a): float => $a['average'], $subjectAverages);
|
||||
$generalAverage = round(array_sum($averages) / count($averages), 2);
|
||||
}
|
||||
|
||||
$result[] = new ChildGradesSummaryDto(
|
||||
childId: $child->childId,
|
||||
firstName: $child->firstName,
|
||||
lastName: $child->lastName,
|
||||
periodId: null,
|
||||
subjectAverages: $subjectAverages,
|
||||
generalAverage: $generalAverage,
|
||||
);
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Application\Query\GetChildrenGrades;
|
||||
|
||||
final readonly class GetChildrenGradesSummaryQuery
|
||||
{
|
||||
public function __construct(
|
||||
public string $parentId,
|
||||
public string $tenantId,
|
||||
public ?string $periodId = null,
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Scolarite\Application\Query\GetChildrenGrades;
|
||||
|
||||
final readonly class ParentGradeDto
|
||||
{
|
||||
public function __construct(
|
||||
public string $id,
|
||||
public string $evaluationId,
|
||||
public string $evaluationTitle,
|
||||
public string $evaluationDate,
|
||||
public int $gradeScale,
|
||||
public float $coefficient,
|
||||
public string $subjectId,
|
||||
public ?string $subjectName,
|
||||
public ?string $subjectColor,
|
||||
public ?float $value,
|
||||
public string $status,
|
||||
public ?string $appreciation,
|
||||
public string $publishedAt,
|
||||
public ?float $classAverage,
|
||||
public ?float $classMin,
|
||||
public ?float $classMax,
|
||||
) {
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user