changed creating logic to modal

This commit is contained in:
Charles 2026-02-17 11:02:47 +01:00
parent 72b40e965a
commit a893c09fcf
6 changed files with 320 additions and 107 deletions

View File

@ -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 => `
<div class="col-md-4 mb-3">
<div class="form-check border p-2 rounded d-flex align-items-center gap-2">
<input class="form-check-input ms-1" type="checkbox" name="applications[]" value="${app.id}" id="app_${app.id}">
<label class="form-check-label d-flex align-items-center gap-2" for="app_${app.id}">
<img src="${app.logoMiniUrl}" alt="${app.name}" style="height: 20px; width: 20px; object-fit: contain;">
${app.name}
</label>
</div>
</div>
`).join('');
return apps;
} catch (error) {
targetElement.innerHTML = '<div class="text-danger">Erreur de chargement.</div>';
console.error("App load error:", error);
}
}
}

View File

@ -2,9 +2,10 @@ import {Controller} from '@hotwired/stimulus';
import { Modal } from "bootstrap"; import { Modal } from "bootstrap";
import {TabulatorFull as Tabulator} from 'tabulator-tables'; import {TabulatorFull as Tabulator} from 'tabulator-tables';
import {eyeIconLink, pencilIcon, TABULATOR_FR_LANG, trashIcon} from "../js/global.js"; 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 = { static values = {
listProject : Boolean, listProject : Boolean,
orgId: Number, orgId: Number,
@ -175,13 +176,13 @@ export default class extends Controller {
this.currentProjectId = projectId; this.currentProjectId = projectId;
this.modal.show(); this.modal.show();
this.nameInputTarget.disabled = true; this.nameInputTarget.disabled = true;
this.formTitleTarget.textContent = "Modifier le projet"; this.formTitleTarget.textContent = "Modifier le projet";
try { try {
// 1. Ensure checkboxes are loaded first // 1. Ensure checkboxes are loaded first
await this.loadApplications(); const apps = await this.fetchAndRenderApplications(this.appListTarget);
// 2. Fetch the project data // 2. Fetch the project data
const response = await fetch(`/project/data/${projectId}`); const response = await fetch(`/project/data/${projectId}`);
const project = await response.json(); const project = await response.json();
@ -203,13 +204,13 @@ export default class extends Controller {
} }
} }
// Update your openCreateModal to reset the state // Update your openCreateModal to reset the state
openCreateModal() { async openCreateModal() {
this.currentProjectId = null; this.currentProjectId = null;
this.modal.show(); this.modal.show();
this.nameInputTarget.disabled = false; this.nameInputTarget.disabled = false;
this.nameInputTarget.value = ""; this.nameInputTarget.value = "";
this.formTitleTarget.textContent = "Nouveau Projet"; this.formTitleTarget.textContent = "Nouveau Projet";
this.loadApplications(); await this.fetchAndRenderApplications();
} }
async deleteProject(event) { async deleteProject(event) {

View File

@ -10,9 +10,10 @@ import {
trashIconForm trashIconForm
} from "../js/global.js"; } from "../js/global.js";
import { Modal } from "bootstrap"; import { Modal } from "bootstrap";
import base_controller from "./base_controller.js";
export default class extends Controller { export default class extends base_controller {
static values = { static values = {
rolesArray: Array, rolesArray: Array,
selectedRoleIds: Array, selectedRoleIds: Array,
@ -26,7 +27,7 @@ export default class extends Controller {
orgId: Number orgId: Number
} }
static targets = ["select", "statusButton", "modal", "userSelect"]; static targets = ["select", "statusButton", "modal", "userSelect", "appList"];
connect() { connect() {
this.roleSelect(); this.roleSelect();
@ -1018,4 +1019,38 @@ export default class extends Controller {
alert("Une erreur réseau est survenue."); 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.");
}
}
} }

View File

@ -195,88 +195,88 @@ class UserController extends AbstractController
} }
#[Route('/new', name: 'new', methods: ['GET', 'POST'])] // #[Route('/new', name: 'new', methods: ['GET', 'POST'])]
public function new(Request $request): Response // public function new(Request $request): Response
{ // {
$this->denyAccessUnlessGranted('ROLE_USER'); // $this->denyAccessUnlessGranted('ROLE_USER');
try { // try {
$actingUser =$this->getUser(); // $actingUser =$this->getUser();
//
$user = new User(); // $user = new User();
$form = $this->createForm(UserForm::class, $user); // $form = $this->createForm(UserForm::class, $user);
$form->handleRequest($request); // $form->handleRequest($request);
//
$orgId = $request->query->get('organizationId') ?? $request->request->get('organizationId'); // $orgId = $request->query->get('organizationId') ?? $request->request->get('organizationId');
if ($orgId) { // if ($orgId) {
$org = $this->organizationRepository->find($orgId); // $org = $this->organizationRepository->find($orgId);
if (!$org) { // if (!$org) {
$this->loggerService->logEntityNotFound('Organization', ['id' => $orgId], $actingUser->getUserIdentifier()); // $this->loggerService->logEntityNotFound('Organization', ['id' => $orgId], $actingUser->getUserIdentifier());
$this->addFlash('danger', "L'organisation n'existe pas."); // $this->addFlash('danger', "L'organisation n'existe pas.");
throw $this->createNotFoundException(self::NOT_FOUND); // throw $this->createNotFoundException(self::NOT_FOUND);
} // }
if (!$this->isGranted('ROLE_ADMIN') && !$this->userService->isAdminOfOrganization($org)) { // if (!$this->isGranted('ROLE_ADMIN') && !$this->userService->isAdminOfOrganization($org)) {
$this->loggerService->logAccessDenied($actingUser->getUserIdentifier()); // $this->loggerService->logAccessDenied($actingUser->getUserIdentifier());
$this->addFlash('danger', "Accès non autorisé."); // $this->addFlash('danger', "Accès non autorisé.");
throw $this->createAccessDeniedException(self::ACCESS_DENIED); // throw $this->createAccessDeniedException(self::ACCESS_DENIED);
} // }
} else{ // } else{
$this->loggerService->logAccessDenied($actingUser->getUserIdentifier()); // $this->loggerService->logAccessDenied($actingUser->getUserIdentifier());
$this->addFlash('danger', "Accès non autorisé."); // $this->addFlash('danger', "Accès non autorisé.");
throw $this->createAccessDeniedException(self::ACCESS_DENIED); // throw $this->createAccessDeniedException(self::ACCESS_DENIED);
} // }
//
if ($form->isSubmitted() && $form->isValid()) { // if ($form->isSubmitted() && $form->isValid()) {
$existingUser = $this->userRepository->findOneBy(['email' => $user->getEmail()]); // $existingUser = $this->userRepository->findOneBy(['email' => $user->getEmail()]);
//
// Case : User exists -> link him to given organization if not already linked, else error message // // Case : User exists -> link him to given organization if not already linked, else error message
if ($existingUser && $org) { // if ($existingUser && $org) {
$this->userService->addExistingUserToOrganization( // $this->userService->addExistingUserToOrganization(
$existingUser, // $existingUser,
$org, // $org,
); // );
//
if ($this->isGranted('ROLE_ADMIN')) { // if ($this->isGranted('ROLE_ADMIN')) {
$this->loggerService->logSuperAdmin( // $this->loggerService->logSuperAdmin(
$existingUser->getId(), // $existingUser->getId(),
$actingUser->getUserIdentifier(), // $actingUser->getUserIdentifier(),
"Super Admin linked user to organization", // "Super Admin linked user to organization",
$org->getId(), // $org->getId(),
); // );
} // }
$this->addFlash('success', 'Utilisateur ajouté avec succès à l\'organisation. '); // $this->addFlash('success', 'Utilisateur ajouté avec succès à l\'organisation. ');
return $this->redirectToRoute('organization_show', ['id' => $orgId]); // return $this->redirectToRoute('organization_show', ['id' => $orgId]);
} // }
//
// Case : user doesn't already exist // // Case : user doesn't already exist
//
$picture = $form->get('pictureUrl')->getData(); // $picture = $form->get('pictureUrl')->getData();
$this->userService->createNewUser($user, $actingUser, $picture); // $this->userService->createNewUser($user, $actingUser, $picture);
//
$this->userService->linkUserToOrganization( // $this->userService->linkUserToOrganization(
$user, // $user,
$org, // $org,
); // );
$this->addFlash('success', 'Nouvel utilisateur créé et ajouté à l\'organisation avec succès. '); // $this->addFlash('success', 'Nouvel utilisateur créé et ajouté à l\'organisation avec succès. ');
return $this->redirectToRoute('organization_show', ['id' => $orgId]); // return $this->redirectToRoute('organization_show', ['id' => $orgId]);
} // }
//
return $this->render('user/new.html.twig', [ // return $this->render('user/new.html.twig', [
'user' => $user, // 'user' => $user,
'form' => $form->createView(), // 'form' => $form->createView(),
'organizationId' => $orgId, // 'organizationId' => $orgId,
]); // ]);
//
} catch (\Exception $e) { // } catch (\Exception $e) {
$this->errorLogger->critical($e->getMessage()); // $this->errorLogger->critical($e->getMessage());
//
if ($orgId) { // if ($orgId) {
$this->addFlash('danger', 'Une erreur est survenue lors de la création de l\'utilisateur pour l\'organisation .'); // $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]); // return $this->redirectToRoute('organization_show', ['id' => $orgId]);
} // }
$this->addFlash('danger', 'Une erreur est survenue lors de la création de l\'utilisateur.'); // $this->addFlash('danger', 'Une erreur est survenue lors de la création de l\'utilisateur.');
return $this->redirectToRoute('user_index'); // return $this->redirectToRoute('user_index');
} // }
} // }
/** /**
* Endpoint to activate/deactivate a user (soft delete) * Endpoint to activate/deactivate a user (soft delete)
@ -832,6 +832,86 @@ class UserController extends AbstractController
throw $this->createNotFoundException(self::NOT_FOUND); 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);
}
}
} }

View File

@ -3,6 +3,7 @@
namespace App\Service; namespace App\Service;
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;
@ -533,17 +534,17 @@ class UserService
* *
* @param User $user * @param User $user
* @param Organizations $organization * @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()) { if (!$user->isActive()) {
$user->setIsActive(true); $user->setIsActive(true);
$this->entityManager->persist($user); $this->entityManager->persist($user);
} }
$uo = $this->linkUserToOrganization($user, $organization); return $this->linkUserToOrganization($user, $organization, $selectedApps)->getId();
return $uo->getId();
} }
/** /**
@ -554,6 +555,8 @@ class UserService
* Handle picture if provided * Handle picture if provided
* *
* @param User $user * @param User $user
* @param $picture
* @param bool $setPassword
* @return void * @return void
*/ */
public function formatUserData(User $user, $picture, bool $setPassword = false): void public function formatUserData(User $user, $picture, bool $setPassword = false): void
@ -589,11 +592,12 @@ class UserService
public function addExistingUserToOrganization( public function addExistingUserToOrganization(
User $existingUser, User $existingUser,
Organizations $org, Organizations $org,
array $selectedApps
): int ): int
{ {
$actingUser = $this->getUserByIdentifier($this->security->getUser()->getUserIdentifier());
try { try {
$uoId = $this->handleExistingUser($existingUser, $org); $uoId = $this->reactivateUser($existingUser, $org, $selectedApps);
$actingUser = $this->getUserByIdentifier($this->security->getUser()->getUserIdentifier());
$this->loggerService->logExistingUserAddedToOrg( $this->loggerService->logExistingUserAddedToOrg(
$existingUser->getId(), $existingUser->getId(),
$org->getId(), $org->getId(),
@ -647,6 +651,7 @@ class UserService
public function linkUserToOrganization( public function linkUserToOrganization(
User $user, User $user,
Organizations $org, Organizations $org,
array $selectedApps
): UsersOrganizations ): UsersOrganizations
{ {
$actingUser = $this->getUserByIdentifier($this->security->getUser()->getUserIdentifier()); $actingUser = $this->getUserByIdentifier($this->security->getUser()->getUserIdentifier());
@ -660,6 +665,7 @@ class UserService
$uo->setRole($roleUser); $uo->setRole($roleUser);
$uo->setModifiedAt(new \DateTimeImmutable('now')); $uo->setModifiedAt(new \DateTimeImmutable('now'));
$this->entityManager->persist($uo); $this->entityManager->persist($uo);
$this->linkUOToApps($uo, $selectedApps);
$this->entityManager->flush(); $this->entityManager->flush();
$this->loggerService->logUserOrganizationLinkCreated( $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();
}
} }

View File

@ -52,20 +52,70 @@
{# User tables #} {# User tables #}
<div class="col-9"> <div class="col-9">
<div class="row mb-3 d-flex gap-2 "> <div class="row mb-3 d-flex gap-2 ">
<div class="col mb-3 card no-header-bg"> <div class="col mb-3 card no-header-bg"
data-controller="user"
data-user-org-id-value="{{ organization.id }}"
data-user-new-value="true"
data-user-list-small-value="true">
<div class="card-header d-flex justify-content-between align-items-center"> <div class="card-header d-flex justify-content-between align-items-center">
<h2> <h2>Nouveaux utilisateurs</h2>
Nouveaux utilisateurs {# Button to trigger modal #}
</h2> <button type="button" class="btn btn-primary" data-action="click->user#openNewUserModal">
<a href="{{ path('user_new', {'organizationId': organization.id}) }}" Ajouter un utilisateur
class="btn btn-primary">Ajouter un utilisateur</a> </button>
</div> </div>
<div class="card-body"> <div class="card-body">
<div id="tabulator-userListSmall" data-controller="user" <div id="tabulator-userListSmall"></div>
data-user-aws-value="{{ aws_url }}" </div>
data-user-new-value="true"
data-user-list-small-value="true" {# New User Modal #}
data-user-org-id-value="{{ organization.id }}"> <div class="modal fade" id="newUserModal" tabindex="-1" aria-hidden="true" data-user-target="modal">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Créer un nouvel utilisateur</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<form data-action="submit->user#submitNewUser">
<div class="modal-body">
<div class="mb-3">
<label class="form-label">Email*</label>
<input type="email" name="email" class="form-control" required>
</div>
<div class="row">
<div class="col-6 mb-3">
<label class="form-label">Prénom*</label>
<input type="text" name="name" class="form-control" required>
</div>
<div class="col-6 mb-3">
<label class="form-label">Nom*</label>
<input type="text" name="surname" class="form-control" required>
</div>
</div>
<div class="mb-3">
<label class="form-label">Numéro de téléphone</label>
<input type="text" name="phoneNumber" class="form-control">
</div>
<div class="mb-3">
<label class="form-label">Photo de profil</label>
<input type="file" name="pictureUrl" class="form-control" accept="image/*">
</div>
<hr>
<label class="form-label"><b>Applications à associer**</b></label>
<div class="row" data-user-target="appList">
{# Applications will be injected here #}
<div class="text-center p-3 text-muted">Chargement des applications...</div>
</div>
<input type="hidden" name="organizationId" value="{{ organization.id }}">
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Annuler</button>
<button type="submit" class="btn btn-primary">Créer l'utilisateur</button>
</div>
</form>
</div>
</div> </div>
</div> </div>
</div> </div>