Skip to content

Commit

Permalink
bugfix: add filter by apps for users list, both for Admin and SuperAdmin
Browse files Browse the repository at this point in the history
  • Loading branch information
Andrew Radulescu committed Nov 20, 2023
1 parent dc9b02b commit f98b239
Show file tree
Hide file tree
Showing 16 changed files with 125 additions and 17 deletions.
Original file line number Diff line number Diff line change
@@ -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<void> {
await queryRunner.query(`CREATE VIEW "UserApplicationsView" AS
Expand All @@ -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
Expand All @@ -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',
],
);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,12 @@ export class ApplicationController {
return this.appService.findAll(filters);
}

@Roles(Role.SUPER_ADMIN, Role.ADMIN)
@Get('/list')
getAllAppsNames(): Promise<Pick<Application, 'id' | 'name'>[]> {
return this.appService.getAllAppsNames();
}

@Roles(Role.SUPER_ADMIN)
@UseInterceptors(FileFieldsInterceptor([{ name: 'logo', maxCount: 1 }]))
@ApiConsumes('multipart/form-data')
Expand Down
11 changes: 11 additions & 0 deletions backend/src/modules/application/services/application.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,17 @@ export class ApplicationService {
}
}

public async getAllAppsNames(): Promise<Pick<Application, 'id' | 'name'>[]> {
const applications = await this.applicationRepository.getMany({
select: {
id: true,
name: true,
},
});

return applications;
}

/**
* Get all aplications with statistics for super-admin
* @param options
Expand Down
2 changes: 1 addition & 1 deletion backend/src/modules/user/constants/user-filters.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export const USER_FILTERS_CONFIG = {
phone: true,
status: true,
availableApps: true,
availableAppsList: true,
availableAppsIDs: true,
},
searchableColumns: ['name', 'email'],
defaultSortBy: 'name',
Expand Down
13 changes: 12 additions & 1 deletion backend/src/modules/user/dto/user-filter.dto.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,20 @@
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';

export class UserFilterDto extends BaseFilterDto {
@IsOptional()
@IsEnum(UserStatus)
status?: UserStatus;

@IsArray()
@IsOptional()
availableAppsIDs?: number[];
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -65,7 +65,7 @@ export class UserApplicationsView {
availableApps: Pick<ApplicationAccess, 'id' | 'name' | 'type'>; // 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;
Expand Down
17 changes: 15 additions & 2 deletions backend/src/modules/user/services/user.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -177,7 +184,13 @@ export class UserService {
options: UserFilterDto,
organizationId?: number,
): Promise<Pagination<UserApplicationsView>> {
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(
Expand Down
3 changes: 2 additions & 1 deletion frontend/src/assets/locales/en/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
3 changes: 2 additions & 1 deletion frontend/src/assets/locales/ro/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
23 changes: 22 additions & 1 deletion frontend/src/pages/users/components/UserList/UserList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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();
Expand All @@ -37,6 +42,7 @@ const UserList = (props: { organizationId?: number }) => {
const [orderDirection, setOrderDirection] = useState<OrderDirection>();
const [searchWord, setSearchWord] = useState<string | null>(null);
const [status, setStatus] = useState<{ status: UserStatus; label: string } | null>();
const [appsFilter, setAppsFilters] = useState<ApplicationListItem[]>([]);
const [range, setRange] = useState<Date[]>([]);
const [isConfirmRemoveModalOpen, setIsConfirmRemoveModalOpen] = useState<boolean>(false);
const { role } = useAuthContext();
Expand All @@ -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);
Expand Down Expand Up @@ -227,6 +236,7 @@ const UserList = (props: { organizationId?: number }) => {
setStatus(null);
setRange([]);
setSearchWord(null);
setAppsFilters([]);
};

const onCancelUserRemoval = () => {
Expand Down Expand Up @@ -283,6 +293,17 @@ const UserList = (props: { organizationId?: number }) => {
onChange={onStatusChange}
/>
</div>
<div className="sm:basis-1/4 w-full">
<Select
config={{
label: t('list.access_to_app'),
collection: appsNameList?.sort((a, b) => (a.name < b.name ? -1 : 1)) || [],
displayedAttribute: 'name',
}}
selected={appsFilter[0]}
onChange={(selection: ApplicationListItem) => setAppsFilters([selection])}
/>
</div>
</div>
</DataTableFilters>
<div className="w-full bg-white shadow rounded-lg my-6">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ export const UserListTableHeaders: TableColumn<IUser>[] = [
{
id: 'availableApps',
name: <DataTableNameHeader text="Access Aplicatii" />,
sortable: true,
sortable: false,
minWidth: '15rem',
cell: (row: IUser) => (
<div>
Expand Down
5 changes: 5 additions & 0 deletions frontend/src/services/application/Application.queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
getApplications,
getApplicationsForCreateUser,
getApplicationsForEditUser,
getApplicationsListNames,
getOngApplications,
removeApplication,
removeOngApplication,
Expand Down Expand Up @@ -79,6 +80,10 @@ export const useApplicationQuery = (applicationId: string) => {
});
};

export const useApplicationListNamesQuery = () => {
return useQuery(['application-list-names'], () => getApplicationsListNames());
};

// As an SuperAdmin get NGO LIST for an organization
export const useApplicationOrganizationQuery = (
applicationId: string,
Expand Down
5 changes: 5 additions & 0 deletions frontend/src/services/application/Application.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { CreateApplicationDto } from './interfaces/Application.dto';
import {
Application,
ApplicationAccess,
ApplicationListItem,
ApplicationOrganization,
ApplicationStatus,
ApplicationWithOngStatus,
Expand Down Expand Up @@ -83,6 +84,10 @@ export const getApplicationById = (applicationId: string): Promise<Application>
return API.get(`/application/${applicationId}`).then((res) => res.data);
};

export const getApplicationsListNames = (): Promise<ApplicationListItem[]> => {
return API.get(`/application/list`).then((res) => res.data);
};

export const getApplicationOrganizations = (
applicationId: string,
limit: number,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@ export interface Application {
userCount: number;
}

export interface ApplicationListItem {
id: number;
name: string;
}

// For Cards List
export interface ApplicationWithOngStatus {
id: number;
Expand Down
28 changes: 26 additions & 2 deletions frontend/src/services/user/User.queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import { PaginatedEntity } from '../../common/interfaces/paginated-entity.interf
import { OrderDirection } from '../../common/enums/sort-direction.enum';
import { UserStatus } from '../../pages/users/enums/UserStatus.enum';
import { IInvite } from '../../pages/users/interfaces/Invite.interface';
import { ApplicationListItem } from '../application/interfaces/Application.interface';

export const useProfileQuery = (queryOptions?: any) => {
const { setProfile, setOrganization } = useStore();
Expand All @@ -42,11 +43,34 @@ export const useUsersQuery = (
status?: UserStatus,
interval?: Date[],
organizationId?: number,
availableAppsIDs?: ApplicationListItem[],
) => {
const { setUsers } = useStore();
return useQuery(
['users', limit, page, orderBy, orderDirection, search, status, interval, organizationId],
() => getUsers(limit, page, orderBy, orderDirection, search, status, interval, organizationId),
[
'users',
limit,
page,
orderBy,
orderDirection,
search,
status,
interval,
organizationId,
availableAppsIDs,
],
() =>
getUsers(
limit,
page,
orderBy,
orderDirection,
search,
status,
interval,
organizationId,
availableAppsIDs?.map((app) => app.id),
),
{
onSuccess: (data: PaginatedEntity<IUser>) => {
setUsers({
Expand Down
7 changes: 6 additions & 1 deletion frontend/src/services/user/User.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ export const getUsers = async (
status?: UserStatus,
interval?: Date[],
organizationId?: number,
availableAppsIDs?: number[],
): Promise<PaginatedEntity<IUser>> => {
let requestUrl = `/user?limit=${limit}&page=${page}&orderBy=${orderBy}&orderDirection=${orderDirection}`;

Expand All @@ -77,7 +78,11 @@ export const getUsers = async (

if (organizationId) requestUrl = `${requestUrl}&organization_id=${organizationId}`;

return API.get(requestUrl).then((res) => res.data);
return API.get(requestUrl, {
params: {
...(availableAppsIDs?.length ? { availableAppsIDs } : {}),
},
}).then((res) => res.data);
};

export const getUsersForDownload = async (
Expand Down

0 comments on commit f98b239

Please sign in to comment.