diff --git a/assets/controllers/base_controller.js b/assets/controllers/base_controller.js
new file mode 100644
index 0000000..379f069
--- /dev/null
+++ b/assets/controllers/base_controller.js
@@ -0,0 +1,27 @@
+import { Controller } from "@hotwired/stimulus";
+
+export default class extends Controller {
+ async fetchAndRenderApplications(targetElement) {
+ try {
+ const response = await fetch('/application/data/all');
+ const apps = await response.json();
+
+ targetElement.innerHTML = apps.map(app => `
+
+ `).join('');
+
+ return apps;
+ } catch (error) {
+ targetElement.innerHTML = 'Erreur de chargement.
';
+ console.error("App load error:", error);
+ }
+ }
+}
\ No newline at end of file
diff --git a/assets/controllers/project_controller.js b/assets/controllers/project_controller.js
index bcf2e12..e7bf00f 100644
--- a/assets/controllers/project_controller.js
+++ b/assets/controllers/project_controller.js
@@ -2,9 +2,10 @@ import {Controller} from '@hotwired/stimulus';
import { Modal } from "bootstrap";
import {TabulatorFull as Tabulator} from 'tabulator-tables';
import {eyeIconLink, pencilIcon, TABULATOR_FR_LANG, trashIcon} from "../js/global.js";
+import base_controller from "./base_controller.js";
-export default class extends Controller {
+export default class extends base_controller {
static values = {
listProject : Boolean,
orgId: Number,
@@ -175,13 +176,13 @@ export default class extends Controller {
this.currentProjectId = projectId;
this.modal.show();
+
this.nameInputTarget.disabled = true;
this.formTitleTarget.textContent = "Modifier le projet";
try {
// 1. Ensure checkboxes are loaded first
- await this.loadApplications();
-
+ const apps = await this.fetchAndRenderApplications(this.appListTarget);
// 2. Fetch the project data
const response = await fetch(`/project/data/${projectId}`);
const project = await response.json();
@@ -203,13 +204,13 @@ export default class extends Controller {
}
}
// Update your openCreateModal to reset the state
- openCreateModal() {
+ async openCreateModal() {
this.currentProjectId = null;
this.modal.show();
this.nameInputTarget.disabled = false;
this.nameInputTarget.value = "";
this.formTitleTarget.textContent = "Nouveau Projet";
- this.loadApplications();
+ await this.fetchAndRenderApplications();
}
async deleteProject(event) {
diff --git a/assets/controllers/user_controller.js b/assets/controllers/user_controller.js
index e7834b0..4a71920 100644
--- a/assets/controllers/user_controller.js
+++ b/assets/controllers/user_controller.js
@@ -10,9 +10,10 @@ import {
trashIconForm
} from "../js/global.js";
import { Modal } from "bootstrap";
+import base_controller from "./base_controller.js";
-export default class extends Controller {
+export default class extends base_controller {
static values = {
rolesArray: Array,
selectedRoleIds: Array,
@@ -26,7 +27,7 @@ export default class extends Controller {
orgId: Number
}
- static targets = ["select", "statusButton", "modal", "userSelect"];
+ static targets = ["select", "statusButton", "modal", "userSelect", "appList"];
connect() {
this.roleSelect();
@@ -1018,4 +1019,38 @@ export default class extends Controller {
alert("Une erreur réseau est survenue.");
}
}
+
+ async openNewUserModal() {
+ this.modal.show();
+ // Call the shared logic and pass the target
+ await this.fetchAndRenderApplications(this.appListTarget);
+ }
+
+ async submitNewUser(event) {
+ event.preventDefault();
+ const form = event.currentTarget;
+ const formData = new FormData(form);
+
+ try {
+ const response = await fetch('/user/new/ajax', { // Adjust path if prefix is different
+ method: 'POST',
+ body: formData,
+ headers: {
+ 'X-Requested-With': 'XMLHttpRequest'
+ }
+ });
+
+ const result = await response.json();
+
+ if (response.ok) {
+ this.modal.hide();
+ form.reset(); // Clear the form
+ location.reload();
+ } else {
+ alert("Erreur: " + (result.error || "Une erreur est survenue lors de la création."));
+ }
+ } 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 d7cf277..5efcdc2 100644
--- a/src/Controller/UserController.php
+++ b/src/Controller/UserController.php
@@ -195,88 +195,88 @@ class UserController extends AbstractController
}
- #[Route('/new', name: 'new', methods: ['GET', 'POST'])]
- public function new(Request $request): Response
- {
- $this->denyAccessUnlessGranted('ROLE_USER');
- try {
- $actingUser =$this->getUser();
-
- $user = new User();
- $form = $this->createForm(UserForm::class, $user);
- $form->handleRequest($request);
-
- $orgId = $request->query->get('organizationId') ?? $request->request->get('organizationId');
- if ($orgId) {
- $org = $this->organizationRepository->find($orgId);
- if (!$org) {
- $this->loggerService->logEntityNotFound('Organization', ['id' => $orgId], $actingUser->getUserIdentifier());
- $this->addFlash('danger', "L'organisation n'existe pas.");
- throw $this->createNotFoundException(self::NOT_FOUND);
- }
- if (!$this->isGranted('ROLE_ADMIN') && !$this->userService->isAdminOfOrganization($org)) {
- $this->loggerService->logAccessDenied($actingUser->getUserIdentifier());
- $this->addFlash('danger', "Accès non autorisé.");
- throw $this->createAccessDeniedException(self::ACCESS_DENIED);
- }
- } else{
- $this->loggerService->logAccessDenied($actingUser->getUserIdentifier());
- $this->addFlash('danger', "Accès non autorisé.");
- throw $this->createAccessDeniedException(self::ACCESS_DENIED);
- }
-
- if ($form->isSubmitted() && $form->isValid()) {
- $existingUser = $this->userRepository->findOneBy(['email' => $user->getEmail()]);
-
- // Case : User exists -> link him to given organization if not already linked, else error message
- if ($existingUser && $org) {
- $this->userService->addExistingUserToOrganization(
- $existingUser,
- $org,
- );
-
- if ($this->isGranted('ROLE_ADMIN')) {
- $this->loggerService->logSuperAdmin(
- $existingUser->getId(),
- $actingUser->getUserIdentifier(),
- "Super Admin linked user to organization",
- $org->getId(),
- );
- }
- $this->addFlash('success', 'Utilisateur ajouté avec succès à l\'organisation. ');
- return $this->redirectToRoute('organization_show', ['id' => $orgId]);
- }
-
- // Case : user doesn't already exist
-
- $picture = $form->get('pictureUrl')->getData();
- $this->userService->createNewUser($user, $actingUser, $picture);
-
- $this->userService->linkUserToOrganization(
- $user,
- $org,
- );
- $this->addFlash('success', 'Nouvel utilisateur créé et ajouté à l\'organisation avec succès. ');
- return $this->redirectToRoute('organization_show', ['id' => $orgId]);
- }
-
- return $this->render('user/new.html.twig', [
- 'user' => $user,
- 'form' => $form->createView(),
- 'organizationId' => $orgId,
- ]);
-
- } catch (\Exception $e) {
- $this->errorLogger->critical($e->getMessage());
-
- if ($orgId) {
- $this->addFlash('danger', 'Une erreur est survenue lors de la création de l\'utilisateur pour l\'organisation .');
- return $this->redirectToRoute('organization_show', ['id' => $orgId]);
- }
- $this->addFlash('danger', 'Une erreur est survenue lors de la création de l\'utilisateur.');
- return $this->redirectToRoute('user_index');
- }
- }
+// #[Route('/new', name: 'new', methods: ['GET', 'POST'])]
+// public function new(Request $request): Response
+// {
+// $this->denyAccessUnlessGranted('ROLE_USER');
+// try {
+// $actingUser =$this->getUser();
+//
+// $user = new User();
+// $form = $this->createForm(UserForm::class, $user);
+// $form->handleRequest($request);
+//
+// $orgId = $request->query->get('organizationId') ?? $request->request->get('organizationId');
+// if ($orgId) {
+// $org = $this->organizationRepository->find($orgId);
+// if (!$org) {
+// $this->loggerService->logEntityNotFound('Organization', ['id' => $orgId], $actingUser->getUserIdentifier());
+// $this->addFlash('danger', "L'organisation n'existe pas.");
+// throw $this->createNotFoundException(self::NOT_FOUND);
+// }
+// if (!$this->isGranted('ROLE_ADMIN') && !$this->userService->isAdminOfOrganization($org)) {
+// $this->loggerService->logAccessDenied($actingUser->getUserIdentifier());
+// $this->addFlash('danger', "Accès non autorisé.");
+// throw $this->createAccessDeniedException(self::ACCESS_DENIED);
+// }
+// } else{
+// $this->loggerService->logAccessDenied($actingUser->getUserIdentifier());
+// $this->addFlash('danger', "Accès non autorisé.");
+// throw $this->createAccessDeniedException(self::ACCESS_DENIED);
+// }
+//
+// if ($form->isSubmitted() && $form->isValid()) {
+// $existingUser = $this->userRepository->findOneBy(['email' => $user->getEmail()]);
+//
+// // Case : User exists -> link him to given organization if not already linked, else error message
+// if ($existingUser && $org) {
+// $this->userService->addExistingUserToOrganization(
+// $existingUser,
+// $org,
+// );
+//
+// if ($this->isGranted('ROLE_ADMIN')) {
+// $this->loggerService->logSuperAdmin(
+// $existingUser->getId(),
+// $actingUser->getUserIdentifier(),
+// "Super Admin linked user to organization",
+// $org->getId(),
+// );
+// }
+// $this->addFlash('success', 'Utilisateur ajouté avec succès à l\'organisation. ');
+// return $this->redirectToRoute('organization_show', ['id' => $orgId]);
+// }
+//
+// // Case : user doesn't already exist
+//
+// $picture = $form->get('pictureUrl')->getData();
+// $this->userService->createNewUser($user, $actingUser, $picture);
+//
+// $this->userService->linkUserToOrganization(
+// $user,
+// $org,
+// );
+// $this->addFlash('success', 'Nouvel utilisateur créé et ajouté à l\'organisation avec succès. ');
+// return $this->redirectToRoute('organization_show', ['id' => $orgId]);
+// }
+//
+// return $this->render('user/new.html.twig', [
+// 'user' => $user,
+// 'form' => $form->createView(),
+// 'organizationId' => $orgId,
+// ]);
+//
+// } catch (\Exception $e) {
+// $this->errorLogger->critical($e->getMessage());
+//
+// if ($orgId) {
+// $this->addFlash('danger', 'Une erreur est survenue lors de la création de l\'utilisateur pour l\'organisation .');
+// return $this->redirectToRoute('organization_show', ['id' => $orgId]);
+// }
+// $this->addFlash('danger', 'Une erreur est survenue lors de la création de l\'utilisateur.');
+// return $this->redirectToRoute('user_index');
+// }
+// }
/**
* Endpoint to activate/deactivate a user (soft delete)
@@ -832,6 +832,86 @@ class UserController extends AbstractController
throw $this->createNotFoundException(self::NOT_FOUND);
}
+ #[Route('/new/ajax', name: 'new_ajax', methods: ['POST'])]
+ public function newUserAjax(Request $request): JsonResponse
+ {
+ $this->denyAccessUnlessGranted('ROLE_USER');
+ $actingUser = $this->getUser();
+ try {
+ $data = $request->request->all();
+ $orgId = $data['organizationId'] ?? null;
+ $selectedApps = $data['applications'] ?? [];
+
+ //unset data that are not part of the User entity to avoid form errors
+ unset($data['organizationId'], $data['applications']);
+ $user = new User();
+
+ $form = $this->createForm(UserForm::class, $user, [
+ 'csrf_protection' => false,
+ 'allow_extra_fields' => true,
+ ]);
+
+ $form->submit($data, false);
+ if (!$orgId) {
+ return $this->json(['error' => 'ID Organisation manquant.'], 400);
+ }
+
+ $org = $this->organizationRepository->find($orgId);
+ if (!$org) {
+ return $this->json(['error' => "L'organisation n'existe pas."], 404);
+ }
+
+ // 3. Permissions Check
+ if (!$this->isGranted('ROLE_ADMIN') && !$this->userService->isAdminOfOrganization($org)) {
+ $this->loggerService->logAccessDenied($actingUser->getUserIdentifier());
+ return $this->json(['error' => "Accès non autorisé."], 403);
+ }
+
+ if ($form->isSubmitted() && $form->isValid()) {
+ $email = $user->getEmail();
+ $existingUser = $this->userRepository->findOneBy(['email' => $email]);
+
+ // CASE A: User exists -> Add to org
+ if ($existingUser) {
+ // Check if already in org to avoid logic errors or duplicate logs
+ $this->userService->addExistingUserToOrganization($existingUser, $org, $selectedApps);
+
+ if ($this->isGranted('ROLE_ADMIN')) {
+ $this->loggerService->logSuperAdmin(
+ $existingUser->getId(),
+ $actingUser->getUserIdentifier(),
+ "Super Admin linked user to organization via AJAX",
+ $org->getId(),
+ );
+ }
+
+ return $this->json([
+ 'success' => true,
+ 'message' => 'Utilisateur existant ajouté à l\'organisation.'
+ ]);
+ }
+
+ // CASE B: New User -> Create
+ // Fetch picture from $request->files since it's a multipart request
+ $picture = $request->files->get('pictureUrl');
+
+ $this->userService->createNewUser($user, $actingUser, $picture);
+ $this->userService->linkUserToOrganization($user, $org, $selectedApps);
+
+ return $this->json([
+ 'success' => true,
+ 'message' => 'Nouvel utilisateur créé et ajouté.'
+ ]);
+ }
+
+ // If form is invalid, return the specific errors
+ return $this->json(['error' => 'Données de formulaire invalides.'], 400);
+
+ } catch (\Exception $e) {
+ $this->errorLogger->critical("AJAX User Creation Error: " . $e->getMessage());
+ return $this->json(['error' => 'Une erreur interne est survenue.'], 500);
+ }
+ }
}
diff --git a/src/Service/UserService.php b/src/Service/UserService.php
index a6429c6..dbe6e6b 100644
--- a/src/Service/UserService.php
+++ b/src/Service/UserService.php
@@ -3,6 +3,7 @@
namespace App\Service;
+use App\Entity\Apps;
use App\Entity\Organizations;
use App\Entity\Roles;
use App\Entity\User;
@@ -533,17 +534,17 @@ class UserService
*
* @param User $user
* @param Organizations $organization
- * @return void
+ * @param array $selectedApps
+ * @return int
+ * @throws Exception
*/
- public function handleExistingUser(User $user, Organizations $organization): int
+ public function reactivateUser(User $user, Organizations $organization, array $selectedApps): int
{
if (!$user->isActive()) {
$user->setIsActive(true);
$this->entityManager->persist($user);
}
- $uo = $this->linkUserToOrganization($user, $organization);
-
- return $uo->getId();
+ return $this->linkUserToOrganization($user, $organization, $selectedApps)->getId();
}
/**
@@ -554,6 +555,8 @@ class UserService
* Handle picture if provided
*
* @param User $user
+ * @param $picture
+ * @param bool $setPassword
* @return void
*/
public function formatUserData(User $user, $picture, bool $setPassword = false): void
@@ -589,11 +592,12 @@ class UserService
public function addExistingUserToOrganization(
User $existingUser,
Organizations $org,
+ array $selectedApps
): int
{
+ $actingUser = $this->getUserByIdentifier($this->security->getUser()->getUserIdentifier());
try {
- $uoId = $this->handleExistingUser($existingUser, $org);
- $actingUser = $this->getUserByIdentifier($this->security->getUser()->getUserIdentifier());
+ $uoId = $this->reactivateUser($existingUser, $org, $selectedApps);
$this->loggerService->logExistingUserAddedToOrg(
$existingUser->getId(),
$org->getId(),
@@ -647,6 +651,7 @@ class UserService
public function linkUserToOrganization(
User $user,
Organizations $org,
+ array $selectedApps
): UsersOrganizations
{
$actingUser = $this->getUserByIdentifier($this->security->getUser()->getUserIdentifier());
@@ -660,6 +665,7 @@ class UserService
$uo->setRole($roleUser);
$uo->setModifiedAt(new \DateTimeImmutable('now'));
$this->entityManager->persist($uo);
+ $this->linkUOToApps($uo, $selectedApps);
$this->entityManager->flush();
$this->loggerService->logUserOrganizationLinkCreated(
@@ -731,4 +737,18 @@ class UserService
}
}
+ private function linkUOToApps(UsersOrganizations $uo, array $selectedApps):void
+ {
+ foreach ($selectedApps as $appId){
+ $uoa = new UserOrganizationApp();
+ $uoa->setUserOrganization($uo);
+ $app = $this->entityManager->getRepository(Apps::class)->find($appId);
+ if ($app) {
+ $uoa->setApplication($app);
+ $this->entityManager->persist($uoa);
+ }
+ }
+ $this->entityManager->flush();
+ }
+
}
diff --git a/templates/organization/show.html.twig b/templates/organization/show.html.twig
index 60ce8e1..a54ce55 100644
--- a/templates/organization/show.html.twig
+++ b/templates/organization/show.html.twig
@@ -52,20 +52,70 @@
{# User tables #}