diff --git a/HELPER.MD b/HELPER.MD new file mode 100644 index 0000000..934bbfb --- /dev/null +++ b/HELPER.MD @@ -0,0 +1,17 @@ +## Notes +- Certaines abbreviations sont utilisées afin de simplifier le code et d'éviter les répétitions ou noms trop longs : + - `uo` pour `User Organization` + - `uoId` pour `User Organization Id` + - `oa` pour `Organization Application` + - `at` pour `Access Token` +- A delete command is available to delete roles + + +### ROLES +```bash + php bin/console app:delete-role ROLE_NAME +``` + +### Tabulator +- Certaines fonctions sont déjà disponibles (snippet) mais commentées, car on ne les utilise pas + - Exemples de sorting et filtering sont disponibles dans 'src/controller/organization.php' L.268 \ No newline at end of file diff --git a/README.MD b/README.MD index da57222..e7fd7a1 100644 --- a/README.MD +++ b/README.MD @@ -24,13 +24,3 @@ php bin/console importmap:require choices.js php bin/console importmap:require choices.js/public/assets/styles/choices.min.css ``` -### Notes -- certaines abbreviations sont utilisées afin de simplifier le code et d'éviter les répétitions ou noms trop longs : - - `uo` pour `User Organization` - - `uoId` pour `User Organization Id` - - `oa` pour `Organization Application` - - `at` pour `Access Token` -- A delete command is available to delete roles -```bash - php bin/console app:delete-role ROLE_NAME -``` diff --git a/assets/controllers/user_controller.js b/assets/controllers/user_controller.js index e85aa0f..b88a03a 100644 --- a/assets/controllers/user_controller.js +++ b/assets/controllers/user_controller.js @@ -1,16 +1,39 @@ -import { Controller } from '@hotwired/stimulus'; +import {Controller} from '@hotwired/stimulus'; import Choices from 'choices.js'; +import {TabulatorFull as Tabulator} from 'tabulator-tables'; 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() { @@ -29,4 +52,695 @@ export default class extends Controller { }); } } + + 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)}`; + + 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: "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-userList", { + langs: { + 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"}, + } + + }, + locale: "fr", //'en' for English, 'fr' for French (en is default, no need to include it) + ajaxURL: "/user/data", + ajaxConfig: "GET", + pagination: true, + paginationMode: "remote", + paginationSize: 25, + + 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 + }); + }; + + + 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: { + 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"}, + } + + }, + 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: { + 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"}, + } + + }, + 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: "Actions", + field: "showUrl", + hozAlign: "center", + width: 100, + vertAlign: "middle", + headerSort: false, + formatter: (cell) => { + const url = cell.getValue(); + if (url) { + return ` + + + + `; + } + return ''; + } + }]; + // 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: { + 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"}, + } + + }, + 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 + }); + }; } \ No newline at end of file diff --git a/assets/styles/tabulator.css b/assets/styles/tabulator.css index 327cd61..160f896 100644 --- a/assets/styles/tabulator.css +++ b/assets/styles/tabulator.css @@ -96,7 +96,7 @@ border:0; } .tabulator .tabulator-header .tabulator-col .tabulator-col-title { - font-size: 22px !important; + font-size: 18px !important; } /* Select hover Désactivé pour l'instant car jpp faire de jolie style */ diff --git a/src/Controller/UserController.php b/src/Controller/UserController.php index d27a371..29bd112 100644 --- a/src/Controller/UserController.php +++ b/src/Controller/UserController.php @@ -11,6 +11,7 @@ use App\Entity\UsersOrganizations; use App\Form\UserForm; use App\Service\ActionService; use App\Service\AwsService; +use App\Service\OrganizationsService; use App\Service\UserOrganizationAppService; use App\Service\UserOrganizationService; use App\Service\UserService; @@ -18,6 +19,7 @@ use Doctrine\ORM\EntityManagerInterface; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\Asset\Packages; use Symfony\Component\HttpFoundation\File\Exception\FileException; +use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Attribute\Route; @@ -29,11 +31,11 @@ class UserController extends AbstractController private const ACCESS_DENIED = 'Access denied'; public function __construct( - private readonly EntityManagerInterface $entityManager, - private readonly UserService $userService, - private readonly ActionService $actionService, + private readonly EntityManagerInterface $entityManager, + private readonly UserService $userService, + private readonly ActionService $actionService, private readonly UserOrganizationAppService $userOrganizationAppService, - private readonly UserOrganizationService $userOrganizationService, + private readonly UserOrganizationService $userOrganizationService, private readonly OrganizationsService $organizationsService, ) { } @@ -371,12 +373,207 @@ class UserController extends AbstractController $user = $uo->getUsers(); return $this->redirectToRoute('user_show', [ - 'user' => $user, - 'id' => $user->getId(), - 'organizationId'=> $uo->getOrganization()->getId() + 'user' => $user, + 'id' => $user->getId(), + 'organizationId' => $uo->getOrganization()->getId() ]); } throw $this->createAccessDeniedException(); } + + /* + * AJAX endpoint for user listing with pagination + * Get all the users that aren´t deleted and are active + */ + #[Route(path: '/data', name: 'data', methods: ['GET'])] + public function data(Request $request): JsonResponse + { + $this->denyAccessUnlessGranted("ROLE_ADMIN"); + + $page = max(1, (int)$request->query->get('page', 1)); + $size = max(1, (int)$request->query->get('size', 10)); + + $repo = $this->entityManager->getRepository(User::class); + + // Base query: keep your constraints intact (isDeleted=false, isActive=true) + $qb = $repo->createQueryBuilder('u') + ->where('u.isDeleted = :del')->setParameter('del', false) + ->andWhere('u.isActive = :active')->setParameter('active', true); + $countQb = clone $qb; + $total = (int)$countQb->select('COUNT(u.id)')->getQuery()->getSingleScalarResult(); + + // Pagination + $offset = ($page - 1) * $size; + $rows = $qb->setFirstResult($offset)->setMaxResults($size)->getQuery()->getResult(); + + // Map to array (keep isConnected) + $data = array_map(function (User $user) { + return [ + 'pictureUrl' => $user->getPictureUrl(), + 'name' => $user->getSurname(), + 'prenom' => $user->getName(), + 'email' => $user->getEmail(), + 'isConnected' => $this->userService->isUserConnected($user->getUserIdentifier()), + 'showUrl' => $this->generateUrl('user_show', ['id' => $user->getId()]), + ]; + }, $rows); + + // Match organizations response shape + $lastPage = (int)ceil($total / $size); + + return $this->json([ + 'data' => $data, + 'last_page' => $lastPage, + 'total' => $total, // optional but handy + ]); + + } + + #[Route(path: '/indexTest', name: 'indexTest', methods: ['GET'])] + public function indexTest(): Response + { + $totalUsers = $this->entityManager->getRepository(User::class)->count(['isDeleted' => false, 'isActive' => true]); + return $this->render('user/indexTest.html.twig', [ + 'users' => $totalUsers + ]); + } + + /* + * AJAX endpoint for new users listing + * Get the 5 most recently created users for an organization + */ + #[Route(path: '/data/new', name: 'dataNew', methods: ['GET'])] + public function dataNew(Request $request): JsonResponse + { + $actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier()); + if ($this->userService->hasAccessTo($actingUser, true) && $this->isGranted("ROLE_ADMIN")) { + $orgId = $request->query->get('orgId'); + $uos = $this->entityManager->getRepository(UsersOrganizations::class)->findBy(['organization' => $orgId], limit: 5, orderBy: ['createdAt' => 'DESC']); + + + // Map to array (keep isConnected) + $data = array_map(function (UsersOrganizations $uo) { + $user = $uo->getUsers(); + $initials = $user->getName()[0] . $user->getSurname()[0]; + return [ + 'pictureUrl' => $user->getPictureUrl(), + 'email' => $user->getEmail(), + 'isConnected' => $this->userService->isUserConnected($user->getUserIdentifier()), + 'showUrl' => $this->generateUrl('user_show', ['id' => $user->getId()]), + 'initials' => strtoupper($initials), + ]; + }, $uos); + return $this->json([ + 'data' => $data, + ]); + } + throw $this->createAccessDeniedException(self::ACCESS_DENIED); + + + } + + /* + * AJAX endpoint for admin users listing + * Get all admin users for an organization + */ + + #[Route(path: '/data/admin', name: 'dataAdmin', methods: ['GET'])] + public function dataAdmin(Request $request): JsonResponse + { + $actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier()); + if ($this->userService->hasAccessTo($actingUser, true) && $this->isGranted("ROLE_ADMIN")) { + $orgId = $request->query->get('orgId'); + $uos = $this->entityManager->getRepository(UsersOrganizations::class)->findBy(['organization' => $orgId]); + $roleAdmin = $this->entityManager->getRepository(Roles::class)->findOneBy(['name' => 'ADMIN']); + $users = []; + foreach ($uos as $uo) { + if ($this->entityManager->getRepository(UserOrganizatonApp::class)->findOneBy(['userOrganization' => $uo, 'role' => $roleAdmin])) { + $users[] = $uo; + } + } + + + // Map to array (keep isConnected) + $data = array_map(function (UsersOrganizations $uo) { + $user = $uo->getUsers(); + $initials = $user->getName()[0] . $user->getSurname()[0]; + return [ + 'pictureUrl' => $user->getPictureUrl(), + 'email' => $user->getEmail(), + 'isConnected' => $this->userService->isUserConnected($user->getUserIdentifier()), + 'showUrl' => $this->generateUrl('user_show', ['id' => $user->getId()]), + 'initials' => strtoupper($initials), + ]; + }, $users); + + + return $this->json([ + 'data' => $data, + ]); + } + throw $this->createAccessDeniedException(self::ACCESS_DENIED); + + + } + + /* + * AJAX endpoint for All users in an organization + */ + #[Route(path: '/data/organization', name: 'dataUserOrganization', methods: ['GET'])] + public function dataUserOrganization(Request $request): JsonResponse + { + $actingUser = $this->userService->getUserByIdentifier($this->getUser()->getUserIdentifier()); + + if ($this->userService->hasAccessTo($actingUser, true) && $this->isGranted("ROLE_ADMIN")) { + $orgId = $request->query->get('orgId'); + $page = max(1, (int)$request->query->get('page', 1)); + $size = max(1, (int)$request->query->get('size', 10)); + + // Optional: read Tabulator remote sort/filter payloads + // $sorters = $request->query->all('sorters') ?? []; + // $filters = $request->query->all('filters') ?? []; + + $repo = $this->entityManager->getRepository(UsersOrganizations::class); + + // Base query + $qb = $repo->createQueryBuilder('uo') + ->where('uo.organization = :orgId') + ->setParameter('orgId', $orgId); + $countQb = clone $qb; + $total = (int)$countQb->select('COUNT(uo.id)')->getQuery()->getSingleScalarResult(); + + // Pagination + $offset = ($page - 1) * $size; + $rows = $qb->setFirstResult($offset)->setMaxResults($size)->getQuery()->getResult(); + + // Map to array + $data = array_map(function (UsersOrganizations $uo) { + $user = $uo->getUsers(); + return [ + 'pictureUrl' => $user->getPictureUrl(), + 'name' => $user->getSurname(), + 'prenom' => $user->getName(), + 'email' => $user->getEmail(), + 'isConnected' => $this->userService->isUserConnected($user->getUserIdentifier()), + 'showUrl' => $this->generateUrl('user_show', [ + 'id' => $user->getId(), + 'organizationId' => $uo->getOrganization()->getId(), + ]), + ]; + }, $rows); + + // Return Tabulator-compatible response + $lastPage = (int)ceil($total / $size); + + return $this->json([ + 'data' => $data, + 'last_page' => $lastPage, + 'total' => $total, + ]); + } + + throw $this->createAccessDeniedException(self::ACCESS_DENIED); + + } } diff --git a/src/Service/UserService.php b/src/Service/UserService.php index 6620c46..1594677 100644 --- a/src/Service/UserService.php +++ b/src/Service/UserService.php @@ -79,6 +79,7 @@ class UserService /** * Check if the user have the rights to access the page + * Self check can be skipped when checking access for the current user * * @param User $user * @param bool $skipSelfCheck diff --git a/templates/organization/show.html.twig b/templates/organization/show.html.twig index 7eef593..24c8fc2 100644 --- a/templates/organization/show.html.twig +++ b/templates/organization/show.html.twig @@ -5,7 +5,7 @@
Vous n'avez pas les permissions nécessaires pour voir la liste des utilisateurs.
+| Picture | -Visualiser | -|
|---|---|---|
| {{ empty_message }} | -||
|
- {% if user.pictureUrl %}
- |
- {{ user.email }} | -- {% if organizationId is defined %} - - {{ ux_icon('fa6-regular:eye', {height: '30px', width: '30px'}) }} - - {% else %} - - {{ ux_icon('fa6-regular:eye', {height: '30px', width: '30px'}) }} - - {% endif %} - - | -