organizations user information

This commit is contained in:
Charles 2025-07-29 15:55:55 +02:00
parent d2c20b9423
commit a3f993b858
12 changed files with 320 additions and 233 deletions

View File

@ -69,7 +69,7 @@ body {
} }
.content-wrapper { .content-wrapper {
background: #F5F7FF; background: #EEF0FD;
width: 100%; width: 100%;
-webkit-flex-grow: 1; -webkit-flex-grow: 1;
flex-grow: 1; flex-grow: 1;

View File

@ -4,6 +4,7 @@ namespace App\Controller;
use App\Entity\UsersOrganizations; use App\Entity\UsersOrganizations;
use App\Service\OrganizationsService; use App\Service\OrganizationsService;
use App\Service\UserOrganizationService;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Routing\Attribute\Route;
@ -18,7 +19,8 @@ class OrganizationController extends AbstractController
private const ACCESS_DENIED = 'Access denied'; private const ACCESS_DENIED = 'Access denied';
public function __construct(private readonly EntityManagerInterface $entityManager, public function __construct(private readonly EntityManagerInterface $entityManager,
private readonly OrganizationsService $organizationsService) private readonly OrganizationsService $organizationsService,
private readonly UserOrganizationService $usersOrganizationService)
{ {
} }
@ -75,7 +77,8 @@ class OrganizationController extends AbstractController
$newUsers = $this->entityManager->getRepository(UsersOrganizations::class)->getLastNewActiveUsersByOrganization($organization); $newUsers = $this->entityManager->getRepository(UsersOrganizations::class)->getLastNewActiveUsersByOrganization($organization);
$adminUsers = $this->entityManager->getRepository(UsersOrganizations::class)->getAdminUsersByOrganization($organization); $adminUsers = $this->entityManager->getRepository(UsersOrganizations::class)->getAdminUsersByOrganization($organization);
// reusing the method to avoid code duplication even though it returns an array of UsersOrganizations // reusing the method to avoid code duplication even though it returns an array of UsersOrganizations
$org = $this->entityManager->getRepository(UsersOrganizations::class)->findActiveUsersByOrganizations([$organization]); $org = $this->usersOrganizationService->findActiveUsersByOrganizations([$organization]);

View File

@ -36,8 +36,8 @@ class UserController extends AbstractController
public function index(EntityManagerInterface $entityManager): Response public function index(EntityManagerInterface $entityManager): Response
{ {
if ($this->isGranted('ROLE_SUPER_ADMIN')) { if ($this->isGranted('ROLE_SUPER_ADMIN')) {
$usersByOrganization = $entityManager->getRepository(UsersOrganizations::class)->getActiveUsersGroupedByOrganization(); $usersByOrganization = $this->userOrganizationService->getActiveUsersGroupedByOrganization();
// dd($usersByOrganization);
} else{ } else{
$user = $this->getUser(); $user = $this->getUser();
if (!$user) { if (!$user) {
@ -49,8 +49,7 @@ class UserController extends AbstractController
// if user is not admin in any organization, throw access denied // if user is not admin in any organization, throw access denied
throw $this->createNotFoundException(self::ACCESS_DENIED); throw $this->createNotFoundException(self::ACCESS_DENIED);
} }
$usersByOrganization = $this->entityManager->getRepository(UsersOrganizations::class) $usersByOrganization = $this->userOrganizationService->findActiveUsersByOrganizations($organizations);
->findActiveUsersByOrganizations($organizations);
} }
return $this->render('user/index.html.twig', [ return $this->render('user/index.html.twig', [
'usersByOrganization' => $usersByOrganization, 'usersByOrganization' => $usersByOrganization,

View File

@ -65,90 +65,8 @@ class UsersOrganizationsRepository extends ServiceEntityRepository
return array_map(fn($uo) => $uo->getOrganization(), $results); return array_map(fn($uo) => $uo->getOrganization(), $results);
} }
/**
* Get all active users grouped by organization.
* Users with no organization are grouped under 'autre'.
*
* @return array
*/
public function getActiveUsersGroupedByOrganization(): array
{
$users = $this->getEntityManager()->getRepository(User::class)->getAllActiveUsers();
$userOrgs = $this->getAllActiveUserOrganizationLinks();
$userToOrgs = $this->mapUserToOrganizations($userOrgs);
$orgs = [];
foreach ($users as $user) {
$userId = $user['id'];
if (isset($userToOrgs[$userId])) {
foreach ($userToOrgs[$userId] as $orgInfo) {
$orgId = $orgInfo['organization_id'];
if (!isset($orgs[$orgId])) {
$orgs[$orgId] = [
'organization_id' => $orgId,
'organization_name' => $orgInfo['organization_name'],
'users' => [],
];
}
$orgs[$orgId]['users'][$userId] = $user;
}
} else {
if (!isset($orgs['autre'])) {
$orgs['autre'] = [
'organization_id' => null,
'organization_name' => 'autre',
'users' => [],
];
}
$orgs['autre']['users'][$userId] = $user;
}
}
// Convert users arrays to indexed arrays
foreach ($orgs as &$org) {
$org['users'] = array_values($org['users']);
}
return array_values($orgs);
}
/**
* Get all active users for each organization in the given array.
*
* @param Organizations[] $organizations
* @return array
*/
public function findActiveUsersByOrganizations(array $organizations): array
{
if (empty($organizations)) {
return [];
}
$userOrgs = $this->getAllActiveUserOrganizationLinks($organizations);
$usersByOrg = [];
foreach ($userOrgs as $uo) {
$org = $uo->getOrganization();
$orgId = $org->getId();
if (!isset($usersByOrg[$orgId])) {
$usersByOrg[$orgId] = [
'organization_id' => $orgId,
'organization_name' => $org->getName(),
'users' => [],
];
}
$userId = $uo->getUsers()->getId();
$usersByOrg[$orgId]['users'][$userId] = $uo->getUsers();
}
// Convert users arrays to indexed arrays
foreach ($usersByOrg as &$org) {
$org['users'] = array_values($org['users']);
}
return array_values($usersByOrg);
}
/** /**
* Helper: Get all active UsersOrganizations links, optionally filtered by organizations. * Helper: Get all active UsersOrganizations links, optionally filtered by organizations.
@ -156,7 +74,7 @@ class UsersOrganizationsRepository extends ServiceEntityRepository
* @param Organizations[]|null $organizations * @param Organizations[]|null $organizations
* @return UsersOrganizations[] * @return UsersOrganizations[]
*/ */
private function getAllActiveUserOrganizationLinks(array $organizations = null): array public function getAllActiveUserOrganizationLinks(array $organizations = null): array
{ {
$qb = $this->createQueryBuilder('uo') $qb = $this->createQueryBuilder('uo')
->innerJoin('uo.organization', 'o') ->innerJoin('uo.organization', 'o')
@ -172,27 +90,6 @@ class UsersOrganizationsRepository extends ServiceEntityRepository
return $qb->getQuery()->getResult(); return $qb->getQuery()->getResult();
} }
/**
* Helper: Map userId to their organizations (id and name), avoiding duplicates.
*
* @param UsersOrganizations[] $userOrgs
* @return array
*/
private function mapUserToOrganizations(array $userOrgs): array
{
$userToOrgs = [];
foreach ($userOrgs as $uo) {
$userId = $uo->getUsers()->getId();
$org = $uo->getOrganization();
$orgId = $org->getId();
$orgName = $org->getName();
$userToOrgs[$userId][$orgId] = [
'organization_id' => $orgId,
'organization_name' => $orgName,
];
}
return $userToOrgs;
}
/** /**
* Get the last 10 new active users for a specific organization. * Get the last 10 new active users for a specific organization.

View File

@ -6,6 +6,7 @@ use App\Entity\Apps;
use App\Entity\Organizations; use App\Entity\Organizations;
use App\Entity\Roles; use App\Entity\Roles;
use App\Entity\User; use App\Entity\User;
use App\Service\UserService;
use App\Entity\UsersOrganizations; use App\Entity\UsersOrganizations;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
@ -20,7 +21,8 @@ readonly class UserOrganizationService
* *
* @param EntityManagerInterface $entityManager Le gestionnaire d'entités Doctrine * @param EntityManagerInterface $entityManager Le gestionnaire d'entités Doctrine
*/ */
public function __construct(private readonly EntityManagerInterface $entityManager) public function __construct(private readonly EntityManagerInterface $entityManager,
private readonly UserService $userService)
{ {
} }
@ -335,4 +337,126 @@ readonly class UserOrganizationService
} }
$this->entityManager->flush(); $this->entityManager->flush();
} }
/**
* Get all active users grouped by organization.
* Users with no organization are grouped under 'autre'.
*
* @return array
*/
public function getActiveUsersGroupedByOrganization(): array
{
$users = $this->entityManager->getRepository(User::class)->getAllActiveUsers();
$userOrgs = $this->entityManager->getRepository(UsersOrganizations::class)->getAllActiveUserOrganizationLinks();
$userToOrgs = $this->mapUserToOrganizations($userOrgs);
$orgs = [];
foreach ($users as $user) {
$userId = $user['id'];
if (isset($userToOrgs[$userId])) {
foreach ($userToOrgs[$userId] as $orgInfo) {
$orgId = $orgInfo['organization_id'];
if (!isset($orgs[$orgId])) {
$orgs[$orgId] = [
'organization_id' => $orgId,
'organization_name' => $orgInfo['organization_name'],
'users' => [],
];
}
// $orgs[$orgId]['users'][$userId] = $user;
$orgs[$orgId]['users'][$userId] = [
'users' => $user,
'is_connected' => $this->userService->isUserConnected($user['email'])
];
}
} else {
if (!isset($orgs['autre'])) {
$orgs['autre'] = [
'organization_id' => null,
'organization_name' => 'autre',
'users' => [],
];
}
$orgs['autre']['users'][$userId] = [
'users' => $user,
'is_connected' => $this->userService->isUserConnected($user['email'])
];
}
}
// Convert users arrays to indexed arrays
foreach ($orgs as &$org) {
$org['users'] = array_values($org['users']);
}
return array_values($orgs);
}
/**
* Get all active users for each organization in the given array.
*
* @param Organizations[] $organizations
* @return array
*/
public function findActiveUsersByOrganizations(array $organizations): array
{
if (empty($organizations)) {
return [];
}
$userOrgs = $this->entityManager->getRepository(UsersOrganizations::class)->getAllActiveUserOrganizationLinks($organizations);
$usersByOrg = [];
foreach ($userOrgs as $uo) {
$org = $uo->getOrganization();
$orgId = $org->getId();
if (!isset($usersByOrg[$orgId])) {
$usersByOrg[$orgId] = [
'organization_id' => $orgId,
'organization_name' => $org->getName(),
'users' => [],
];
}
$user = $uo->getUsers();
$userId = $user->getId();
// Add connection status to user data
$usersByOrg[$orgId]['users'][$userId] = [
'users' => $user,
'is_connected' => $this->userService->isUserConnected($user->getUserIdentifier())
];
}
// Convert users arrays to indexed arrays
foreach ($usersByOrg as &$org) {
$org['users'] = array_values($org['users']);
}
return array_values($usersByOrg);
}
/**
* Helper: Map userId to their organizations (id and name), avoiding duplicates.
*
* @param UsersOrganizations[] $userOrgs
* @return array
*/
private function mapUserToOrganizations(array $userOrgs): array
{
$userToOrgs = [];
foreach ($userOrgs as $uo) {
$userId = $uo->getUsers()->getId();
$org = $uo->getOrganization();
$orgId = $org->getId();
$orgName = $org->getName();
$userToOrgs[$userId][$orgId] = [
'organization_id' => $orgId,
'organization_name' => $orgName,
];
}
return $userToOrgs;
}
} }

