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 {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) {

View File

@ -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.");
}
}
}

View File

@ -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);
}
}
}

View File

@ -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
{
try {
$uoId = $this->handleExistingUser($existingUser, $org);
$actingUser = $this->getUserByIdentifier($this->security->getUser()->getUserIdentifier());
try {
$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();
}
}

View File

@ -52,20 +52,70 @@
{# User tables #}
<div class="col-9">
<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">
<h2>
Nouveaux utilisateurs
</h2>
<a href="{{ path('user_new', {'organizationId': organization.id}) }}"
class="btn btn-primary">Ajouter un utilisateur</a>
<h2>Nouveaux utilisateurs</h2>
{# Button to trigger modal #}
<button type="button" class="btn btn-primary" data-action="click->user#openNewUserModal">
Ajouter un utilisateur
</button>
</div>
<div class="card-body">
<div id="tabulator-userListSmall" data-controller="user"
data-user-aws-value="{{ aws_url }}"
data-user-new-value="true"
data-user-list-small-value="true"
data-user-org-id-value="{{ organization.id }}">
<div id="tabulator-userListSmall"></div>
</div>
{# New User Modal #}
<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>