diff --git a/apps/api/src/common/graphql/services/core.service.ts b/apps/api/src/common/graphql/services/core.service.ts index d204a8b2..347ee962 100644 --- a/apps/api/src/common/graphql/services/core.service.ts +++ b/apps/api/src/common/graphql/services/core.service.ts @@ -25,8 +25,9 @@ export abstract class CoreService { const queryBuilder = this.repository.createQueryBuilder(alias); - // Perform a Left Join to fetch and display related applicationDetails only for 'notification' findAll - if (alias === 'notification') { + // Perform a Left Join to fetch and display related applicationDetails & providerDetails + // For 'notification' or 'archivedNotification' findAll + if (alias === 'notification' || alias === 'archivedNotification') { queryBuilder.leftJoinAndSelect(`${alias}.applicationDetails`, 'application'); queryBuilder.leftJoinAndSelect(`${alias}.providerDetails`, 'provider'); } diff --git a/apps/api/src/modules/archived-notifications/archived-notifications.module.ts b/apps/api/src/modules/archived-notifications/archived-notifications.module.ts index aa2ab996..5b621956 100644 --- a/apps/api/src/modules/archived-notifications/archived-notifications.module.ts +++ b/apps/api/src/modules/archived-notifications/archived-notifications.module.ts @@ -5,13 +5,14 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import { NotificationsModule } from '../notifications/notifications.module'; import { ConfigService } from '@nestjs/config'; import { ArchivedNotificationsController } from './archived-notifications.controller'; +import { ArchivedNotificationsResolver } from './archived-notifications.resolver'; @Module({ imports: [ TypeOrmModule.forFeature([ArchivedNotification]), forwardRef(() => NotificationsModule), ], - providers: [ArchivedNotificationsService, Logger, ConfigService], + providers: [ArchivedNotificationsService, ArchivedNotificationsResolver, Logger, ConfigService], exports: [ArchivedNotificationsService], controllers: [ArchivedNotificationsController], }) diff --git a/apps/api/src/modules/archived-notifications/archived-notifications.resolver.spec.ts b/apps/api/src/modules/archived-notifications/archived-notifications.resolver.spec.ts new file mode 100644 index 00000000..729ecb13 --- /dev/null +++ b/apps/api/src/modules/archived-notifications/archived-notifications.resolver.spec.ts @@ -0,0 +1,18 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { ArchivedNotificationsResolver } from './archived-notifications.resolver'; + +describe('ArchivedNotificationsResolver', () => { + let resolver: ArchivedNotificationsResolver; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ArchivedNotificationsResolver], + }).compile(); + + resolver = module.get(ArchivedNotificationsResolver); + }); + + it('should be defined', () => { + expect(resolver).toBeDefined(); + }); +}); diff --git a/apps/api/src/modules/archived-notifications/archived-notifications.resolver.ts b/apps/api/src/modules/archived-notifications/archived-notifications.resolver.ts new file mode 100644 index 00000000..d91bf494 --- /dev/null +++ b/apps/api/src/modules/archived-notifications/archived-notifications.resolver.ts @@ -0,0 +1,22 @@ +import { Args, Context, Query, Resolver } from '@nestjs/graphql'; +import { ArchivedNotificationsService } from './archived-notifications.service'; +import { ArchivedNotification } from './entities/archived-notification.entity'; +import { UseGuards } from '@nestjs/common'; +import { ArchivedNotificationResponse } from './dtos/archived-notification-response.dto'; +import { QueryOptionsDto } from 'src/common/graphql/dtos/query-options.dto'; +import { GqlAuthGuard } from 'src/common/guards/api-key/gql-auth.guard'; + +@Resolver(() => ArchivedNotification) +@UseGuards(GqlAuthGuard) +export class ArchivedNotificationsResolver { + constructor(private readonly archivedNotificationsService: ArchivedNotificationsService) {} + + @Query(() => ArchivedNotificationResponse, { name: 'archivedNotifications' }) + async findAll( + @Context() context, + @Args('options', { type: () => QueryOptionsDto, nullable: true, defaultValue: {} }) + options: QueryOptionsDto, + ): Promise { + return this.archivedNotificationsService.getAllArchivedNotifications(options); + } +} diff --git a/apps/api/src/modules/archived-notifications/archived-notifications.service.ts b/apps/api/src/modules/archived-notifications/archived-notifications.service.ts index f3d95930..e161099d 100644 --- a/apps/api/src/modules/archived-notifications/archived-notifications.service.ts +++ b/apps/api/src/modules/archived-notifications/archived-notifications.service.ts @@ -1,19 +1,27 @@ import { Injectable, Logger } from '@nestjs/common'; import { ArchivedNotification } from './entities/archived-notification.entity'; -import { DataSource, In, QueryRunner } from 'typeorm'; +import { DataSource, In, QueryRunner, Repository } from 'typeorm'; import { Notification } from 'src/modules/notifications/entities/notification.entity'; import { ConfigService } from '@nestjs/config'; import { DeliveryStatus } from 'src/common/constants/notifications'; import { Status } from 'src/common/constants/database'; +import { QueryOptionsDto } from 'src/common/graphql/dtos/query-options.dto'; +import { ArchivedNotificationResponse } from './dtos/archived-notification-response.dto'; +import { InjectRepository } from '@nestjs/typeorm'; +import { CoreService } from 'src/common/graphql/services/core.service'; @Injectable() -export class ArchivedNotificationsService { +export class ArchivedNotificationsService extends CoreService { protected readonly logger = new Logger(ArchivedNotificationsService.name); constructor( + @InjectRepository(ArchivedNotification) + private readonly archivedNotificationRepository: Repository, private readonly configService: ConfigService, private dataSource: DataSource, - ) {} + ) { + super(archivedNotificationRepository); + } private convertToArchivedNotifications(notifications: Notification[]): ArchivedNotification[] { return notifications.map((notification) => { @@ -108,4 +116,21 @@ export class ArchivedNotificationsService { throw error; } } + + async getAllArchivedNotifications( + options: QueryOptionsDto, + ): Promise { + this.logger.log('Getting all archived notifications with options.'); + + const baseConditions = [{ field: 'status', value: Status.ACTIVE }]; + const searchableFields = ['createdBy', 'data', 'result']; + + const { items, total } = await super.findAll( + options, + 'archivedNotification', + searchableFields, + baseConditions, + ); + return new ArchivedNotificationResponse(items, total, options.offset, options.limit); + } } diff --git a/apps/api/src/modules/archived-notifications/dtos/archived-notification-response.dto.ts b/apps/api/src/modules/archived-notifications/dtos/archived-notification-response.dto.ts new file mode 100644 index 00000000..bd57a258 --- /dev/null +++ b/apps/api/src/modules/archived-notifications/dtos/archived-notification-response.dto.ts @@ -0,0 +1,24 @@ +import { ObjectType, Field, Int } from '@nestjs/graphql'; +import { ArchivedNotification } from '../entities/archived-notification.entity'; + +@ObjectType() +export class ArchivedNotificationResponse { + @Field(() => [ArchivedNotification]) + archivedNotifications: ArchivedNotification[]; + + @Field(() => Int) + total: number; + + @Field(() => Int) + offset: number; + + @Field(() => Int) + limit: number; + + constructor(items: ArchivedNotification[], total: number, offset?: number, limit?: number) { + this.archivedNotifications = items; + this.total = total; + this.offset = offset ?? 0; + this.limit = limit ?? items.length; + } +} diff --git a/apps/api/src/modules/archived-notifications/entities/archived-notification.entity.ts b/apps/api/src/modules/archived-notifications/entities/archived-notification.entity.ts index ca53bc40..f83cc756 100644 --- a/apps/api/src/modules/archived-notifications/entities/archived-notification.entity.ts +++ b/apps/api/src/modules/archived-notifications/entities/archived-notification.entity.ts @@ -23,6 +23,7 @@ export class ArchivedNotification { id: number; @Column({ name: 'notification_id' }) + @Field() notificationId: number; @Column({ name: 'provider_id', default: null }) diff --git a/apps/portal/src/app/graphql/graphql.queries.ts b/apps/portal/src/app/graphql/graphql.queries.ts index c08dfdb0..53f7935d 100644 --- a/apps/portal/src/app/graphql/graphql.queries.ts +++ b/apps/portal/src/app/graphql/graphql.queries.ts @@ -30,6 +30,36 @@ export const GetNotifications = gql` } `; +export const GetArchivedNotifications = gql` + query GetArchivedNotifications($limit: Int!, $offset: Int!, $filters: [UniversalFilter!]) { + archivedNotifications( + options: { + limit: $limit + offset: $offset + sortBy: "createdOn" + sortOrder: DESC + filters: $filters + } + ) { + archivedNotifications { + channelType + createdBy + createdOn + data + deliveryStatus + notificationId + result + status + updatedBy + updatedOn + } + total + offset + limit + } + } +`; + export const GetApplications = gql` query GetApplications($limit: Int!, $offset: Int!, $filters: [UniversalFilter!]) { applications( diff --git a/apps/portal/src/app/views/notifications/notification.model.ts b/apps/portal/src/app/views/notifications/notification.model.ts index b85b4572..47db305a 100644 --- a/apps/portal/src/app/views/notifications/notification.model.ts +++ b/apps/portal/src/app/views/notifications/notification.model.ts @@ -17,3 +17,16 @@ export interface NotificationResponse { offset: number; limit: number; } + +export interface ArchivedNotification { + notificationId: number; + channelType: number; + data: Record; + deliveryStatus: number; + result?: Record | null; + createdOn: Date; + updatedOn: Date; + createdBy: string; + updatedBy: string; + status: number; +} diff --git a/apps/portal/src/app/views/notifications/notifications.component.ts b/apps/portal/src/app/views/notifications/notifications.component.ts index 5739909b..ef6b5d45 100644 --- a/apps/portal/src/app/views/notifications/notifications.component.ts +++ b/apps/portal/src/app/views/notifications/notifications.component.ts @@ -95,6 +95,8 @@ export class NotificationsComponent implements OnInit { toggleArchive() { this.archivedNotificationToggle = !this.archivedNotificationToggle; + // Now that toggle has been activated, load notifications + this.loadNotificationsLazy({ first: 0, rows: this.pageSize }); } getApplications() { @@ -320,34 +322,66 @@ export class NotificationsComponent implements OnInit { // Set current page this.currentPage = Math.floor(event.first / event.rows) + 1; - // Fetch notifications and handle errors - this.notificationService - .getNotifications(variables, loginToken) - .pipe( - // catchError operator to handle errors - catchError((error) => { - this.messageService.add({ - key: 'tst', - severity: 'error', - summary: 'Error', - detail: `There was an error while loading notifications. Reason: ${error.message}`, - }); + // Check if we need to fetch from archive table or notifications table + if (this.archivedNotificationToggle) { + // Fetch archived notifications and handle errors + this.notificationService + .getArchivedNotifications(variables, loginToken) + .pipe( + // catchError operator to handle errors + catchError((error) => { + this.messageService.add({ + key: 'tst', + severity: 'error', + summary: 'Error', + detail: `There was an error while loading notifications. Reason: ${error.message}`, + }); + this.loading = false; + return of(null); + }), + ) + .subscribe((notificationResponse: NotificationResponse | null) => { + if (notificationResponse && notificationResponse.notifications) { + // pagination is handled by p-table component of primeng + this.notifications = notificationResponse.notifications; + this.totalRecords = notificationResponse.total; + } else { + this.notifications = []; + this.totalRecords = 0; + } + this.loading = false; - return of(null); - }), - ) - .subscribe((notificationResponse: NotificationResponse | null) => { - if (notificationResponse && notificationResponse.notifications) { - // pagination is handled by p-table component of primeng - this.notifications = notificationResponse.notifications; - this.totalRecords = notificationResponse.total; - } else { - this.notifications = []; - this.totalRecords = 0; - } + }); + } else { + // Fetch notifications and handle errors + this.notificationService + .getNotifications(variables, loginToken) + .pipe( + // catchError operator to handle errors + catchError((error) => { + this.messageService.add({ + key: 'tst', + severity: 'error', + summary: 'Error', + detail: `There was an error while loading notifications. Reason: ${error.message}`, + }); + this.loading = false; + return of(null); + }), + ) + .subscribe((notificationResponse: NotificationResponse | null) => { + if (notificationResponse && notificationResponse.notifications) { + // pagination is handled by p-table component of primeng + this.notifications = notificationResponse.notifications; + this.totalRecords = notificationResponse.total; + } else { + this.notifications = []; + this.totalRecords = 0; + } - this.loading = false; - }); + this.loading = false; + }); + } } showJsonObject(json: Record): void { diff --git a/apps/portal/src/app/views/notifications/notifications.service.ts b/apps/portal/src/app/views/notifications/notifications.service.ts index ddba9937..3110ae55 100644 --- a/apps/portal/src/app/views/notifications/notifications.service.ts +++ b/apps/portal/src/app/views/notifications/notifications.service.ts @@ -1,9 +1,9 @@ import { Injectable } from '@angular/core'; import { Observable, catchError, map } from 'rxjs'; import { GraphqlService } from 'src/app/graphql/graphql.service'; -import { GetNotifications } from 'src/app/graphql/graphql.queries'; +import { GetArchivedNotifications, GetNotifications } from 'src/app/graphql/graphql.queries'; import { ApolloQueryResult } from '@apollo/client/core'; -import { Notification, NotificationResponse } from './notification.model'; +import { ArchivedNotification, Notification, NotificationResponse } from './notification.model'; interface GetNotificationsResponse { notifications: { @@ -13,6 +13,16 @@ interface GetNotificationsResponse { limit?: number; }; } + +interface GetArchivedNotificationsResponse { + archivedNotifications: { + archivedNotifications?: ArchivedNotification[]; + total?: number; + offset?: number; + limit?: number; + }; +} + @Injectable({ providedIn: 'root', }) @@ -38,4 +48,37 @@ export class NotificationsService { }), ); } + + getArchivedNotifications(variables, inputToken): Observable { + return this.graphqlService.query(GetArchivedNotifications, variables, inputToken).pipe( + map((response: ApolloQueryResult) => { + const archivedNotificationArray = + response.data?.archivedNotifications.archivedNotifications; + + const notificationResponseObject: NotificationResponse = { + notifications: + archivedNotificationArray?.map((item) => ({ + id: item.notificationId, + channelType: item.channelType, + data: item.data, + deliveryStatus: item.deliveryStatus, + result: item.result, + createdOn: item.createdOn, + updatedOn: item.updatedOn, + createdBy: item.createdBy, + updatedBy: item.updatedBy, + status: item.status, + })) ?? [], + total: response.data?.archivedNotifications.total, + offset: response.data?.archivedNotifications.offset, + limit: response.data?.archivedNotifications.limit, + }; + return notificationResponseObject; + }), + catchError((error) => { + const errorMessage: string = error.message; + throw new Error(errorMessage); + }), + ); + } }