From 8a19b01893e6f4cae09c2efddd2aaa72d3c03af8 Mon Sep 17 00:00:00 2001 From: Charles Date: Tue, 12 Aug 2025 13:39:37 +0200 Subject: [PATCH] Visualisation Utilisateurs --- src/Controller/UserController.php | 31 ++++-- src/Entity/Apps.php | 36 ++++++- .../UserOrganizatonAppRepository.php | 39 ++++++++ src/Repository/UserRepository.php | 48 ++++++++++ .../UsersOrganizationsRepository.php | 65 ++++++++++++- src/Service/UserService.php | 94 ++++++++++++++++++- templates/elements/menu.html.twig | 34 +++---- templates/user/index.html.twig | 30 ++++-- templates/user/userList.html.twig | 53 ++++++----- 9 files changed, 367 insertions(+), 63 deletions(-) diff --git a/src/Controller/UserController.php b/src/Controller/UserController.php index 7b25855..36106c1 100644 --- a/src/Controller/UserController.php +++ b/src/Controller/UserController.php @@ -2,13 +2,8 @@ namespace App\Controller; -use App\Entity\Actions; -use App\Entity\Apps; -use App\Entity\Organizations; -use App\Entity\Roles; use App\Entity\User; use App\Form\UserForm; -use App\Entity\UsersOrganizations; use App\Service\ActionService; use App\Service\UserOrganizationService; use App\Service\UserService; @@ -28,8 +23,30 @@ class UserController extends AbstractController public function __construct( private readonly UserOrganizationService $userOrganizationService, private readonly EntityManagerInterface $entityManager, - private readonly UserService $userService, private readonly ActionService $actionService) - { + private readonly UserService $userService, + private readonly ActionService $actionService, + ) { } + #[Route('/', name: 'index', methods: ['GET'])] + public function index(): Response + { + $this->denyAccessUnlessGranted('ROLE_USER'); + + $user = $this->getUser(); + + if ($this->isGranted('ROLE_SUPER_ADMIN')) { + $usersByOrganization = $this->userService->getUsersGroupedForIndex(); + + + } elseif ($this->isGranted('ROLE_ADMIN')) { + $usersByOrganization = $this->userService->getUsersGroupedForAdmin($user); + } else { + $usersByOrganization = []; + } + + return $this->render('user/index.html.twig', [ + 'usersByOrganization' => $usersByOrganization, + ]); + } } diff --git a/src/Entity/Apps.php b/src/Entity/Apps.php index 9779a56..3306c8c 100644 --- a/src/Entity/Apps.php +++ b/src/Entity/Apps.php @@ -42,9 +42,19 @@ class Apps #[ORM\OneToMany(targetEntity: UserOrganizatonApp::class, mappedBy: 'application')] private Collection $userOrganizatonApps; + /** + * Inverse side of Organization<->Apps ManyToMany. + * Matches Organizations::$apps which is mappedBy "organization". + * + * @var Collection + */ + #[ORM\ManyToMany(targetEntity: Organizations::class, inversedBy: 'apps')] + private Collection $organization; + public function __construct() { $this->userOrganizatonApps = new ArrayCollection(); + $this->organization = new ArrayCollection(); } public function getId(): ?int @@ -124,7 +134,6 @@ class Apps return $this; } - public function getDescriptionSmall(): ?string { return $this->descriptionSmall; @@ -158,7 +167,6 @@ class Apps public function removeUserOrganizatonApp(UserOrganizatonApp $userOrganizatonApp): static { if ($this->userOrganizatonApps->removeElement($userOrganizatonApp)) { - // set the owning side to null (unless already changed) if ($userOrganizatonApp->getApplication() === $this) { $userOrganizatonApp->setApplication(null); } @@ -166,4 +174,28 @@ class Apps return $this; } + + /** + * @return Collection + */ + public function getOrganization(): Collection + { + return $this->organization; + } + + public function addOrganization(Organizations $organization): static + { + if (!$this->organization->contains($organization)) { + $this->organization->add($organization); + } + + return $this; + } + + public function removeOrganization(Organizations $organization): static + { + $this->organization->removeElement($organization); + + return $this; + } } diff --git a/src/Repository/UserOrganizatonAppRepository.php b/src/Repository/UserOrganizatonAppRepository.php index 23a1f01..e4e29ff 100644 --- a/src/Repository/UserOrganizatonAppRepository.php +++ b/src/Repository/UserOrganizatonAppRepository.php @@ -2,6 +2,7 @@ namespace App\Repository; +use App\Entity\User; use App\Entity\UserOrganizatonApp; use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; use Doctrine\Persistence\ManagerRegistry; @@ -40,4 +41,42 @@ class UserOrganizatonAppRepository extends ServiceEntityRepository // ->getOneOrNullResult() // ; // } + + /** + * Returns organization IDs where the given user is an admin (role name = 'ADMIN') + * for at least one application that the organization has access to. + * + * @return int[] + */ + public function findAdminOrganizationIdsForUser(User $user): array + { + $qb = $this->createQueryBuilder('uoa') + ->select('DISTINCT o.id') + ->leftJoin('uoa.userOrganization', 'uo') + ->leftJoin('uo.organization', 'o') + ->leftJoin('uo.users', 'u') + ->leftJoin('uoa.role', 'r') + ->leftJoin('uoa.application', 'app') + ->leftJoin('app.organization', 'ao') + ->where('uoa.isActive = :uoaActive') + ->andWhere('uo.isActive = :uoActive') + ->andWhere('u = :user') + ->andWhere('o.isActive = :oActive') + ->andWhere('o.isDeleted = :oDeleted') + ->andWhere('u.isActive = :uActive') + ->andWhere('u.isDeleted = :uDeleted') + ->andWhere('r.name = :adminRole') + ->andWhere('ao = o') + ->setParameter('uoaActive', true) + ->setParameter('uoActive', true) + ->setParameter('user', $user) + ->setParameter('oActive', true) + ->setParameter('oDeleted', false) + ->setParameter('uActive', true) + ->setParameter('uDeleted', false) + ->setParameter('adminRole', 'ADMIN'); + + $rows = $qb->getQuery()->getScalarResult(); + return array_map(static fn(array $row) => (int)$row['id'], $rows); + } } diff --git a/src/Repository/UserRepository.php b/src/Repository/UserRepository.php index e059597..27142a1 100644 --- a/src/Repository/UserRepository.php +++ b/src/Repository/UserRepository.php @@ -43,4 +43,52 @@ class UserRepository extends ServiceEntityRepository implements PasswordUpgrader return $queryBuilder->getQuery()->getResult(); } + /** + * Returns active users that are NOT in any UsersOrganizations mapping. + * Returns User entities. + * + * @return User[] + */ + public function findActiveUsersWithoutOrganization(): array + { + $qb = $this->createQueryBuilder('u') + ->select('u') + ->leftJoin('App\\Entity\\UsersOrganizations', 'uo', 'WITH', 'uo.users = u') + ->where('u.isActive = :uActive') + ->andWhere('u.isDeleted = :uDeleted') + ->andWhere('uo.id IS NULL') + ->orderBy('u.surname', 'ASC') + ->setParameter('uActive', true) + ->setParameter('uDeleted', false); + + return $qb->getQuery()->getResult(); + } + + /** + * Get all active users with their organization relationships + * @return array + */ + public function getAllActiveUsersWithOrganizations(): array + { + $queryBuilder = $this->createQueryBuilder('u') + ->select('u', 'uo', 'o') + ->leftJoin('App\Entity\UsersOrganizations', 'uo', 'WITH', 'u.id = uo.users') + ->leftJoin('App\Entity\Organizations', 'o', 'WITH', 'uo.organization = o.id') + ->where('u.isActive = :isActive') + ->andWhere('u.isDeleted = :isDeleted') + ->andWhere('(uo.isActive = :uoActive OR uo.isActive IS NULL)') + ->andWhere('(o.isActive = :oActive OR o.isActive IS NULL)') + ->andWhere('(o.isDeleted = :oDeleted OR o.isDeleted IS NULL)') + ->orderBy('o.name', 'ASC') + ->addOrderBy('u.surname', 'ASC'); + + $queryBuilder->setParameter('isActive', true); + $queryBuilder->setParameter('isDeleted', false); + $queryBuilder->setParameter('uoActive', true); + $queryBuilder->setParameter('oActive', true); + $queryBuilder->setParameter('oDeleted', false); + + return $queryBuilder->getQuery()->getResult(); + } + } diff --git a/src/Repository/UsersOrganizationsRepository.php b/src/Repository/UsersOrganizationsRepository.php index 6f4da99..cf48976 100644 --- a/src/Repository/UsersOrganizationsRepository.php +++ b/src/Repository/UsersOrganizationsRepository.php @@ -2,10 +2,7 @@ namespace App\Repository; -use App\Entity\User; use App\Entity\UsersOrganizations; -use App\Entity\Organizations; -use App\Entity\Roles; use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository; use Doctrine\Persistence\ManagerRegistry; @@ -19,5 +16,65 @@ class UsersOrganizationsRepository extends ServiceEntityRepository parent::__construct($registry, UsersOrganizations::class); } + /** + * Returns active user-organization mappings with joined User and Organization. + * Only active and non-deleted users and organizations are included. + * + * @return UsersOrganizations[] + */ + public function findActiveWithUserAndOrganization(): array + { + $qb = $this->createQueryBuilder('uo') + ->addSelect('u', 'o') + ->leftJoin('uo.users', 'u') + ->leftJoin('uo.organization', 'o') + ->where('uo.isActive = :uoActive') + ->andWhere('u.isActive = :uActive') + ->andWhere('u.isDeleted = :uDeleted') + ->andWhere('o.isActive = :oActive') + ->andWhere('o.isDeleted = :oDeleted') + ->orderBy('o.name', 'ASC') + ->addOrderBy('u.surname', 'ASC') + ->setParameter('uoActive', true) + ->setParameter('uActive', true) + ->setParameter('uDeleted', false) + ->setParameter('oActive', true) + ->setParameter('oDeleted', false); - } + return $qb->getQuery()->getResult(); + } + + /** + * Same as above, filtered by a list of organization IDs. + * + * @param int[] $organizationIds + * @return UsersOrganizations[] + */ + public function findActiveWithUserAndOrganizationByOrganizationIds(array $organizationIds): array + { + if (empty($organizationIds)) { + return []; + } + + $qb = $this->createQueryBuilder('uo') + ->addSelect('u', 'o') + ->leftJoin('uo.users', 'u') + ->leftJoin('uo.organization', 'o') + ->where('uo.isActive = :uoActive') + ->andWhere('u.isActive = :uActive') + ->andWhere('u.isDeleted = :uDeleted') + ->andWhere('o.isActive = :oActive') + ->andWhere('o.isDeleted = :oDeleted') + ->andWhere('o.id IN (:orgIds)') + ->orderBy('o.name', 'ASC') + ->addOrderBy('u.surname', 'ASC') + ->setParameter('uoActive', true) + ->setParameter('uActive', true) + ->setParameter('uDeleted', false) + ->setParameter('oActive', true) + ->setParameter('oDeleted', false) + ->setParameter('orgIds', $organizationIds); + + return $qb->getQuery()->getResult(); + } +} diff --git a/src/Service/UserService.php b/src/Service/UserService.php index 4bc3779..a26e42b 100644 --- a/src/Service/UserService.php +++ b/src/Service/UserService.php @@ -3,9 +3,8 @@ namespace App\Service; -use App\Entity\Organizations; -use App\Entity\Roles; use App\Entity\User; +use App\Entity\UserOrganizatonApp; use App\Entity\UsersOrganizations; use Doctrine\ORM\EntityManagerInterface; use League\Bundle\OAuth2ServerBundle\Model\AccessToken; @@ -61,4 +60,95 @@ class UserService return false; } + + /** + * Returns UsersOrganizations rows joined with User and Organization, grouped by organization name. + * @return array }> + */ + private function groupUserOrganizationsByOrganizationFromRows(array $rows): array + { + $grouped = []; + foreach ($rows as $userOrg) { + $organization = $userOrg->getOrganization(); + $user = $userOrg->getUsers(); + $orgName = $organization?->getName() ?? 'No Organization'; + + if (!isset($grouped[$orgName])) { + $grouped[$orgName] = [ + 'organization' => $organization, + 'users' => [], + ]; + } + + $grouped[$orgName]['users'][] = [ + 'users' => $user, + 'userOrganization' => $userOrg, + 'is_connected' => $this->isUserConnected($user->getEmail()), + ]; + } + + return $grouped; + } + + /** + * Returns users that have no UsersOrganizations mapping. + * @return array + */ + private function buildUsersWithoutOrganization(): array + { + $userRepository = $this->entityManager->getRepository(User::class); + $users = $userRepository->findActiveUsersWithoutOrganization(); + + $list = []; + foreach ($users as $user) { + $list[] = [ + 'users' => $user, + 'userOrganization' => null, + 'is_connected' => $this->isUserConnected($user->getEmail()), + ]; + } + + return $list; + } + + /** + * Public API: build the final structure for the index view. + * Groups users by organization and adds a "No Organization" group for users without mapping. + * @return array }> + */ + public function getUsersGroupedForIndex(): array + { + $usersOrganizationsRepo = $this->entityManager->getRepository(UsersOrganizations::class); + $rows = $usersOrganizationsRepo->findActiveWithUserAndOrganization(); + + $grouped = $this->groupUserOrganizationsByOrganizationFromRows($rows); + $noOrgUsers = $this->buildUsersWithoutOrganization(); + + if (!empty($noOrgUsers)) { + $grouped['No Organization'] = [ + 'organization' => null, + 'users' => $noOrgUsers, + ]; + } + + ksort($grouped); + return $grouped; + } + + /** + * For admins: return users grouped only for organizations where the given user is ADMIN. + * @return array }> + */ + public function getUsersGroupedForAdmin(User $adminUser): array + { + $uoaRepo = $this->entityManager->getRepository(UserOrganizatonApp::class); + $orgIds = $uoaRepo->findAdminOrganizationIdsForUser($adminUser); + + $uoRepo = $this->entityManager->getRepository(UsersOrganizations::class); + $rows = $uoRepo->findActiveWithUserAndOrganizationByOrganizationIds($orgIds); + + $grouped = $this->groupUserOrganizationsByOrganizationFromRows($rows); + ksort($grouped); + return $grouped; + } } diff --git a/templates/elements/menu.html.twig b/templates/elements/menu.html.twig index 82e78d4..4f6d589 100644 --- a/templates/elements/menu.html.twig +++ b/templates/elements/menu.html.twig @@ -37,23 +37,23 @@ {% endif %} diff --git a/templates/user/index.html.twig b/templates/user/index.html.twig index 2015b4b..431f328 100644 --- a/templates/user/index.html.twig +++ b/templates/user/index.html.twig @@ -6,13 +6,31 @@