View File

@ -8,6 +8,7 @@ use App\Entity\Roles;
use App\Entity\User; use App\Entity\User;
use App\Entity\UsersOrganizations; use App\Entity\UsersOrganizations;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use League\Bundle\OAuth2ServerBundle\Model\AccessToken;
class UserService class UserService
{ {
@ -59,4 +60,29 @@ class UserService
'roleId' => $roleAdmin[0]->getId()])); 'roleId' => $roleAdmin[0]->getId()]));
} }
/**
* Check if the user is currently connected.
* This method check if the user is currently connected to one of the applications.
*
* @param String $userIdentifier
* @return bool
*/
public function isUserConnected(string $userIdentifier): bool
{
$now = new \DateTimeImmutable('now', new \DateTimeZone('Europe/Paris'));
$tokens = $this->entityManager->getRepository(AccessToken::class)->findBy([
'userIdentifier' => $userIdentifier,
'revoked' => false
]);
foreach ($tokens as $token) {
// Assuming $token->getExpiry() returns a DateTimeInterface
if ($token->getExpiry() > $now) {
return true;
}
}
return false;
}
} }

View File

@ -1,8 +1,10 @@
{% block body %} {% block body %}
<h3> {{ title }}</h3>
<div class="card">
<div class="card border-0">
<div class="card-header d-flex justify-content-between align-items-center">
<h3>{{ title }}</h3>
</div>
<div class="card-body"> <div class="card-body">
{# {% if activities|length == 0 %}#} {# {% if activities|length == 0 %}#}
{# <p>Aucune activité récente.</p>#} {# <p>Aucune activité récente.</p>#}
@ -10,11 +12,24 @@
<ul class="list-group"> <ul class="list-group">
{# {% for activity in activities %}#} {# {% for activity in activities %}#}
<li class="list-group-item"> <li class="list-group-item">
{# <strong>{{ activity.timestamp|date('Y-m-d H:i') }}</strong> - #}
{# {{ activity.user.name }} a {{ activity.action }} sur {{ activity.targetType }}: #}
{# <a href="{{ path(activity.targetPath) }}">{{ activity.targetName }}</a>#}
<p> 5 mins ago</p> <p> 5 mins ago</p>
</li> </li>
<li class="list-group-item">
<p> 5 mins ago</p>
</li>
<li class="list-group-item">
<p> 5 mins ago</p>
</li>
<li class="list-group-item">
<p> 5 mins ago</p>
</li>
<li class="list-group-item">
<p> 5 mins ago</p>
</li>
<li class="list-group-item">
<p> 5 mins ago</p>
</li>
{# {% endfor %}#} {# {% endfor %}#}
</ul> </ul>
{# {% endif %}#} {# {% endif %}#}

View File

@ -8,15 +8,18 @@
{# <a href="{{ path('user_deactivate', {'id': user.id}) }}" class="btn btn-danger">Désactiver</a> #} {# <a href="{{ path('user_deactivate', {'id': user.id}) }}" class="btn btn-danger">Désactiver</a> #}
{% endif %} {% endif %}
</div> </div>
{# USER ROW#}
<div class="row">
<div class="col-9">
<div class="row mb-4"> <div class="row mb-4">
<div class="col-sm-6 mb-3 mb-sm-0"> <div class="col mb-3 mb-sm-0">
{% include 'user/userListSmall.html.twig' with { {% include 'user/userListSmall.html.twig' with {
title: 'Nouveaux utilisateurs', title: 'Nouveaux utilisateurs',
users: newUsers, users: newUsers,
empty_message: 'Aucun nouvel utilisateur trouvé.' empty_message: 'Aucun nouvel utilisateur trouvé.'
} %} } %}
</div> </div>
<div class="col-sm-6 mb-3 mb-sm-0"> <div class="col mb-3 mb-sm-0">
{% include 'user/userListSmall.html.twig' with { {% include 'user/userListSmall.html.twig' with {
title: 'Administrateurs', title: 'Administrateurs',
users: adminUsers, users: adminUsers,
@ -24,23 +27,26 @@
} %} } %}
</div> </div>
</div> </div>
<div class="m-auto">
{% include 'user/userList.html.twig' with { {% include 'user/userList.html.twig' with {
title: 'Mes utilisateurs', title: 'Mes utilisateurs',
} %} } %}
</div>
</div>
{# <div class="row">#} <div class="col-3 m-auto">
{# <div class="col-9 m-auto">#} {% include 'organization/activity.html.twig' with {
{# {% include 'user/userList.html.twig' with {#} title: 'Activités récentes',
{# title: 'Mes utilisateurs',#} empty_message: 'Aucune activité récente.'
{# } %}#} } %}
{# </div>#} </div>
{# <div class="col-3 m-auto">#}
{# {% include 'organization/activity.html.twig' with {#} </div>
{# title: 'Activités récentes',#}
{# empty_message: 'Aucune activité récente.'#} {# APPLICATION ROW#}
{# } %}#} <div class="row">
{# </div>#}
{# </div>#} </div>
</div> </div>
{% endblock %} {% endblock %}

View File

@ -10,8 +10,9 @@
</div> </div>
{% for org in usersByOrganization %} {% for org in usersByOrganization %}
<h2 class="mt-5 mb-3">{{ org.organization_name|title }}</h2> {% include 'user/userList.html.twig' with {
{% include 'user/userList.html.twig' %} title: org.organization_name|capitalize
} %}
{% endfor %} {% endfor %}
</div> </div>
{% endblock %} {% endblock %}

View File

@ -1,6 +1,6 @@
{% block body %} {% block body %}
<div class="card"> <div class="card border-0">
<div class="card-title shadow-sm p-3 d-flex justify-content-between align-items-center"> <div class="card-title shadow-sm p-3 d-flex justify-content-between align-items-center">
<h2>{{ user.surname|capitalize }} {{ user.name|capitalize }}</h2> <h2>{{ user.surname|capitalize }} {{ user.name|capitalize }}</h2>
<a href="{{ path('user_edit', {'id': user.id}) }}" class="btn btn-primary">Modifier</a> <a href="{{ path('user_edit', {'id': user.id}) }}" class="btn btn-primary">Modifier</a>

View File

@ -1,10 +1,17 @@
{% block body %} {% block body %}
<div class="card border-0 p-3 mb-4">
{% if title is defined %} {% if title is defined %}
<div class="card-title d-flex justify-content-between align-items-center ">
<h3>{{ title }}</h3> <h3>{{ title }}</h3>
</div>
{% endif %} {% endif %}
<table class="table align-middle shadow"> <div class="card-body">
<thead class="table-light shadow-sm">
<table class="table align-middle ">
<thead class="table-light">
<tr> <tr>
<th>Picture</th> <th>Picture</th>
<th>Surname</th> <th>Surname</th>
@ -23,23 +30,24 @@
{% for user in org.users %} {% for user in org.users %}
<tr> <tr>
<td> <td>
{% if user.pictureUrl %} {% if user.users.pictureUrl %}
<img src="{{ asset(user.pictureUrl) }}" alt="User profile pic" class="rounded-circle" style="width:40px; height:40px;"> <img src="{{ asset(user.users.pictureUrl) }}" alt="User profile pic"
class="rounded-circle"
style="width:40px; height:40px;">
{% endif %} {% endif %}
</td> </td>
<td>{{ user.surname }}</td> <td>{{ user.users.surname }}</td>
<td>{{ user.name }}</td> <td>{{ user.users.name }}</td>
<td>{{ user.email }}</td> <td>{{ user.users.email }}</td>
{# <td>#}
{# {% if user.isActive %}#}
{# <span class="badge bg-success">Actif</span>#}
{# {% else %}#}
{# <span class="badge bg-secondary">Inactif</span>#}
{# {% endif %}#}
{# </td>#}
<td> <span class="badge bg-success">Actif</span></td>
<td> <td>
<a href="{{ path('user_show', {'id': user.id}) }}" class="p-3 align-middle"> {% if user.is_connected %}
<span class="badge bg-success">Actif</span>
{% else %}
<span class="badge bg-secondary">Inactif</span>
{% endif %}
</td>
<td>
<a href="{{ path('user_show', {'id': user.users.id}) }}" class="p-3 align-middle">
<i class="icon-grid menu-icon color-primary"> <i class="icon-grid menu-icon color-primary">
{{ ux_icon('fa6-regular:eye', {height: '30px', width: '30px'}) }} {{ ux_icon('fa6-regular:eye', {height: '30px', width: '30px'}) }}
</a> </a>
@ -49,4 +57,6 @@
</tbody> </tbody>
</table> </table>
</div>
</div>
{% endblock %} {% endblock %}

View File

@ -1,4 +1,6 @@
<div class="card"> {% block body %}
<div class="card border-0">
<div class="card-title p-3 d-flex justify-content-between align-items-center "> <div class="card-title p-3 d-flex justify-content-between align-items-center ">
<h3>{{ title }}</h3> <h3>{{ title }}</h3>
</div> </div>
@ -21,12 +23,14 @@
<tr> <tr>
<td> <td>
{% if user.pictureUrl %} {% if user.pictureUrl %}
<img src="{{ asset(user.pictureUrl) }}" alt="User profile pic" class="rounded-circle" style="width:40px; height:40px;"> <img src="{{ asset(user.pictureUrl) }}" alt="User profile pic"
class="rounded-circle" style="width:40px; height:40px;">
{% endif %} {% endif %}
</td> </td>
<td>{{ user.email }}</td> <td>{{ user.email }}</td>
<td> <td>
<a href="{{ path('user_show', {'id': user.id}) }}" class="p-3 align-middle color-primary"> <a href="{{ path('user_show', {'id': user.id}) }}"
class="p-3 align-middle color-primary">
{{ ux_icon('fa6-regular:eye', {height: '30px', width: '30px'}) }} {{ ux_icon('fa6-regular:eye', {height: '30px', width: '30px'}) }}
</a> </a>
</td> </td>
@ -36,4 +40,6 @@
</tbody> </tbody>
</table> </table>
</div> </div>
</div> </div>
{% endblock %}