import {Controller} from '@hotwired/stimulus'; import Choices from 'choices.js'; import {TabulatorFull as Tabulator} from 'tabulator-tables'; const TABULATOR_FR_LANG = { fr: { ajax: {loading: "Chargement...", error: "Erreur"}, pagination: { page_size: "Taille de page", page_title: "Afficher la page", first: "Premier", first_title: "Première page", last: "Dernier", last_title: "Dernière page", prev: "Précédent", prev_title: "Page précédente", next: "Suivant", next_title: "Page suivante", all: "Tout", counter: {showing: "Affiche", of: "de", rows: "lignes", pages: "pages"}, }, headerFilters: {default: "Filtrer la colonne...", columns: {}}, data: {loading: "Chargement des données...", error: "Erreur de chargement des données"}, groups: {item: "élément", items: "éléments"}, }, }; export default class extends Controller { static values = { rolesArray: Array, selectedRoleIds: Array, id: Number, aws: String, list: Boolean, listOrganization: Boolean, new: Boolean, admin: Boolean, listSmall: Boolean, statut: Boolean, orgId: Number } static targets = ["select"]; connect() { this.roleSelect(); if (this.listValue) { this.table(); } if (this.newValue) { this.tableSmall(); } if (this.adminValue) { this.tableSmallAdmin(); } if (this.listOrganizationValue) { this.tableOrganization() } } 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 = [ { 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(); const url = cell.getValue(); const first = (s) => (typeof s === "string" && s.length ? s.trim()[0].toUpperCase() : ""); const initials = `${first(data.name)}${first(data.prenom)}`; // wrapper is for centering and circle clipping const wrapper = document.createElement("div"); wrapper.className = "avatar-wrapper"; // same size for both cases 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"; // ensure image clips to circle if (!url) { wrapper.style.background = "#6c757d"; // gray background 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); return wrapper; } // Image case: make it fill the same wrapper const img = document.createElement("img"); img.src = `${this.awsValue || ""}${url}`; img.alt = initials || "avatar"; img.style.width = "100%"; img.style.height = "100%"; img.style.objectFit = "cover"; // keep aspect and cover circle wrapper.appendChild(img); // Optional: fallback if image fails img.addEventListener("error", () => { wrapper.innerHTML = ""; wrapper.style.background = "#6c757d"; const span = document.createElement("span"); span.className = "avatar-initials"; span.style.color = "#fff"; span.style.fontWeight = "600"; span.style.fontSize = "12px"; span.textContent = initials || "•"; wrapper.appendChild(span); }); 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' : 'Réactiver'; const actionColorClass = isActive ? 'color-secondary' : 'color-primary'; // SVGs const deactivateSvg = ` `; const activateSvg = ` `; const actionSvg = isActive ? deactivateSvg : activateSvg; return `
${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 ?')) { fetch(`/user/deactivate/${userId}`, { method: 'POST', headers: {'X-Requested-With': 'XMLHttpRequest'} }) .then(async (response) => { if (response.ok) { // Option 1: update row status and re-render to switch icon 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 ?')) { fetch(`/user/activate/${userId}`, { method: 'POST', 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 }); }; onSelectChange(row, newValue) { const data = row.getData(); console.log("Change select" + data); }; tableSmall() { const columns = [ { title: "Profil", field: "pictureUrl", width: 80, hozAlign: "center", headerSort: false, formatter: (cell) => { const data = cell.getRow().getData(); const url = cell.getValue(); const first = (s) => (typeof s === "string" && s.length ? s.trim()[0].toUpperCase() : ""); const initials = `${data.initials}`; const wrapper = document.createElement("div"); wrapper.className = "avatar-wrapper"; // same size for both cases 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"; // ensure image clips to circle if (!url) { wrapper.style.background = "#6c757d"; // gray background 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); return wrapper; } // Image case: make it fill the same wrapper const img = document.createElement("img"); img.src = `${this.awsValue || ""}${url}`; img.alt = initials || "avatar"; img.style.width = "100%"; img.style.height = "100%"; img.style.objectFit = "cover"; // keep aspect and cover circle wrapper.appendChild(img); // Optional: fallback if image fails img.addEventListener("error", () => { wrapper.innerHTML = ""; wrapper.style.background = "#6c757d"; const span = document.createElement("span"); span.className = "avatar-initials"; span.style.color = "#fff"; span.style.fontWeight = "600"; span.style.fontSize = "12px"; span.textContent = initials || "•"; wrapper.appendChild(span); }); 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(); if (url) { return ` `; } 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(); const url = cell.getValue(); const first = (s) => (typeof s === "string" && s.length ? s.trim()[0].toUpperCase() : ""); const initials = `${data.initials}`; const wrapper = document.createElement("div"); wrapper.className = "avatar-wrapper"; // same size for both cases 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"; // ensure image clips to circle if (!url) { wrapper.style.background = "#6c757d"; // gray background 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); return wrapper; } // Image case: make it fill the same wrapper const img = document.createElement("img"); img.src = `${this.awsValue || ""}${url}`; img.alt = initials || "avatar"; img.style.width = "100%"; img.style.height = "100%"; img.style.objectFit = "cover"; // keep aspect and cover circle wrapper.appendChild(img); // Optional: fallback if image fails img.addEventListener("error", () => { wrapper.innerHTML = ""; wrapper.style.background = "#6c757d"; const span = document.createElement("span"); span.className = "avatar-initials"; span.style.color = "#fff"; span.style.fontWeight = "600"; span.style.fontSize = "12px"; span.textContent = initials || "•"; wrapper.appendChild(span); }); 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(); if (url) { return ` `; } 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(); const url = cell.getValue(); const first = (s) => (typeof s === "string" && s.length ? s.trim()[0].toUpperCase() : ""); const initials = `${first(data.name)}${first(data.prenom)}`; const wrapper = document.createElement("div"); wrapper.className = "avatar-wrapper"; // same size for both cases 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"; // ensure image clips to circle if (!url) { wrapper.style.background = "#6c757d"; // gray background 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); return wrapper; } // Image case: make it fill the same wrapper const img = document.createElement("img"); img.src = `${this.awsValue || ""}${url}`; img.alt = initials || "avatar"; img.style.width = "100%"; img.style.height = "100%"; img.style.objectFit = "cover"; // keep aspect and cover circle wrapper.appendChild(img); // Optional: fallback if image fails img.addEventListener("error", () => { wrapper.innerHTML = ""; wrapper.style.background = "#6c757d"; const span = document.createElement("span"); span.className = "avatar-initials"; span.style.color = "#fff"; span.style.fontWeight = "600"; span.style.fontSize = "12px"; span.textContent = initials || "•"; wrapper.appendChild(span); }); 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; const orgId = this.orgIdValue; // Decide which action (deactivate vs activate) const isActive = Boolean(statut); const actionClass = isActive ? 'deactivate-user' : 'activate-user'; const actionTitle = isActive ? 'Désactiver' : 'Réactiver'; const actionColorClass = isActive ? 'color-secondary' : 'color-primary'; // SVGs const deactivateSvg = ` `; const activateSvg = ` `; const actionSvg = isActive ? deactivateSvg : activateSvg; return `
${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('organizationId', target.getAttribute('data-org-id')); fetch(`/user/organization/deactivate/${userId}`, { method: 'POST', body: formData, headers: {'X-Requested-With': 'XMLHttpRequest'} }) .then(async (response) => { if (response.ok) { // Option 1: update row status and re-render to switch icon 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('organizationId', target.getAttribute('data-org-id')); fetch(`/user/organization/activate/${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')); } } } }]; // if (this.statutValue) { // columns.push( // { // title: "Statut", field: "role", // or any field you want // headerSort: false, // 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, rowHeight: 60, layout: "fitColumns", // activate French columns }); }; }