From d4ec396fb3d6aaa898a052d698730324d3b3c3f5 Mon Sep 17 00:00:00 2001 From: niladic Date: Mon, 17 Jul 2023 14:48:35 +0200 Subject: [PATCH] Nouvelle page utilisateurs pour tout le monde + bump tabulator 5.5.0 (#1762) --- app/controllers/Operators.scala | 10 -- app/controllers/UserController.scala | 95 +---------------- app/models/EventType.scala | 2 - app/models/User.scala | 13 ++- app/serializers/ApiModel.scala | 29 ++++-- app/services/UserService.scala | 2 +- app/views/allUsersByGroup.scala.html | 5 - app/views/users.scala | 37 ++++++- conf/routes | 1 - package.json | 4 +- typescript/src/admin.ts | 12 +-- typescript/src/applicationsAdmin.ts | 12 +-- typescript/src/franceServices.ts | 38 +++---- typescript/src/users.ts | 146 ++++++++++++++++++++++----- 14 files changed, 223 insertions(+), 183 deletions(-) diff --git a/app/controllers/Operators.scala b/app/controllers/Operators.scala index 401fc8856..058891496 100644 --- a/app/controllers/Operators.scala +++ b/app/controllers/Operators.scala @@ -124,16 +124,6 @@ object Operators { payload() } - def asAdminWhoSeesUsersOfArea(areaId: UUID)(errorEventType: EventType, errorMessage: => String)( - payload: () => Future[Result] - )(implicit request: RequestWithUserData[_], ec: ExecutionContext): Future[Result] = - if (not(request.currentUser.admin) || not(request.currentUser.canSeeUsersInArea(areaId))) { - eventService.log(errorEventType, errorMessage) - Future(Unauthorized("Vous n'avez pas le droit de faire ça")) - } else { - payload() - } - def asUserWhoSeesUsersOfArea(areaId: UUID)(errorEventType: EventType, errorMessage: => String)( payload: () => Future[Result] )(implicit request: RequestWithUserData[_], ec: ExecutionContext): Future[Result] = diff --git a/app/controllers/UserController.scala b/app/controllers/UserController.scala index e9e7866f3..6d8ec54ca 100644 --- a/app/controllers/UserController.scala +++ b/app/controllers/UserController.scala @@ -13,8 +13,6 @@ import helper.{Time, UUIDHelper} import javax.inject.{Inject, Singleton} import models.EventType.{ AddUserError, - AllUserCSVUnauthorized, - AllUserCsvShowed, AllUserIncorrectSetup, AllUserUnauthorized, CGUShowed, @@ -64,7 +62,7 @@ import play.filters.csrf.CSRF import play.filters.csrf.CSRF.Token import scala.concurrent.{ExecutionContext, Future} import scala.util.Try -import serializers.{Keys, UserAndGroupCsvSerializer} +import serializers.Keys import serializers.ApiModel.{SearchResult, UserGroupInfos, UserInfos} import services._ @@ -234,7 +232,7 @@ case class UserController @Inject() ( val applications = applicationService.allByArea(selectedArea.id, anonymous = true) eventService.log(UsersShowed, "Visualise la vue des utilisateurs") val result = request.getQueryString(Keys.QueryParam.vue).getOrElse("nouvelle") match { - case "nouvelle" if request.currentUser.admin => + case "nouvelle" => views.users.page(request.currentUser, request.rights, selectedArea) case _ => views.html.allUsersByGroup(request.currentUser, request.rights)( @@ -249,99 +247,12 @@ case class UserController @Inject() ( } } - def allCSV(areaId: UUID): Action[AnyContent] = - loginAction.async { implicit request: RequestWithUserData[AnyContent] => - asAdminWhoSeesUsersOfArea(areaId)( - AllUserCSVUnauthorized, - "Accès non autorisé à l'export utilisateur" - ) { () => - val area = Area.fromId(areaId).get - val usersFuture: Future[List[User]] = if (areaId === Area.allArea.id) { - if (Authorization.isAdmin(request.rights)) { - // Includes users without any group for debug purpose - userService.all - } else { - groupService.byAreas(request.currentUser.areas).map { groupsOfArea => - userService.byGroupIds(groupsOfArea.map(_.id), includeDisabled = true) - } - } - } else { - groupService.byArea(areaId).map { groupsOfArea => - userService.byGroupIds(groupsOfArea.map(_.id), includeDisabled = true) - } - } - val groupsFuture: Future[List[UserGroup]] = - groupService.byAreas(request.currentUser.areas) - eventService.log(AllUserCsvShowed, "Visualise le CSV de tous les zones de l'utilisateur") - - usersFuture.zip(groupsFuture).map { case (users, groups) => - def userToCSV(user: User): String = { - val userGroups = user.groupIds.flatMap(id => groups.find(_.id === id)) - List[String]( - user.id.toString, - user.firstName.orEmpty, - user.lastName.orEmpty, - user.email, - Time.formatPatternFr(user.creationDate, "dd-MM-YYYY-HHhmm"), - if (user.sharedAccount) "Compte Partagé" else " ", - if (user.sharedAccount) user.name else " ", - user.helperRoleName.getOrElse(""), - if (user.instructor) "Instructeur" else " ", - if (user.groupAdmin) "Responsable" else " ", - if (user.expert) "Expert" else " ", - if (user.admin) "Admin" else " ", - if (user.disabled) "Désactivé" else " ", - user.communeCode, - user.areas.flatMap(Area.fromId).map(_.name).mkString(", "), - userGroups.map(_.name).mkString(", "), - userGroups - .flatMap(_.organisation) - .map(_.shortName) - .mkString(", "), - if (user.cguAcceptationDate.nonEmpty) "CGU Acceptées" else "", - if (user.newsletterAcceptationDate.nonEmpty) "Newsletter Acceptée" else "" - ).mkString(";") - } - - val headers = List[String]( - "Id", - UserAndGroupCsvSerializer.USER_FIRST_NAME.prefixes.head, - UserAndGroupCsvSerializer.USER_LAST_NAME.prefixes.head, - UserAndGroupCsvSerializer.USER_EMAIL.prefixes.head, - "Création", - UserAndGroupCsvSerializer.USER_ACCOUNT_IS_SHARED.prefixes.head, - UserAndGroupCsvSerializer.SHARED_ACCOUNT_NAME.prefixes.head, - "Aidant", - UserAndGroupCsvSerializer.USER_INSTRUCTOR.prefixes.head, - UserAndGroupCsvSerializer.USER_GROUP_MANAGER.prefixes.head, - "Expert", - "Admin", - "Actif", - "Commune INSEE", - UserAndGroupCsvSerializer.GROUP_AREAS_IDS.prefixes.head, - UserAndGroupCsvSerializer.GROUP_NAME.prefixes.head, - UserAndGroupCsvSerializer.GROUP_ORGANISATION.prefixes.head, - "CGU", - "Newsletter" - ).mkString(";") - - val csvContent = (List(headers) ++ users.map(userToCSV)).mkString("\n") - val date = Time.formatPatternFr(Time.nowParis(), "dd-MMM-YYY-HH'h'mm") - val filename = "aplus-" + date + "-users-" + area.name.replace(" ", "-") + ".csv" - - Ok(csvContent) - .withHeaders("Content-Disposition" -> s"""attachment; filename="$filename"""") - .as("text/csv") - } - } - } - def search: Action[AnyContent] = loginAction.async { implicit request => def toUserInfos(usersAndGroups: (List[User], List[UserGroup])): List[UserInfos] = { val (users, groups) = usersAndGroups val idToGroup = groups.map(group => (group.id, group)).toMap - users.map(user => UserInfos.fromUser(user, idToGroup)) + users.map(user => UserInfos.fromUser(user, request.rights, idToGroup)) } val area = request .getQueryString(Keys.QueryParam.searchAreaId) diff --git a/app/models/EventType.scala b/app/models/EventType.scala index d24099263..8c2799a38 100644 --- a/app/models/EventType.scala +++ b/app/models/EventType.scala @@ -47,8 +47,6 @@ object EventType { object AllAsShowed extends Info object AllAsUnauthorized extends Warn object AllCSVShowed extends Info - object AllUserCSVUnauthorized extends Warn - object AllUserCsvShowed extends Info object AllUserIncorrectSetup extends Info object AllUserUnauthorized extends Warn object ApplicationCreated extends Info diff --git a/app/models/User.scala b/app/models/User.scala index 2898edf21..608dbe860 100644 --- a/app/models/User.scala +++ b/app/models/User.scala @@ -7,7 +7,12 @@ import cats.syntax.all._ import constants.Constants import helper.{Hash, Pseudonymizer, Time, UUIDHelper} import helper.Time.zonedDateTimeInstance -import helper.StringHelper.{notLetterNorNumberRegex, withQuotes} +import helper.StringHelper.{ + capitalizeName, + commonStringInputNormalization, + notLetterNorNumberRegex, + withQuotes +} case class User( id: UUID, @@ -236,4 +241,10 @@ object User { internalSupportComment = None ) + def standardName(firstName: String, lastName: String): String = { + val normalizedFirstName = commonStringInputNormalization(firstName) + val normalizedLastName = commonStringInputNormalization(lastName) + s"${normalizedLastName.toUpperCase} ${capitalizeName(normalizedFirstName)}" + } + } diff --git a/app/serializers/ApiModel.scala b/app/serializers/ApiModel.scala index 05696b665..29d020e60 100644 --- a/app/serializers/ApiModel.scala +++ b/app/serializers/ApiModel.scala @@ -150,18 +150,25 @@ object ApiModel { } object UserInfos { - case class Group(id: UUID, name: String) + case class Group(id: UUID, name: String, currentUserCanEditGroup: Boolean) implicit val userInfosGroupFormat: Format[UserInfos.Group] = Json.format[UserInfos.Group] implicit val userInfosFormat: Format[UserInfos] = Json.format[UserInfos] - def fromUser(user: User, idToGroup: Map[UUID, UserGroup]): UserInfos = { - val completeName = { - val firstName = user.firstName.getOrElse("") - val lastName = user.lastName.getOrElse("") - if (firstName.nonEmpty || lastName.nonEmpty) s"${user.name} ($lastName $firstName)" - else user.name - } + def fromUser( + user: User, + rights: Authorization.UserRights, + idToGroup: Map[UUID, UserGroup] + ): UserInfos = { + val completeName = + if (user.sharedAccount) + user.name + else { + val firstName = user.firstName.getOrElse("") + val lastName = user.lastName.getOrElse("") + if (firstName.nonEmpty || lastName.nonEmpty) User.standardName(firstName, lastName) + else user.name + } UserInfos( id = user.id, firstName = user.firstName, @@ -173,11 +180,13 @@ object ApiModel { phoneNumber = user.phoneNumber, helper = user.helperRoleName.nonEmpty, instructor = user.instructorRoleName.nonEmpty, - areas = user.areas.flatMap(Area.fromId).map(_.toString), + areas = user.areas.flatMap(Area.fromId).map(_.toString).sorted, groupNames = user.groupIds.flatMap(idToGroup.get).map(_.name), groups = user.groupIds .flatMap(idToGroup.get) - .map(group => UserInfos.Group(group.id, group.name)), + .map(group => + UserInfos.Group(group.id, group.name, Authorization.canEditGroup(group)(rights)) + ), groupEmails = user.groupIds.flatMap(idToGroup.get).flatMap(_.email), groupAdmin = user.groupAdminRoleName.nonEmpty, admin = user.adminRoleName.nonEmpty, diff --git a/app/services/UserService.scala b/app/services/UserService.scala index 5e7ee4e99..ec50a25b4 100644 --- a/app/services/UserService.scala +++ b/app/services/UserService.scala @@ -342,7 +342,7 @@ class UserService @Inject() ( val normalizedFirstName = firstName.normalized val normalizedLastName = lastName.normalized val normalizedQualite = qualite.normalized - val name = s"${normalizedLastName.toUpperCase} ${normalizedFirstName.capitalizeWords}" + val name = User.standardName(firstName, lastName) SQL""" UPDATE "user" SET name = $name, diff --git a/app/views/allUsersByGroup.scala.html b/app/views/allUsersByGroup.scala.html index c3088d69b..fdf420bc7 100644 --- a/app/views/allUsersByGroup.scala.html +++ b/app/views/allUsersByGroup.scala.html @@ -83,11 +83,6 @@ }

- } - @if(currentUser.admin) { - } @for(userGroup <- userGroups.sortBy(_.name)) { @defining(allUsers.filter(_.groupIds.contains(userGroup.id))){ groupUsers => diff --git a/app/views/users.scala b/app/views/users.scala index f1bc103db..593781a37 100644 --- a/app/views/users.scala +++ b/app/views/users.scala @@ -22,7 +22,7 @@ object users { mainInfos: MainInfos ): Html = views.html.main(currentUser, currentUserRights, maxWidth = false)( - s"Gestion des groupes utilisateurs - ${selectedArea.name}" + s"Gestion des utilisateurs - ${selectedArea.name}" )( views.helpers.head.publicCss("stylesheets/newForm.css") )( @@ -75,14 +75,43 @@ object users { ) ), div(id := "current-area-value", data("area-id") := selectedArea.id.toString), + div( + id := "user-role", + data("is-admin") := currentUser.admin.toString, + data("can-see-edit-user-page") := Authorization + .canSeeEditUserPage(currentUserRights) + .toString, + ), currentUser.admin.some.filter(identity).map(_ => searchForm()), div(cls := "mdl-cell mdl-cell--12-col", id := "tabulator-users-table"), - div(cls := "mdl-cell mdl-cell--12-col", id := "tabulator-groups-table"), currentUser.admin.some .filter(identity) - .map(_ => views.addGroup.innerForm(currentUser, selectedArea)) + .map(_ => div(cls := "mdl-cell mdl-cell--12-col", id := "tabulator-groups-table")), + currentUser.admin.some + .filter(identity) + .map(_ => views.addGroup.innerForm(currentUser, selectedArea)), + div( + cls := "mdl-cell", + a( + id := "users-download-btn-csv", + href := "#", + i(cls := "fas fa-download"), + " Téléchargement au format CSV" + ) + ), + div( + cls := "mdl-cell", + a( + id := "users-download-btn-xlsx", + href := "#", + i(cls := "fas fa-download"), + " Téléchargement au format XLSX" + ) + ), ) - )(Nil) + )( + views.helpers.head.publicScript("generated-js/xlsx.full.min.js") + ) def searchForm(): Tag = div( diff --git a/conf/routes b/conf/routes index 59cdaad60..62603c464 100644 --- a/conf/routes +++ b/conf/routes @@ -106,7 +106,6 @@ GET /territoires/deploiement GET /territoires/deploiement/france-service controllers.AreaController.franceServiceDeploymentDashboard GET /territoires/deploiement/france-services controllers.AreaController.franceServices GET /territoires/:areaId/utilisateurs controllers.UserController.all(areaId: java.util.UUID) -GET /territoires/:areaId/utilisateurs.csv controllers.UserController.allCSV(areaId: java.util.UUID) # Check GET /status controllers.HomeController.status diff --git a/package.json b/package.json index 2ff78961a..1c5737254 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ "dialog-polyfill": "0.5.6", "proxy-polyfill": "0.3.2", "slim-select": "1.27.1", - "tabulator-tables": "5.2.7", + "tabulator-tables": "5.5.0", "ts-polyfill": "3.8.2", "unfetch": "4.2.0", "unorm": "1.6.0", @@ -22,7 +22,7 @@ "devDependencies": { "@babel/core": "7.21.8", "@babel/preset-env": "7.21.5", - "@types/tabulator-tables": "5.2.0", + "@types/tabulator-tables": "5.4.8", "babel-loader": "9.1.2", "copy-webpack-plugin": "11.0.0", "css-loader": "6.7.4", diff --git a/typescript/src/admin.ts b/typescript/src/admin.ts index abc941275..da70fbbaa 100644 --- a/typescript/src/admin.ts +++ b/typescript/src/admin.ts @@ -1,4 +1,4 @@ -import { Tabulator, TabulatorFull } from 'tabulator-tables'; +import { ColumnDefinition, Formatter, TabulatorFull } from 'tabulator-tables'; import "tabulator-tables/dist/css/tabulator.css"; @@ -19,7 +19,7 @@ const franceServiceDeploymentDownloadBtnXlsxId = "aplus-admin-deployment-france- // TODO: macro that will generate field names from the scala case class (Scala => TS) -const franceServiceDeploymentColumns: Array = [ +const franceServiceDeploymentColumns: Array = [ { title: "Nom", field: "nomFranceService", sorter: "string", width: 200 }, { title: "Département", @@ -175,7 +175,7 @@ if (window.document.getElementById(deploymentTableTagId)) { } fetch(url).then((response) => response.json()).then((deploymentData: DeploymentData) => { - const organisationFormatter: Tabulator.Formatter = (cell) => { + const organisationFormatter: Formatter = (cell) => { const value = cell.getValue(); const areaName = cell.getRow().getData().areaName; @@ -208,7 +208,7 @@ if (window.document.getElementById(deploymentTableTagId)) { const title = orgSet.organisations.map((organisation) => organisation.shortName).join(" / "); const field = orgSet.id; - const column: Tabulator.ColumnDefinition = { + const column: ColumnDefinition = { title, field, sorter: "number", @@ -218,12 +218,12 @@ if (window.document.getElementById(deploymentTableTagId)) { return column; }); - const areaColumn: Tabulator.ColumnDefinition = + const areaColumn: ColumnDefinition = { title: "Département", field: "areaName", sorter: "string", width: 150, frozen: true }; - const totalColumn: Tabulator.ColumnDefinition = + const totalColumn: ColumnDefinition = { title: "Couverture", field: "total", diff --git a/typescript/src/applicationsAdmin.ts b/typescript/src/applicationsAdmin.ts index ef271df2a..2e71de390 100644 --- a/typescript/src/applicationsAdmin.ts +++ b/typescript/src/applicationsAdmin.ts @@ -1,5 +1,5 @@ /* global jsRoutes */ -import { Tabulator, TabulatorFull } from 'tabulator-tables'; +import { ColumnDefinition, Formatter, Options, Tabulator, TabulatorFull } from 'tabulator-tables'; import "tabulator-tables/dist/css/tabulator.css"; const applicationsTableId = "tabulator-applications-table"; @@ -100,7 +100,7 @@ if (window.document.getElementById(applicationsTableId)) { // Setup Tabulator - const linkFormatter: Tabulator.Formatter = (cell) => { + const linkFormatter: Formatter = (cell) => { let uuid = cell.getRow().getData().id; let authorized = cell.getRow().getData().currentUserCanSeeAnonymousApplication; let url = jsRoutes.controllers.ApplicationController.show(uuid).url; @@ -111,7 +111,7 @@ if (window.document.getElementById(applicationsTableId)) { } }; - const usefulnessFormatter: Tabulator.Formatter = (cell) => { + const usefulnessFormatter: Formatter = (cell) => { let value = cell.getValue(); if (value) { if (value === "Oui") { @@ -123,7 +123,7 @@ if (window.document.getElementById(applicationsTableId)) { return value; }; - const pertinenceFormatter: Tabulator.Formatter = (cell) => { + const pertinenceFormatter: Formatter = (cell) => { let value = cell.getValue(); if (value && value === "Non") { cell.getElement().classList.add("mdl-color--red"); @@ -131,7 +131,7 @@ if (window.document.getElementById(applicationsTableId)) { return value; }; - const columns: Array = [ + const columns: Array = [ { title: "", field: "id", @@ -288,7 +288,7 @@ if (window.document.getElementById(applicationsTableId)) { }, ]; - const options: Tabulator.Options = { + const options: Options = { height: "75vh", langs: { "fr-fr": { diff --git a/typescript/src/franceServices.ts b/typescript/src/franceServices.ts index 31a30a046..c5aeb67f2 100644 --- a/typescript/src/franceServices.ts +++ b/typescript/src/franceServices.ts @@ -1,5 +1,5 @@ /* global jsRoutes */ -import { Tabulator, TabulatorFull } from 'tabulator-tables'; +import { CellEditEventCallback, CellEventCallback, ColumnDefinition, Formatter, Options, Tabulator, TabulatorFull } from 'tabulator-tables'; import 'tabulator-tables/dist/css/tabulator.css'; const tableId = 'tabulator-france-services-table'; @@ -456,7 +456,7 @@ if (window.document.getElementById(tableId)) { // Tables Config // - const deleteColBase: Tabulator.ColumnDefinition = { + const deleteColBase: ColumnDefinition = { title: '', field: '', hozAlign: 'center', @@ -465,18 +465,18 @@ if (window.document.getElementById(tableId)) { download: false, }; - const addTableDeleteColCellFormatter: Tabulator.Formatter = + const addTableDeleteColCellFormatter: Formatter = (cell) => { cell.getElement().classList.add('mdl-color--red-200'); return ''; }; - const addTableDeleteColCellClick: Tabulator.CellEventCallback = + const addTableDeleteColCellClick: CellEventCallback = (_e, cell) => { cell.getRow().delete(); }; - const matriculeColBase: Tabulator.ColumnDefinition = { + const matriculeColBase: ColumnDefinition = { title: 'Matricule', field: 'matricule', hozAlign: 'right', @@ -492,7 +492,7 @@ if (window.document.getElementById(tableId)) { }, }; - const matriculeColEdited: Tabulator.CellEditEventCallback = + const matriculeColEdited: CellEditEventCallback = (cell) => { const oldMatricule = cell.getOldValue(); const matricule = cell.getValue(); @@ -536,7 +536,7 @@ if (window.document.getElementById(tableId)) { return options; }); - const addTableGroupColEdited: Tabulator.CellEditEventCallback = + const addTableGroupColEdited: CellEditEventCallback = (cell) => { const groupId = cell.getValue(); const group = groupList.find((g) => g.id === groupId); @@ -545,7 +545,7 @@ if (window.document.getElementById(tableId)) { } }; - const groupColBase: Tabulator.ColumnDefinition = { + const groupColBase: ColumnDefinition = { title: 'Groupe', field: 'groupId', headerFilter: 'input', @@ -565,36 +565,36 @@ if (window.document.getElementById(tableId)) { } }; - const groupCol: Tabulator.ColumnDefinition = Object.assign(groupColEditorParams, groupColBase); + const groupCol: ColumnDefinition = Object.assign(groupColEditorParams, groupColBase); - const nameCol: Tabulator.ColumnDefinition = { + const nameCol: ColumnDefinition = { title: 'Nom', field: 'name', headerFilter: 'input', width: 300, }; - const descriptionCol: Tabulator.ColumnDefinition = { + const descriptionCol: ColumnDefinition = { title: 'Description', field: 'description', headerFilter: 'input', maxWidth: 300, }; - const areasCol: Tabulator.ColumnDefinition = { + const areasCol: ColumnDefinition = { title: 'Départements', field: 'areas', headerFilter: 'input', maxWidth: 300, }; - const organisationCol: Tabulator.ColumnDefinition = { + const organisationCol: ColumnDefinition = { title: 'Organisme', field: 'organisation', headerFilter: 'input', }; - const emailCol: Tabulator.ColumnDefinition = { + const emailCol: ColumnDefinition = { title: 'BAL', field: 'email', headerFilter: 'input', @@ -602,21 +602,21 @@ if (window.document.getElementById(tableId)) { maxWidth: 300, }; - const publicNoteCol: Tabulator.ColumnDefinition = { + const publicNoteCol: ColumnDefinition = { title: 'Description détaillée', field: 'publicNote', headerFilter: 'input', maxWidth: 300, }; - const addTableAreaCodeCol: Tabulator.ColumnDefinition = { + const addTableAreaCodeCol: ColumnDefinition = { title: 'Code INSEE', field: 'areaCode', headerFilter: 'input', editor: 'input', }; - const addTableInternalCommentCol: Tabulator.ColumnDefinition = { + const addTableInternalCommentCol: ColumnDefinition = { title: 'Commentaire interne', field: 'internalSupportComment', headerFilter: 'input', @@ -624,7 +624,7 @@ if (window.document.getElementById(tableId)) { maxWidth: 300, }; - const options: Tabulator.Options = { + const options: Options = { height: '50vh', langs: { 'fr-fr': { @@ -655,7 +655,7 @@ if (window.document.getElementById(tableId)) { }; table = new TabulatorFull('#' + tableId, options); - const addOptions: Tabulator.Options = { + const addOptions: Options = { langs: { 'fr-fr': { 'data': { diff --git a/typescript/src/users.ts b/typescript/src/users.ts index 75b334dd8..cc117d7f0 100644 --- a/typescript/src/users.ts +++ b/typescript/src/users.ts @@ -1,4 +1,5 @@ -import { Tabulator, TabulatorFull } from 'tabulator-tables'; +/* global jsRoutes */ +import { ColumnDefinition, CustomAccessor, Formatter, Options, RowComponent, Tabulator, TabulatorFull } from 'tabulator-tables'; import "tabulator-tables/dist/css/tabulator.css"; import { debounceAsync } from './helpers'; @@ -15,6 +16,7 @@ let groupsTable: Tabulator | null = null; interface UserInfosGroup { id: string; name: string; + currentUserCanEditGroup: boolean; } interface UserInfos { @@ -70,14 +72,14 @@ async function callSearch(searchString: string): Promise { if (window.document.getElementById(usersTableId)) { const verticalHeader = false; - const editIcon: Tabulator.Formatter = function(cell) { + const editIcon: Formatter = function(cell) { //plain text value let uuid = cell.getRow().getData().id; let url = jsRoutes.controllers.UserController.editUser(uuid).url; return ""; }; - const groupsFormatter: Tabulator.Formatter = function(cell) { + const groupsFormatter: Formatter = function(cell) { const groups = >cell.getRow().getData().groups; let links = ""; let isNotFirst = false; @@ -87,20 +89,32 @@ if (window.document.getElementById(usersTableId)) { if (isNotFirst) { links += ", "; } - links += "" + groupName + ""; + if (group.currentUserCanEditGroup) { + links += "" + groupName + ""; + } else { + links += groupName; + } isNotFirst = true; }); return links; }; - const groupNameFormatter: Tabulator.Formatter = function(cell) { + const joinWithCommaDownload: CustomAccessor = function(value) { + if (value != null) { + return value.join(", "); + } else { + return value; + } + }; + + const groupNameFormatter: Formatter = function(cell) { const group = cell.getRow().getData(); const groupUrl = jsRoutes.controllers.GroupController.editGroup(group.id).url; const html = "" + group.name + ""; return html; }; - const rowFormatter = function(row: Tabulator.RowComponent) { + const rowFormatter = function(row: RowComponent) { let element = row.getElement(), data = row.getData(); if (data.disabled) { @@ -108,8 +122,15 @@ if (window.document.getElementById(usersTableId)) { } }; - const usersColumns: Array = [ - { title: "", formatter: editIcon, width: 40, frozen: true }, + const adminColumns: Array = [ + { + title: "", + field: "id", + formatter: editIcon, + hozAlign: "center", + width: 40, + frozen: true, + }, { title: "Email", field: "email", @@ -120,8 +141,10 @@ if (window.document.getElementById(usersTableId)) { title: "Groupes", field: "groupNames", formatter: groupsFormatter, + sorter: "string", headerFilter: "input", width: 400, + accessorDownload: joinWithCommaDownload, }, { title: "Nom Complet", @@ -133,8 +156,10 @@ if (window.document.getElementById(usersTableId)) { { title: "BALs", field: "groupEmails", + sorter: "string", headerFilter: "input", width: 200, + accessorDownload: joinWithCommaDownload, }, { title: "Qualité", @@ -231,8 +256,10 @@ if (window.document.getElementById(usersTableId)) { { title: "Départements", field: "areas", + sorter: "string", headerFilter: "input", width: 200, + accessorDownload: joinWithCommaDownload, }, { title: "Nom et Prénom", @@ -257,8 +284,7 @@ if (window.document.getElementById(usersTableId)) { }, ]; - - const groupsColumns: Array = [ + const groupsColumns: Array = [ { title: "Nom", field: "name", @@ -308,7 +334,19 @@ if (window.document.getElementById(usersTableId)) { } } - const usersOptions: Tabulator.Options = { + let isAdmin = false; + let canSeeEditUserPage = false; + const roleDataField = document.getElementById("user-role"); + if (roleDataField != null) { + if (roleDataField.dataset["isAdmin"] === "true") { + isAdmin = true; + } + if (roleDataField.dataset["canSeeEditUserPage"] === "true") { + canSeeEditUserPage = true; + } + } + + const usersOptionsForAdmins: Options = { height: "48vh", rowFormatter, langs: { @@ -318,32 +356,68 @@ if (window.document.getElementById(usersTableId)) { } } }, - columns: usersColumns, + columns: adminColumns, }; - usersTable = new TabulatorFull("#" + usersTableId, usersOptions); - usersTable.on("tableBuilt", function() { - usersTable?.setLocale("fr-fr"); - usersTable?.setSort("name", "asc"); - }); - const groupsOptions: Tabulator.Options = { - height: "25vh", + const excludedFieldsForNonAdmins = ["name", "lastName", "firstName", "helper", "expert", "admin"]; + if (!canSeeEditUserPage) { + excludedFieldsForNonAdmins.push("id"); + } + const usersOptionsForNonAdmins: Options = { + height: "75vh", rowFormatter, langs: { "fr-fr": { + "data": { + "loading": "Chargement", + "error": "Erreur", + }, headerFilters: { "default": "filtrer..." } } }, - columns: groupsColumns, + columns: adminColumns + .filter((item) => { + if (item.field) { + return !excludedFieldsForNonAdmins.includes(item.field); + } else { + return true; + } + }), + initialSort: [{ column: "areas", dir: "asc" }], + ajaxURL: jsRoutes.controllers.UserController.search().url, + ajaxResponse(_url, _params, response) { + return response.users; + } }; - groupsTable = new TabulatorFull("#" + groupsTableId, groupsOptions); - groupsTable.on("tableBuilt", function() { - groupsTable?.setLocale("fr-fr"); - groupsTable?.setSort("name", "asc"); + + const usersOptions: Options = isAdmin ? usersOptionsForAdmins : usersOptionsForNonAdmins; + usersTable = new TabulatorFull("#" + usersTableId, usersOptions); + usersTable.on("tableBuilt", function() { + usersTable?.setLocale("fr-fr"); }); + if (document.getElementById(groupsTableId) != null) { + const groupsOptions: Options = { + height: "25vh", + rowFormatter, + langs: { + "fr-fr": { + headerFilters: { + "default": "filtrer..." + } + } + }, + columns: groupsColumns, + }; + groupsTable = new TabulatorFull("#" + groupsTableId, groupsOptions); + groupsTable.on("tableBuilt", function() { + groupsTable?.setLocale("fr-fr"); + groupsTable?.setSort("name", "asc"); + }); + } + const searchBox = document.getElementById(searchBoxId); @@ -360,4 +434,28 @@ if (window.document.getElementById(usersTableId)) { fillData(); } + const csvDownloadBtn = window.document.getElementById("users-download-btn-csv"); + if (csvDownloadBtn) { + csvDownloadBtn.onclick = () => { + const date = new Date().toLocaleDateString( + 'fr-fr', + { year: 'numeric', month: 'numeric', day: 'numeric' } + ); + const filename = 'Utilisateurs - ' + date; + usersTable?.download('csv', filename + '.csv'); + }; + } + + const xlsxDownloadBtn = window.document.getElementById("users-download-btn-xlsx"); + if (xlsxDownloadBtn) { + xlsxDownloadBtn.onclick = () => { + const date = new Date().toLocaleDateString( + 'fr-fr', + { year: 'numeric', month: 'numeric', day: 'numeric' } + ); + const filename = 'Utilisateurs - ' + date; + usersTable?.download('xlsx', filename + '.xlsx', { sheetName: 'Utilisateurs' }); + }; + } + }