Gestion Utilisateurs

- Ajouter un utilisateur + {% if is_granted('ROLE_SUPER_ADMIN') %} + Ajouter un utilisateur + {% endif %}
- {% for org in usersByOrganization %} - {% include 'user/userList.html.twig' with { - title: org.organization_name|capitalize - } %} - {% endfor %} + {% if is_granted('ROLE_SUPER_ADMIN') or is_granted('ROLE_ADMIN') %} + {% if usersByOrganization|length == 0 %} +
+

Aucun utilisateur trouvé

+

Il n'y a actuellement aucun utilisateur correspondant à votre périmètre.

+
+ {% else %} + {% for orgName, orgData in usersByOrganization %} + {% include 'user/userList.html.twig' with { + title: orgName, + org: orgData, + organizationId: orgData.organization ? orgData.organization.id : null + } %} + {% endfor %} + {% endif %} + {% else %} +
+

Accès limité

+

Vous n'avez pas les permissions nécessaires pour voir la liste des utilisateurs.

+
+ {% endif %}
{% endblock %} \ No newline at end of file diff --git a/templates/user/userList.html.twig b/templates/user/userList.html.twig index 242c5dc..57c6026 100644 --- a/templates/user/userList.html.twig +++ b/templates/user/userList.html.twig @@ -5,7 +5,9 @@ {% if title is defined %}

