diff --git a/assets/controllers/user_controller.js b/assets/controllers/user_controller.js index 25ce4bf..864edb6 100644 --- a/assets/controllers/user_controller.js +++ b/assets/controllers/user_controller.js @@ -27,7 +27,7 @@ export default class extends base_controller { orgId: Number } - static targets = ["select", "statusButton", "modal", "userSelect", "appList"]; + static targets = ["select", "statusButton", "modal", "userSelect", "appList", "emailInput", "phoneInput", "nameInput", "surnameInput"]; connect() { this.roleSelect(); @@ -1055,4 +1055,69 @@ export default class extends base_controller { alert("Erreur réseau."); } } + + async openEditUserModal(event) { + const userId = event.currentTarget.dataset.id; + this.currentUserId = userId; + this.modal.show(); + + try { + // 1. Fetch all available apps using your shared base method + await this.fetchAndRenderApplications(this.appListTarget); + + // 2. Fetch specific user data WITH the orgId query parameter + // We use this.orgIdValue which is mapped to data-user-org-id-value + const response = await fetch(`/user/data/${userId}?orgId=${this.orgIdValue}`); + + if (!response.ok) throw new Error('Failed to fetch user data'); + + const user = await response.json(); + + // 3. Fill text inputs + this.emailInputTarget.value = user.email; + this.phoneInputTarget.value = user.phoneNumber || ''; + this.nameInputTarget.value = user.name; + this.surnameInputTarget.value = user.surname; + + // 4. Check the application boxes + const checkboxes = this.appListTarget.querySelectorAll('input[type="checkbox"]'); + + // Ensure we handle IDs as strings or numbers consistently + const activeAppIds = user.applicationIds.map(id => id.toString()); + + checkboxes.forEach(cb => { + cb.checked = activeAppIds.includes(cb.value.toString()); + }); + + } catch (error) { + console.error("Erreur lors du chargement des données utilisateur:", error); + alert("Impossible de charger les informations de l'utilisateur."); + } + } + + async submitEditUser(event) { + event.preventDefault(); + const formData = new FormData(event.target); + + // Force Uppercase on Surname as requested + formData.set('surname', formData.get('surname').toUpperCase()); + + try { + const response = await fetch(`/user/edit/${this.currentUserId}/ajax`, { + method: 'POST', + body: formData, + headers: { 'X-Requested-With': 'XMLHttpRequest' } + }); + + if (response.ok) { + this.modal.hide(); + location.reload(); + } else { + const result = await response.json(); + alert(result.error || "Erreur lors de la mise à jour."); + } + } catch (error) { + alert("Erreur réseau."); + } + } } \ No newline at end of file diff --git a/src/Controller/UserController.php b/src/Controller/UserController.php index 5efcdc2..e197bf6 100644 --- a/src/Controller/UserController.php +++ b/src/Controller/UserController.php @@ -913,5 +913,93 @@ class UserController extends AbstractController return $this->json(['error' => 'Une erreur interne est survenue.'], 500); } } + + #[Route('/data/{id}', name: 'user_data_json', methods: ['GET'])] + public function userData(User $user, Request $request): JsonResponse { + $orgId = $request->query->get('orgId'); + $org = $this->organizationRepository->find($orgId); + if (!$org) { + $this->loggerService->logEntityNotFound('Organization', ['id' => $orgId], $this->getUser()->getUserIdentifier()); + return $this->json(['error' => "L'organisation n'existe pas."], 404); + } + $uo = $this->uoRepository->findOneBy(['users' => $user, 'organization' => $org]); + $apps = $this->userOrganizationAppService->getUserApplicationByOrganization($uo); + return $this->json([ + 'email' => $user->getEmail(), + 'name' => $user->getName(), + 'surname' => $user->getSurname(), + 'phoneNumber' => $user->getPhoneNumber(), + 'applicationIds' => array_map(fn($app) => $app->getId(), $apps), + ]); + } + + #[Route('/edit/{id}/ajax', name: 'edit_ajax', methods: ['POST'])] + public function editAjax(int $id, Request $request): JsonResponse + { + $this->denyAccessUnlessGranted('ROLE_USER'); + $actingUser = $this->getUser(); + + $user = $this->userRepository->find($id); + if (!$user) { + return $this->json(['error' => "L'utilisateur n'existe pas."], 404); + } + + try { + if (!$this->userService->isAdminOfUser($user)) { + return $this->json(['error' => "Accès non autorisé."], 403); + } + + $data = $request->request->all(); + $orgId = $data['organizationId'] ?? null; + $selectedApps = $data['applications'] ?? []; + + // 1. Clean data for the form (remove non-entity fields) + unset($data['organizationId'], $data['applications']); + + $form = $this->createForm(UserForm::class, $user, [ + 'csrf_protection' => false, + 'allow_extra_fields' => true, + ]); + + $form->submit($data, false); + + if ($form->isSubmitted() && $form->isValid()) { + // 2. Handle User Info & Picture + $picture = $request->files->get('pictureUrl'); + $this->userService->formatUserData($user, $picture); + $user->setModifiedAt(new \DateTimeImmutable('now')); + + $this->entityManager->persist($user); + $this->entityManager->flush(); + + // 3. Handle Organization-specific Application Sync + if ($orgId) { + $org = $this->organizationRepository->find($orgId); + if ($org) { + // Logic to sync applications for THIS specific organization + $uo = $this->uoRepository->findOneBy(['users' => $user, 'organization' => $org]); + $this->userOrganizationAppService->syncUserApplicationsByOrganization($uo, $selectedApps); + + // Create Action Log + $this->actionService->createAction("Edit user information", $actingUser, $org, $user->getUserIdentifier()); + } + } + + // 4. Logging + $this->loggerService->logUserAction($user->getId(), $actingUser->getUserIdentifier(), 'User information edited via AJAX'); + if ($this->isGranted('ROLE_SUPER_ADMIN')) { + $this->loggerService->logSuperAdmin($user->getId(), $actingUser->getUserIdentifier(), "Super Admin edited user via AJAX"); + } + + return $this->json(['success' => true, 'message' => 'Informations modifiées avec succès.']); + } + + return $this->json(['error' => 'Données de formulaire invalides.'], 400); + + } catch (\Exception $e) { + $this->errorLogger->critical($e->getMessage()); + return $this->json(['error' => 'Une erreur est survenue lors de la modification.'], 500); + } + } } diff --git a/src/Entity/UserOrganizationApp.php b/src/Entity/UserOrganizationApp.php index d4a6a35..d83513b 100644 --- a/src/Entity/UserOrganizationApp.php +++ b/src/Entity/UserOrganizationApp.php @@ -2,10 +2,10 @@ namespace App\Entity; -use App\Repository\UserOrganizatonAppRepository; +use App\Repository\UserOrganizationAppRepository; use Doctrine\ORM\Mapping as ORM; -#[ORM\Entity(repositoryClass: UserOrganizatonAppRepository::class)] +#[ORM\Entity(repositoryClass: UserOrganizationAppRepository::class)] class UserOrganizationApp { #[ORM\Id] diff --git a/src/Repository/UserOrganizatonAppRepository.php b/src/Repository/UserOrganizationAppRepository.php similarity index 97% rename from src/Repository/UserOrganizatonAppRepository.php rename to src/Repository/UserOrganizationAppRepository.php index 0942b4d..d1627c9 100644 --- a/src/Repository/UserOrganizatonAppRepository.php +++ b/src/Repository/UserOrganizationAppRepository.php @@ -10,7 +10,7 @@ use Doctrine\Persistence\ManagerRegistry; /** * @extends ServiceEntityRepository */ -class UserOrganizatonAppRepository extends ServiceEntityRepository +class UserOrganizationAppRepository extends ServiceEntityRepository { public function __construct(ManagerRegistry $registry) { diff --git a/src/Service/UserOrganizationAppService.php b/src/Service/UserOrganizationAppService.php index fe81114..40166f1 100644 --- a/src/Service/UserOrganizationAppService.php +++ b/src/Service/UserOrganizationAppService.php @@ -7,6 +7,8 @@ use App\Entity\Roles; use App\Entity\User; use App\Entity\UserOrganizationApp; use App\Entity\UsersOrganizations; +use App\Repository\UserOrganizationAppRepository; +use App\Repository\UsersOrganizationsRepository; use App\Service\ActionService; use App\Service\LoggerService; use App\Service\UserService; @@ -16,7 +18,15 @@ use Symfony\Bundle\SecurityBundle\Security; class UserOrganizationAppService { - public function __construct(private readonly EntityManagerInterface $entityManager, private readonly ActionService $actionService, private readonly Security $security, private readonly UserService $userService, private readonly LoggerInterface $logger, private readonly LoggerService $loggerService) + + public function __construct(private readonly EntityManagerInterface $entityManager, + private readonly ActionService $actionService, + private readonly Security $security, + private readonly UserService $userService, + private readonly LoggerInterface $logger, + private readonly LoggerService $loggerService, + private readonly UsersOrganizationsRepository $usersOrganizationsRepository, + private readonly UserOrganizationAppRepository $uoaRepository) { } @@ -76,9 +86,9 @@ class UserOrganizationAppService public function deactivateAllUserOrganizationsAppLinks(UsersOrganizations $userOrganization, Apps $app = null): void { if($app) { - $uoas = $this->entityManager->getRepository(UserOrganizationApp::class)->findBy(['userOrganization' => $userOrganization, 'application' => $app, 'isActive' => true]); + $uoas = $this->uoaRepository->findBy(['userOrganization' => $userOrganization, 'application' => $app, 'isActive' => true]); } else { - $uoas = $this->entityManager->getRepository(UserOrganizationApp::class)->findBy(['userOrganization' => $userOrganization, 'isActive' => true]); + $uoas = $this->uoaRepository->findBy(['userOrganization' => $userOrganization, 'isActive' => true]); } foreach ($uoas as $uoa) { try{ @@ -98,156 +108,6 @@ class UserOrganizationAppService } } - /** - * Synchronizes user roles for a specific application within an organization. - * - * This method handles the complete lifecycle of user-application role assignments: - * - Activates/deactivates existing role links based on selection - * - Creates new role assignments for newly selected roles - * - Updates the user's global Symfony security roles when ADMIN/SUPER_ADMIN roles are assigned - * - * @param UsersOrganizations $uo The user-organization relationship - * @param Apps $application The target application - * @param array $selectedRoleIds Array of role IDs that should be active for this user-app combination - * @param User $actingUser The user performing this action (for audit logging) - * - * @return void - * - * @throws \Exception If role entities cannot be found or persisted - */ - public function syncRolesForUserOrganizationApp( - UsersOrganizations $uo, - Apps $application, - array $selectedRoleIds, - User $actingUser - ): void { - - // Fetch existing UserOrganizationApp links for this user and application - $uoas = $this->entityManager->getRepository(UserOrganizationApp::class)->findBy([ - 'userOrganization' => $uo, - 'application' => $application, - ]); - - $currentRoleIds = []; - // Process existing role links - activate or deactivate based on selection - foreach ($uoas as $uoa) { - $roleId = $uoa->getRole()->getId(); - $currentRoleIds[] = $roleId; - $roleName = $uoa->getRole()->getName(); - - if (in_array((string) $roleId, $selectedRoleIds, true)) { - // Role is selected - ensure it's active - if (!$uoa->isActive()) { - $uoa->setIsActive(true); - $this->entityManager->persist($uoa); - $this->loggerService->logOrganizationInformation( - $uo->getOrganization()->getId(), - $actingUser->getId(), - "Re-activated role '$roleName' for user '{$uo->getUsers()->getId()}' in application '{$application->getName()} with UOA ID {$uoa->getId()}'" - ); - $this->actionService->createAction( - "Re-activate user role for application", - $actingUser, - $uo->getOrganization(), - "App: {$application->getName()}, Role: $roleName for user {$uo->getUsers()->getUserIdentifier()}" - ); - // Sync Admins roles to user's global Symfony security roles - if (in_array($roleName, ['ADMIN', 'SUPER ADMIN'], true)) { - $this->userService->syncUserRoles($uo->getUsers(), $roleName, true); - } - // Ensure ADMIN role is assigned if SUPER ADMIN is activated - if ($roleName === 'SUPER ADMIN') { - $this->ensureAdminRoleForSuperAdmin($uoa); - } - } - } else { - // Role is not selected - ensure it's inactive - if ($uoa->isActive()) { - $uoa->setIsActive(false); - $this->entityManager->persist($uoa); - $this->loggerService->logOrganizationInformation( - $uo->getOrganization()->getId(), - $actingUser->getId(), - "Deactivated role '$roleName' for user '{$uo->getUsers()->getId()}' in application '{$application->getName()}' with UOA ID {$uoa->getId()}'" - ); - $this->actionService->createAction( - "Deactivate user role for application", - $actingUser, - $uo->getOrganization(), - "App: {$application->getName()}, Role: $roleName for user {$uo->getUsers()->getUserIdentifier()}" - ); - // Sync Admins roles to user's global Symfony security roles - if (in_array($roleName, ['ADMIN', 'SUPER ADMIN'], true)) { - $this->userService->syncUserRoles($uo->getUsers(), $roleName, false); - } - - } - } - } - - // Create new role assignments for roles that don't exist yet - foreach ($selectedRoleIds as $roleId) { - if (!in_array($roleId, $currentRoleIds)) { - $role = $this->entityManager->getRepository(Roles::class)->find($roleId); - if ($role) { - // Create new user-organization-application role link - $newUoa = new UserOrganizationApp(); - $newUoa->setUserOrganization($uo); - $newUoa->setApplication($application); - $newUoa->setRole($role); - $newUoa->setIsActive(true); - - // Sync Admins roles to user's global Symfony security roles - if (in_array($role->getName(), ['ADMIN', 'SUPER ADMIN'], true)) { - $this->userService->syncUserRoles($uo->getUsers(), $role->getName(), true); - } - // Ensure ADMIN role is assigned if SUPER ADMIN is activated - if ($role->getName() === 'SUPER ADMIN') { - $this->ensureAdminRoleForSuperAdmin($newUoa); - } - $this->entityManager->persist($newUoa); - $this->loggerService->logOrganizationInformation( - $uo->getOrganization()->getId(), - $actingUser->getId(), - "Created new role '{$role->getName()}' for user '{$uo->getUsers()->getId()}' in application '{$application->getName()}' with UOA ID {$newUoa->getId()}'" - ); - $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(); - } - - /** - * Attribute the role Admin to the user if the user has the role Super Admin - * - * @param UserOrganizationApp $uoa - * - * @return void - */ - public function ensureAdminRoleForSuperAdmin(UserOrganizationApp $uoa): void - { - $uoaAdmin = $this->entityManager->getRepository(UserOrganizationApp::class)->findOneBy([ - 'userOrganization' => $uoa->getUserOrganization(), - 'application' => $uoa->getApplication(), - 'role' => $this->entityManager->getRepository(Roles::class)->findOneBy(['name' => 'ADMIN']) - ]); - if(!$uoaAdmin) { - $uoaAdmin = new UserOrganizationApp(); - $uoaAdmin->setUserOrganization($uoa->getUserOrganization()); - $uoaAdmin->setApplication($uoa->getApplication()); - $uoaAdmin->setRole($this->entityManager->getRepository(Roles::class)->findOneBy(['name' => 'ADMIN'])); - $uoaAdmin->setIsActive(true); - $this->entityManager->persist($uoaAdmin); - } - // If the ADMIN role link exists but is inactive, activate it - if ($uoaAdmin && !$uoaAdmin->isActive()) { - $uoaAdmin->setIsActive(true); - } - } /** * Get users applications links for a given user @@ -257,10 +117,10 @@ class UserOrganizationAppService */ public function getUserApplications(User $user): array { - $uos = $this->entityManager->getRepository(UsersOrganizations::class)->findBy(['users' => $user]); + $uos = $this->entityManager->getRepository(UsersOrganizations::class)->findBy(['users' => $user, 'isActive' => true]); $apps = []; foreach ($uos as $uo) { - $uoas = $this->entityManager->getRepository(UserOrganizationApp::class)->findBy(['userOrganization' => $uo, 'isActive' => true]); + $uoas = $this->uoaRepository->findBy(['userOrganization' => $uo, 'isActive' => true]); foreach ($uoas as $uoa) { $app = $uoa->getApplication(); if (!in_array($app, $apps, true)) { @@ -270,4 +130,82 @@ class UserOrganizationAppService } return $apps; } + + public function getUserApplicationByOrganization(UsersOrganizations $uo): array + { + $uoas = $this->uoaRepository->findBy(['userOrganization' => $uo, 'isActive' => true]); + $apps = []; + foreach ($uoas as $uoa) { + $app = $uoa->getApplication(); + if (!in_array($app, $apps, true)) { + $apps[] = $app; + } + } + return $apps; + } + + public function syncUserApplicationsByOrganization(UsersOrganizations $uo, array $selectedApps): void + { + // 1. Get all currently active applications for this specific User-Organization link + $currentUolas = $this->uoaRepository->findBy([ + 'userOrganization' => $uo, + 'isActive' => true + ]); + + // Track which app IDs are currently active in the DB + $currentAppIds = array_map(fn($uoa) => $uoa->getApplication()->getId(), $currentUolas); + + // 2. REMOVAL: Deactivate apps that are in the DB but NOT in the new selection + foreach ($currentUolas as $uoa) { + $appId = $uoa->getApplication()->getId(); + if (!in_array($appId, $selectedApps)) { + $uoa->setIsActive(false); + $this->actionService->createAction( + "Deactivate UOA link", + $uo->getUsers(), + $uo->getOrganization(), + "App: " . $uoa->getApplication()->getName() + ); + } + } + + // 3. ADDITION / REACTIVATION: Handle the selected apps + foreach ($selectedApps as $appId) { + $app = $this->entityManager->getRepository(Apps::class)->find($appId); + if (!$app) continue; + + // Check if a record (active or inactive) already exists + $existingUOA = $this->uoaRepository->findOneBy([ + 'userOrganization' => $uo, + 'application' => $app + ]); + + if (!$existingUOA) { + // Create new if it never existed + $newUOA = new UserOrganizationApp(); + $newUOA->setUserOrganization($uo); + $newUOA->setApplication($app); + $newUOA->setIsActive(true); + $this->entityManager->persist($newUOA); + + $this->actionService->createAction( + "Activate UOA link", + $uo->getUsers(), + $uo->getOrganization(), + "App: " . $app->getName() + ); + } elseif (!$existingUOA->isActive()) { + // Reactivate if it was previously disabled + $existingUOA->setIsActive(true); + $this->actionService->createAction( + "Reactivate UOA link", + $uo->getUsers(), + $uo->getOrganization(), + "App: " . $app->getName() + ); + } + } + + $this->entityManager->flush(); + } } diff --git a/templates/user/show.html.twig b/templates/user/show.html.twig index 924a9bd..e79bde0 100644 --- a/templates/user/show.html.twig +++ b/templates/user/show.html.twig @@ -33,111 +33,18 @@ {% include 'user/userInformation.html.twig' %} -
-
-
-

Information d'organisation

-
-
-
+{#
#} +{#
#} +{#
#} +{#

Information d'organisation

#} +{#
#} +{#
#} +{#
#} {# TODO: dynamic number of project#} -

Projet : 69 projets vous sont attribués

-
- - -{#
#} -{#
#} -{# {% for app in apps %}#} -{#
#} -{#
#} -{#
#} -{# {% if app.logoMiniUrl %}#} -{# Logo {{ app.name }}#} -{# {% endif %}#} -{#
#} -{#

{{ app.name|title }}

#} -{#
#} -{#
#} - -{#
#} -{#
#} -{#

#} -{# Description :#} -{# {{ app.descriptionSmall|default('Aucune description disponible.')|raw }}#} -{#

#} -{#
#} - -{# #}{# find appGroup once, used in both editable and read-only branches #} -{# {% set appGroup = data.uoas[app.id]|default(null) %}#} - -{# {% if canEdit %}#} -{#
#} -{#
#} -{# #} -{#
#} -{# {% if appGroup %}#} -{# {% for role in data.rolesArray %}#} -{# #} -{# #} -{# {% endfor %}#} -{# {% else %}#} -{#

Aucun rôle défini pour cette application.

#} -{# {% endif %}#} -{#
#} -{# #} -{#
#} -{#
#} -{# {% else %}#} -{#
#} -{# #} -{#
#} -{# {% if appGroup %}#} -{# {% for role in data.rolesArray %}#} -{# #} -{# #} -{# {% endfor %}#} -{# {% else %}#} -{#

Aucun rôle défini pour cette application.

#} -{# {% endif %}#} -{#
#} -{#
#} -{# {% endif %}#} -{#
#} -{#
#} -{#
#} -{# {% endfor %}#} -{#
#} +{#

Projet : 69 projets vous sont attribués

#} {#
#} -
+{#
#}
diff --git a/templates/user/userInformation.html.twig b/templates/user/userInformation.html.twig index ed47e03..4444a28 100644 --- a/templates/user/userInformation.html.twig +++ b/templates/user/userInformation.html.twig @@ -1,31 +1,80 @@ {% block body %} -
+
+
{% if user.pictureUrl is not empty %} - user + user {% endif %} -
+

{{ user.surname|capitalize }} {{ user.name|capitalize }}

- Modifier + {# Trigger the edit modal with the user ID #} +
-

Email: {{ user.email }}

-

Dernière connection: {{ user.lastConnection|date('d/m/Y') }} - à {{ user.lastConnection|date('H:m:s') }}

+

Dernière connection: {{ user.lastConnection|date('d/m/Y') }} à {{ user.lastConnection|date('H:m') }}

Compte crée le: {{ user.createdAt|date('d/m/Y') }}

Numéro de téléphone: {{ user.phoneNumber ? user.phoneNumber : 'Non renseigné' }}

+ + {# Reusable Edit Modal #} +
- {% endblock %}