From cfe89f58db0a428a8ffd55e612a8a5c516acc26d Mon Sep 17 00:00:00 2001 From: Charles Date: Fri, 25 Jul 2025 10:30:00 +0200 Subject: [PATCH] edit user roles and app per organization --- README.MD | 9 +- assets/app.js | 3 +- assets/controllers.json | 2 +- assets/controllers/user_controller.js | 99 ++++++++++++++++ composer.lock | 12 +- importmap.php | 7 ++ migrations/Version20250724133531.php | 52 +++++++++ migrations/Version20250725065027.php | 56 +++++++++ src/Controller/UserController.php | 92 ++++++++++++--- src/Form/UserOrganization.php | 27 +++++ .../UserOrganizationAppRepository.php | 43 +++++++ .../UserOrganizationRolesRepository.php | 43 +++++++ src/Service/UserOrganizationService.php | 108 ++++++++++++++++-- symfony.lock | 3 +- .../userOrganizationInformation.html.twig | 25 ++-- templates/user/index.html.twig | 2 +- templates/user/organization/edit.html.twig | 50 ++++++++ templates/user/profile.html.twig | 2 +- 18 files changed, 587 insertions(+), 48 deletions(-) create mode 100644 assets/controllers/user_controller.js create mode 100644 migrations/Version20250724133531.php create mode 100644 migrations/Version20250725065027.php create mode 100644 src/Form/UserOrganization.php create mode 100644 src/Repository/UserOrganizationAppRepository.php create mode 100644 src/Repository/UserOrganizationRolesRepository.php create mode 100644 templates/user/organization/edit.html.twig diff --git a/README.MD b/README.MD index 642dff3..b0d5014 100644 --- a/README.MD +++ b/README.MD @@ -10,7 +10,7 @@ - Les icones sont gérées via symfony UX (https://ux.symfony.com/icons) - Les icones sont prises en prioritées dans la bibliothèque bootstrap - Les icones n'éxistants pas dans cette bibliothèques seront prises en priorité dans fontawesome regular (pour une cohérence visuelle) - - Sinon privilégier la bibliothèque ayant le visuel le plus proche + - Sinon privilégier la bibliothèque ayant le visuel le plus proche ### Version 0.1 : (17/03/2025) - Contient la logique de login mot de passe avec une entité user (email et password seuelement) @@ -19,5 +19,12 @@ - Une base de template est gérée pour toutes les pages de l'application aya,t besoin de l'entête et du menu général - Une ébauche de page d'accueil est en cours +### Installation +#### Choices.js +```bash + php bin/console importmap:require choices.js + php bin/console importmap:require choices.js/public/assets/styles/choices.min.css +``` + diff --git a/assets/app.js b/assets/app.js index 3f18426..3c3859d 100644 --- a/assets/app.js +++ b/assets/app.js @@ -15,4 +15,5 @@ import './js/template.js'; import './js/off_canvas.js'; import './js/hoverable-collapse.js'; import './js/cookies.js'; - +import 'choices.js'; +import 'choices.js/public/assets/styles/choices.min.css'; diff --git a/assets/controllers.json b/assets/controllers.json index 3ed9c65..480ad64 100644 --- a/assets/controllers.json +++ b/assets/controllers.json @@ -11,7 +11,7 @@ }, "@symfony/ux-turbo": { "turbo-core": { - "enabled": false, + "enabled": true, "fetch": "eager" }, "mercure-turbo-stream": { diff --git a/assets/controllers/user_controller.js b/assets/controllers/user_controller.js new file mode 100644 index 0000000..fd6eeca --- /dev/null +++ b/assets/controllers/user_controller.js @@ -0,0 +1,99 @@ +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'}, + + roleSelect() { + const element = document.getElementById('roles'); + if (element) { + const choicesData = this.rolesArrayValue.map(role => ({ + value: role.id, + label: role.name, + selected: this.selectedRoleIdsValue.includes(role.id) + })); + + const choices = new Choices(element, { + choices: choicesData, + removeItemButton: true, + placeholder: true, + 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', + // callbackOnCreateTemplates: function(template) { + // return { + // // Custom rendering for dropdown choices + // choice: (classNames, data) => { + // return template(` + //
0 ? 'role="treeitem"' : 'role="option"'} > + // ${data.customProperties && data.customProperties.icon ? `` : ''} + // ${data.label} + //
+ // `); + // }, + // // Custom rendering for selected items + // item: (classNames, data) => { + // return template(` + //
+ // ${data.customProperties && data.customProperties.icon ? `` : ''} + // ${data.label} + //
+ // `); + // } + }) + } + }; + // } + // } + + 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) + } +} diff --git a/composer.lock b/composer.lock index 68ba93a..014e148 100644 --- a/composer.lock +++ b/composer.lock @@ -2423,16 +2423,16 @@ }, { "name": "phpstan/phpdoc-parser", - "version": "2.1.0", + "version": "2.2.0", "source": { "type": "git", "url": "https://github.com/phpstan/phpdoc-parser.git", - "reference": "9b30d6fd026b2c132b3985ce6b23bec09ab3aa68" + "reference": "b9e61a61e39e02dd90944e9115241c7f7e76bfd8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/9b30d6fd026b2c132b3985ce6b23bec09ab3aa68", - "reference": "9b30d6fd026b2c132b3985ce6b23bec09ab3aa68", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/b9e61a61e39e02dd90944e9115241c7f7e76bfd8", + "reference": "b9e61a61e39e02dd90944e9115241c7f7e76bfd8", "shasum": "" }, "require": { @@ -2464,9 +2464,9 @@ "description": "PHPDoc parser with support for nullable, intersection and generic types", "support": { "issues": "https://github.com/phpstan/phpdoc-parser/issues", - "source": "https://github.com/phpstan/phpdoc-parser/tree/2.1.0" + "source": "https://github.com/phpstan/phpdoc-parser/tree/2.2.0" }, - "time": "2025-02-19T13:28:12+00:00" + "time": "2025-07-13T07:04:09+00:00" }, { "name": "psr/cache", diff --git a/importmap.php b/importmap.php index a5326d8..5111ef4 100644 --- a/importmap.php +++ b/importmap.php @@ -35,4 +35,11 @@ return [ 'version' => '5.3.5', 'type' => 'css', ], + 'choices.js' => [ + 'version' => '11.1.0', + ], + 'choices.js/public/assets/styles/choices.min.css' => [ + 'version' => '11.1.0', + 'type' => 'css', + ], ]; diff --git a/migrations/Version20250724133531.php b/migrations/Version20250724133531.php new file mode 100644 index 0000000..4532054 --- /dev/null +++ b/migrations/Version20250724133531.php @@ -0,0 +1,52 @@ +addSql('CREATE TABLE user_organization_app (id SERIAL NOT NULL, applications_id INT DEFAULT NULL, organization_id INT DEFAULT NULL, users_id INT DEFAULT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX IDX_BEF66DF129A0022 ON user_organization_app (applications_id)'); + $this->addSql('CREATE INDEX IDX_BEF66DF132C8A3DE ON user_organization_app (organization_id)'); + $this->addSql('CREATE INDEX IDX_BEF66DF167B3B43D ON user_organization_app (users_id)'); + $this->addSql('CREATE TABLE user_organization_roles (id SERIAL NOT NULL, role_id INT DEFAULT NULL, users_id INT DEFAULT NULL, organization_id INT DEFAULT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX IDX_94FD2EFBD60322AC ON user_organization_roles (role_id)'); + $this->addSql('CREATE INDEX IDX_94FD2EFB67B3B43D ON user_organization_roles (users_id)'); + $this->addSql('CREATE INDEX IDX_94FD2EFB32C8A3DE ON user_organization_roles (organization_id)'); + $this->addSql('ALTER TABLE user_organization_app ADD CONSTRAINT FK_BEF66DF129A0022 FOREIGN KEY (applications_id) REFERENCES apps (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE user_organization_app ADD CONSTRAINT FK_BEF66DF132C8A3DE FOREIGN KEY (organization_id) REFERENCES organizations (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE user_organization_app ADD CONSTRAINT FK_BEF66DF167B3B43D FOREIGN KEY (users_id) REFERENCES "user" (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE user_organization_roles ADD CONSTRAINT FK_94FD2EFBD60322AC FOREIGN KEY (role_id) REFERENCES roles (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE user_organization_roles ADD CONSTRAINT FK_94FD2EFB67B3B43D FOREIGN KEY (users_id) REFERENCES "user" (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE user_organization_roles ADD CONSTRAINT FK_94FD2EFB32C8A3DE FOREIGN KEY (organization_id) REFERENCES organizations (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('CREATE SCHEMA public'); + $this->addSql('ALTER TABLE user_organization_app DROP CONSTRAINT FK_BEF66DF129A0022'); + $this->addSql('ALTER TABLE user_organization_app DROP CONSTRAINT FK_BEF66DF132C8A3DE'); + $this->addSql('ALTER TABLE user_organization_app DROP CONSTRAINT FK_BEF66DF167B3B43D'); + $this->addSql('ALTER TABLE user_organization_roles DROP CONSTRAINT FK_94FD2EFBD60322AC'); + $this->addSql('ALTER TABLE user_organization_roles DROP CONSTRAINT FK_94FD2EFB67B3B43D'); + $this->addSql('ALTER TABLE user_organization_roles DROP CONSTRAINT FK_94FD2EFB32C8A3DE'); + $this->addSql('DROP TABLE user_organization_app'); + $this->addSql('DROP TABLE user_organization_roles'); + } +} diff --git a/migrations/Version20250725065027.php b/migrations/Version20250725065027.php new file mode 100644 index 0000000..6629ba3 --- /dev/null +++ b/migrations/Version20250725065027.php @@ -0,0 +1,56 @@ +addSql('DROP SEQUENCE user_organization_roles_id_seq CASCADE'); + $this->addSql('DROP SEQUENCE user_organization_app_id_seq CASCADE'); + $this->addSql('ALTER TABLE user_organization_app DROP CONSTRAINT fk_bef66df129a0022'); + $this->addSql('ALTER TABLE user_organization_app DROP CONSTRAINT fk_bef66df132c8a3de'); + $this->addSql('ALTER TABLE user_organization_app DROP CONSTRAINT fk_bef66df167b3b43d'); + $this->addSql('ALTER TABLE user_organization_roles DROP CONSTRAINT fk_94fd2efb32c8a3de'); + $this->addSql('ALTER TABLE user_organization_roles DROP CONSTRAINT fk_94fd2efb67b3b43d'); + $this->addSql('ALTER TABLE user_organization_roles DROP CONSTRAINT fk_94fd2efbd60322ac'); + $this->addSql('DROP TABLE user_organization_app'); + $this->addSql('DROP TABLE user_organization_roles'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('CREATE SCHEMA public'); + $this->addSql('CREATE SEQUENCE user_organization_roles_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE SEQUENCE user_organization_app_id_seq INCREMENT BY 1 MINVALUE 1 START 1'); + $this->addSql('CREATE TABLE user_organization_app (id SERIAL NOT NULL, applications_id INT DEFAULT NULL, organization_id INT DEFAULT NULL, users_id INT DEFAULT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX idx_bef66df129a0022 ON user_organization_app (applications_id)'); + $this->addSql('CREATE INDEX idx_bef66df132c8a3de ON user_organization_app (organization_id)'); + $this->addSql('CREATE INDEX idx_bef66df167b3b43d ON user_organization_app (users_id)'); + $this->addSql('CREATE TABLE user_organization_roles (id SERIAL NOT NULL, role_id INT DEFAULT NULL, users_id INT DEFAULT NULL, organization_id INT DEFAULT NULL, PRIMARY KEY(id))'); + $this->addSql('CREATE INDEX idx_94fd2efb32c8a3de ON user_organization_roles (organization_id)'); + $this->addSql('CREATE INDEX idx_94fd2efb67b3b43d ON user_organization_roles (users_id)'); + $this->addSql('CREATE INDEX idx_94fd2efbd60322ac ON user_organization_roles (role_id)'); + $this->addSql('ALTER TABLE user_organization_app ADD CONSTRAINT fk_bef66df129a0022 FOREIGN KEY (applications_id) REFERENCES apps (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE user_organization_app ADD CONSTRAINT fk_bef66df132c8a3de FOREIGN KEY (organization_id) REFERENCES organizations (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE user_organization_app ADD CONSTRAINT fk_bef66df167b3b43d FOREIGN KEY (users_id) REFERENCES "user" (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE user_organization_roles ADD CONSTRAINT fk_94fd2efb32c8a3de FOREIGN KEY (organization_id) REFERENCES organizations (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE user_organization_roles ADD CONSTRAINT fk_94fd2efb67b3b43d FOREIGN KEY (users_id) REFERENCES "user" (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + $this->addSql('ALTER TABLE user_organization_roles ADD CONSTRAINT fk_94fd2efbd60322ac FOREIGN KEY (role_id) REFERENCES roles (id) NOT DEFERRABLE INITIALLY IMMEDIATE'); + } +} diff --git a/src/Controller/UserController.php b/src/Controller/UserController.php index ec892bb..e2ccf64 100644 --- a/src/Controller/UserController.php +++ b/src/Controller/UserController.php @@ -2,12 +2,16 @@ namespace App\Controller; +use App\Entity\Apps; +use App\Entity\Roles; use App\Entity\User; use App\Form\UserForm; +use App\Entity\UsersOrganizations; use App\Service\UserOrganizationService; use App\Service\UserService; use Doctrine\ORM\EntityManagerInterface; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; +use Symfony\Component\Asset\Packages; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Attribute\Route; @@ -15,11 +19,12 @@ use Symfony\Component\Routing\Attribute\Route; #[Route(path: '/user', name: 'user_')] class UserController extends AbstractController { - private const NOT_FOUND = 'User not found'; + private const NOT_FOUND = 'Entity not found'; + public function __construct( private readonly UserOrganizationService $userOrganizationService, - private readonly EntityManagerInterface $entityManager, - private readonly UserService $userService) + private readonly EntityManagerInterface $entityManager, + private readonly UserService $userService) { } @@ -199,23 +204,84 @@ class UserController extends AbstractController return new Response('Unauthorized', Response::HTTP_UNAUTHORIZED); } - #Route('/organizationsUserEdit/{id}', name: 'organization_user_edit', requirements: ['id' => '\d+'], methods: ['POST'])] - public function organizationUserEdit(int $id, Request $request, EntityManagerInterface $entityManager): Response + /** + * Update organization user /userOrganizationEdit/{id} - Update organization user + * The id parameter is the ID of the UsersOrganizations entity. + */ + #[Route('/userOrganizationEdit/{id}', name: 'organization_edit', requirements: ['id' => '\d+'], methods: ['GET', 'POST'])] + public function userOrganizationEdit(int $id, Request $request, EntityManagerInterface $entityManager, Packages $packages): Response { - if (!$this->isGranted('ROLE_SUPER_ADMIN')) { - throw $this->createAccessDeniedException('Access denied'); + $this->denyAccessUnlessGranted('ROLE_SUPER_ADMIN'); + +// get the UsersOrganizations entity by ID and handle not found case same for user + $userOrganization = $entityManager->getRepository(UsersOrganizations::class)->find($id); + if (!$userOrganization) { + throw $this->createNotFoundException(self::NOT_FOUND); + } + $user = $userOrganization->getUsers() ?? throw $this->createNotFoundException(self::NOT_FOUND); + $organization = $userOrganization->getOrganization() ?? throw $this->createNotFoundException(self::NOT_FOUND); + + //Handle the POST + if ($request->isMethod('POST')) { + // Get the selected roles and apps from the request + $selectedRoles = $request->request->all('roles'); + $selectedApps = $request->request->all('applications'); + +// order in important here. apps MUST be before roles + $this->userOrganizationService->setUserOrganizationsApps($user, $organization,$selectedApps); + $this->userOrganizationService->setUserOrganizations($user, $organization, $selectedRoles); + + + + // Redirect to the user profile after successful update + return $this->redirectToRoute('user_show', ['id' => $user->getId()]); } - $user = $entityManager->getRepository(User::class)->find($id); - if (!$user) { + //Overwrite the userOrganization with the userOrganizationsService for data consistency + // NULL pointer won't occur here because a valid UsersOrganizations entity was fetched above + $userOrganization = $this->userOrganizationService->getUserOrganizations($userOrganization->getUsers(), $userOrganization->getOrganization()->getId()); + + // Fetch all roles and apps + $roles = $entityManager->getRepository(Roles::class)->findAll(); + $apps = $entityManager->getRepository(Apps::class)->findAll(); + if (!$roles) { + throw $this->createNotFoundException(self::NOT_FOUND); + } + if (!$apps) { throw $this->createNotFoundException(self::NOT_FOUND); } - // Handle organization user edit logic here + // Map roles and apps to arrays for rendering + $rolesArray = array_map(static function ($role) { + return [ + 'id' => $role->getId(), + 'name' => $role->getName() + ]; + }, $roles); + $appsArray = []; + foreach ($apps as $app) { + $appsArray[] = [ + 'id' => $app->getId(), + 'name' => $app->getName(), + 'icon' => $packages->getUrl($app->getLogoUrl()), + ]; + } - return $this->redirectToRoute('user_show', ['id' => $user->getId()]); + // Map selected roles and apps to their IDs for the form + $selectedRoles = array_map(static function ($role) { + return $role->getId(); + }, $userOrganization[0]["roles"]); + $selectedApps = array_map(static function ($app) { + return $app->getId(); + }, $userOrganization[0]["apps"]); + + return $this->render('user/organization/edit.html.twig', [ + 'userOrganization' => $userOrganization, + 'user' => $user, + 'rolesArray' => $rolesArray, + 'selectedRoleIds' => $selectedRoles, + 'appsArray' => $appsArray, + 'selectedAppIds' => $selectedApps,]); } - - } diff --git a/src/Form/UserOrganization.php b/src/Form/UserOrganization.php new file mode 100644 index 0000000..d4fe7b9 --- /dev/null +++ b/src/Form/UserOrganization.php @@ -0,0 +1,27 @@ +add('admin' , CheckboxType::class, [ + 'label' => 'Admin', + 'required' => false]) + ->add('application' , ChoiceType::class, [ + 'label' => 'Application', + 'choices' => [ + 'Application 1' => 'app1', + 'Application 2' => 'app2', + 'Application 3' => 'app3', + ]]); + } + +} diff --git a/src/Repository/UserOrganizationAppRepository.php b/src/Repository/UserOrganizationAppRepository.php new file mode 100644 index 0000000..8776484 --- /dev/null +++ b/src/Repository/UserOrganizationAppRepository.php @@ -0,0 +1,43 @@ + + */ +class UserOrganizationAppRepository extends ServiceEntityRepository +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, UserOrganizationApp::class); + } + +// /** +// * @return UserOrganizationApp[] Returns an array of UserOrganizationApp objects +// */ +// public function findByExampleField($value): array +// { +// return $this->createQueryBuilder('u') +// ->andWhere('u.exampleField = :val') +// ->setParameter('val', $value) +// ->orderBy('u.id', 'ASC') +// ->setMaxResults(10) +// ->getQuery() +// ->getResult() +// ; +// } + +// public function findOneBySomeField($value): ?UserOrganizationApp +// { +// return $this->createQueryBuilder('u') +// ->andWhere('u.exampleField = :val') +// ->setParameter('val', $value) +// ->getQuery() +// ->getOneOrNullResult() +// ; +// } +} diff --git a/src/Repository/UserOrganizationRolesRepository.php b/src/Repository/UserOrganizationRolesRepository.php new file mode 100644 index 0000000..bcad976 --- /dev/null +++ b/src/Repository/UserOrganizationRolesRepository.php @@ -0,0 +1,43 @@ + + */ +class UserOrganizationRolesRepository extends ServiceEntityRepository +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, UserOrganizationRoles::class); + } + + // /** + // * @return UserOrganizationRoles[] Returns an array of UserOrganizationRoles objects + // */ + // public function findByExampleField($value): array + // { + // return $this->createQueryBuilder('u') + // ->andWhere('u.exampleField = :val') + // ->setParameter('val', $value) + // ->orderBy('u.id', 'ASC') + // ->setMaxResults(10) + // ->getQuery() + // ->getResult() + // ; + // } + + // public function findOneBySomeField($value): ?UserOrganizationRoles + // { + // return $this->createQueryBuilder('u') + // ->andWhere('u.exampleField = :val') + // ->setParameter('val', $value) + // ->getQuery() + // ->getOneOrNullResult() + // ; + // } +} diff --git a/src/Service/UserOrganizationService.php b/src/Service/UserOrganizationService.php index f927ee8..49ddf19 100644 --- a/src/Service/UserOrganizationService.php +++ b/src/Service/UserOrganizationService.php @@ -2,6 +2,8 @@ namespace App\Service; +use App\Entity\Apps; +use App\Entity\Organizations; use App\Entity\Roles; use App\Entity\User; use App\Entity\UsersOrganizations; @@ -9,16 +11,19 @@ use Doctrine\ORM\EntityManagerInterface; readonly class UserOrganizationService { - public function __construct(private readonly EntityManagerInterface $entityManager) {} + public function __construct(private readonly EntityManagerInterface $entityManager) + { + } /** * Returns all organizations the given user belongs to, * including the unique roles and apps the user has in each organization. * - * @param User $user The user whose organizations are being fetched + * @param User $user The user whose organizations are being fetched + * @param int|null $organizationsId Optional organization ID to filter by * @return array */ - public function getUserOrganizations(User $user): array + public function getUserOrganizations(User $user, int $organizationsId = null): array { $userOrganizations = $this->entityManager ->getRepository(UsersOrganizations::class) @@ -28,8 +33,14 @@ readonly class UserOrganizationService foreach ($userOrganizations as $uo) { $orgId = $uo->getOrganization()->getId(); + // If $organizationsId is provided, skip other organizations + if ($organizationsId !== null && $orgId !== $organizationsId) { + continue; + } + // Initialize the organization entry if it doesn't exist $organizations[$orgId] = $organizations[$orgId] ?? $this->createEmptyOrganizationBucket($uo); + $organizations[$orgId]['uoId'] = $uo->getId(); // Aggregate roles & apps $this->addRole($organizations[$orgId]['roles'], $uo->getRole()); @@ -44,23 +55,23 @@ readonly class UserOrganizationService /** * Build the initial array structure for a fresh organization entry. * - * @param UsersOrganizations $link + * @param UsersOrganizations $link * @return array{organization:object, roles:Roles[], apps:array} */ private function createEmptyOrganizationBucket(UsersOrganizations $link): array { return [ 'organization' => $link->getOrganization(), - 'roles' => [], - 'apps' => [], + 'roles' => [], + 'apps' => [], ]; } /** * Add a Role entity to the roles array only if it is not already present (by ID). * - * @param Roles[] &$roles - * @param Roles|null $role + * @param Roles[] &$roles + * @param Roles|null $role */ private function addRole(array &$roles, ?Roles $role): void { @@ -78,8 +89,8 @@ readonly class UserOrganizationService /** * Merge one or many apps into the apps map, keeping only one entry per id. * - * @param array &$apps - * @param iterable $appsToAdd Collection returned by $userOrganizations->getApps() + * @param array &$apps + * @param iterable $appsToAdd Collection returned by $userOrganizations->getApps() */ private function addApps(array &$apps, iterable $appsToAdd): void { @@ -92,7 +103,7 @@ readonly class UserOrganizationService * Convert apps from associative maps (keyed by id) to plain indexed arrays, * so the final output is clean JSON-able. * - * @param array &$organizations + * @param array &$organizations */ private function normalizeAppsIndexes(array &$organizations): void { @@ -100,4 +111,79 @@ readonly class UserOrganizationService $org['apps'] = array_values($org['apps']); } } + + public function setUserOrganizations(User $user, Organizations $organization, array $selectedRoles): void + { + $repo = $this->entityManager->getRepository(UsersOrganizations::class); + + // 1. Get all current UsersOrganizations for this user/org + $currentUserOrgs = $repo->findBy([ + 'users' => $user, + 'organization' => $organization + ]); + + // 2. Build a map: roleId => UsersOrganizations entity + $currentRolesMap = []; + foreach ($currentUserOrgs as $uo) { + $currentRolesMap[$uo->getRole()->getId()] = $uo; + } + + // 3. Add new roles that are selected but not present + foreach ($selectedRoles as $roleId) { + if (!isset($currentRolesMap[$roleId])) { + $roleEntity = $this->entityManager->getRepository(Roles::class)->find($roleId); + if ($roleEntity) { + $newUserOrganization = new UsersOrganizations(); + $newUserOrganization->setUsers($user); + $newUserOrganization->setRole($roleEntity); + $newUserOrganization->setOrganization($organization); + $this->entityManager->persist($newUserOrganization); + } + } + // Remove from map so we know which ones to delete later + unset($currentRolesMap[$roleId]); + } + + // 4. Remove roles that are present but not selected (deactivate them) + foreach ($currentRolesMap as $uo) { + $uo->setIsActive(false); + $this->entityManager->persist($uo); + } + + $this->entityManager->flush(); + } + + public function setUserOrganizationsApps(User $user, Organizations $organization, array $selectedApps) + { + $apps = []; + $userOrganizations = $this->entityManager + ->getRepository(UsersOrganizations::class) + ->findAllDistinctOrganizationsByUserId($user->getId()); + foreach ($userOrganizations as $uo) { + $this->addApps($apps, $uo->getApps()); + } + + $roleUser = $this->entityManager->getRepository(Roles::class)->findOneBy(['name' => 'USER']); + $uoEntity = $this->entityManager + ->getRepository(UsersOrganizations::class) + ->findOneBy(['users' => $user, 'organization' => $organization, 'role' => $roleUser]); +// dd($roleUser); + // 1. Remove apps that are no longer selected + foreach ($uoEntity->getApps() as $existingApp) { + if (!in_array($existingApp->getId(), $selectedApps)) { + $uoEntity->removeApp($existingApp); + } + } + + // 2. Add newly selected apps (your existing logic) + foreach ($selectedApps as $appId) { + $appEntity = $this->entityManager->getRepository(Apps::class)->find($appId); + if ($appEntity && !$uoEntity->getApps()->contains($appEntity)) { + $uoEntity->addApp($appEntity); + } + } + + $this->entityManager->persist($uoEntity); + $this->entityManager->flush(); + } } diff --git a/symfony.lock b/symfony.lock index 0993096..781a3e1 100644 --- a/symfony.lock +++ b/symfony.lock @@ -291,7 +291,8 @@ "assets/bootstrap.js", "assets/controllers.json", "assets/controllers/csrf_protection_controller.js", - "assets/controllers/hello_controller.js" + "assets/controllers/hello_controller.js", + "assets/controllers/igg_controller.js" ] }, "symfony/translation": { diff --git a/templates/elements/userOrganizationInformation.html.twig b/templates/elements/userOrganizationInformation.html.twig index 277ed42..b1cfc72 100644 --- a/templates/elements/userOrganizationInformation.html.twig +++ b/templates/elements/userOrganizationInformation.html.twig @@ -1,18 +1,21 @@ {% block body %} -