From d5e1ad057ea806e892d547746183f46533eb7225 Mon Sep 17 00:00:00 2001 From: Charles Date: Tue, 3 Mar 2026 11:28:02 +0100 Subject: [PATCH] Ajax call to create + edit organizations --- assets/controllers/organization_controller.js | 74 +++++++- migrations/Version20260302105113.php | 38 ++++ src/Controller/OrganizationController.php | 175 +++++++++++------- src/Entity/Organizations.php | 4 +- src/Form/OrganizationForm.php | 5 +- templates/organization/index.html.twig | 86 +++++++-- .../organization/organizationModal.html.twig | 38 ++++ templates/organization/show.html.twig | 20 +- templates/project/index.html.twig | 5 - 9 files changed, 346 insertions(+), 99 deletions(-) create mode 100644 migrations/Version20260302105113.php create mode 100644 templates/organization/organizationModal.html.twig delete mode 100644 templates/project/index.html.twig diff --git a/assets/controllers/organization_controller.js b/assets/controllers/organization_controller.js index a1fc967..255793e 100644 --- a/assets/controllers/organization_controller.js +++ b/assets/controllers/organization_controller.js @@ -2,17 +2,18 @@ import {Controller} from '@hotwired/stimulus' // Important: include a build with Ajax + pagination (TabulatorFull is simplest) import {TabulatorFull as Tabulator} from 'tabulator-tables'; import {eyeIconLink, TABULATOR_FR_LANG} from "../js/global.js"; +import { Modal } from "bootstrap"; export default class extends Controller { static values = { - id: String, + id: Number, activities: Boolean, table: Boolean, sadmin: Boolean, user: Number }; - static targets = ["activityList", "emptyMessage"] + static targets = ["activityList", "emptyMessage", "modal", "modalTitle", "nameInput", "emailInput", "numberInput", "addressInput"]; connect() { if(this.activitiesValue){ this.loadActivities(); @@ -23,6 +24,10 @@ export default class extends Controller { if (this.tableValue && this.sadminValue) { this.table(); } + if (this.hasModalTarget) { + this.modal = new Modal(this.modalTarget); + this.currentOrgId = null; + } } @@ -153,4 +158,69 @@ export default class extends Controller { this.activityListTarget.innerHTML = html; } + + openCreateModal() { + this.currentOrgId = null; + this.modalTitleTarget.textContent = "Créer une organisation"; + this.resetForm(); + this.modal.show(); + } + + async openEditModal(event) { + this.currentOrgId = event.currentTarget.dataset.id; + this.modalTitleTarget.textContent = "Modifier l'organisation"; + + try { + const response = await fetch(`/organization/${this.currentOrgId}`); + const data = await response.json(); + + // Fill targets + this.nameInputTarget.value = data.name; + this.emailInputTarget.value = data.email; + this.numberInputTarget.value = data.number || ''; + this.addressInputTarget.value = data.address || ''; + + this.modal.show(); + } catch (error) { + alert("Erreur lors du chargement des données."); + } + } + + async submitForm(event) { + event.preventDefault(); + const formData = new FormData(event.target); + + const method = this.currentOrgId ? 'PUT' : 'POST'; + const url = this.currentOrgId ? `/organization/${this.currentOrgId}` : `/organization/`; + + + if (this.currentOrgId) { + formData.append('_method', 'PUT'); + } + + try { + const response = await fetch(url, { + 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 || "Une erreur est survenue."); + } + } catch (e) { + alert("Erreur réseau."); + } + } + + resetForm() { + this.nameInputTarget.value = ""; + this.emailInputTarget.value = ""; + this.numberInputTarget.value = ""; + this.addressInputTarget.value = ""; + } } \ No newline at end of file diff --git a/migrations/Version20260302105113.php b/migrations/Version20260302105113.php new file mode 100644 index 0000000..581959d --- /dev/null +++ b/migrations/Version20260302105113.php @@ -0,0 +1,38 @@ +addSql('ALTER TABLE organizations ALTER number DROP NOT NULL'); + $this->addSql('ALTER TABLE project ALTER bdd_name SET NOT NULL'); + $this->addSql('ALTER TABLE project ALTER timestamp_precision SET NOT NULL'); + $this->addSql('ALTER TABLE project ALTER deletion_allowed SET NOT NULL'); + } + + 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 organizations ALTER number SET NOT NULL'); + $this->addSql('ALTER TABLE project ALTER bdd_name DROP NOT NULL'); + $this->addSql('ALTER TABLE project ALTER timestamp_precision DROP NOT NULL'); + $this->addSql('ALTER TABLE project ALTER deletion_allowed DROP NOT NULL'); + } +} diff --git a/src/Controller/OrganizationController.php b/src/Controller/OrganizationController.php index fddc367..a8f851d 100644 --- a/src/Controller/OrganizationController.php +++ b/src/Controller/OrganizationController.php @@ -4,9 +4,6 @@ namespace App\Controller; use App\Entity\Actions; use App\Entity\Apps; -use App\Entity\Roles; -use App\Entity\User; -use App\Entity\UserOrganizationApp; use App\Entity\UsersOrganizations; use App\Form\OrganizationForm; use App\Repository\OrganizationsRepository; @@ -17,12 +14,8 @@ use App\Service\LoggerService; use App\Service\OrganizationsService; use App\Service\UserOrganizationService; use App\Service\UserService; -use Doctrine\DBAL\Exception\NonUniqueFieldNameException; -use Doctrine\DBAL\Exception\UniqueConstraintViolationException; use Doctrine\ORM\EntityManagerInterface; -use Doctrine\ORM\NonUniqueResultException; use Exception; -use Psr\Log\LoggerInterface; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; @@ -82,89 +75,114 @@ class OrganizationController extends AbstractController return $this->redirectToRoute('app_index'); } - - #[Route(path: '/create', name: 'create', methods: ['GET', 'POST'])] - public function new(Request $request): Response + #[Route(path: '/', name: 'create', methods: ['POST'])] + public function create(Request $request): JsonResponse { $this->denyAccessUnlessGranted('ROLE_SUPER_ADMIN'); $actingUser = $this->getUser(); - if ($request->isMethod('POST')) { - $organization = new Organizations(); - $form = $this->createForm(OrganizationForm::class, $organization); - $form->handleRequest($request); - if ($form->isSubmitted() && $form->isValid()) { - $logoFile = $form->get('logoUrl')->getData(); + + $data = $request->request->all(); + + // 2. Access file data specifically + $logoFile = $request->files->get('logoUrl'); + + $organization = new Organizations(); + $existingOrg = $this->organizationsRepository->findOneBy(['email' => $data['email']]); + if ($existingOrg) { + return $this->json(['error' => 'Une organisation avec cet email existe déjà.'], 400); + } + + $form = $this->createForm(OrganizationForm::class, $organization,[ + 'csrf_protection' => false, + 'allow_extra_fields' => true, + ]); + + + $form->submit(array_merge($data, ['logoUrl' => $logoFile])); + + if ($form->isValid()) { + try { if ($logoFile) { $this->organizationsService->handleLogo($organization, $logoFile); } - try { - $organization->setProjectPrefix($this->organizationsService->generateUniqueProjectPrefix()); - $this->entityManager->persist($organization); - $this->entityManager->flush(); - $this->loggerService->logOrganizationInformation($organization->getId(), $actingUser->getUserIdentifier(), "Organization Created"); - $this->loggerService->logSuperAdmin($actingUser->getUserIdentifier(), $actingUser->getUserIdentifier(), "Organization Created", $organization->getId()); - $this->actionService->createAction("Create Organization", $actingUser, $organization, $organization->getName()); - $this->addFlash('success', 'Organisation crée avec succès.'); - return $this->redirectToRoute('organization_index'); - } catch (Exception $e) { - $this->addFlash('danger', 'Erreur lors de la création de l\'organization'); - $this->loggerService->logError('Error creating organization', ['acting_user_id' => $actingUser->getUserIdentifier(), 'error' => $e->getMessage()]); - } + + $organization->setProjectPrefix($this->organizationsService->generateUniqueProjectPrefix()); + + $this->entityManager->persist($organization); + $this->entityManager->flush(); + + // Loggers... + $this->loggerService->logOrganizationInformation($organization->getId(), $actingUser->getUserIdentifier(), "Organization Created via ajax"); + + return $this->json([ + 'message' => 'Organisation créée avec succès.', + 'id' => $organization->getId() + ], Response::HTTP_CREATED); + + } catch (Exception $e) { + return $this->json(['error' => $e->getMessage()], 500); } - return $this->render('organization/new.html.twig', [ - 'form' => $form->createView(), - ]); } - $form = $this->createForm(OrganizationForm::class); - return $this->render('organization/new.html.twig', [ - 'form' => $form->createView(), - ]); + // 4. Return specific validation errors to help debugging + $errors = []; + foreach ($form->getErrors(true) as $error) { + $errors[] = $error->getMessage(); + } + + return $this->json(['error' => 'Validation failed', 'details' => $errors], 400); } - #[Route(path: '/edit/{id}', name: 'edit', methods: ['GET', 'POST'])] - public function edit(Request $request, $id): Response + #[Route(path: '/{id}', name: 'edit', methods: ['PUT'])] + public function edit(Request $request, int $id): JsonResponse { - $this->denyAccessUnlessGranted('ROLE_ADMIN'); + $this->denyAccessUnlessGranted('ROLE_USER'); $actingUser = $this->getUser(); $organization = $this->organizationsRepository->find($id); if (!$organization) { $this->loggerService->logEntityNotFound('Organization', [ 'org_id' => $id, - 'message' => 'Organization not found for edit'], $actingUser->getUserIdentifier() - ); - $this->addFlash('danger', 'Erreur, l\'organization est introuvable.'); - return $this->redirectToRoute('organization_index'); + 'message' => 'Organization not found for get endpoint' + ], $actingUser->getUserIdentifier()); + return $this->json(['error' => self::NOT_FOUND], Response::HTTP_NOT_FOUND); } - - $form = $this->createForm(OrganizationForm::class, $organization); - $form->handleRequest($request); + if (!$this->userService->isAdminOfOrganization($organization) && !$this->isGranted("ROLE_ADMIN")) { + $this->loggerService->logAccessDenied($actingUser->getUserIdentifier()); + return $this->json(['error' => self::ACCESS_DENIED], Response::HTTP_FORBIDDEN); + } + $data = $request->request->all(); + $logoFile = $request->files->get('logoUrl'); + $form = $this->createForm(OrganizationForm::class, $organization,[ + 'csrf_protection' => false, + 'allow_extra_fields' => true, + ]); + if ($logoFile) { + $form->submit(array_merge($data, ['logoUrl' => $logoFile]), false); + } + $form->submit(array_merge($request->request->all(), $request->files->all())); if ($form->isSubmitted() && $form->isValid()) { - $logoFile = $form->get('logoUrl')->getData(); - if ($logoFile) { - $this->organizationsService->handleLogo($organization, $logoFile); - } try { + if ($logoFile) { + $this->organizationsService->handleLogo($organization, $logoFile); + } $this->entityManager->persist($organization); $this->entityManager->flush(); - $this->loggerService->logOrganizationInformation($organization->getId(), $actingUser->getUserIdentifier(), "Organization Edited"); - if ($this->isGranted("ROLE_SUPER_ADMIN")) { - $this->loggerService->logSuperAdmin($actingUser->getUserIdentifier(), $actingUser->getUserIdentifier(), "Organization Edited", $organization->getId()); - } $this->actionService->createAction("Edit Organization", $actingUser, $organization, $organization->getName()); - $this->addFlash('success', 'Organisation modifiée avec succès.'); - return $this->redirectToRoute('organization_index'); - }catch (Exception $e) { - $this->addFlash('danger', 'Erreur lors de la modification de l\'organization'); - $this->loggerService->logError('Error editing organization', ['acting_user_id' => $actingUser->getUserIdentifier(), 'error' => $e->getMessage()]); + $this->loggerService->logOrganizationInformation($organization->getId(), $actingUser->getUserIdentifier(), "Organization Edited via ajax"); + return $this->json(['message' => 'Organisation modifiée avec succès.']); + } catch (Exception $e) { + return $this->json(['error' => $e->getMessage()], 500); } + } else { + $errors = []; + foreach ($form->getErrors(true) as $error) { + $errors[] = $error->getMessage(); + } + return $this->json(['error' => 'Validation failed', 'details' => $errors], 400); } - return $this->render('organization/edit.html.twig', [ - 'form' => $form->createView(), - 'organization' => $organization, - ]); } + #[Route(path: '/view/{id}', name: 'show', methods: ['GET'])] public function view($id): Response { @@ -286,7 +304,7 @@ class OrganizationController extends AbstractController return $this->redirectToRoute('organization_index'); } -// API endpoint to fetch organization data for Tabulator +// API endpoint to fetch organizations data for Tabulator #[Route(path: '/data', name: 'data', methods: ['GET'])] public function data(Request $request): JsonResponse { @@ -325,6 +343,7 @@ class OrganizationController extends AbstractController ]); } + /* Ajax route to get users of an organization ( used in select field for admin of an org ) */ #[Route(path: '/{id}/users', name: 'users', methods: ['GET'])] public function users($id): JsonResponse{ $this->denyAccessUnlessGranted("ROLE_USER"); @@ -351,4 +370,32 @@ class OrganizationController extends AbstractController }, $uos); return $this->json(['users' => $users]); } + + /* + * Path used to get data on an organization for the edit modal + */ + #[Route(path: '/{id}', name: 'get', methods: ['GET'])] + public function get(int $id): JsonResponse{ + $this->denyAccessUnlessGranted('ROLE_USER'); + $actingUser = $this->getUser(); + $organization = $this->organizationsRepository->find($id); + if (!$organization) { + $this->loggerService->logEntityNotFound('Organization', [ + 'org_id' => $id, + 'message' => 'Organization not found for get endpoint' + ], $actingUser->getUserIdentifier()); + return $this->json(['error' => self::NOT_FOUND], Response::HTTP_NOT_FOUND); + } + if (!$this->userService->isAdminOfOrganization($organization) && !$this->isGranted("ROLE_ADMIN")) { + $this->loggerService->logAccessDenied($actingUser->getUserIdentifier()); + return $this->json(['error' => self::ACCESS_DENIED], Response::HTTP_FORBIDDEN); + } + return $this->json([ + 'id' => $organization->getId(), + 'name' => $organization->getName(), + 'email' => $organization->getEmail(), + 'logoUrl' => $organization->getLogoUrl() ?: null, + 'active' => $organization->isActive(), + ]); + } } diff --git a/src/Entity/Organizations.php b/src/Entity/Organizations.php index 9080665..cf5f7f2 100644 --- a/src/Entity/Organizations.php +++ b/src/Entity/Organizations.php @@ -21,7 +21,7 @@ class Organizations #[ORM\Column(length: 255)] private ?string $email = null; - #[ORM\Column] + #[ORM\Column(nullable: true)] private ?int $number = null; #[ORM\Column(length: 255)] @@ -101,7 +101,7 @@ class Organizations return $this->number; } - public function setNumber(int $number): static + public function setNumber(?int $number): static { $this->number = $number; diff --git a/src/Form/OrganizationForm.php b/src/Form/OrganizationForm.php index 2bfd07b..ea3c907 100644 --- a/src/Form/OrganizationForm.php +++ b/src/Form/OrganizationForm.php @@ -7,6 +7,7 @@ use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\Type\EmailType; use Symfony\Component\Form\Extension\Core\Type\FileType; use Symfony\Component\Form\Extension\Core\Type\TextType; +use Symfony\Component\Form\Extension\Core\Type\IntegerType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; @@ -17,8 +18,8 @@ class OrganizationForm extends AbstractType $builder ->add('email', EmailType::class, ['required' => true, 'label' => 'Email*','empty_data' => '']) ->add('name', TextType::class, ['required' => true, 'label' => 'Nom de l\'organisation*','empty_data' => '']) - ->add('address', TextType::class, ['required' => true, 'label' => 'Adresse','empty_data' => '']) - ->add('number', TextType::class, ['required' => true, 'label' => 'Numéro de téléphone','empty_data' => '']) + ->add('address', TextType::class, [ 'label' => 'Adresse','empty_data' => '']) + ->add('number', IntegerType::class, [ 'label' => 'Numéro de téléphone']) ->add('logoUrl', FileType::class, [ 'required' => false, 'label' => 'Logo', diff --git a/templates/organization/index.html.twig b/templates/organization/index.html.twig index 6b3e280..143bb15 100644 --- a/templates/organization/index.html.twig +++ b/templates/organization/index.html.twig @@ -11,38 +11,86 @@ {% endfor %} {% endfor %} -
+

