From e4b31b809b744e94a6f6f8032db7ac68641288aa Mon Sep 17 00:00:00 2001 From: Andrew Radulescu Date: Wed, 24 Jul 2024 11:28:32 +0300 Subject: [PATCH] feat: allow SuperAdmins to view all users --- ...OrganizationAliasToUserApplicationsView.ts | 84 +++++++++++++++++++ .../services/organization.service.ts | 8 -- .../user/constants/invites-filters.config.ts | 13 ++- .../user/constants/user-filters.config.ts | 4 +- .../entities/user-applications-view.entity.ts | 47 ++++++----- .../src/modules/user/services/user.service.ts | 25 +----- .../src/assets/locales/en/translation.json | 9 +- .../src/assets/locales/ro/translation.json | 9 +- .../src/common/router/Routes.constants.ts | 5 +- .../components/UserInvites/UserInvites.tsx | 20 ++++- .../UserInvitesTable.headers.tsx | 23 +++++ .../users/components/UserList/UserList.tsx | 33 +++++--- .../table-headers/UserListTable.headers.tsx | 28 +++++-- .../users/interfaces/Invite.interface.ts | 2 + .../pages/users/interfaces/User.interface.ts | 11 +++ .../organization/Organization.queries.ts | 7 +- frontend/src/services/user/User.queries.ts | 4 +- frontend/src/services/user/User.service.ts | 5 +- .../store/organization/organizations.slice.ts | 7 +- frontend/src/store/store.ts | 11 +-- frontend/src/store/user/Users.slice.ts | 4 +- 21 files changed, 257 insertions(+), 102 deletions(-) create mode 100644 backend/src/migrations/1721741119685-AddOrganizationAliasToUserApplicationsView.ts diff --git a/backend/src/migrations/1721741119685-AddOrganizationAliasToUserApplicationsView.ts b/backend/src/migrations/1721741119685-AddOrganizationAliasToUserApplicationsView.ts new file mode 100644 index 000000000..986bd3868 --- /dev/null +++ b/backend/src/migrations/1721741119685-AddOrganizationAliasToUserApplicationsView.ts @@ -0,0 +1,84 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddOrganizationAliasToUserApplicationsView1721741119685 + implements MigrationInterface +{ + name = 'AddOrganizationAliasToUserApplicationsView1721741119685'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `DELETE FROM "typeorm_metadata" WHERE "type" = $1 AND "name" = $2 AND "schema" = $3`, + ['VIEW', 'UserApplicationsView', 'public'], + ); + await queryRunner.query(`DROP VIEW "UserApplicationsView"`); + await queryRunner.query(`CREATE VIEW "UserApplicationsView" AS + SELECT u.id, + u.name, + u.email, + u.phone, + u.status, + u.role, + u.organization_id AS "organizationId", + u.created_on AS "createdOn", + u.updated_on AS "updatedOn", + og.alias as "organizationAlias", + array_agg(DISTINCT a.id) AS "availableAppsIDs", + json_agg(DISTINCT jsonb_build_object('id', a.id, 'name', a.name, 'type', a.type)) AS "availableApps" + FROM "user" u + LEFT JOIN user_ong_application uoa ON u.id = uoa.user_id AND uoa.status = 'active'::user_ong_application_status_enum + LEFT JOIN ong_application oa ON uoa.ong_application_id = oa.id AND oa.status = 'active'::ong_application_status_enum + LEFT JOIN application a ON (oa.application_id = a.id OR a.type = 'independent'::application_type_enum) AND a.status = 'active'::application_status_enum + LEFT JOIN organization o ON u.organization_id = o.id + LEFT JOIN organization_general og ON o.organization_general_id = og.id + WHERE u.role = 'employee'::user_role_enum AND (u.status = ANY (ARRAY['active'::user_status_enum, 'restricted'::user_status_enum])) AND u.deleted_on IS NULL + GROUP BY u.id, og.alias; + + `); + await queryRunner.query( + `INSERT INTO "typeorm_metadata"("database", "schema", "table", "type", "name", "value") VALUES (DEFAULT, $1, DEFAULT, $2, $3, $4)`, + [ + 'public', + 'VIEW', + 'UserApplicationsView', + "SELECT u.id,\n u.name,\n u.email,\n u.phone,\n u.status,\n u.role,\n u.organization_id AS \"organizationId\",\n u.created_on AS \"createdOn\",\n u.updated_on AS \"updatedOn\",\n og.alias as \"organizationAlias\",\n array_agg(DISTINCT a.id) AS \"availableAppsIDs\",\n json_agg(DISTINCT jsonb_build_object('id', a.id, 'name', a.name, 'type', a.type)) AS \"availableApps\"\n FROM \"user\" u\n LEFT JOIN user_ong_application uoa ON u.id = uoa.user_id AND uoa.status = 'active'::user_ong_application_status_enum\n LEFT JOIN ong_application oa ON uoa.ong_application_id = oa.id AND oa.status = 'active'::ong_application_status_enum\n LEFT JOIN application a ON (oa.application_id = a.id OR a.type = 'independent'::application_type_enum) AND a.status = 'active'::application_status_enum\n LEFT JOIN organization o ON u.organization_id = o.id\n LEFT JOIN organization_general og ON o.organization_general_id = og.id\n WHERE u.role = 'employee'::user_role_enum AND (u.status = ANY (ARRAY['active'::user_status_enum, 'restricted'::user_status_enum])) AND u.deleted_on IS NULL\n GROUP BY u.id, og.alias;", + ], + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `DELETE FROM "typeorm_metadata" WHERE "type" = $1 AND "name" = $2 AND "schema" = $3`, + ['VIEW', 'UserApplicationsView', 'public'], + ); + await queryRunner.query(`DROP VIEW "UserApplicationsView"`); + await queryRunner.query(`CREATE VIEW "UserApplicationsView" AS SELECT u.id, + u.name, + u.email, + u.phone, + u.status, + u.role, + u.organization_id AS "organizationId", + u.created_on AS "createdOn", + u.updated_on AS "updatedOn", + og.alias, + array_agg(DISTINCT a.id) AS "availableAppsIDs", + json_agg(DISTINCT jsonb_build_object('id', a.id, 'name', a.name, 'type', a.type)) AS "availableApps" + FROM "user" u + LEFT JOIN user_ong_application uoa ON u.id = uoa.user_id AND uoa.status = 'active'::user_ong_application_status_enum + LEFT JOIN ong_application oa ON uoa.ong_application_id = oa.id AND oa.status = 'active'::ong_application_status_enum + LEFT JOIN application a ON (oa.application_id = a.id OR a.type = 'independent'::application_type_enum) AND a.status = 'active'::application_status_enum + LEFT JOIN organization o ON u.organization_id = o.id + LEFT JOIN organization_general og ON o.organization_general_id = og.id + WHERE u.role = 'employee'::user_role_enum AND (u.status = ANY (ARRAY['active'::user_status_enum, 'restricted'::user_status_enum])) AND u.deleted_on IS NULL + GROUP BY u.id, og.alias;`); + await queryRunner.query( + `INSERT INTO "typeorm_metadata"("database", "schema", "table", "type", "name", "value") VALUES (DEFAULT, $1, DEFAULT, $2, $3, $4)`, + [ + 'public', + 'VIEW', + 'UserApplicationsView', + "SELECT u.id,\n u.name,\n u.email,\n u.phone,\n u.status,\n u.role,\n u.organization_id AS \"organizationId\",\n u.created_on AS \"createdOn\",\n u.updated_on AS \"updatedOn\",\n og.alias,\n array_agg(DISTINCT a.id) AS \"availableAppsIDs\",\n json_agg(DISTINCT jsonb_build_object('id', a.id, 'name', a.name, 'type', a.type)) AS \"availableApps\"\n FROM \"user\" u\n LEFT JOIN user_ong_application uoa ON u.id = uoa.user_id AND uoa.status = 'active'::user_ong_application_status_enum\n LEFT JOIN ong_application oa ON uoa.ong_application_id = oa.id AND oa.status = 'active'::ong_application_status_enum\n LEFT JOIN application a ON (oa.application_id = a.id OR a.type = 'independent'::application_type_enum) AND a.status = 'active'::application_status_enum\n LEFT JOIN organization o ON u.organization_id = o.id\n LEFT JOIN organization_general og ON o.organization_general_id = og.id\n WHERE u.role = 'employee'::user_role_enum AND (u.status = ANY (ARRAY['active'::user_status_enum, 'restricted'::user_status_enum])) AND u.deleted_on IS NULL\n GROUP BY u.id, og.alias;", + ], + ); + } +} diff --git a/backend/src/modules/organization/services/organization.service.ts b/backend/src/modules/organization/services/organization.service.ts index 9b3b4e1ee..f488d14c6 100644 --- a/backend/src/modules/organization/services/organization.service.ts +++ b/backend/src/modules/organization/services/organization.service.ts @@ -325,14 +325,6 @@ export class OrganizationService { paginationOptions, ); - for (let index in ongList.items) { - const data = await this.findWithRelations(ongList.items[index].id); - ongList.items[index] = { - ...ongList.items[index], - ...data, - }; - } - // Map the logo url const items = await this.fileManagerService.mapLogoToEntity( diff --git a/backend/src/modules/user/constants/invites-filters.config.ts b/backend/src/modules/user/constants/invites-filters.config.ts index cdbb46197..950f7199e 100644 --- a/backend/src/modules/user/constants/invites-filters.config.ts +++ b/backend/src/modules/user/constants/invites-filters.config.ts @@ -7,10 +7,21 @@ export const INVITE_FILTERS_CONFIG = { email: true, phone: true, createdOn: true, + role: true, + organization: { + id: true, + organizationGeneral: { + alias: true, + }, + }, }, searchableColumns: ['name'], defaultSortBy: 'name', defaultOrderDirection: OrderDirection.ASC, - relations: {}, + relations: { + organization: { + organizationGeneral: true, + }, + }, rangeColumn: 'createdOn', }; diff --git a/backend/src/modules/user/constants/user-filters.config.ts b/backend/src/modules/user/constants/user-filters.config.ts index 356259cd8..f0d6400ae 100644 --- a/backend/src/modules/user/constants/user-filters.config.ts +++ b/backend/src/modules/user/constants/user-filters.config.ts @@ -1,6 +1,6 @@ import { OrderDirection } from 'src/common/enums/order-direction.enum'; -export const USER_FILTERS_CONFIG = { +export const USER_APPLICATIONS_FILTERS_CONFIG = { selectColumns: { id: true, name: true, @@ -10,6 +10,8 @@ export const USER_FILTERS_CONFIG = { status: true, availableApps: true, availableAppsIDs: true, + organizationId: true, + organizationAlias: true, }, searchableColumns: ['name', 'email'], defaultSortBy: 'name', diff --git a/backend/src/modules/user/entities/user-applications-view.entity.ts b/backend/src/modules/user/entities/user-applications-view.entity.ts index 05acfde5e..b7c0b1752 100644 --- a/backend/src/modules/user/entities/user-applications-view.entity.ts +++ b/backend/src/modules/user/entities/user-applications-view.entity.ts @@ -17,29 +17,27 @@ import { Role } from '../enums/role.enum'; @ViewEntity('UserApplicationsView', { expression: ` - SELECT - u.id, - u.name, - u.email, - u.phone, - u.status, - u.role, - u.organization_id as "organizationId", - u.created_on as "createdOn", - u.updated_on as "updatedOn", - ARRAY_AGG(DISTINCT a.id) as "availableAppsIDs", - json_agg(json_build_object('id', a.id, 'name', a.name, 'type', a.type)) as "availableApps" - FROM - "user" u - LEFT JOIN user_ong_application uoa ON u.id = uoa.user_id AND uoa.status = 'active' - LEFT JOIN ong_application oa ON uoa.ong_application_id = oa.id AND oa.status = 'active' - LEFT JOIN application a ON (oa.application_id = a.id OR a.type = 'independent') AND a.status = 'active' - WHERE - "u"."role" = 'employee' AND - "u"."status" IN('active', 'restricted') AND - "u"."deleted_on" IS NULL - GROUP BY - u.id + SELECT u.id, + u.name, + u.email, + u.phone, + u.status, + u.role, + u.organization_id AS "organizationId", + u.created_on AS "createdOn", + u.updated_on AS "updatedOn", + og.alias as "organizationAlias", + array_agg(DISTINCT a.id) AS "availableAppsIDs", + json_agg(DISTINCT jsonb_build_object('id', a.id, 'name', a.name, 'type', a.type)) AS "availableApps" + FROM "user" u + LEFT JOIN user_ong_application uoa ON u.id = uoa.user_id AND uoa.status = 'active'::user_ong_application_status_enum + LEFT JOIN ong_application oa ON uoa.ong_application_id = oa.id AND oa.status = 'active'::ong_application_status_enum + LEFT JOIN application a ON (oa.application_id = a.id OR a.type = 'independent'::application_type_enum) AND a.status = 'active'::application_status_enum + LEFT JOIN organization o ON u.organization_id = o.id + LEFT JOIN organization_general og ON o.organization_general_id = og.id + WHERE u.role = 'employee'::user_role_enum AND (u.status = ANY (ARRAY['active'::user_status_enum, 'restricted'::user_status_enum])) AND u.deleted_on IS NULL + GROUP BY u.id, og.alias; + `, }) export class UserApplicationsView { @@ -75,4 +73,7 @@ export class UserApplicationsView { @ViewColumn() organizationId: number; + + @ViewColumn() + organizationAlias: string; } diff --git a/backend/src/modules/user/services/user.service.ts b/backend/src/modules/user/services/user.service.ts index a3acd1a02..1e7a6b941 100644 --- a/backend/src/modules/user/services/user.service.ts +++ b/backend/src/modules/user/services/user.service.ts @@ -16,7 +16,7 @@ import { Not, UpdateResult, } from 'typeorm'; -import { USER_FILTERS_CONFIG } from '../constants/user-filters.config'; +import { USER_APPLICATIONS_FILTERS_CONFIG } from '../constants/user-filters.config'; import { CreateUserDto } from '../dto/create-user.dto'; import { UpdateUserDto } from '../dto/update-user.dto'; import { UserFilterDto } from '../dto/user-filter.dto'; @@ -194,26 +194,7 @@ export class UserService { // For Admin user we will sort by organizationId return this.userApplicationsView.getManyPaginated( - USER_FILTERS_CONFIG, - organizationId - ? { ...paginationOptions, organizationId } - : paginationOptions, - ); - } - - public async findAll( - options: UserFilterDto, - organizationId?: number, - ): Promise> { - const paginationOptions: any = { - role: Role.EMPLOYEE, - status: `$in:${UserStatus.ACTIVE},${UserStatus.RESTRICTED}`, - ...options, - }; - - // For Admin user we will sort by organizationId - return this.userRepository.getManyPaginated( - USER_FILTERS_CONFIG, + USER_APPLICATIONS_FILTERS_CONFIG, organizationId ? { ...paginationOptions, organizationId } : paginationOptions, @@ -353,7 +334,7 @@ export class UserService { // For Admin user we will sort by organizationId const users = await this.userApplicationsView.getManyPaginated( - USER_FILTERS_CONFIG, + USER_APPLICATIONS_FILTERS_CONFIG, organizationId ? { ...paginationOptions, organizationId } : paginationOptions, diff --git a/frontend/src/assets/locales/en/translation.json b/frontend/src/assets/locales/en/translation.json index 27b6549ca..1095da4c9 100644 --- a/frontend/src/assets/locales/en/translation.json +++ b/frontend/src/assets/locales/en/translation.json @@ -843,13 +843,16 @@ "email": "E-mail", "phone": "Phone", "status": "General access", - "created": "Date added" + "created": "Date added", + "organizationAlias": "Organization" }, "invites_header": { "name": "Name", "email": "E-mail", "phone": "Phone", - "added_on": "Date added" + "added_on": "Date added", + "role": "Role", + "organizationAlias": "Organization" }, "status": { "active": "Activ", @@ -1580,4 +1583,4 @@ "date_added": "Date of feedback" } } -} \ No newline at end of file +} diff --git a/frontend/src/assets/locales/ro/translation.json b/frontend/src/assets/locales/ro/translation.json index 64a9979d4..24128a52d 100644 --- a/frontend/src/assets/locales/ro/translation.json +++ b/frontend/src/assets/locales/ro/translation.json @@ -846,13 +846,16 @@ "email": "E-mail", "phone": "Telefon", "status": "Acces general", - "created": "Data adăugării" + "created": "Data adăugării", + "organizationAlias": "Organizație" }, "invites_header": { "name": "Nume", "email": "E-mail", "phone": "Telefon", - "added_on": "Data adăugării" + "added_on": "Data adăugării", + "role": "Rol", + "organizationAlias": "Organizație" }, "status": { "active": "Activ", @@ -1584,4 +1587,4 @@ "date_added": "Data acordării feedback-ului" } } -} \ No newline at end of file +} diff --git a/frontend/src/common/router/Routes.constants.ts b/frontend/src/common/router/Routes.constants.ts index 304cd3963..88eaeda50 100644 --- a/frontend/src/common/router/Routes.constants.ts +++ b/frontend/src/common/router/Routes.constants.ts @@ -35,6 +35,7 @@ export const ADMIN_ROUTES = [ export const SUPER_ADMIN_ROUTES = [ { id: 0, name: translations.dashboard, href: '', icon: RectangleGroupIcon }, { id: 1, name: translations.organizations, href: 'organizations', icon: CircleStackIcon }, - { id: 2, name: translations.requests, href: 'requests', icon: UserPlusIcon }, - { id: 3, name: translations.store, href: 'applications', icon: RectangleStackIcon }, + { id: 2, name: translations.users, href: 'users/list', icon: UserGroupIcon }, + { id: 3, name: translations.requests, href: 'requests', icon: UserPlusIcon }, + { id: 4, name: translations.store, href: 'applications', icon: RectangleStackIcon }, ]; diff --git a/frontend/src/pages/users/components/UserInvites/UserInvites.tsx b/frontend/src/pages/users/components/UserInvites/UserInvites.tsx index 503c9299d..5e5ae617f 100644 --- a/frontend/src/pages/users/components/UserInvites/UserInvites.tsx +++ b/frontend/src/pages/users/components/UserInvites/UserInvites.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useMemo, useState } from 'react'; import { SortOrder, TableColumn } from 'react-data-table-component'; import DataTableFilters from '../../../../components/data-table-filters/DataTableFilters'; import DataTableComponent from '../../../../components/data-table/DataTableComponent'; @@ -10,13 +10,18 @@ import { useResendInviteMutation, } from '../../../../services/user/User.queries'; import { useUser } from '../../../../store/selectors'; -import { UserInvitesTableHeaders } from './table-headers/UserInvitesTable.headers'; +import { + UserInvitesTableHeaderSuperAdmin, + UserInvitesTableHeaders, +} from './table-headers/UserInvitesTable.headers'; import { useTranslation } from 'react-i18next'; import { ArrowUturnLeftIcon, TrashIcon } from '@heroicons/react/24/outline'; import { useErrorToast, useSuccessToast } from '../../../../common/hooks/useToast'; import ConfirmationModal from '../../../../components/confim-removal-modal/ConfirmationModal'; import { IInvite } from '../../interfaces/Invite.interface'; import { OrderDirection } from '../../../../common/enums/sort-direction.enum'; +import { useAuthContext } from '../../../../contexts/AuthContext'; +import { UserRole } from '../../enums/UserRole.enum'; const UserInvites = () => { const [isConfirmRemoveModalOpen, setIsConfirmRemoveModalOpen] = useState(false); @@ -28,6 +33,7 @@ const UserInvites = () => { const [orderByColumn, setOrderByColumn] = useState(); const [orderDirection, setOrderDirection] = useState(); + const { role } = useAuthContext(); const { invites } = useUser(); const { t } = useTranslation(['user', 'common']); @@ -53,6 +59,14 @@ const UserInvites = () => { if (selectedInviteeForDeletion) setIsConfirmRemoveModalOpen(true); }, [selectedInviteeForDeletion]); + const ListTableHeader = useMemo(() => { + if (role === UserRole.SUPER_ADMIN) { + return [...UserInvitesTableHeaderSuperAdmin, ...UserInvitesTableHeaders]; + } + + return UserInvitesTableHeaders; + }, [role]); + const buildUserActionColumn = (): TableColumn => { const pendingUserMenuItems = [ { @@ -164,7 +178,7 @@ const UserInvites = () => { */} [] = [ @@ -42,3 +44,24 @@ export const UserInvitesTableHeaders: TableColumn[] = [ selector: (row: IInvite) => formatDate(row?.createdOn), }, ]; + +export const UserInvitesTableHeaderSuperAdmin: TableColumn[] = [ + { + id: 'organization.organizationGeneral.alias', + name: , + sortable: true, + grow: 1, + wrap: false, + minWidth: '15rem', + selector: (row: IInvite) => row.organization.organizationGeneral.alias, + }, + { + id: 'role', + name: , + sortable: true, + grow: 1, + wrap: false, + minWidth: '10rem', + selector: (row: IInvite) => row.role, + }, +]; diff --git a/frontend/src/pages/users/components/UserList/UserList.tsx b/frontend/src/pages/users/components/UserList/UserList.tsx index 04ed0baa9..a14f374a4 100644 --- a/frontend/src/pages/users/components/UserList/UserList.tsx +++ b/frontend/src/pages/users/components/UserList/UserList.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useMemo, useState } from 'react'; import { NoSymbolIcon } from '@heroicons/react/24/outline'; import { PencilIcon, ArrowPathIcon, TrashIcon } from '@heroicons/react/24/solid'; import { SortOrder, TableColumn } from 'react-data-table-component'; @@ -16,11 +16,14 @@ import { useRestrictUserMutation, useUsersQuery, } from '../../../../services/user/User.queries'; -import { useOngApplications, useUser } from '../../../../store/selectors'; +import { useUser } from '../../../../store/selectors'; import { UserStatusOptions } from '../../constants/filters.constants'; import { UserStatus } from '../../enums/UserStatus.enum'; -import { IUser } from '../../interfaces/User.interface'; -import { UserListTableHeaders } from './table-headers/UserListTable.headers'; +import { IUserWithApplications } from '../../interfaces/User.interface'; +import { + UserListTableHeaderOrganizationAlias, + UserListTableHeaders, +} from './table-headers/UserListTable.headers'; import { useNavigate } from 'react-router-dom'; import ConfirmationModal from '../../../../components/confim-removal-modal/ConfirmationModal'; import { useTranslation } from 'react-i18next'; @@ -35,7 +38,7 @@ import { ApplicationListItem } from '../../../../services/application/interfaces const UserList = (props: { organizationId?: number }) => { const navigate = useNavigate(); - const [selectedUser, setSelectedUser] = useState(null); + const [selectedUser, setSelectedUser] = useState(null); const [page, setPage] = useState(); const [rowsPerPage, setRowsPerPage] = useState(); const [orderByColumn, setOrderByColumn] = useState(); @@ -98,7 +101,15 @@ const UserList = (props: { organizationId?: number }) => { removeUserMutation.error, ]); - const buildUserActionColumn = (): TableColumn => { + const ListTableHeader = useMemo(() => { + if (role === UserRole.SUPER_ADMIN) { + return [UserListTableHeaderOrganizationAlias, ...UserListTableHeaders]; + } + + return UserListTableHeaders; + }, [role]); + + const buildUserActionColumn = (): TableColumn => { const activeUserMenuItems = [ { name: t('list.restrict'), @@ -142,7 +153,7 @@ const UserList = (props: { organizationId?: number }) => { return { name: '', - cell: (row: IUser) => ( + cell: (row: IUserWithApplications) => ( { /** * ROW ACTIONS */ - const onRestoreAccess = (row: IUser) => { + const onRestoreAccess = (row: IUserWithApplications) => { restoreUserAccessMutation.mutate([row.id], { onSuccess: () => { useSuccessToast(`${t('list.restore_success')} ${row.name}`); @@ -187,7 +198,7 @@ const UserList = (props: { organizationId?: number }) => { }); }; - const onEdit = (row: IUser) => { + const onEdit = (row: IUserWithApplications) => { navigate(`/user/${row.id}`); }; @@ -206,7 +217,7 @@ const UserList = (props: { organizationId?: number }) => { setIsConfirmRemoveModalOpen(false); }; - const onRestrictAccess = (row: IUser) => { + const onRestrictAccess = (row: IUserWithApplications) => { restrictUserAccessMutation.mutate([row.id], { onSuccess: () => { useSuccessToast(`${t('list.restrict_success')} ${row.name}`); @@ -326,7 +337,7 @@ const UserList = (props: { organizationId?: number }) => { [] = [ +export const UserListTableHeaders: TableColumn[] = [ { id: 'name', name: , @@ -25,7 +26,7 @@ export const UserListTableHeaders: TableColumn[] = [ grow: 1, wrap: false, minWidth: '15rem', - selector: (row: IUser) => row.name, + selector: (row: IUserWithApplications) => row.name, }, { id: 'email', @@ -33,21 +34,21 @@ export const UserListTableHeaders: TableColumn[] = [ sortable: true, grow: 1, minWidth: '20rem', - selector: (row: IUser) => row.email, + selector: (row: IUserWithApplications) => row.email, }, { id: 'phone', name: , sortable: true, minWidth: '9rem', - selector: (row: IUser) => row.phone, + selector: (row: IUserWithApplications) => row.phone, }, { id: 'availableApps', name: , sortable: false, minWidth: '15rem', - cell: (row: IUser) => ( + cell: (row: IUserWithApplications) => (
{`(${row.availableApps.length})`}{' '} {row?.availableApps?.map((app, i) => ( @@ -62,7 +63,6 @@ export const UserListTableHeaders: TableColumn[] = [ ))}
), - selector: (row: IUser) => row.availableAppsList || '', }, { id: 'status', @@ -70,7 +70,7 @@ export const UserListTableHeaders: TableColumn[] = [ sortField: 'status', name: , minWidth: '10rem', - cell: (row: IUser) => ( + cell: (row: IUserWithApplications) => ( [] = [ minWidth: '10rem', name: , sortable: true, - selector: (row: IUser) => formatDate(row?.createdOn as string), + selector: (row: IUserWithApplications) => formatDate(row?.createdOn as string), }, ]; + +export const UserListTableHeaderOrganizationAlias: TableColumn = { + id: 'organizationAlias', + name: , + sortable: true, + grow: 1, + wrap: false, + minWidth: '15rem', + selector: (row: IUserWithApplications) => row.organizationAlias, +}; diff --git a/frontend/src/pages/users/interfaces/Invite.interface.ts b/frontend/src/pages/users/interfaces/Invite.interface.ts index 83d09ad86..2d23f4ef5 100644 --- a/frontend/src/pages/users/interfaces/Invite.interface.ts +++ b/frontend/src/pages/users/interfaces/Invite.interface.ts @@ -4,4 +4,6 @@ export interface IInvite { email: string; phone: string; createdOn: Date; + role: string; + organization: { id: number; organizationGeneral: { alias: string } }; } diff --git a/frontend/src/pages/users/interfaces/User.interface.ts b/frontend/src/pages/users/interfaces/User.interface.ts index da587065c..c049de342 100644 --- a/frontend/src/pages/users/interfaces/User.interface.ts +++ b/frontend/src/pages/users/interfaces/User.interface.ts @@ -13,3 +13,14 @@ export interface IUser extends BaseEntity { availableAppsList?: string; availableApps: { id: number; name: string; type: string }[]; } + +export interface IUserWithApplications extends BaseEntity { + name: string; + email: string; + phone: string; + status: UserStatus; + availableApps: { id: number; name: string; type: string }[]; + availableAppsIDs: number[]; + organizationId: number; + organizationAlias: string; +} diff --git a/frontend/src/services/organization/Organization.queries.ts b/frontend/src/services/organization/Organization.queries.ts index 2a52ca656..0c0eaf391 100644 --- a/frontend/src/services/organization/Organization.queries.ts +++ b/frontend/src/services/organization/Organization.queries.ts @@ -7,7 +7,10 @@ import { CompletionStatus } from '../../pages/organization/enums/CompletionStatu import { Contact } from '../../pages/organization/interfaces/Contact.interface'; import { Expense } from '../../pages/organization/interfaces/Expense.interface'; import { Income } from '../../pages/organization/interfaces/Income.interface'; -import { IOrganizationFull } from '../../pages/organization/interfaces/Organization.interface'; +import { + IOrganizationFull, + IOrganizationView, +} from '../../pages/organization/interfaces/Organization.interface'; import { IOrganizationActivity } from '../../pages/organization/interfaces/OrganizationActivity.interface'; import { IOrganizationFinancial } from '../../pages/organization/interfaces/OrganizationFinancial.interface'; import { IOrganizationGeneral } from '../../pages/organization/interfaces/OrganizationGeneral.interface'; @@ -78,7 +81,7 @@ export const useOrganizationsQuery = ( () => getOrganizations(limit, page, orderBy, orderDirection, search, status, interval, userCount), { - onSuccess: (data: PaginatedEntity) => { + onSuccess: (data: PaginatedEntity) => { setOrganizations({ items: data.items, meta: { ...data.meta, orderByColumn: orderBy, orderDirection }, diff --git a/frontend/src/services/user/User.queries.ts b/frontend/src/services/user/User.queries.ts index e526dd61a..02247f882 100644 --- a/frontend/src/services/user/User.queries.ts +++ b/frontend/src/services/user/User.queries.ts @@ -14,7 +14,7 @@ import { import { useMutation, useQuery } from 'react-query'; import useStore from '../../store/store'; import { IUserPayload } from '../../pages/users/interfaces/UserPayload.interface'; -import { IUser } from '../../pages/users/interfaces/User.interface'; +import { IUserWithApplications } from '../../pages/users/interfaces/User.interface'; import { PaginatedEntity } from '../../common/interfaces/paginated-entity.interface'; import { OrderDirection } from '../../common/enums/sort-direction.enum'; import { UserStatus } from '../../pages/users/enums/UserStatus.enum'; @@ -72,7 +72,7 @@ export const useUsersQuery = ( availableAppsIDs?.map((app) => app.id), ), { - onSuccess: (data: PaginatedEntity) => { + onSuccess: (data: PaginatedEntity) => { setUsers({ items: data.items, meta: { ...data.meta, orderByColumn: orderBy, orderDirection }, diff --git a/frontend/src/services/user/User.service.ts b/frontend/src/services/user/User.service.ts index d1ae4b158..a4f6c2f21 100644 --- a/frontend/src/services/user/User.service.ts +++ b/frontend/src/services/user/User.service.ts @@ -1,10 +1,9 @@ -import { AxiosResponseHeaders } from 'axios'; import { formatISO9075 } from 'date-fns'; import { OrderDirection } from '../../common/enums/sort-direction.enum'; import { PaginatedEntity } from '../../common/interfaces/paginated-entity.interface'; import { UserStatus } from '../../pages/users/enums/UserStatus.enum'; import { IInvite } from '../../pages/users/interfaces/Invite.interface'; -import { IUser } from '../../pages/users/interfaces/User.interface'; +import { IUser, IUserWithApplications } from '../../pages/users/interfaces/User.interface'; import { IUserPayload } from '../../pages/users/interfaces/UserPayload.interface'; import API from '../API'; @@ -64,7 +63,7 @@ export const getUsers = async ( interval?: Date[], organizationId?: number, availableAppsIDs?: number[], -): Promise> => { +): Promise> => { let requestUrl = `/user?limit=${limit}&page=${page}&orderBy=${orderBy}&orderDirection=${orderDirection}`; if (search) requestUrl = `${requestUrl}&search=${search}`; diff --git a/frontend/src/store/organization/organizations.slice.ts b/frontend/src/store/organization/organizations.slice.ts index fbc3bab44..a31bf52ce 100644 --- a/frontend/src/store/organization/organizations.slice.ts +++ b/frontend/src/store/organization/organizations.slice.ts @@ -1,6 +1,9 @@ import { OrderDirection } from '../../common/enums/sort-direction.enum'; import { PaginatedEntity } from '../../common/interfaces/paginated-entity.interface'; -import { IOrganizationFull } from '../../pages/organization/interfaces/Organization.interface'; +import { + IOrganizationFull, + IOrganizationView, +} from '../../pages/organization/interfaces/Organization.interface'; // eslint-disable-next-line @typescript-eslint/no-explicit-any export const organizationsSlice = (set: any) => ({ @@ -16,7 +19,7 @@ export const organizationsSlice = (set: any) => ({ orderDirection: OrderDirection.ASC, }, }, - setOrganizations: (organizations: PaginatedEntity) => { + setOrganizations: (organizations: PaginatedEntity) => { set({ organizations }); }, }); diff --git a/frontend/src/store/store.ts b/frontend/src/store/store.ts index 9b7157e17..a2b26eaf7 100644 --- a/frontend/src/store/store.ts +++ b/frontend/src/store/store.ts @@ -16,10 +16,11 @@ import { Federation } from '../common/interfaces/federations.interface'; import { organizationLegalSlice } from './organization/organization-legal.slice'; import { organizationReportsSlice } from './organization/organization-reports.slice'; import { profileSlice } from './user/Profile.slice'; -import { IUser } from '../pages/users/interfaces/User.interface'; +import { IUser, IUserWithApplications } from '../pages/users/interfaces/User.interface'; import { IOrganization, IOrganizationFull, + IOrganizationView, } from '../pages/organization/interfaces/Organization.interface'; import { organizationSlice } from './organization/organization.slice'; import { usersSlice } from './user/Users.slice'; @@ -47,7 +48,7 @@ import { IFeedback } from '../pages/civic-center-service/interfaces/Feedback.int import { feedbacksSlice } from './civic-center-service/Feedback.slice'; interface OrganizationState { - organizations: PaginatedEntity; + organizations: PaginatedEntity; organization: IOrganization | null; organizationGeneral: IOrganizationGeneral | null; organizationFinancial: IOrganizationFinancial[]; @@ -60,7 +61,7 @@ interface OrganizationState { setOrganizationFinancial: (organizationFinancial: IOrganizationFinancial[]) => void; setOrganizationReport: (organizationReport: IOrganizationReport) => void; setOrganizationLegal: (organizationLegal: IOrganizationLegal) => void; - setOrganizations: (organizations: PaginatedEntity) => void; + setOrganizations: (organizations: PaginatedEntity) => void; } interface NomenclatureState { counties: County[]; @@ -87,8 +88,8 @@ interface ProfileState { } interface UserState { - users: PaginatedEntity; - setUsers: (users: PaginatedEntity) => void; + users: PaginatedEntity; + setUsers: (users: PaginatedEntity) => void; } interface InviteState { diff --git a/frontend/src/store/user/Users.slice.ts b/frontend/src/store/user/Users.slice.ts index c8ed2c321..0c0a3109f 100644 --- a/frontend/src/store/user/Users.slice.ts +++ b/frontend/src/store/user/Users.slice.ts @@ -1,6 +1,6 @@ import { OrderDirection } from '../../common/enums/sort-direction.enum'; import { PaginatedEntity } from '../../common/interfaces/paginated-entity.interface'; -import { IUser } from '../../pages/users/interfaces/User.interface'; +import { IUserWithApplications } from '../../pages/users/interfaces/User.interface'; export const usersSlice = (set: any) => ({ users: { @@ -15,7 +15,7 @@ export const usersSlice = (set: any) => ({ orderDirection: OrderDirection.ASC, }, }, - setUsers: (users: PaginatedEntity) => { + setUsers: (users: PaginatedEntity) => { set({ users }); }, });