{{ title }}

- + {% if organizationId %} +{# ID: {{ organizationId }}#} + {% endif %}
{% endif %}
@@ -22,46 +24,47 @@ - {% if org|length == 0 %} - - Aucun utilisateur trouvé. - - {% elseif org.users|length == 0 %} + {% if org.users|length == 0 %} - Aucun utilisateur trouvé. + Aucun utilisateur trouvé dans cette organisation. {% else %} - {% for user in org.users %} + {% for userData in org.users %} - {% if user.users.pictureUrl %} - User profile pic + {% else %} +
+ {{ userData.users.name|first|upper }}{{ userData.users.surname|first|upper }} +
{% endif %} - {{ user.users.surname }} - {{ user.users.name }} - {{ user.users.email }} + {{ userData.users.surname }} + {{ userData.users.name }} + {{ userData.users.email }} - {% if user.is_connected %} + {% if userData.is_connected %} Actif {% else %} Inactif {% endif %} - {% if organizationId is defined %} - - {{ ux_icon('fa6-regular:eye', {height: '30px', width: '30px'}) }} - - {% else %} - - {{ ux_icon('fa6-regular:eye', {height: '30px', width: '30px'}) }} - - {% endif %} +{# {% if organizationId is defined and organizationId %}#} +{# #} +{# {{ ux_icon('fa6-regular:eye', {height: '30px', width: '30px'}) }}#} +{# #} +{# {% else %}#} +{# #} +{# {{ ux_icon('fa6-regular:eye', {height: '30px', width: '30px'}) }}#} +{# #} +{# {% endif %}#} {% endfor %}