enabled ajax function to edit user

This commit is contained in:
Charles 2026-02-17 14:32:33 +01:00
parent c2ea41f0a1
commit b9b0efd6c6
7 changed files with 318 additions and 271 deletions

View File

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

View File

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

View File

@ -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]

View File

@ -10,7 +10,7 @@ use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<UserOrganizationApp>
*/
class UserOrganizatonAppRepository extends ServiceEntityRepository
class UserOrganizationAppRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{

View File

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

View File

@ -33,112 +33,19 @@
{% include 'user/userInformation.html.twig' %}
<div class="card border-0 no-header-bg ">
<div class="card-header">
<div class="card-title">
<h1>Information d'organisation</h1>
</div>
</div>
<div class="card-body ms-4">
{# TODO: dynamic number of project#}
<p><b>Projet : </b>69 projets vous sont attribués</p>
</div>
{# <div class="card-body">#}
{# <div class="row g-2">#}
{# {% for app in apps %}#}
{# <div class="col-12 col-md-6">#}
{# <div class="card h-100">#}
{# <div class="card-header d-flex gap-2">#}
{# {% if app.logoMiniUrl %}#}
{# <img src="{{ asset(appli.entity.logoMiniUrl) }}" alt="Logo {{ app.name }}"#}
{# class="rounded-circle" style="width:50px; height:50px;">#}
{# {% endif %}#}
{# <div class="card border-0 no-header-bg ">#}
{# <div class="card-header">#}
{# <div class="card-title">#}
{# <h1>{{ app.name|title }}</h1>#}
{# <h1>Information d'organisation</h1>#}
{# </div>#}
{# </div>#}
{# <div class="card-body ms-4">#}
{# TODO: dynamic number of project#}
{# <p><b>Projet : </b>69 projets vous sont attribués</p>#}
{# </div>#}
{# <div class="card-body">#}
{# <div class="row">#}
{# <p>#}
{# <b>Description :</b>#}
{# {{ app.descriptionSmall|default('Aucune description disponible.')|raw }}#}
{# </p>#}
{# </div>#}
{# #}{# find appGroup once, used in both editable and read-only branches #}
{# {% set appGroup = data.uoas[app.id]|default(null) %}#}
{# {% if canEdit %}#}
{# <form method="POST"#}
{# action="{{ path('user_application_role', { id: data.singleUo.id }) }}">#}
{# <div class="form-group mb-3">#}
{# <label for="roles-{{ app.id }}"><b>Rôles :</b></label>#}
{# <div class="form-check">#}
{# {% if appGroup %}#}
{# {% for role in data.rolesArray %}#}
{# <input class="form-check-input" type="checkbox"#}
{# name="roles[]"#}
{# value="{{ role.id }}"#}
{# id="role-{{ role.id }}-app-{{ app.id }}"#}
{# {% if role.id in appGroup.selectedRoleIds %}checked{% endif %}>#}
{# <label class="form-check"#}
{# for="role-{{ role.id }}-app-{{ app.id }}">#}
{# {% if role.name == 'USER' %}#}
{# Accès#}
{# {% else %}#}
{# {{ role.name|capitalize }}#}
{# {% endif %}#}
{# </label>#}
{# {% endfor %}#}
{# {% else %}#}
{# <p class="text-muted">Aucun rôle défini pour cette application.</p>#}
{# {% endif %}#}
{# </div>#}
{# <button type="submit" name="appId" value="{{ app.id }}"#}
{# class="btn btn-primary mt-2">#}
{# Sauvegarder#}
{# </button>#}
{# </div>#}
{# </form>#}
{# {% else %}#}
{# <div class="form-group mb-3">#}
{# <label for="roles-{{ app.id }}"><b>Rôles :</b></label>#}
{# <div class="form-check">#}
{# {% if appGroup %}#}
{# {% for role in data.rolesArray %}#}
{# <input class="form-check-input" type="checkbox"#}
{# disabled#}
{# name="roles[]"#}
{# value="{{ role.id }}"#}
{# id="role-{{ role.id }}-app-{{ app.id }}"#}
{# {% if role.id in appGroup.selectedRoleIds %}checked{% endif %}>#}
{# <label class="form-check"#}
{# for="role-{{ role.id }}-app-{{ app.id }}">#}
{# {% if role.name == 'USER' %}#}
{# Accès#}
{# {% else %}#}
{# {{ role.name|capitalize }}#}
{# {% endif %}#}
{# </label>#}
{# {% endfor %}#}
{# {% else %}#}
{# <p class="text-muted">Aucun rôle défini pour cette application.</p>#}
{# {% endif %}#}
{# </div>#}
{# </div>#}
{# {% endif %}#}
{# </div>#}
{# </div>#}
{# </div>#}
{# {% endfor %}#}
{# </div>#}
{# </div>#}
</div>
</div>
</div>

View File

@ -1,10 +1,12 @@
{% block body %}
<div class="card no-header-bg border-0 ">
<div class="card no-header-bg border-0"
data-controller="user"
data-user-org-id-value="{{organizationId}}">
<div class="card-header d-flex justify-content-between align-items-center">
<div class="d-flex gap-2">
{% if user.pictureUrl is not empty %}
<img src="{{asset(user.pictureUrl)}}" alt="user" class="rounded-circle"
style="width:40px; height:40px;">
<img src="{{ asset(user.pictureUrl) }}" alt="user" class="rounded-circle" style="width:40px; height:40px;">
{% endif %}
<div class="card-title">
<h2>{{ user.surname|capitalize }} {{ user.name|capitalize }}</h2>
@ -12,20 +14,67 @@
</div>
<div class="d-flex gap-2">
<a href="{{ path('user_edit', {'id': user.id}) }}"
class="btn btn-primary">Modifier</a>
{# Trigger the edit modal with the user ID #}
<button type="button" class="btn btn-primary"
data-action="click->user#openEditUserModal"
data-id="{{ user.id }}">
Modifier
</button>
</div>
</div>
<div class="card-body ms-4">
<p><b>Email: </b>{{ user.email }}</p>
<p><b>Dernière connection: </b>{{ user.lastConnection|date('d/m/Y') }}
à {{ user.lastConnection|date('H:m:s') }} </p>
<p><b>Dernière connection: </b>{{ user.lastConnection|date('d/m/Y') }} à {{ user.lastConnection|date('H:m') }}</p>
<p><b>Compte crée le: </b>{{ user.createdAt|date('d/m/Y') }}</p>
<p><b>Numéro de téléphone: </b>{{ user.phoneNumber ? user.phoneNumber : 'Non renseigné' }}</p>
</div>
{# Reusable Edit Modal #}
<div class="modal fade" id="editUserModal" 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">Modifier l'utilisateur</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<form data-action="submit->user#submitEditUser">
<div class="modal-body">
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label">Email*</label>
<input type="email" name="email" class="form-control" data-user-target="emailInput" required>
</div>
<div class="col-md-6 mb-3">
<label class="form-label">Numéro de téléphone</label>
<input type="text" name="phoneNumber" class="form-control" data-user-target="phoneInput">
</div>
</div>
<div class="row">
<div class="col-md-6 mb-3">
<label class="form-label">Prénom*</label>
<input type="text" name="name" class="form-control" data-user-target="nameInput" required>
</div>
<div class="col-md-6 mb-3">
<label class="form-label">Nom*</label>
<input type="text" name="surname" class="form-control" data-user-target="surnameInput" required>
</div>
</div>
<input type="hidden" name="organizationId" value="{{ organizationId }}">
<hr>
<label class="form-label">**Accès aux applications**</label>
<div class="row" data-user-target="appList">
{# Checkboxes loaded here #}
</div>
</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">Enregistrer les modifications</button>
</div>
</form>
</div>
</div>
</div>
</div>
{% endblock %}