import {Controller} from '@hotwired/stimulus'; import Choices from 'choices.js'; import {TabulatorFull as Tabulator} from 'tabulator-tables'; import { activateUserIcon, deactivateUserIcon, eyeIconLink, sendEmailIcon, TABULATOR_FR_LANG, trashIconForm } from "../js/global.js"; import { Modal } from "bootstrap"; import base_controller from "./base_controller.js"; export default class extends base_controller { static values = { rolesArray: Array, selectedRoleIds: Array, id: Number, list: Boolean, listOrganization: Boolean, new: Boolean, admin: Boolean, listSmall: Boolean, statut: Boolean, orgId: Number } static targets = ["select", "statusButton", "modal", "userSelect", "appList"]; connect() { this.roleSelect(); if (this.listValue) { this.table(); } if (this.newValue) { this.tableNew(); } if (this.adminValue) { this.tableSmallAdmin(); } if (this.listOrganizationValue) { this.tableOrganization() } if (this.hasModalTarget) { this.modal = new Modal(this.modalTarget); } } roleSelect() { if (this.hasSelectTarget) { const choicesData = this.rolesArrayValue.map(role => ({ value: role.id, label: role.name, selected: this.selectedRoleIdsValue.includes(role.id) })); new Choices(this.selectTarget, { choices: choicesData, removeItemButton: true, placeholder: true, placeholderValue: 'Ajouter un ou plusieurs rôles', }); } } // TODO: vérifier le style des header filter et vertAlign/hozalign table() { const columns = [ { placeholder: "Aucun utilisateur trouvé", title: "", field: "isConnected", width: 40, // small column hozAlign: "center", vertAlign: "middle", headerSort: false, tooltip: false, formatter: (cell) => { const online = !!cell.getValue(); const color = online ? "#80F20E" : "#E42E31"; // green/red return ``; }, // Optional: for accessibility formatterPrint: (cell) => (cell.getValue() ? "online" : "offline"), formatterClipboard: (cell) => (cell.getValue() ? "online" : "offline"), }, { title: "Profil", field: "pictureUrl", width: 80, hozAlign: "center", headerSort: false, formatter: (cell) => { const data = cell.getRow().getData(); let url = cell.getValue(); // use 'let' so we can modify it // 1. GENERATE INITIALS const first = (s) => (typeof s === "string" && s.length ? s.trim()[0].toUpperCase() : ""); const initials = `${first(data.name)}${first(data.prenom)}`; // 2. CREATE WRAPPER const wrapper = document.createElement("div"); wrapper.className = "avatar-wrapper"; wrapper.style.width = "40px"; wrapper.style.height = "40px"; wrapper.style.display = "flex"; wrapper.style.alignItems = "center"; wrapper.style.justifyContent = "center"; wrapper.style.borderRadius = "50%"; wrapper.style.overflow = "hidden"; // Helper to render fallback initials const renderFallback = () => { wrapper.innerHTML = ""; // clear any broken img wrapper.style.background = "#6c757d"; const span = document.createElement("span"); span.className = "avatar-initials"; span.style.color = "#fff"; span.style.fontWeight = "600"; span.style.fontSize = "14px"; span.textContent = initials || "•"; wrapper.appendChild(span); }; // 3. IF NO URL, RENDER FALLBACK IMMEDIATELY if (!url) { renderFallback(); return wrapper; } // --- THE FIX: HANDLE RELATIVE PATHS --- // If the path doesn't start with 'http' or '/', add a leading '/' // This ensures 'uploads/file.jpg' becomes '/uploads/file.jpg' if (!url.startsWith('http') && !url.startsWith('/')) { url = '/' + url; } // 4. CREATE IMAGE const img = document.createElement("img"); img.src = url; img.alt = initials || "avatar"; img.style.width = "100%"; img.style.height = "100%"; img.style.objectFit = "cover"; // 5. ERROR HANDLING (triggers your fallback) img.onerror = () => { console.warn("Image failed to load:", url); // Debug log to see the wrong path renderFallback(); }; wrapper.appendChild(img); return wrapper; }, }, {title: "Nom", field: "name", headerFilter: "input", widthGrow: 2, vertAlign: "middle"}, {title: "Prénom", field: "prenom", headerFilter: "input", widthGrow: 2, vertAlign: "middle"}, {title: "Email", field: "email", headerFilter: "input", widthGrow: 3, vertAlign: "middle"}, { title: "Statut", field: "statut", vertAlign: "middle", formatter: (cell) => { const statut = cell.getValue(); if (statut) { return `Actif` } else { return `Inactif` } } }, { title: "Actions", field: "showUrl", vertAlign: "middle", headerSort: false, formatter: (cell) => { const url = cell.getValue(); if (!url) return ''; const rowData = cell.getRow().getData(); const userId = rowData.id; const statut = rowData.statut; // Decide which action (deactivate vs activate) const isActive = Boolean(statut); const actionClass = isActive ? 'deactivate-user' : 'activate-user'; const actionTitle = isActive ? 'Désactiver l\'utilisateur': 'Réactiver l\'utilisateur'; const actionColorClass = isActive ? 'color-secondary' : 'color-primary'; // SVGs const deactivateSvg = deactivateUserIcon(); const activateSvg = activateUserIcon(); const actionSvg = isActive ? deactivateSvg : activateSvg; return `
${eyeIconLink(url)} ${actionSvg}
`; }, cellClick: function (e, cell) { const target = e.target.closest('a'); if (!target) return; // Deactivate if (target.classList.contains('deactivate-user')) { e.preventDefault(); const userId = target.getAttribute('data-id'); if (confirm('Voulez-vous vraiment désactiver cet utilisateur ?')) { const formData = new FormData(); formData.append('status', 'deactivate'); fetch(`/user/activeStatus/${userId}`, { method: 'POST', body: formData, headers: {'X-Requested-With': 'XMLHttpRequest'} }) .then(async (response) => { if (response.ok) { const data = cell.getRow().getData(); data.statut = false; cell.getRow().reformat(); } else { const text = await response.text(); alert('Erreur lors de la désactivation: ' + text); } }) .catch(() => alert('Erreur lors de la désactivation')); } } // Activate if (target.classList.contains('activate-user')) { e.preventDefault(); const userId = target.getAttribute('data-id'); if (confirm('Voulez-vous réactiver cet utilisateur ?')) { const formData = new FormData(); formData.append('status','activate'); fetch(`/user/activeStatus/${userId}`, { method: 'POST', body: formData, headers: {'X-Requested-With': 'XMLHttpRequest'} }) .then(async (response) => { if (response.ok) { // Switch status back to active and re-render row const data = cell.getRow().getData(); data.statut = true; cell.getRow().reformat(); } else { const text = await response.text(); alert('Erreur lors de la réactivation: ' + text); } }) .catch(() => alert('Erreur lors de la réactivation')); } } } }]; const tabulator = new Tabulator("#tabulator-userList", { langs: TABULATOR_FR_LANG, locale: "fr", ajaxURL: "/user/data", ajaxConfig: "GET", pagination: true, paginationMode: "remote", paginationSize: 10, ajaxResponse: (url, params, response) => response, paginationDataSent: {page: "page", size: "size"}, paginationDataReceived: {last_page: "last_page"}, ajaxSorting: true, ajaxFiltering: true, filterMode: "remote", // Add this to send filter data ajaxURLGenerator: function(url, config, params) { let queryParams = new URLSearchParams(); queryParams.append('page', params.page || 1); queryParams.append('size', params.size || 10); // Add filters if (params.filter) { params.filter.forEach(filter => { queryParams.append(`filter[${filter.field}]`, filter.value); }); } return `${url}?${queryParams.toString()}`; }, rowHeight: 60, layout: "fitColumns", columns }); }; tableNew() { const columns = [ { title: "Profil", field: "pictureUrl", width: 80, hozAlign: "center", headerSort: false, formatter: (cell) => { const data = cell.getRow().getData(); let url = cell.getValue(); // use 'let' so we can modify it // 1. GENERATE INITIALS const first = (s) => (typeof s === "string" && s.length ? s.trim()[0].toUpperCase() : ""); const initials = `${first(data.name)}${first(data.prenom)}`; // 2. CREATE WRAPPER const wrapper = document.createElement("div"); wrapper.className = "avatar-wrapper"; wrapper.style.width = "40px"; wrapper.style.height = "40px"; wrapper.style.display = "flex"; wrapper.style.alignItems = "center"; wrapper.style.justifyContent = "center"; wrapper.style.borderRadius = "50%"; wrapper.style.overflow = "hidden"; // Helper to render fallback initials const renderFallback = () => { wrapper.innerHTML = ""; // clear any broken img wrapper.style.background = "#6c757d"; const span = document.createElement("span"); span.className = "avatar-initials"; span.style.color = "#fff"; span.style.fontWeight = "600"; span.style.fontSize = "14px"; span.textContent = initials || "•"; wrapper.appendChild(span); }; // 3. IF NO URL, RENDER FALLBACK IMMEDIATELY if (!url) { renderFallback(); return wrapper; } // --- THE FIX: HANDLE RELATIVE PATHS --- // If the path doesn't start with 'http' or '/', add a leading '/' // This ensures 'uploads/file.jpg' becomes '/uploads/file.jpg' if (!url.startsWith('http') && !url.startsWith('/')) { url = '/' + url; } // 4. CREATE IMAGE const img = document.createElement("img"); img.src = url; img.alt = initials || "avatar"; img.style.width = "100%"; img.style.height = "100%"; img.style.objectFit = "cover"; // 5. ERROR HANDLING (triggers your fallback) img.onerror = () => { console.warn("Image failed to load:", url); // Debug log to see the wrong path renderFallback(); }; wrapper.appendChild(img); return wrapper; }, }, {title: "Email", field: "email", widthGrow: 3, vertAlign: "middle"}, { title: "Actions", field: "showUrl", hozAlign: "center", width: 100, vertAlign: "middle", headerSort: false, formatter: (cell) => { const url = cell.getValue() + '?organizationId=' + this.orgIdValue; console.log(url); if (url) { return eyeIconLink(url); } return ''; } } ]; const tabulator = new Tabulator("#tabulator-userListSmall", { locale: "fr", //'en' for English, 'fr' for French (en is default, no need to include it) ajaxURL: "/user/data/new", ajaxConfig: "GET", pagination: false, paginationMode: "remote", // paginationSize: 5, ajaxParams: {orgId: this.orgIdValue}, langs: TABULATOR_FR_LANG, ajaxResponse: (url, params, response) => response.data, // paginationDataSent: {page: "page", size: "size"}, // paginationDataReceived: {last_page: "last_page"}, // ajaxSorting: true, // ajaxFiltering: true, rowHeight: 60, layout: "fitColumns", // activate French columns }); } tableSmallAdmin() { const columns = [ { title: "Profil", field: "pictureUrl", width: 80, hozAlign: "center", headerSort: false, formatter: (cell) => { const data = cell.getRow().getData(); let url = cell.getValue(); // use 'let' so we can modify it // 1. GENERATE INITIALS const first = (s) => (typeof s === "string" && s.length ? s.trim()[0].toUpperCase() : ""); const initials = `${first(data.name)}${first(data.prenom)}`; // 2. CREATE WRAPPER const wrapper = document.createElement("div"); wrapper.className = "avatar-wrapper"; wrapper.style.width = "40px"; wrapper.style.height = "40px"; wrapper.style.display = "flex"; wrapper.style.alignItems = "center"; wrapper.style.justifyContent = "center"; wrapper.style.borderRadius = "50%"; wrapper.style.overflow = "hidden"; // Helper to render fallback initials const renderFallback = () => { wrapper.innerHTML = ""; // clear any broken img wrapper.style.background = "#6c757d"; const span = document.createElement("span"); span.className = "avatar-initials"; span.style.color = "#fff"; span.style.fontWeight = "600"; span.style.fontSize = "14px"; span.textContent = initials || "•"; wrapper.appendChild(span); }; // 3. IF NO URL, RENDER FALLBACK IMMEDIATELY if (!url) { renderFallback(); return wrapper; } // --- THE FIX: HANDLE RELATIVE PATHS --- // If the path doesn't start with 'http' or '/', add a leading '/' // This ensures 'uploads/file.jpg' becomes '/uploads/file.jpg' if (!url.startsWith('http') && !url.startsWith('/')) { url = '/' + url; } // 4. CREATE IMAGE const img = document.createElement("img"); img.src = url; img.alt = initials || "avatar"; img.style.width = "100%"; img.style.height = "100%"; img.style.objectFit = "cover"; // 5. ERROR HANDLING (triggers your fallback) img.onerror = () => { console.warn("Image failed to load:", url); // Debug log to see the wrong path renderFallback(); }; wrapper.appendChild(img); return wrapper; }, }, {title: "Email", field: "email", widthGrow: 3, vertAlign: "middle"}, { title: "Actions", field: "showUrl", hozAlign: "center", width: 100, vertAlign: "middle", headerSort: false, formatter: (cell) => { const url = cell.getValue(); const orgId = this.orgIdValue; if (url) { return trashIconForm(url, orgId); } return ''; } } ]; const tabulator = new Tabulator("#tabulator-userListSmallAdmin", { locale: "fr", //'en' for English, 'fr' for French (en is default, no need to include it) ajaxURL: "/user/data/admin", ajaxConfig: "GET", pagination: false, paginationMode: "remote", // paginationSize: 5, ajaxParams: {orgId: this.orgIdValue}, langs: TABULATOR_FR_LANG, ajaxResponse: (url, params, response) => response.data, // paginationDataSent: {page: "page", size: "size"}, // paginationDataReceived: {last_page: "last_page"}, // ajaxSorting: true, // ajaxFiltering: true, rowHeight: 60, layout: "fitColumns", // activate French columns }); } tableOrganization() { const columns = [ { title: "", field: "isConnected", width: 40, // small column hozAlign: "center", vertAlign: "middle", headerSort: false, tooltip: false, formatter: (cell) => { const online = !!cell.getValue(); const color = online ? "#80F20E" : "#E42E31"; // green/red return ``; }, // Optional: for accessibility formatterPrint: (cell) => (cell.getValue() ? "online" : "offline"), formatterClipboard: (cell) => (cell.getValue() ? "online" : "offline"), }, { title: "Profil", field: "pictureUrl", width: 80, hozAlign: "center", headerSort: false, formatter: (cell) => { const data = cell.getRow().getData(); let url = cell.getValue(); // use 'let' so we can modify it // 1. GENERATE INITIALS const first = (s) => (typeof s === "string" && s.length ? s.trim()[0].toUpperCase() : ""); const initials = `${first(data.name)}${first(data.prenom)}`; // 2. CREATE WRAPPER const wrapper = document.createElement("div"); wrapper.className = "avatar-wrapper"; wrapper.style.width = "40px"; wrapper.style.height = "40px"; wrapper.style.display = "flex"; wrapper.style.alignItems = "center"; wrapper.style.justifyContent = "center"; wrapper.style.borderRadius = "50%"; wrapper.style.overflow = "hidden"; // Helper to render fallback initials const renderFallback = () => { wrapper.innerHTML = ""; // clear any broken img wrapper.style.background = "#6c757d"; const span = document.createElement("span"); span.className = "avatar-initials"; span.style.color = "#fff"; span.style.fontWeight = "600"; span.style.fontSize = "14px"; span.textContent = initials || "•"; wrapper.appendChild(span); }; // 3. IF NO URL, RENDER FALLBACK IMMEDIATELY if (!url) { renderFallback(); return wrapper; } // --- THE FIX: HANDLE RELATIVE PATHS --- // If the path doesn't start with 'http' or '/', add a leading '/' // This ensures 'uploads/file.jpg' becomes '/uploads/file.jpg' if (!url.startsWith('http') && !url.startsWith('/')) { url = '/' + url; } // 4. CREATE IMAGE const img = document.createElement("img"); img.src = url; img.alt = initials || "avatar"; img.style.width = "100%"; img.style.height = "100%"; img.style.objectFit = "cover"; // 5. ERROR HANDLING (triggers your fallback) img.onerror = () => { console.warn("Image failed to load:", url); // Debug log to see the wrong path renderFallback(); }; wrapper.appendChild(img); return wrapper; }, }, {title: "Nom", field: "name", headerFilter: "input", widthGrow: 2, vertAlign: "middle"}, {title: "Prénom", field: "prenom", headerFilter: "input", widthGrow: 2, vertAlign: "middle"}, {title: "Email", field: "email", headerFilter: "input", widthGrow: 3, vertAlign: "middle"}, { title: "Statut", field: "statut", vertAlign: "middle", formatter: (cell) => { const statut = cell.getValue(); if (statut === "INVITED") { return `Invité` } else if (statut === "ACTIVE") { return `Actif` }else if( statut === "EXPIRED"){ return `Expiré` } else{ return `Inactif` } } }, { title: "Actions", field: "showUrl", vertAlign: "middle", headerSort: false, formatter: (cell) => { const url = cell.getValue(); if (!url) return ''; const rowData = cell.getRow().getData(); const userId = rowData.id; const statut = rowData.statut; const orgId = this.orgIdValue; // Check if user is expired if (statut === "EXPIRED") { return `
${eyeIconLink(url)} ${sendEmailIcon(userId, orgId)}
`; }if (statut === "INVITED") { return `
${eyeIconLink(url)}
`; } // Decide which action (deactivate vs activate) for non-expired users const isActive = (statut === "ACTIVE"); const actionClass = isActive ? 'deactivate-user' : 'activate-user'; const actionTitle = isActive ? 'Désactiver l\'utilisateur': 'Réactiver l\'utilisateur' ; const actionColorClass = isActive ? 'color-secondary' : 'color-primary'; // SVGs const deactivateSvg = deactivateUserIcon(); const activateSvg = activateUserIcon(); const actionSvg = isActive ? deactivateSvg : activateSvg; return `
${eyeIconLink(url)} ${actionSvg}
`; }, cellClick: function (e, cell) { const target = e.target.closest('a'); if (!target) return; // Handle resend invitation for expired users if (target.classList.contains('resend-invitation')) { e.preventDefault(); const userId = target.getAttribute('data-id'); if (confirm('Voulez-vous renvoyer l\'invitation à cet utilisateur ?')) { const formData = new FormData(); formData.append('organizationId', target.getAttribute('data-org-id')); fetch(`/user/organization/resend-invitation/${userId}`, { method: 'POST', body: formData, headers: {'X-Requested-With': 'XMLHttpRequest'} }) .then(async (response) => { if (response.ok) { const data = cell.getRow().getData(); data.statut = "INVITED"; cell.getRow().reformat(); alert('Invitation renvoyée avec succès'); } else { const text = await response.text(); alert('Erreur lors de l\'envoi : ' + text); } }) .catch(() => alert('Erreur lors de l\'envoi')); } } // Deactivate if (target.classList.contains('deactivate-user')) { e.preventDefault(); const userId = target.getAttribute('data-id'); if (confirm('Voulez-vous vraiment désactiver cet utilisateur ?')) { const formData = new FormData(); formData.append('status', 'deactivate'); formData.append('organizationId', target.getAttribute('data-org-id')); fetch(`/user/organization/activateStatus/${userId}`, { method: 'POST', body: formData, headers: {'X-Requested-With': 'XMLHttpRequest'} }) .then(async (response) => { if (response.ok) { const data = cell.getRow().getData(); data.statut = "INACTIVE"; cell.getRow().reformat(); } else { const text = await response.text(); alert('Erreur lors de la désactivation: ' + text); } }) .catch(() => alert('Erreur lors de la désactivation')); } } // Activate if (target.classList.contains('activate-user')) { e.preventDefault(); const userId = target.getAttribute('data-id'); if (confirm('Voulez-vous réactiver cet utilisateur ?')) { const formData = new FormData(); formData.append('status', 'activate'); formData.append('organizationId', target.getAttribute('data-org-id')); fetch(`/user/organization/activateStatus/${userId}`, { method: 'POST', body: formData, headers: {'X-Requested-With': 'XMLHttpRequest'} }) .then(async (response) => { if (response.ok) { const data = cell.getRow().getData(); data.statut = "ACTIVE"; cell.getRow().reformat(); } else { const text = await response.text(); alert('Erreur lors de la réactivation: ' + text); } }) .catch(() => alert('Erreur lors de la réactivation')); } } } }]; // if (this.statutValue) { // columns.push( // { // title: "Statut", field: "role", // or any field you want // headerSort: false,x // hozAlign: "center", // vertAlign: "middle", // formatter: (cell) => { // const row = cell.getRow(); // const current = cell.getValue() ?? ""; // // const select = document.createElement("select"); // select.className = "table-select-action"; // // Options // [ // {value: "", label: "Choisir..."}, // {value: "viewer", label: "Viewer"}, // {value: "editor", label: "Editor"}, // {value: "admin", label: "Admin"}, // ].forEach(opt => { // const o = document.createElement("option"); // o.value = opt.value; // o.textContent = opt.label; // if (opt.value === current) o.selected = true; // select.appendChild(o); // }); // // // Hook change // select.addEventListener("change", (e) => { // this.onSelectChange(row, e.target.value); // }); // // // Return a DOM node from a formatter → Tabulator will mount it // return select; // }, // // Optional: provide text for clipboard/print // formatterClipboard: cell => cell.getValue(), // formatterPrint: cell => cell.getValue(), // }, // ) // } const tabulator = new Tabulator("#tabulator-userListOrganization", { langs: TABULATOR_FR_LANG, locale: "fr", ajaxURL: "/user/data/organization", ajaxConfig: "GET", ajaxParams: {orgId: this.orgIdValue}, pagination: true, paginationMode: "remote", paginationSize: 10, ajaxResponse: (url, params, response) => response, paginationDataSent: {page: "page", size: "size"}, paginationDataReceived: {last_page: "last_page"}, ajaxSorting: true, ajaxFiltering: true, filterMode: "remote", ajaxURLGenerator: function(url, config, params) { let queryParams = new URLSearchParams(); // console.log("orgId:", params.orgId); queryParams.append('orgId', params.orgId); queryParams.append('page', params.page || 1); queryParams.append('size', params.size || 10); // Add filters if (params.filter) { params.filter.forEach(filter => { queryParams.append(`filter[${filter.field}]`, filter.value); }); } return `${url}?${queryParams.toString()}`; }, rowHeight: 60, layout: "fitColumns", // activate French columns }); }; async toggleStatus(event) { event.preventDefault(); const button = this.statusButtonTarget; const isActive = button.dataset.active === "true"; const newStatus = isActive ? 'deactivate' : 'activate'; const confirmMsg = isActive ? "Désactiver cet utilisateur ?" : "Réactiver cet utilisateur ?"; if (!confirm(confirmMsg)) return; try { const formData = new FormData(); formData.append('status', newStatus); const response = await fetch(`/user/activeStatus/${this.idValue}`, { method: 'POST', body: formData, headers: {'X-Requested-With': 'XMLHttpRequest'} }); if (response.ok) { this.updateButtonUI(!isActive); } else { alert('Erreur lors de la mise à jour'); } } catch (err) { alert('Erreur de connexion'); } } updateButtonUI(nowActive) { const btn = this.statusButtonTarget; if (nowActive) { btn.textContent = "Désactiver"; btn.classList.replace("btn-success", "btn-secondary"); btn.dataset.active = "true"; } else { btn.textContent = "Réactiver"; btn.classList.replace("btn-secondary", "btn-success"); btn.dataset.active = "false"; } } async openAddAdminModal() { this.modal.show(); await this.loadAvailableUsers(); } async loadAvailableUsers() { try { const response = await fetch(`/organization/${this.orgIdValue}/users`); const data = await response.json(); if (data.users) { this.userSelectTarget.innerHTML = '' + data.users.map(user => ` `).join(''); } } catch (error) { this.userSelectTarget.innerHTML = ''; } } async submitAddAdmin(event) { event.preventDefault(); const formData = new FormData(event.target); const userId = formData.get('userId'); try { const response = await fetch(`/user/organization/admin/${userId}`, { method: 'POST', body: formData, // Sends userId, status="add", and organizationId headers: {'X-Requested-With': 'XMLHttpRequest'} }); if (response.ok) { this.modal.hide(); // If you use Tabulator, refresh it here // e.g., this.table.setData(); location.reload(); } else { if (response.status === 409) { alert("Cet utilisateur est déjà administrateur de l'organisation."); return; } alert("Erreur lors de l'ajout de l'administrateur."); } } catch (error) { alert("Une erreur est survenue."); } } async removeAdmin(event) { // 1. Prevent any default behavior event.preventDefault(); const button = event.currentTarget; const url = button.dataset.url; const organizationId = button.dataset.orgId; if (!confirm("Voulez-vous vraiment retirer les droits d'administrateur à cet utilisateur ?")) { return; } // 2. Prepare the payload (matching your previous hidden fields) const formData = new FormData(); formData.append('status', 'remove'); formData.append('organizationId', organizationId); try { const response = await fetch(url, { method: 'POST', body: formData, headers: { 'X-Requested-With': 'XMLHttpRequest' } }); if (response.ok) { // 3. Success! Reload the page to see the updated list location.reload(); } else { const errorData = await response.json(); alert("Erreur: " + (errorData.error || "Impossible de supprimer les droits.")); } } catch (error) { 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."); } } }