diff --git a/backend/src/migrations/1700316222351-UserApplicationsView.ts b/backend/src/migrations/1700483035028-UserApplicationsView.ts similarity index 67% rename from backend/src/migrations/1700316222351-UserApplicationsView.ts rename to backend/src/migrations/1700483035028-UserApplicationsView.ts index 75c2f2e11..d5008df90 100644 --- a/backend/src/migrations/1700316222351-UserApplicationsView.ts +++ b/backend/src/migrations/1700483035028-UserApplicationsView.ts @@ -1,7 +1,7 @@ import { MigrationInterface, QueryRunner } from 'typeorm'; -export class UserApplicationsView1700316222351 implements MigrationInterface { - name = 'UserApplicationsView1700316222351'; +export class UserApplicationsView1700483035028 implements MigrationInterface { + name = 'UserApplicationsView1700483035028'; public async up(queryRunner: QueryRunner): Promise { await queryRunner.query(`CREATE VIEW "UserApplicationsView" AS @@ -15,7 +15,7 @@ export class UserApplicationsView1700316222351 implements MigrationInterface { u.organization_id as "organizationId", u.created_on as "createdOn", u.updated_on as "updatedOn", - STRING_AGG(DISTINCT a.name, ',') as "availableAppsList", + 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 @@ -35,7 +35,7 @@ export class UserApplicationsView1700316222351 implements MigrationInterface { 'public', 'VIEW', 'UserApplicationsView', - 'SELECT\n 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 STRING_AGG(DISTINCT a.name, \',\') AS user_apps,\n json_agg(json_build_object(\'id\', a.id, \'name\', a.name, \'type\', a.type)) as "availableApps"\n FROM\n "user" u\n LEFT JOIN user_ong_application uoa ON u.id = uoa.user_id AND uoa.status = \'active\'\n LEFT JOIN ong_application oa ON uoa.ong_application_id = oa.id AND oa.status = \'active\'\n LEFT JOIN application a ON (oa.application_id = a.id OR a.type = \'independent\') AND a.status = \'active\'\n WHERE\n "u"."role" = \'employee\' AND\n "u"."status" IN(\'active\', \'restricted\') AND\n "u"."deleted_on" IS NULL\n GROUP BY\n u.id', + 'SELECT\n 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 ARRAY_AGG(DISTINCT a.id) as "availableAppsIDs",\n json_agg(json_build_object(\'id\', a.id, \'name\', a.name, \'type\', a.type)) as "availableApps"\n FROM\n "user" u\n LEFT JOIN user_ong_application uoa ON u.id = uoa.user_id AND uoa.status = \'active\'\n LEFT JOIN ong_application oa ON uoa.ong_application_id = oa.id AND oa.status = \'active\'\n LEFT JOIN application a ON (oa.application_id = a.id OR a.type = \'independent\') AND a.status = \'active\'\n WHERE\n "u"."role" = \'employee\' AND\n "u"."status" IN(\'active\', \'restricted\') AND\n "u"."deleted_on" IS NULL\n GROUP BY\n u.id', ], ); } diff --git a/backend/src/modules/application/controllers/application.controller.ts b/backend/src/modules/application/controllers/application.controller.ts index d73fe0fbf..28fae5c19 100644 --- a/backend/src/modules/application/controllers/application.controller.ts +++ b/backend/src/modules/application/controllers/application.controller.ts @@ -62,6 +62,12 @@ export class ApplicationController { return this.appService.findAll(filters); } + @Roles(Role.SUPER_ADMIN, Role.ADMIN) + @Get('/list') + getAllAppsNames(): Promise[]> { + return this.appService.getAllAppsNames(); + } + @Roles(Role.SUPER_ADMIN) @UseInterceptors(FileFieldsInterceptor([{ name: 'logo', maxCount: 1 }])) @ApiConsumes('multipart/form-data') diff --git a/backend/src/modules/application/services/application.service.ts b/backend/src/modules/application/services/application.service.ts index a38947d24..ed151ba63 100644 --- a/backend/src/modules/application/services/application.service.ts +++ b/backend/src/modules/application/services/application.service.ts @@ -99,6 +99,17 @@ export class ApplicationService { } } + public async getAllAppsNames(): Promise[]> { + const applications = await this.applicationRepository.getMany({ + select: { + id: true, + name: true, + }, + }); + + return applications; + } + /** * Get all aplications with statistics for super-admin * @param options diff --git a/backend/src/modules/user/constants/user-filters.config.ts b/backend/src/modules/user/constants/user-filters.config.ts index a7ee2f961..356259cd8 100644 --- a/backend/src/modules/user/constants/user-filters.config.ts +++ b/backend/src/modules/user/constants/user-filters.config.ts @@ -9,7 +9,7 @@ export const USER_FILTERS_CONFIG = { phone: true, status: true, availableApps: true, - availableAppsList: true, + availableAppsIDs: true, }, searchableColumns: ['name', 'email'], defaultSortBy: 'name', diff --git a/backend/src/modules/user/dto/user-filter.dto.ts b/backend/src/modules/user/dto/user-filter.dto.ts index 08b53bca2..57b891757 100644 --- a/backend/src/modules/user/dto/user-filter.dto.ts +++ b/backend/src/modules/user/dto/user-filter.dto.ts @@ -1,4 +1,11 @@ -import { IsEnum, IsOptional } from 'class-validator'; +import { + IsArray, + IsEnum, + IsNumber, + IsOptional, + IsString, + Length, +} from 'class-validator'; import { BaseFilterDto } from 'src/common/base/base-filter.dto'; import { UserStatus } from '../enums/user-status.enum'; @@ -6,4 +13,8 @@ export class UserFilterDto extends BaseFilterDto { @IsOptional() @IsEnum(UserStatus) status?: UserStatus; + + @IsArray() + @IsOptional() + availableAppsIDs?: number[]; } 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 ad6b7e429..025ef9dc6 100644 --- a/backend/src/modules/user/entities/user-applications-view.entity.ts +++ b/backend/src/modules/user/entities/user-applications-view.entity.ts @@ -27,7 +27,7 @@ import { Role } from '../enums/role.enum'; u.organization_id as "organizationId", u.created_on as "createdOn", u.updated_on as "updatedOn", - STRING_AGG(DISTINCT a.name, ',') as "availableAppsList", + 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 @@ -65,7 +65,7 @@ export class UserApplicationsView { availableApps: Pick; // e.g. [{"id" : 2, "name" : "Beats Data", "type" : "independent"}, ...] @ViewColumn() - availableAppsList: string; // e.g. Beats Data, TEO, Vic, etc. (for filtering purpose) + availableAppsIDs: number[]; // e.g. 26, 28, 39 (for filtering purpose, to avoid searching in JSONs) @ViewColumn() createdOn: Date; diff --git a/backend/src/modules/user/services/user.service.ts b/backend/src/modules/user/services/user.service.ts index cd0ae5f53..db3dd3c68 100644 --- a/backend/src/modules/user/services/user.service.ts +++ b/backend/src/modules/user/services/user.service.ts @@ -8,7 +8,14 @@ import { } from '@nestjs/common'; import { ORGANIZATION_ERRORS } from 'src/modules/organization/constants/errors.constants'; import { OrganizationService } from 'src/modules/organization/services'; -import { FindManyOptions, FindOneOptions, Not, UpdateResult } from 'typeorm'; +import { + ArrayContains, + ArrayOverlap, + FindManyOptions, + FindOneOptions, + Not, + UpdateResult, +} from 'typeorm'; import { USER_FILTERS_CONFIG } from '../constants/user-filters.config'; import { CreateUserDto } from '../dto/create-user.dto'; import { UpdateUserDto } from '../dto/update-user.dto'; @@ -177,7 +184,13 @@ export class UserService { options: UserFilterDto, organizationId?: number, ): Promise> { - const paginationOptions: any = { ...options }; + const paginationOptions: any = { + ...options, + availableAppsIDs: + options.availableAppsIDs?.length > 0 + ? ArrayContains(options.availableAppsIDs) + : null, + }; // For Admin user we will sort by organizationId return this.userApplicationsView.getManyPaginated( diff --git a/frontend/src/assets/locales/en/translation.json b/frontend/src/assets/locales/en/translation.json index 834e33b66..096ffe5d4 100644 --- a/frontend/src/assets/locales/en/translation.json +++ b/frontend/src/assets/locales/en/translation.json @@ -665,7 +665,8 @@ "date": "Data adaugarii", "confirmation": "Esti sigur ca doresti stergerea utilizatorului?", "description": "Lorem ipsum.Închiderea contului ONG Hub înseamnă că nu vei mai avea acces în aplicațiile puse la dispozitie prin intemediul acestui portal. Lorem ipsum", - "download": "Descarca Tabel" + "download": "Descarca Tabel", + "access_to_app": "Acces la aplicatii" }, "invites": { "title": "Lista cu invitatii", diff --git a/frontend/src/assets/locales/ro/translation.json b/frontend/src/assets/locales/ro/translation.json index 4b9b956dd..45885ca13 100644 --- a/frontend/src/assets/locales/ro/translation.json +++ b/frontend/src/assets/locales/ro/translation.json @@ -665,7 +665,8 @@ "date": "Data adăugării", "confirmation": "Confirmă ștergerea utilizatorului", "description": "După ștergerea utilizatorului, acesta nu va mai avea acces în NGO Hug și toate informațiile despre acel cont vor fi eliminate.", - "download": "Descarcă tabelul" + "download": "Descarcă tabelul", + "access_to_app": "Acces la aplicatii" }, "invites": { "title": "Lista cu invitații", diff --git a/frontend/src/pages/users/components/UserList/UserList.tsx b/frontend/src/pages/users/components/UserList/UserList.tsx index 0c572fa0f..6bff6cec4 100644 --- a/frontend/src/pages/users/components/UserList/UserList.tsx +++ b/frontend/src/pages/users/components/UserList/UserList.tsx @@ -16,7 +16,7 @@ import { useRestrictUserMutation, useUsersQuery, } from '../../../../services/user/User.queries'; -import { useUser } from '../../../../store/selectors'; +import { useOngApplications, useUser } from '../../../../store/selectors'; import { UserStatusOptions } from '../../constants/filters.constants'; import { UserStatus } from '../../enums/UserStatus.enum'; import { IUser } from '../../interfaces/User.interface'; @@ -27,6 +27,11 @@ import { useTranslation } from 'react-i18next'; import { useAuthContext } from '../../../../contexts/AuthContext'; import { UserRole } from '../../enums/UserRole.enum'; import { getUsersForDownload } from '../../../../services/user/User.service'; +import { + useApplicationListNamesQuery, + useOngApplicationsQuery, +} from '../../../../services/application/Application.queries'; +import { ApplicationListItem } from '../../../../services/application/interfaces/Application.interface'; const UserList = (props: { organizationId?: number }) => { const navigate = useNavigate(); @@ -37,6 +42,7 @@ const UserList = (props: { organizationId?: number }) => { const [orderDirection, setOrderDirection] = useState(); const [searchWord, setSearchWord] = useState(null); const [status, setStatus] = useState<{ status: UserStatus; label: string } | null>(); + const [appsFilter, setAppsFilters] = useState([]); const [range, setRange] = useState([]); const [isConfirmRemoveModalOpen, setIsConfirmRemoveModalOpen] = useState(false); const { role } = useAuthContext(); @@ -56,11 +62,14 @@ const UserList = (props: { organizationId?: number }) => { status?.status, range, organizationId as number, + appsFilter as ApplicationListItem[], ); const restrictUserAccessMutation = useRestrictUserMutation(); const restoreUserAccessMutation = useRestoreUserMutation(); const removeUserMutation = useRemoveUserMutation(); + const { data: appsNameList } = useApplicationListNamesQuery(); + useEffect(() => { if (users?.meta) { setPage(users.meta.currentPage); @@ -227,6 +236,7 @@ const UserList = (props: { organizationId?: number }) => { setStatus(null); setRange([]); setSearchWord(null); + setAppsFilters([]); }; const onCancelUserRemoval = () => { @@ -283,6 +293,17 @@ const UserList = (props: { organizationId?: number }) => { onChange={onStatusChange} /> +
+