Gestion des organisations

{% if is_granted("ROLE_SUPER_ADMIN") %} - Ajouter une organisation + {% endif %} + + {# New organization modal #} +
+
+ {% if is_granted('ROLE_SUPER_ADMIN') and not hasOrganizations %} +
+

Aucune organisation trouvée.

+ Créer une + organisation +
-
- {% if is_granted('ROLE_SUPER_ADMIN') and not hasOrganizations %} + {% else %} -
-

Aucune organisation trouvée.

- Créer une organisation -
+
+
- {% else %} - -
-
- - {% endif %} + {% endif %} +
-
{% endblock %} \ No newline at end of file diff --git a/templates/organization/organizationModal.html.twig b/templates/organization/organizationModal.html.twig new file mode 100644 index 0000000..00759f0 --- /dev/null +++ b/templates/organization/organizationModal.html.twig @@ -0,0 +1,38 @@ + \ No newline at end of file diff --git a/templates/organization/show.html.twig b/templates/organization/show.html.twig index b83fc9f..926a367 100644 --- a/templates/organization/show.html.twig +++ b/templates/organization/show.html.twig @@ -10,7 +10,7 @@ {% endfor %} {% endfor %} -
+
{% if organization.logoUrl %} Organization logo
{% if isSA %} - Gérer - l'organisation + + {{ include('organization/organizationModal.html.twig') }}
@@ -41,8 +46,13 @@
{% endif %} {% elseif is_granted("ROLE_ADMIN") %} - Gérer mon - organisation + + {{ include('organization/organizationModal.html.twig') }} {% endif %}
diff --git a/templates/project/index.html.twig b/templates/project/index.html.twig deleted file mode 100644 index 0749a00..0000000 --- a/templates/project/index.html.twig +++ /dev/null @@ -1,5 +0,0 @@ -{% extends 'base.html.twig' %} - -{% block body %} - -{% endblock %}