Les administrateurs et secrétaires avaient besoin de pouvoir inscrire un élève en cours d'année sans passer par un import CSV. Cette fonctionnalité pose aussi les fondations du modèle élève↔classe (ClassAssignment) qui sera réutilisé par l'import CSV en masse (Story 3.1). L'email est désormais optionnel pour les élèves : si fourni, une invitation est envoyée (User::inviter) ; sinon l'élève est créé avec le statut INSCRIT sans accès compte (User::inscrire). La création de l'utilisateur et l'affectation à la classe sont atomiques (transaction DBAL). Côté frontend, la page /admin/students offre liste paginée, recherche, filtrage par classe, création via modale (avec détection de doublons côté serveur), et changement de classe avec optimistic update.
127 lines
4.2 KiB
PHP
127 lines
4.2 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Administration\Application\Query\GetStudentsWithClass;
|
|
|
|
use App\Administration\Application\Dto\PaginatedResult;
|
|
use App\Administration\Domain\Model\User\Role;
|
|
use Doctrine\DBAL\Connection;
|
|
use Symfony\Component\Messenger\Attribute\AsMessageHandler;
|
|
|
|
#[AsMessageHandler(bus: 'query.bus')]
|
|
final readonly class GetStudentsWithClassHandler
|
|
{
|
|
public function __construct(
|
|
private Connection $connection,
|
|
) {
|
|
}
|
|
|
|
/**
|
|
* @return PaginatedResult<StudentWithClassDto>
|
|
*/
|
|
public function __invoke(GetStudentsWithClassQuery $query): PaginatedResult
|
|
{
|
|
$params = [
|
|
'tenant_id' => $query->tenantId,
|
|
'academic_year_id' => $query->academicYearId,
|
|
'role' => json_encode([Role::ELEVE->value]),
|
|
];
|
|
|
|
$whereClause = 'u.tenant_id = :tenant_id AND u.roles::jsonb @> :role::jsonb';
|
|
|
|
if ($query->classId !== null) {
|
|
$whereClause .= ' AND ca.school_class_id = :class_id';
|
|
$params['class_id'] = $query->classId;
|
|
}
|
|
|
|
if ($query->search !== null && $query->search !== '') {
|
|
$whereClause .= ' AND (LOWER(u.last_name) LIKE :search OR LOWER(u.first_name) LIKE :search)';
|
|
$params['search'] = '%' . mb_strtolower($query->search) . '%';
|
|
}
|
|
|
|
// Count total
|
|
$countSql = <<<SQL
|
|
SELECT COUNT(*)
|
|
FROM users u
|
|
LEFT JOIN class_assignments ca ON ca.user_id = u.id AND ca.academic_year_id = :academic_year_id
|
|
WHERE {$whereClause}
|
|
SQL;
|
|
|
|
/** @var int|string|false $totalRaw */
|
|
$totalRaw = $this->connection->fetchOne($countSql, $params);
|
|
$total = (int) $totalRaw;
|
|
|
|
// Fetch paginated results
|
|
$offset = ($query->page - 1) * $query->limit;
|
|
|
|
$selectSql = <<<SQL
|
|
SELECT
|
|
u.id,
|
|
u.first_name,
|
|
u.last_name,
|
|
u.email,
|
|
u.statut,
|
|
u.student_number,
|
|
u.date_naissance,
|
|
ca.school_class_id AS class_id,
|
|
sc.name AS class_name,
|
|
sc.level AS class_level
|
|
FROM users u
|
|
LEFT JOIN class_assignments ca ON ca.user_id = u.id AND ca.academic_year_id = :academic_year_id
|
|
LEFT JOIN school_classes sc ON sc.id = ca.school_class_id
|
|
WHERE {$whereClause}
|
|
ORDER BY u.last_name ASC, u.first_name ASC
|
|
LIMIT :limit OFFSET :offset
|
|
SQL;
|
|
|
|
$params['limit'] = $query->limit;
|
|
$params['offset'] = $offset;
|
|
|
|
$rows = $this->connection->fetchAllAssociative($selectSql, $params);
|
|
|
|
$items = array_map(static function (array $row): StudentWithClassDto {
|
|
/** @var string $id */
|
|
$id = $row['id'];
|
|
/** @var string $firstName */
|
|
$firstName = $row['first_name'];
|
|
/** @var string $lastName */
|
|
$lastName = $row['last_name'];
|
|
/** @var string|null $email */
|
|
$email = $row['email'];
|
|
/** @var string $statut */
|
|
$statut = $row['statut'];
|
|
/** @var string|null $studentNumber */
|
|
$studentNumber = $row['student_number'];
|
|
/** @var string|null $dateNaissance */
|
|
$dateNaissance = $row['date_naissance'];
|
|
/** @var string|null $classId */
|
|
$classId = $row['class_id'];
|
|
/** @var string|null $className */
|
|
$className = $row['class_name'];
|
|
/** @var string|null $classLevel */
|
|
$classLevel = $row['class_level'];
|
|
|
|
return new StudentWithClassDto(
|
|
id: $id,
|
|
firstName: $firstName,
|
|
lastName: $lastName,
|
|
email: $email,
|
|
statut: $statut,
|
|
studentNumber: $studentNumber,
|
|
dateNaissance: $dateNaissance,
|
|
classId: $classId,
|
|
className: $className,
|
|
classLevel: $classLevel,
|
|
);
|
|
}, $rows);
|
|
|
|
return new PaginatedResult(
|
|
items: $items,
|
|
total: $total,
|
|
page: $query->page,
|
|
limit: $query->limit,
|
|
);
|
|
}
|
|
}
|