diff --git a/assets/app.js b/assets/app.js index 3c3859d..c70ee04 100644 --- a/assets/app.js +++ b/assets/app.js @@ -9,6 +9,7 @@ import 'bootstrap/dist/css/bootstrap.min.css'; import './styles/app.css'; import './styles/navbar.css'; import './styles/sidebar.css'; +import './styles/choices.css' import 'bootstrap'; import './js/template.js'; diff --git a/assets/controllers/user_controller.js b/assets/controllers/user_controller.js index 55355c5..e85aa0f 100644 --- a/assets/controllers/user_controller.js +++ b/assets/controllers/user_controller.js @@ -1,77 +1,32 @@ -import {Controller} from '@hotwired/stimulus'; +import { Controller } from '@hotwired/stimulus'; import Choices from 'choices.js'; -/* -* The following line makes this controller "lazy": it won't be downloaded until needed -* See https://symfony.com/bundles/StimulusBundle/current/index.html#lazy-stimulus-controllers -*/ -/* stimulusFetch: 'lazy' */ export default class extends Controller { static values = { rolesArray: Array, selectedRoleIds: Array, - applicationsArray: Array, - selectedApplicationIds: Array } - // {value: 'choice1', label: 'Choice 1'}, - // {value: 'choice2', label: 'Choice 2'}, - // {value: 'choice3', label: 'Choice 3'}, + + static targets = ["select"]; + + connect() { + this.roleSelect(); + } roleSelect() { - const element = document.getElementById('roles'); - if (element) { + if (this.hasSelectTarget) { const choicesData = this.rolesArrayValue.map(role => ({ value: role.id, label: role.name, selected: this.selectedRoleIdsValue.includes(role.id) })); - const choices = new Choices(element, { + new Choices(this.selectTarget, { choices: choicesData, removeItemButton: true, placeholder: true, - placeholderValue: 'Ajouter un ou plusieurs rôles' + placeholderValue: 'Ajouter un ou plusieurs rôles', }); } } - - appSelect() { - const element = document.getElementById('applications'); - if (element) { - - const choicesData = this.applicationsArrayValue.map(app => ({ - value: app.id, - label: app.name, - customProperties: {icon: app.icon}, - selected: this.selectedApplicationIdsValue.includes(app.id) - })); - - const choices = new Choices(element, { - choices: choicesData, - removeItemButton: true, - placeholder: true, - placeholderValue: 'Ajouter une ou plusieurs applications', - }); - } - } - - connect() { - this.roleSelect(); - this.appSelect(); - - // Set choices after initialization - // choices.setValue(choicesData); - } - - -// Add custom controller actions here -// fooBar() { this.fooTarget.classList.toggle(this.bazClass) } - - disconnect() { - // Called anytime its element is disconnected from the DOM - // (on page change, when it's removed from or moved in the DOM, etc.) - - // Here you should remove all event listeners added in "connect()" - // this.fooTarget.removeEventListener('click', this._fooBar) - } -} +} \ No newline at end of file diff --git a/assets/styles/choices.css b/assets/styles/choices.css new file mode 100644 index 0000000..79cf189 --- /dev/null +++ b/assets/styles/choices.css @@ -0,0 +1,63 @@ +.choices { + font-size: 0.9rem; + width: 100%; +} + +/* Input style */ +.choices__inner { + background: #fff; + border: 1px solid var(--primary-blue-light); + border-radius: 0.375rem; /* same as Bootstrap `.form-control` */ + padding: 0.5rem; + min-height: 2.5rem; + box-shadow: none; + cursor: text; +} + +/* Placeholder */ +.choices__placeholder { + color: #6c757d; /* Bootstrap muted */ + opacity: 0.9; +} + +/* Selected items (tags) */ +.choices__list--multiple .choices__item { + background-color: var(--primary-blue-light) !important; + color: #fff; + border: none; + border-radius: 0.25rem; + padding: 0.25rem 0.5rem; + margin: 0.15rem; + font-size: 0.85rem; +} + +/* Remove "x" button */ +.choices__list--multiple .choices__item .choices__button { + border-left: 1px solid rgba(255,255,255,0.3); + margin-left: 0.25rem; + color: #fff; + opacity: 0.9; +} +.choices__list--multiple .choices__item .choices__button:hover { + opacity: 1; +} + +/* Dropdown list */ +.choices__list--dropdown { + border: 1px solid var(--primary-blue-light); + border-radius: 0.25rem; + box-shadow: 0 3px 6px rgba(0,0,0,0.1); + margin-top: 0.2rem; +} + +/* Dropdown options */ +.choices__list--dropdown .choices__item { + padding: 0.5rem; + font-size: 0.9rem; +} + +/* Hover/active in dropdown */ +.choices__list--dropdown .choices__item--highlighted { + background-color: var(--primary-blue-light); + color: #fff; +} \ No newline at end of file diff --git a/src/Controller/UserController.php b/src/Controller/UserController.php index 7853ef0..f8ea70f 100644 --- a/src/Controller/UserController.php +++ b/src/Controller/UserController.php @@ -2,7 +2,9 @@ namespace App\Controller; +use App\Entity\Apps; use App\Entity\Organizations; +use App\Entity\Roles; use App\Entity\User; use App\Entity\UserOrganizatonApp; use App\Entity\UsersOrganizations; @@ -91,7 +93,7 @@ class UserController extends AbstractController } } $uoa = $this->entityManager->getRepository(UserOrganizatonApp::class)->findBy(['userOrganization' => $uo, 'isActive' => true]); - $uoa = $this->userOrganizationAppService->groupUserOrganizationAppsByApplication($uoa); + $uoas = $this->userOrganizationAppService->groupUserOrganizationAppsByApplication($uoa); $this->actionService->createAction("View user information", $actingUser, null, $user->getUserIdentifier()); } catch (\Exception $e) { //ignore @@ -99,10 +101,9 @@ class UserController extends AbstractController } else { throw $this->createAccessDeniedException(self::ACCESS_DENIED); } - return $this->render('user/show.html.twig', [ 'user' => $user, - 'uoas' => $uoa ?? null, + 'uoas' => $uoas ?? null, 'orgs' => $orgs ?? null, 'organizationId' => $orgId ?? null, 'uoActive' => $uoActive ?? null// specific for single organization context and deactivate user from said org @@ -332,4 +333,46 @@ class UserController extends AbstractController $this->actionService->createAction("Delete user", $actingUser, null, $user->getUserIdentifier()); return $this->redirectToRoute('user_index'); } + + #[Route(path: '/application/roles/{id}', name: 'application_role', methods: ['GET', 'POST'])] + public function applicationRole(int $id, Request $request): Response + { + $this->denyAccessUnlessGranted("ROLE_ADMIN"); + $actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier()); + + if ($this->userService->hasAccessTo($actingUser, true)) { + $uo = $this->userOrganizationService->getByIdOrFail($id); + + $application = $this->entityManager->getRepository(Apps::class)->find($request->get('applicationId')); + if (!$application) { + throw $this->createNotFoundException(self::NOT_FOUND); + } + + $selectedRolesIds = $request->get('roles', []); + $roleUser = $this->entityManager->getRepository(Roles::class)->findOneBy(['name' => 'USER']); + if (!$roleUser) { + throw $this->createNotFoundException('Default role not found'); + } + + if (in_array($roleUser->getId(), $selectedRolesIds)) { + $this->userOrganizationAppService->syncRolesForUserOrganizationApp( + $uo, + $application, + $selectedRolesIds, + $actingUser + ); + } else { + $this->userOrganizationAppService->deactivateAllUserOrganizationsAppLinks($uo, $application); + } + + $user = $uo->getUsers(); + return $this->redirectToRoute('user_show', [ + 'user' => $user, + 'id' => $user->getId(), + 'organizationId'=> $uo->getOrganization()->getId() + ]); + } + + throw $this->createAccessDeniedException(); + } } diff --git a/src/Service/UserOrganizationAppService.php b/src/Service/UserOrganizationAppService.php index 2023b05..858f99a 100644 --- a/src/Service/UserOrganizationAppService.php +++ b/src/Service/UserOrganizationAppService.php @@ -2,6 +2,9 @@ namespace App\Service; +use App\Entity\Apps; +use App\Entity\Roles; +use App\Entity\User; use App\Entity\UserOrganizatonApp; use App\Entity\UsersOrganizations; use App\Service\ActionService; @@ -14,7 +17,8 @@ class UserOrganizationAppService } /** - * Groups UserOrganizationApp entities by their associated Application. + * Groups UserOrganizationApp entities by Application + * and prepares data for Twig. * * @param UserOrganizatonApp[] $userOrgApps * @return array @@ -30,19 +34,33 @@ class UserOrganizationAppService if (!isset($grouped[$appId])) { $grouped[$appId] = [ - 'userOrganization'=> $uoa->getUserOrganization(), - 'application' => $app, - 'roles' => [], + 'uoId' => $uoa->getUserOrganization()->getId(), + 'application' => $app, // you can still pass entity here + 'roles' => [], // selected roles for display + 'rolesArray' => [], // all possible roles + 'selectedRoleIds' => [], ]; } $grouped[$appId]['roles'][] = [ 'id' => $roleEntity->getId(), - 'name' => $roleEntity->getName(), // adjust to your Role entity fields + 'name' => $roleEntity->getName(), ]; + $grouped[$appId]['selectedRoleIds'][] = $roleEntity->getId(); + } + + // roles are the same for all apps → load once, inject into each appGroup + $allRoles = $this->entityManager->getRepository(Roles::class)->findAll(); + + foreach ($grouped as &$appGroup) { + foreach ($allRoles as $role) { + $appGroup['rolesArray'][] = [ + 'id' => $role->getId(), + 'name' => $role->getName(), + ]; + } } - // if you want a simple indexed array instead of associative keyed by appId return array_values($grouped); } @@ -52,9 +70,13 @@ class UserOrganizationAppService * @param UsersOrganizations $userOrganization * @return void */ - public function deactivateAllUserOrganizationsAppLinks(UsersOrganizations $userOrganization): void + public function deactivateAllUserOrganizationsAppLinks(UsersOrganizations $userOrganization, Apps $app = null): void { - $uoas = $this->entityManager->getRepository(UserOrganizatonApp::class)->findBy(['userOrganization' => $userOrganization, 'isActive' => true]); + if($app) { + $uoas = $this->entityManager->getRepository(UserOrganizatonApp::class)->findBy(['userOrganization' => $userOrganization, 'application' => $app, 'isActive' => true]); + } else { + $uoas = $this->entityManager->getRepository(UserOrganizatonApp::class)->findBy(['userOrganization' => $userOrganization, 'isActive' => true]); + } foreach ($uoas as $uoa) { $uoa->setIsActive(false); $this->actionService->createAction("Deactivate UOA link", $userOrganization->getUsers(), @@ -62,4 +84,71 @@ class UserOrganizationAppService $this->entityManager->persist($uoa); } } + + public function syncRolesForUserOrganizationApp( + UsersOrganizations $uo, + Apps $application, + array $selectedRoleIds, + User $actingUser + ): void { + $repo = $this->entityManager->getRepository(UserOrganizatonApp::class); + $currentLinks = $repo->findBy([ + 'userOrganization' => $uo, + 'application' => $application, + ]); + + $currentRoleIds = []; + foreach ($currentLinks as $uoa) { + $roleId = $uoa->getRole()->getId(); + $currentRoleIds[] = $roleId; + + if (in_array($roleId, $selectedRoleIds)) { + if (!$uoa->isActive()) { + $uoa->setIsActive(true); + $this->entityManager->persist($uoa); + $this->actionService->createAction( + "Re-activate user role for application", + $actingUser, + $uo->getOrganization(), + "App: {$application->getName()}, Role: {$uoa->getRole()->getName()} for user {$uo->getUsers()->getUserIdentifier()}" + ); + } + } else { + if ($uoa->isActive()) { + $uoa->setIsActive(false); + $this->entityManager->persist($uoa); + + $this->actionService->createAction( + "Deactivate user role for application", + $actingUser, + $uo->getOrganization(), + "App: {$application->getName()}, Role: {$uoa->getRole()->getName()} for user {$uo->getUsers()->getUserIdentifier()}" + ); + } + } + } + + // Add missing roles + foreach ($selectedRoleIds as $roleId) { + if (!in_array($roleId, $currentRoleIds)) { + $role = $this->entityManager->getRepository(Roles::class)->find($roleId); + if ($role) { + $newUoa = new UserOrganizatonApp(); + $newUoa->setUserOrganization($uo); + $newUoa->setApplication($application); + $newUoa->setRole($role); + $newUoa->setIsActive(true); + + $this->entityManager->persist($newUoa); + $this->actionService->createAction("New user role for application", + $actingUser, + $uo->getOrganization(), + "App: {$application->getName()}, Role: {$role->getName()} for user {$uo->getUsers()->getUserIdentifier()}"); + } + } + } + + $this->entityManager->flush(); + } + } diff --git a/src/Service/UserOrganizationService.php b/src/Service/UserOrganizationService.php index 8000b12..05508a1 100644 --- a/src/Service/UserOrganizationService.php +++ b/src/Service/UserOrganizationService.php @@ -9,6 +9,7 @@ use App\Entity\UsersOrganizations; use App\Service\ActionService; use \App\Service\UserOrganizationAppService; use Doctrine\ORM\EntityManagerInterface; +use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; /** * Service pour la gestion des organisations d'utilisateurs. @@ -43,5 +44,14 @@ readonly class UserOrganizationService } } + public function getByIdOrFail(int $id): UsersOrganizations + { + $uo = $this->entityManager->getRepository(UsersOrganizations::class)->find($id); + if (!$uo) { + throw new NotFoundHttpException("UserOrganization not found"); + } + return $uo; + } + } diff --git a/templates/user/application/information.html.twig b/templates/user/application/information.html.twig index 24ef4a3..1d498ba 100644 --- a/templates/user/application/information.html.twig +++ b/templates/user/application/information.html.twig @@ -14,41 +14,30 @@
Description : {{ uoa.application.description|default('Aucune description disponible.') }}
- {% if roles|length is not null %} -Rôles : - - {% for role in roles %} - {% if role.name == "SUPER ADMIN" %} - {{ role.name|capitalize }} - {% elseif role.name == "ADMIN" %} - {{ role.name|capitalize }} - {% else %} - {{ role.name|capitalize }} - {% endif %} - {% if not loop.last %} - {% endif %} - {% else %} -
Aucun rôle attribué.
- {% endfor %} - -