From d2a7309a372f62983767f2fa4f89c30360afcea7 Mon Sep 17 00:00:00 2001 From: Daniel Haselhan Date: Tue, 10 Oct 2023 11:08:57 -0700 Subject: [PATCH 1/2] Add Public Notification View * Add public.dto to backend to better match front end structure * Small cleanups --- .../submission-details.component.spec.ts | 7 - .../submission-details.component.ts | 9 +- .../submission-details.component.spec.ts | 7 - .../submission-details.component.ts | 8 +- .../alc-review/alc-review.component.html | 8 ++ .../alc-review/alc-review.component.scss | 8 ++ .../alc-review/alc-review.component.spec.ts | 25 ++++ .../alc-review/alc-review.component.ts | 16 +++ .../submission-documents.component.html | 43 ++++++ .../submission-documents.component.scss | 25 ++++ .../submission-documents.component.spec.ts | 35 +++++ .../submission-documents.component.ts | 56 ++++++++ .../public-notification.component.html | 56 ++++++++ .../public-notification.component.scss | 105 ++++++++++++++ .../public-notification.component.spec.ts | 49 +++++++ .../public-notification.component.ts | 48 +++++++ .../public-notification.module.ts | 44 ++++++ .../additional-information.component.html | 76 ++++++++++ .../additional-information.component.scss | 5 + .../additional-information.component.spec.ts | 31 ++++ .../additional-information.component.ts | 77 ++++++++++ .../submission/parcel/parcel.component.html | 42 ++++++ .../submission/parcel/parcel.component.scss | 13 ++ .../parcel/parcel.component.spec.ts | 40 ++++++ .../submission/parcel/parcel.component.ts | 36 +++++ .../proposal-details.component.html | 27 ++++ .../proposal-details.component.scss | 0 .../proposal-details.component.spec.ts | 23 +++ .../proposal-details.component.ts | 14 ++ .../submission-details.component.html | 52 +++++++ .../submission-details.component.scss | 117 ++++++++++++++++ .../submission-details.component.spec.ts | 36 +++++ .../submission-details.component.ts | 59 ++++++++ .../src/app/features/public/public.module.ts | 4 + .../public/public-notification.dto.ts | 30 ++++ .../src/app/services/public/public.dto.ts | 1 + .../src/app/services/public/public.service.ts | 25 ++++ .../automapper/public.automapper.profile.ts | 101 +++++++++++++- .../application/public-application.dto.ts | 83 +---------- .../application/public-application.service.ts | 3 +- .../public-notice-of-intent.dto.ts | 2 +- .../public-notice-of-intent.service.ts | 5 +- .../notification/public-notification.dto.ts | 55 ++++++++ .../public-notification.service.spec.ts | 132 ++++++++++++++++++ .../public-notification.service.ts | 108 ++++++++++++++ .../portal/public/public.controller.spec.ts | 25 ++++ .../src/portal/public/public.controller.ts | 33 +++++ .../apps/alcs/src/portal/public/public.dto.ts | 86 ++++++++++++ .../alcs/src/portal/public/public.module.ts | 6 + 49 files changed, 1777 insertions(+), 119 deletions(-) create mode 100644 portal-frontend/src/app/features/public/notification/alc-review/alc-review.component.html create mode 100644 portal-frontend/src/app/features/public/notification/alc-review/alc-review.component.scss create mode 100644 portal-frontend/src/app/features/public/notification/alc-review/alc-review.component.spec.ts create mode 100644 portal-frontend/src/app/features/public/notification/alc-review/alc-review.component.ts create mode 100644 portal-frontend/src/app/features/public/notification/alc-review/submission-documents/submission-documents.component.html create mode 100644 portal-frontend/src/app/features/public/notification/alc-review/submission-documents/submission-documents.component.scss create mode 100644 portal-frontend/src/app/features/public/notification/alc-review/submission-documents/submission-documents.component.spec.ts create mode 100644 portal-frontend/src/app/features/public/notification/alc-review/submission-documents/submission-documents.component.ts create mode 100644 portal-frontend/src/app/features/public/notification/public-notification.component.html create mode 100644 portal-frontend/src/app/features/public/notification/public-notification.component.scss create mode 100644 portal-frontend/src/app/features/public/notification/public-notification.component.spec.ts create mode 100644 portal-frontend/src/app/features/public/notification/public-notification.component.ts create mode 100644 portal-frontend/src/app/features/public/notification/public-notification.module.ts create mode 100644 portal-frontend/src/app/features/public/notification/submission/additional-information/additional-information.component.html create mode 100644 portal-frontend/src/app/features/public/notification/submission/additional-information/additional-information.component.scss create mode 100644 portal-frontend/src/app/features/public/notification/submission/additional-information/additional-information.component.spec.ts create mode 100644 portal-frontend/src/app/features/public/notification/submission/additional-information/additional-information.component.ts create mode 100644 portal-frontend/src/app/features/public/notification/submission/parcel/parcel.component.html create mode 100644 portal-frontend/src/app/features/public/notification/submission/parcel/parcel.component.scss create mode 100644 portal-frontend/src/app/features/public/notification/submission/parcel/parcel.component.spec.ts create mode 100644 portal-frontend/src/app/features/public/notification/submission/parcel/parcel.component.ts create mode 100644 portal-frontend/src/app/features/public/notification/submission/proposal-details/proposal-details.component.html create mode 100644 portal-frontend/src/app/features/public/notification/submission/proposal-details/proposal-details.component.scss create mode 100644 portal-frontend/src/app/features/public/notification/submission/proposal-details/proposal-details.component.spec.ts create mode 100644 portal-frontend/src/app/features/public/notification/submission/proposal-details/proposal-details.component.ts create mode 100644 portal-frontend/src/app/features/public/notification/submission/submission-details.component.html create mode 100644 portal-frontend/src/app/features/public/notification/submission/submission-details.component.scss create mode 100644 portal-frontend/src/app/features/public/notification/submission/submission-details.component.spec.ts create mode 100644 portal-frontend/src/app/features/public/notification/submission/submission-details.component.ts create mode 100644 portal-frontend/src/app/services/public/public-notification.dto.ts create mode 100644 services/apps/alcs/src/portal/public/notification/public-notification.dto.ts create mode 100644 services/apps/alcs/src/portal/public/notification/public-notification.service.spec.ts create mode 100644 services/apps/alcs/src/portal/public/notification/public-notification.service.ts create mode 100644 services/apps/alcs/src/portal/public/public.dto.ts diff --git a/portal-frontend/src/app/features/public/application/submission/submission-details.component.spec.ts b/portal-frontend/src/app/features/public/application/submission/submission-details.component.spec.ts index a06f7a8d15..4f407dd4e4 100644 --- a/portal-frontend/src/app/features/public/application/submission/submission-details.component.spec.ts +++ b/portal-frontend/src/app/features/public/application/submission/submission-details.component.spec.ts @@ -1,7 +1,6 @@ import { NO_ERRORS_SCHEMA } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { createMock, DeepMocked } from '@golevelup/ts-jest'; -import { ApplicationDocumentService } from '../../../../services/application-document/application-document.service'; import { CodeService } from '../../../../services/code/code.service'; import { SubmissionDetailsComponent } from './submission-details.component'; @@ -10,11 +9,9 @@ describe('SubmissionDetailsComponent', () => { let component: SubmissionDetailsComponent; let fixture: ComponentFixture; let mockCodeService: DeepMocked; - let mockAppDocumentService: DeepMocked; beforeEach(async () => { mockCodeService = createMock(); - mockAppDocumentService = createMock(); await TestBed.configureTestingModule({ providers: [ @@ -22,10 +19,6 @@ describe('SubmissionDetailsComponent', () => { provide: CodeService, useValue: mockCodeService, }, - { - provide: ApplicationDocumentService, - useValue: mockAppDocumentService, - }, ], declarations: [SubmissionDetailsComponent], schemas: [NO_ERRORS_SCHEMA], diff --git a/portal-frontend/src/app/features/public/application/submission/submission-details.component.ts b/portal-frontend/src/app/features/public/application/submission/submission-details.component.ts index ff55884b0b..bde5ff49fd 100644 --- a/portal-frontend/src/app/features/public/application/submission/submission-details.component.ts +++ b/portal-frontend/src/app/features/public/application/submission/submission-details.component.ts @@ -1,11 +1,11 @@ import { Component, Input, OnDestroy, OnInit } from '@angular/core'; import { Subject } from 'rxjs'; -import { ApplicationDocumentService } from '../../../../services/application-document/application-document.service'; import { PARCEL_TYPE } from '../../../../services/application-parcel/application-parcel.dto'; import { LocalGovernmentDto } from '../../../../services/code/code.dto'; import { CodeService } from '../../../../services/code/code.service'; import { PublicApplicationSubmissionDto } from '../../../../services/public/public-application.dto'; import { PublicDocumentDto, PublicOwnerDto, PublicParcelDto } from '../../../../services/public/public.dto'; +import { PublicService } from '../../../../services/public/public.service'; import { OWNER_TYPE } from '../../../../shared/dto/owner.dto'; @Component({ @@ -27,7 +27,7 @@ export class SubmissionDetailsComponent implements OnInit, OnDestroy { private localGovernments: LocalGovernmentDto[] = []; - constructor(private codeService: CodeService, private applicationDocumentService: ApplicationDocumentService) {} + constructor(private codeService: CodeService) {} ngOnInit(): void { this.loadGovernments(); @@ -44,11 +44,6 @@ export class SubmissionDetailsComponent implements OnInit, OnDestroy { this.$destroy.complete(); } - async openFile(uuid: string) { - const res = await this.applicationDocumentService.openFile(uuid); - window.open(res?.url, '_blank'); - } - private async loadGovernments() { const codes = await this.codeService.loadCodes(); this.localGovernments = codes.localGovernments.sort((a, b) => (a.name > b.name ? 1 : -1)); diff --git a/portal-frontend/src/app/features/public/notice-of-intent/submission/submission-details.component.spec.ts b/portal-frontend/src/app/features/public/notice-of-intent/submission/submission-details.component.spec.ts index a06f7a8d15..4f407dd4e4 100644 --- a/portal-frontend/src/app/features/public/notice-of-intent/submission/submission-details.component.spec.ts +++ b/portal-frontend/src/app/features/public/notice-of-intent/submission/submission-details.component.spec.ts @@ -1,7 +1,6 @@ import { NO_ERRORS_SCHEMA } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { createMock, DeepMocked } from '@golevelup/ts-jest'; -import { ApplicationDocumentService } from '../../../../services/application-document/application-document.service'; import { CodeService } from '../../../../services/code/code.service'; import { SubmissionDetailsComponent } from './submission-details.component'; @@ -10,11 +9,9 @@ describe('SubmissionDetailsComponent', () => { let component: SubmissionDetailsComponent; let fixture: ComponentFixture; let mockCodeService: DeepMocked; - let mockAppDocumentService: DeepMocked; beforeEach(async () => { mockCodeService = createMock(); - mockAppDocumentService = createMock(); await TestBed.configureTestingModule({ providers: [ @@ -22,10 +19,6 @@ describe('SubmissionDetailsComponent', () => { provide: CodeService, useValue: mockCodeService, }, - { - provide: ApplicationDocumentService, - useValue: mockAppDocumentService, - }, ], declarations: [SubmissionDetailsComponent], schemas: [NO_ERRORS_SCHEMA], diff --git a/portal-frontend/src/app/features/public/notice-of-intent/submission/submission-details.component.ts b/portal-frontend/src/app/features/public/notice-of-intent/submission/submission-details.component.ts index 92def23ca1..1d21cf83fc 100644 --- a/portal-frontend/src/app/features/public/notice-of-intent/submission/submission-details.component.ts +++ b/portal-frontend/src/app/features/public/notice-of-intent/submission/submission-details.component.ts @@ -1,6 +1,5 @@ import { Component, Input, OnDestroy, OnInit } from '@angular/core'; import { Subject } from 'rxjs'; -import { ApplicationDocumentService } from '../../../../services/application-document/application-document.service'; import { PARCEL_TYPE } from '../../../../services/application-parcel/application-parcel.dto'; import { LocalGovernmentDto } from '../../../../services/code/code.dto'; import { CodeService } from '../../../../services/code/code.service'; @@ -27,7 +26,7 @@ export class SubmissionDetailsComponent implements OnInit, OnDestroy { private localGovernments: LocalGovernmentDto[] = []; - constructor(private codeService: CodeService, private applicationDocumentService: ApplicationDocumentService) {} + constructor(private codeService: CodeService) {} ngOnInit(): void { this.loadGovernments(); @@ -44,11 +43,6 @@ export class SubmissionDetailsComponent implements OnInit, OnDestroy { this.$destroy.complete(); } - async openFile(uuid: string) { - const res = await this.applicationDocumentService.openFile(uuid); - window.open(res?.url, '_blank'); - } - private async loadGovernments() { const codes = await this.codeService.loadCodes(); this.localGovernments = codes.localGovernments.sort((a, b) => (a.name > b.name ? 1 : -1)); diff --git a/portal-frontend/src/app/features/public/notification/alc-review/alc-review.component.html b/portal-frontend/src/app/features/public/notification/alc-review/alc-review.component.html new file mode 100644 index 0000000000..6e81d7ef34 --- /dev/null +++ b/portal-frontend/src/app/features/public/notification/alc-review/alc-review.component.html @@ -0,0 +1,8 @@ +
+
+

ALC Review and Decision

+
+
+ +
+
diff --git a/portal-frontend/src/app/features/public/notification/alc-review/alc-review.component.scss b/portal-frontend/src/app/features/public/notification/alc-review/alc-review.component.scss new file mode 100644 index 0000000000..7ca8d1c637 --- /dev/null +++ b/portal-frontend/src/app/features/public/notification/alc-review/alc-review.component.scss @@ -0,0 +1,8 @@ +@use '../../../../../styles/functions' as *; +@use '../../../../../styles/colors'; + +.warning { + background-color: rgba(colors.$accent-color-light, 0.5); + padding: rem(16); + margin-bottom: rem(24); +} diff --git a/portal-frontend/src/app/features/public/notification/alc-review/alc-review.component.spec.ts b/portal-frontend/src/app/features/public/notification/alc-review/alc-review.component.spec.ts new file mode 100644 index 0000000000..23284b9844 --- /dev/null +++ b/portal-frontend/src/app/features/public/notification/alc-review/alc-review.component.spec.ts @@ -0,0 +1,25 @@ +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { PublicAlcReviewComponent } from './alc-review.component'; + +describe('PublicAlcReviewComponent', () => { + let component: PublicAlcReviewComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + providers: [], + declarations: [PublicAlcReviewComponent], + schemas: [NO_ERRORS_SCHEMA], + }).compileComponents(); + + fixture = TestBed.createComponent(PublicAlcReviewComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/portal-frontend/src/app/features/public/notification/alc-review/alc-review.component.ts b/portal-frontend/src/app/features/public/notification/alc-review/alc-review.component.ts new file mode 100644 index 0000000000..d822d4b3ed --- /dev/null +++ b/portal-frontend/src/app/features/public/notification/alc-review/alc-review.component.ts @@ -0,0 +1,16 @@ +import { Component, Input } from '@angular/core'; +import { ApplicationPortalDecisionDto } from '../../../../services/application-decision/application-decision.dto'; +import { PublicNoticeOfIntentSubmissionDto } from '../../../../services/public/public-notice-of-intent.dto'; +import { PublicNotificationSubmissionDto } from '../../../../services/public/public-notification.dto'; +import { PublicDocumentDto } from '../../../../services/public/public.dto'; + +@Component({ + selector: 'app-public-alc-review', + templateUrl: './alc-review.component.html', + styleUrls: ['./alc-review.component.scss'], +}) +export class PublicAlcReviewComponent { + @Input() documents!: PublicDocumentDto[]; + + constructor() {} +} diff --git a/portal-frontend/src/app/features/public/notification/alc-review/submission-documents/submission-documents.component.html b/portal-frontend/src/app/features/public/notification/alc-review/submission-documents/submission-documents.component.html new file mode 100644 index 0000000000..45f473c3d5 --- /dev/null +++ b/portal-frontend/src/app/features/public/notification/alc-review/submission-documents/submission-documents.component.html @@ -0,0 +1,43 @@ +

Application Documents

+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Type + {{ element.type?.label }} + Document Name + {{ element.fileName }} + Source{{ element.source }}Upload Date{{ element.uploadedAt | date }}Actions + +
Documents will be visible here once provided by ALC
+
diff --git a/portal-frontend/src/app/features/public/notification/alc-review/submission-documents/submission-documents.component.scss b/portal-frontend/src/app/features/public/notification/alc-review/submission-documents/submission-documents.component.scss new file mode 100644 index 0000000000..0027146765 --- /dev/null +++ b/portal-frontend/src/app/features/public/notification/alc-review/submission-documents/submission-documents.component.scss @@ -0,0 +1,25 @@ +@use '../../../../../../styles/colors'; +@use '../../../../../../styles/functions' as *; + +.header { + display: flex; + justify-content: space-between; +} + +.table-container { + margin: rem(4); + overflow-x: auto; +} + +.documents { + margin-top: rem(12); +} + +.mat-mdc-no-data-row { + height: rem(56); + color: colors.$grey-dark; +} + +a { + word-break: break-all; +} diff --git a/portal-frontend/src/app/features/public/notification/alc-review/submission-documents/submission-documents.component.spec.ts b/portal-frontend/src/app/features/public/notification/alc-review/submission-documents/submission-documents.component.spec.ts new file mode 100644 index 0000000000..a2f54298d3 --- /dev/null +++ b/portal-frontend/src/app/features/public/notification/alc-review/submission-documents/submission-documents.component.spec.ts @@ -0,0 +1,35 @@ +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { PublicService } from '../../../../../services/public/public.service'; + +import { PublicSubmissionDocumentsComponent } from './submission-documents.component'; + +describe('PublicSubmissionDocumentsComponent', () => { + let component: PublicSubmissionDocumentsComponent; + let fixture: ComponentFixture; + let mockPublicService: DeepMocked; + + beforeEach(async () => { + mockPublicService = createMock(); + + await TestBed.configureTestingModule({ + declarations: [PublicSubmissionDocumentsComponent], + providers: [ + { + provide: PublicService, + useValue: mockPublicService, + }, + ], + schemas: [NO_ERRORS_SCHEMA], + }).compileComponents(); + + fixture = TestBed.createComponent(PublicSubmissionDocumentsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/portal-frontend/src/app/features/public/notification/alc-review/submission-documents/submission-documents.component.ts b/portal-frontend/src/app/features/public/notification/alc-review/submission-documents/submission-documents.component.ts new file mode 100644 index 0000000000..1fe9103978 --- /dev/null +++ b/portal-frontend/src/app/features/public/notification/alc-review/submission-documents/submission-documents.component.ts @@ -0,0 +1,56 @@ +import { Component, Input, OnDestroy, OnInit, ViewChild } from '@angular/core'; +import { MatSort } from '@angular/material/sort'; +import { MatTableDataSource } from '@angular/material/table'; +import { Subject } from 'rxjs'; +import { PublicNoticeOfIntentSubmissionDto } from '../../../../../services/public/public-notice-of-intent.dto'; +import { PublicDocumentDto } from '../../../../../services/public/public.dto'; +import { PublicService } from '../../../../../services/public/public.service'; + +@Component({ + selector: 'app-submission-documents', + templateUrl: './submission-documents.component.html', + styleUrls: ['./submission-documents.component.scss'], +}) +export class PublicSubmissionDocumentsComponent implements OnInit, OnDestroy { + private $destroy = new Subject(); + + displayedColumns: string[] = ['type', 'fileName', 'source', 'uploadedAt', 'actions']; + @Input() documents!: PublicDocumentDto[]; + @Input() submission!: PublicNoticeOfIntentSubmissionDto; + + @ViewChild(MatSort) sort!: MatSort; + dataSource: MatTableDataSource = new MatTableDataSource(); + + constructor(private publicService: PublicService) {} + + ngOnInit(): void { + this.dataSource = new MatTableDataSource(this.documents); + } + + async openFile(uuid: string) { + const res = await this.publicService.getNoticeOfIntentOpenFileUrl(this.submission.fileNumber, uuid); + if (res) { + window.open(res.url, '_blank'); + } + } + + async downloadFile(uuid: string) { + const res = await this.publicService.getNoticeOfIntentOpenFileUrl(this.submission.fileNumber, uuid); + if (res) { + const downloadLink = document.createElement('a'); + downloadLink.href = res.url; + downloadLink.download = res.url.split('/').pop()!; + if (window.webkitURL == null) { + downloadLink.onclick = (event: MouseEvent) => document.body.removeChild(event.target); + downloadLink.style.display = 'none'; + document.body.appendChild(downloadLink); + } + downloadLink.click(); + } + } + + ngOnDestroy(): void { + this.$destroy.next(); + this.$destroy.complete(); + } +} diff --git a/portal-frontend/src/app/features/public/notification/public-notification.component.html b/portal-frontend/src/app/features/public/notification/public-notification.component.html new file mode 100644 index 0000000000..bd6c7ab639 --- /dev/null +++ b/portal-frontend/src/app/features/public/notification/public-notification.component.html @@ -0,0 +1,56 @@ + + + +
+ +
diff --git a/portal-frontend/src/app/features/public/notification/public-notification.component.scss b/portal-frontend/src/app/features/public/notification/public-notification.component.scss new file mode 100644 index 0000000000..0e74e9ca5b --- /dev/null +++ b/portal-frontend/src/app/features/public/notification/public-notification.component.scss @@ -0,0 +1,105 @@ +@use '../../../../styles/functions' as *; +@use '../../../../styles/colors'; + +.navigation { + ::ng-deep .mdc-tab__text-label { + font-weight: bold; + } +} + +.content { + margin: rem(24) 0; +} + +:host::ng-deep { + .header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: rem(24); + flex-direction: column; + + @media screen and (min-width: $tabletBreakpoint) { + flex-direction: row; + } + + h3 { + margin-top: rem(8) !important; + } + + .btns-wrapper { + display: flex; + flex-direction: column-reverse; + width: 100%; + + button { + margin-top: rem(16) !important; + } + + @media screen and (min-width: $tabletBreakpoint) { + display: inline-block; + width: unset; + + button { + margin-right: rem(16) !important; + margin-top: rem(8) !important; + } + + button:last-child { + margin-right: 0 !important; + } + } + } + } +} + +.absolute { + position: absolute; + left: 0; + right: 0; +} + +.banner { + background-color: colors.$secondary-color; + width: 100%; + color: #fff; + padding: rem(16) rem(24); + margin-bottom: rem(32); + + @media screen and (min-width: $tabletBreakpoint) { + padding: rem(12) rem(36) !important; + } + + // TODO: this is just a placeholder and will be addressed later + @media screen and (min-width: $desktopBreakpoint) { + padding: rem(18) rem(80) !important; + } + + .banner-status { + margin: rem(16) 0; + display: grid; + grid-template-columns: 1fr; + + div { + margin-top: rem(16); + } + + @media screen and (min-width: $tabletBreakpoint) { + grid-template-columns: 1fr 1fr; + margin-bottom: 0; + } + } +} + +.no-comment { + color: colors.$grey-dark; + font-style: italic; +} + +section { + margin-bottom: rem(24); +} + +.document { + margin-bottom: rem(8); +} diff --git a/portal-frontend/src/app/features/public/notification/public-notification.component.spec.ts b/portal-frontend/src/app/features/public/notification/public-notification.component.spec.ts new file mode 100644 index 0000000000..075df84c1b --- /dev/null +++ b/portal-frontend/src/app/features/public/notification/public-notification.component.spec.ts @@ -0,0 +1,49 @@ +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ActivatedRoute } from '@angular/router'; +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { BehaviorSubject } from 'rxjs'; +import { PublicService } from '../../../services/public/public.service'; + +import { PublicNotificationComponent } from './public-notification.component'; + +describe('PublicNoticeOfIntentComponent', () => { + let component: PublicNotificationComponent; + let fixture: ComponentFixture; + + let mockRoute; + let mockPublicService: DeepMocked; + + let routeParamMap: BehaviorSubject>; + + beforeEach(async () => { + mockRoute = createMock(); + mockPublicService = createMock(); + + routeParamMap = new BehaviorSubject(new Map()); + mockRoute.paramMap = routeParamMap; + + await TestBed.configureTestingModule({ + providers: [ + { + provide: ActivatedRoute, + useValue: mockRoute, + }, + { + provide: PublicService, + useValue: mockPublicService, + }, + ], + declarations: [PublicNotificationComponent], + schemas: [NO_ERRORS_SCHEMA], + }).compileComponents(); + + fixture = TestBed.createComponent(PublicNotificationComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/portal-frontend/src/app/features/public/notification/public-notification.component.ts b/portal-frontend/src/app/features/public/notification/public-notification.component.ts new file mode 100644 index 0000000000..7d51fc676e --- /dev/null +++ b/portal-frontend/src/app/features/public/notification/public-notification.component.ts @@ -0,0 +1,48 @@ +import { Component, OnDestroy, OnInit } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; +import { Subject, takeUntil } from 'rxjs'; +import { ApplicationPortalDecisionDto } from '../../../services/application-decision/application-decision.dto'; +import { PublicNotificationSubmissionDto } from '../../../services/public/public-notification.dto'; +import { PublicDocumentDto, PublicParcelDto } from '../../../services/public/public.dto'; +import { PublicService } from '../../../services/public/public.service'; + +@Component({ + selector: 'app-public-notification', + templateUrl: './public-notification.component.html', + styleUrls: ['./public-notification.component.scss'], +}) +export class PublicNotificationComponent implements OnInit, OnDestroy { + $destroy = new Subject(); + + submission: PublicNotificationSubmissionDto | undefined; + documents: PublicDocumentDto[] = []; + parcels: PublicParcelDto[] = []; + decisions: ApplicationPortalDecisionDto[] = []; + + constructor(private publicService: PublicService, private route: ActivatedRoute) {} + + ngOnInit(): void { + this.route.paramMap.pipe(takeUntil(this.$destroy)).subscribe((routeParams) => { + const fileId = routeParams.get('fileId'); + if (fileId) { + this.loadApplication(fileId); + } + }); + } + + private async loadApplication(fileId: string) { + const res = await this.publicService.getNotification(fileId); + if (res) { + const { submission, documents, parcels } = res; + + this.submission = submission; + this.documents = documents; + this.parcels = parcels; + } + } + + ngOnDestroy(): void { + this.$destroy.next(); + this.$destroy.complete(); + } +} diff --git a/portal-frontend/src/app/features/public/notification/public-notification.module.ts b/portal-frontend/src/app/features/public/notification/public-notification.module.ts new file mode 100644 index 0000000000..712957f4d1 --- /dev/null +++ b/portal-frontend/src/app/features/public/notification/public-notification.module.ts @@ -0,0 +1,44 @@ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { MatAutocompleteModule } from '@angular/material/autocomplete'; +import { MatPaginatorModule } from '@angular/material/paginator'; +import { MatSortModule } from '@angular/material/sort'; +import { MatTreeModule } from '@angular/material/tree'; +import { RouterModule, Routes } from '@angular/router'; +import { SharedModule } from '../../../shared/shared.module'; +import { PublicAlcReviewComponent } from './alc-review/alc-review.component'; +import { PublicSubmissionDocumentsComponent } from './alc-review/submission-documents/submission-documents.component'; +import { PublicNotificationComponent } from './public-notification.component'; +import { AdditionalInformationComponent } from './submission/additional-information/additional-information.component'; +import { ParcelComponent } from './submission/parcel/parcel.component'; +import { ProposalDetailsComponent } from './submission/proposal-details/proposal-details.component'; +import { SubmissionDetailsComponent } from './submission/submission-details.component'; + +const routes: Routes = [ + { + path: ':fileId', + component: PublicNotificationComponent, + }, +]; + +@NgModule({ + declarations: [ + PublicNotificationComponent, + PublicAlcReviewComponent, + PublicSubmissionDocumentsComponent, + ParcelComponent, + SubmissionDetailsComponent, + ProposalDetailsComponent, + AdditionalInformationComponent, + ], + imports: [ + CommonModule, + SharedModule, + MatPaginatorModule, + MatSortModule, + RouterModule.forChild(routes), + MatAutocompleteModule, + MatTreeModule, + ], +}) +export class PublicNotificationModule {} diff --git a/portal-frontend/src/app/features/public/notification/submission/additional-information/additional-information.component.html b/portal-frontend/src/app/features/public/notification/submission/additional-information/additional-information.component.html new file mode 100644 index 0000000000..dcc15e93fa --- /dev/null +++ b/portal-frontend/src/app/features/public/notification/submission/additional-information/additional-information.component.html @@ -0,0 +1,76 @@ +
+
+ {{ firstQuestion }} +
+
+ + {{ noiSubmission.soilIsRemovingSoilForNewStructure ? 'Yes' : 'No' }} + + +
+ + +
The total floor area (m2) of the proposed structure(s)
+
+
#
+
Type
+
Area
+ +
+ {{ i + 1 }} +
+
+ {{ structure.type }} + +
+
+ {{ structure.area }} + +
+
+
+ +
+
+ + +
Describe how the structure is necessary for farm use
+
+ {{ noiSubmission.soilStructureFarmUseReason }} + +
+
+ + +
Describe how the structure is necessary for residential use
+
+ {{ noiSubmission.soilStructureResidentialUseReason }} + +
+
+ + +
Describe the current agricultural activity on the parcel(s)
+
+ {{ noiSubmission.soilAgriParcelActivity }} + +
+
+ + +
Describe the intended use of the residential accessory structure
+
+ {{ noiSubmission.soilStructureResidentialAccessoryUseReason }} + +
+
+ + +
Describe the intended use of the 'Other' structure
+
+ {{ noiSubmission.soilStructureOtherUseReason }} + +
+
+
+
diff --git a/portal-frontend/src/app/features/public/notification/submission/additional-information/additional-information.component.scss b/portal-frontend/src/app/features/public/notification/submission/additional-information/additional-information.component.scss new file mode 100644 index 0000000000..bfe0899632 --- /dev/null +++ b/portal-frontend/src/app/features/public/notification/submission/additional-information/additional-information.component.scss @@ -0,0 +1,5 @@ +.multiple-documents { + a { + display: block; + } +} diff --git a/portal-frontend/src/app/features/public/notification/submission/additional-information/additional-information.component.spec.ts b/portal-frontend/src/app/features/public/notification/submission/additional-information/additional-information.component.spec.ts new file mode 100644 index 0000000000..417628f883 --- /dev/null +++ b/portal-frontend/src/app/features/public/notification/submission/additional-information/additional-information.component.spec.ts @@ -0,0 +1,31 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { DeepMocked } from '@golevelup/ts-jest'; +import { PublicService } from '../../../../../services/public/public.service'; + +import { AdditionalInformationComponent } from './additional-information.component'; + +describe('AdditionalInformationComponent', () => { + let component: AdditionalInformationComponent; + let fixture: ComponentFixture; + let mockPublicService: DeepMocked; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [AdditionalInformationComponent], + providers: [ + { + provide: PublicService, + useValue: mockPublicService, + }, + ], + }).compileComponents(); + + fixture = TestBed.createComponent(AdditionalInformationComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/portal-frontend/src/app/features/public/notification/submission/additional-information/additional-information.component.ts b/portal-frontend/src/app/features/public/notification/submission/additional-information/additional-information.component.ts new file mode 100644 index 0000000000..a21f812bd0 --- /dev/null +++ b/portal-frontend/src/app/features/public/notification/submission/additional-information/additional-information.component.ts @@ -0,0 +1,77 @@ +import { Component, Input, OnInit } from '@angular/core'; +import { Router } from '@angular/router'; +import { PublicNoticeOfIntentSubmissionDto } from '../../../../../services/public/public-notice-of-intent.dto'; +import { PublicDocumentDto } from '../../../../../services/public/public.dto'; +import { PublicService } from '../../../../../services/public/public.service'; +import { DOCUMENT_TYPE } from '../../../../../shared/dto/document.dto'; +import { + RESIDENTIAL_STRUCTURE_TYPES, + STRUCTURE_TYPES, +} from '../../../../notice-of-intents/edit-submission/additional-information/additional-information.component'; + +@Component({ + selector: 'app-additional-information', + templateUrl: './additional-information.component.html', + styleUrls: ['./additional-information.component.scss'], +}) +export class AdditionalInformationComponent implements OnInit { + firstQuestion = 'FIX THIS'; + + @Input() noiSubmission!: PublicNoticeOfIntentSubmissionDto; + + @Input() set noiDocuments(documents: PublicDocumentDto[]) { + this.buildingPlans = documents.filter((document) => document.type?.code === DOCUMENT_TYPE.BUILDING_PLAN); + } + + buildingPlans: PublicDocumentDto[] = []; + + isSoilStructureFarmUseReasonVisible = false; + isSoilStructureResidentialUseReasonVisible = false; + isSoilAgriParcelActivityVisible = false; + isSoilStructureResidentialAccessoryUseReasonVisible = false; + isSoilOtherStructureVisible = false; + + constructor(private router: Router, private publicService: PublicService) {} + + ngOnInit(): void { + this.setVisibilityForResidentialFields(); + this.setValidatorsForAccessoryFields(); + this.setVisibilityForFarmFields(); + this.setVisibilityForOtherFields(); + } + + private setVisibilityForResidentialFields() { + this.isSoilStructureResidentialUseReasonVisible = !!this.noiSubmission?.soilProposedStructures.some( + (structure) => structure.type && RESIDENTIAL_STRUCTURE_TYPES.includes(structure.type) + ); + } + + private setValidatorsForAccessoryFields() { + this.isSoilStructureResidentialAccessoryUseReasonVisible = !!this.noiSubmission?.soilProposedStructures.some( + (structure) => structure.type === STRUCTURE_TYPES.ACCESSORY_STRUCTURE + ); + } + + private setVisibilityForFarmFields() { + if ( + this.noiSubmission?.soilProposedStructures.some((structure) => structure.type === STRUCTURE_TYPES.FARM_STRUCTURE) + ) { + this.isSoilAgriParcelActivityVisible = true; + this.isSoilStructureFarmUseReasonVisible = true; + } else { + this.isSoilAgriParcelActivityVisible = false; + this.isSoilStructureFarmUseReasonVisible = false; + } + } + + private setVisibilityForOtherFields() { + this.isSoilOtherStructureVisible = !!this.noiSubmission?.soilProposedStructures.some( + (structure) => structure.type === STRUCTURE_TYPES.OTHER_STRUCTURE + ); + } + + async openFile(uuid: string) { + const res = await this.publicService.getNoticeOfIntentOpenFileUrl(this.noiSubmission.fileNumber, uuid); + window.open(res?.url, '_blank'); + } +} diff --git a/portal-frontend/src/app/features/public/notification/submission/parcel/parcel.component.html b/portal-frontend/src/app/features/public/notification/submission/parcel/parcel.component.html new file mode 100644 index 0000000000..26c2dd4dd4 --- /dev/null +++ b/portal-frontend/src/app/features/public/notification/submission/parcel/parcel.component.html @@ -0,0 +1,42 @@ +

+ {{ pageTitle }} +

+
+ +
+

Parcel {{ parcelInd + 1 }}: Parcel and Owner Information

+
+
Ownership Type
+
+ {{ parcel.ownershipType?.label }} + +
+
Legal Description
+
+ {{ parcel.legalDescription }} +
+
Area (Hectares)
+
+ {{ parcel.mapAreaHectares }} +
+
+ PID {{ parcel.ownershipType?.code === PARCEL_OWNERSHIP_TYPES.CROWN ? '(optional)' : '' }} +
+
+ {{ parcel.pid | mask : '000-000-000' }} + +
+ +
PIN (optional)
+
+ {{ parcel.pin }} + +
+
+
Civic Address
+
+ {{ parcel.civicAddress }} + +
+
+
diff --git a/portal-frontend/src/app/features/public/notification/submission/parcel/parcel.component.scss b/portal-frontend/src/app/features/public/notification/submission/parcel/parcel.component.scss new file mode 100644 index 0000000000..70c42d9449 --- /dev/null +++ b/portal-frontend/src/app/features/public/notification/submission/parcel/parcel.component.scss @@ -0,0 +1,13 @@ +@use '../../../../../../styles/functions' as *; + +.review-table { + grid-template-columns: 1fr 1fr !important; + + .full-width { + grid-column: 1/3; + } +} + +.crown-land { + text-transform: capitalize; +} diff --git a/portal-frontend/src/app/features/public/notification/submission/parcel/parcel.component.spec.ts b/portal-frontend/src/app/features/public/notification/submission/parcel/parcel.component.spec.ts new file mode 100644 index 0000000000..ab7cf978f1 --- /dev/null +++ b/portal-frontend/src/app/features/public/notification/submission/parcel/parcel.component.spec.ts @@ -0,0 +1,40 @@ +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { Router } from '@angular/router'; +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { PublicService } from '../../../../../services/public/public.service'; +import { ParcelComponent } from './parcel.component'; + +describe('ParcelComponent', () => { + let component: ParcelComponent; + let fixture: ComponentFixture; + + let mockPublicService: DeepMocked; + + beforeEach(async () => { + mockPublicService = createMock(); + await TestBed.configureTestingModule({ + declarations: [ParcelComponent], + providers: [ + { + provide: PublicService, + useValue: mockPublicService, + }, + { + provides: Router, + useValue: {}, + }, + ], + schemas: [NO_ERRORS_SCHEMA], + }).compileComponents(); + + fixture = TestBed.createComponent(ParcelComponent); + component = fixture.componentInstance; + component.submission = {} as any; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/portal-frontend/src/app/features/public/notification/submission/parcel/parcel.component.ts b/portal-frontend/src/app/features/public/notification/submission/parcel/parcel.component.ts new file mode 100644 index 0000000000..acaab1ad3e --- /dev/null +++ b/portal-frontend/src/app/features/public/notification/submission/parcel/parcel.component.ts @@ -0,0 +1,36 @@ +import { Component, Input } from '@angular/core'; +import { Subject } from 'rxjs'; +import { PARCEL_OWNERSHIP_TYPE } from '../../../../../services/application-parcel/application-parcel.dto'; +import { PublicNoticeOfIntentSubmissionDto } from '../../../../../services/public/public-notice-of-intent.dto'; +import { PublicNotificationSubmissionDto } from '../../../../../services/public/public-notification.dto'; +import { PublicParcelDto } from '../../../../../services/public/public.dto'; + +@Component({ + selector: 'app-parcel', + templateUrl: './parcel.component.html', + styleUrls: ['./parcel.component.scss'], +}) +export class ParcelComponent { + $destroy = new Subject(); + + @Input() submission!: PublicNotificationSubmissionDto; + @Input() parcels: PublicParcelDto[] = []; + + PARCEL_OWNERSHIP_TYPES = PARCEL_OWNERSHIP_TYPE; + pageTitle: string = 'Identify Parcel(s) Under Application'; + + fileId = ''; + submissionUuid = ''; + + constructor() {} + + ngOnInit(): void { + this.fileId = this.submission.fileNumber; + this.submissionUuid = this.submission.uuid; + } + + ngOnDestroy(): void { + this.$destroy.next(); + this.$destroy.complete(); + } +} diff --git a/portal-frontend/src/app/features/public/notification/submission/proposal-details/proposal-details.component.html b/portal-frontend/src/app/features/public/notification/submission/proposal-details/proposal-details.component.html new file mode 100644 index 0000000000..ddaca2a3e9 --- /dev/null +++ b/portal-frontend/src/app/features/public/notification/submission/proposal-details/proposal-details.component.html @@ -0,0 +1,27 @@ +
+
Submitter’s File Number
+
+ {{ submission.submittersFileNumber }} + +
+ +
What is the purpose of the SRW?
+
+ {{ submission.purpose }} + +
+ +
Total area of the SRW
+
+ {{ submission.totalArea }} + +
+ +
Is there a survey plan associated with the SRW?
+
+ + {{ submission.hasSurveyPlan ? 'Yes' : 'No' }} + + +
+
diff --git a/portal-frontend/src/app/features/public/notification/submission/proposal-details/proposal-details.component.scss b/portal-frontend/src/app/features/public/notification/submission/proposal-details/proposal-details.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/portal-frontend/src/app/features/public/notification/submission/proposal-details/proposal-details.component.spec.ts b/portal-frontend/src/app/features/public/notification/submission/proposal-details/proposal-details.component.spec.ts new file mode 100644 index 0000000000..7183a90aa9 --- /dev/null +++ b/portal-frontend/src/app/features/public/notification/submission/proposal-details/proposal-details.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ProposalDetailsComponent } from './proposal-details.component'; + +describe('ProposalDetailsComponent', () => { + let component: ProposalDetailsComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ProposalDetailsComponent], + providers: [], + }).compileComponents(); + + fixture = TestBed.createComponent(ProposalDetailsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/portal-frontend/src/app/features/public/notification/submission/proposal-details/proposal-details.component.ts b/portal-frontend/src/app/features/public/notification/submission/proposal-details/proposal-details.component.ts new file mode 100644 index 0000000000..aa2aa7eb5b --- /dev/null +++ b/portal-frontend/src/app/features/public/notification/submission/proposal-details/proposal-details.component.ts @@ -0,0 +1,14 @@ +import { Component, Input } from '@angular/core'; +import { PublicNotificationSubmissionDto } from '../../../../../services/public/public-notification.dto'; + +@Component({ + selector: 'app-proposal-details[submission]', + templateUrl: './proposal-details.component.html', + styleUrls: ['./proposal-details.component.scss'], +}) +export class ProposalDetailsComponent { + @Input() showErrors = true; + @Input() showEdit = true; + + @Input() submission: PublicNotificationSubmissionDto | undefined; +} diff --git a/portal-frontend/src/app/features/public/notification/submission/submission-details.component.html b/portal-frontend/src/app/features/public/notification/submission/submission-details.component.html new file mode 100644 index 0000000000..e4e90ec4d5 --- /dev/null +++ b/portal-frontend/src/app/features/public/notification/submission/submission-details.component.html @@ -0,0 +1,52 @@ +
+ +
+
+

Identify Transferee(s)

+
+
+
Name
+
Organization Name
+ +
{{ transferee.displayName }}
+
+ {{ transferee.organizationName }} + +
+
+
+
+
+
+

Primary Contact

+
+
First Name
+
+ {{ submission.contactFirstName }} +
+
Last Name
+
+ {{ submission.contactLastName }} +
+
+ Organization (optional) +
+
+ {{ submission.contactOrganization }} + +
+
+
+
+

Government

+
+
Local or First Nation Government
+
+ {{ localGovernment?.name }} +
+
+
+
+

5. Purpose of SRW

+ +
diff --git a/portal-frontend/src/app/features/public/notification/submission/submission-details.component.scss b/portal-frontend/src/app/features/public/notification/submission/submission-details.component.scss new file mode 100644 index 0000000000..821554db72 --- /dev/null +++ b/portal-frontend/src/app/features/public/notification/submission/submission-details.component.scss @@ -0,0 +1,117 @@ +@use '../../../../../styles/functions' as *; +@use '../../../../../styles/colors'; + +.transferee-table { + padding: rem(8); + margin: rem(12) 0 rem(20) 0; + background-color: colors.$grey-light; + display: grid; + grid-row-gap: rem(24); + grid-column-gap: rem(16); + grid-template-columns: minmax(3.75rem, 1fr) minmax(3.75rem, 1fr); + word-wrap: break-word; + hyphens: auto; +} + +:host::ng-deep { + .view-grid-item { + display: grid; + grid-template-columns: minmax(rem(100), 0.5fr) 1fr; + column-gap: rem(16); + margin-bottom: rem(12); + } + + .details-wrapper { + margin-top: rem(24); + margin-bottom: rem(24); + + .title { + margin-bottom: rem(16) !important; + } + } + + h3 .subtext { + margin: 0.5rem 0 !important; + } + + label { + font-weight: 600; + } + + .custom-mat-expansion-panel-header { + height: fit-content; + } + + .table-wrapper { + overflow-x: auto; + width: 100%; + } + + @media screen and (min-width: $tabletBreakpoint) { + .flex-item { + display: flex; + gap: rem(16); + } + } +} + +:host::ng-deep { + .scrollable { + overflow-x: auto; + } + + .review-table { + padding: rem(8); + margin: rem(12) 0 rem(20) 0; + background-color: colors.$grey-light; + display: grid; + grid-row-gap: rem(24); + grid-column-gap: rem(16); + grid-template-columns: 1fr; + word-wrap: break-word; + hyphens: auto; + + .edit-button { + display: flex; + justify-content: center; + + button { + width: 100%; + + @media screen and (min-width: $tabletBreakpoint) { + width: unset; + } + } + } + + .subheading2 { + margin-bottom: rem(4) !important; + } + + @media screen and (min-width: $tabletBreakpoint) { + padding: rem(16); + margin: rem(24) 0 rem(40) 0; + grid-template-columns: minmax(rem(60), 1fr) minmax(rem(60), 1fr) minmax(rem(60), 1fr) minmax(rem(60), 1fr); + + .full-width { + grid-column: 1/5; + } + + .grid-double { + grid-column: 2/5; + } + + .grid-1 { + grid-column: 1/2; + } + + .grid-2 { + grid-column: 2/3; + } + + .grid-3 { + grid-column: 3/5; + } + } + } +} diff --git a/portal-frontend/src/app/features/public/notification/submission/submission-details.component.spec.ts b/portal-frontend/src/app/features/public/notification/submission/submission-details.component.spec.ts new file mode 100644 index 0000000000..1043a2cfcf --- /dev/null +++ b/portal-frontend/src/app/features/public/notification/submission/submission-details.component.spec.ts @@ -0,0 +1,36 @@ +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { ApplicationDocumentService } from '../../../../services/application-document/application-document.service'; +import { CodeService } from '../../../../services/code/code.service'; + +import { SubmissionDetailsComponent } from './submission-details.component'; + +describe('SubmissionDetailsComponent', () => { + let component: SubmissionDetailsComponent; + let fixture: ComponentFixture; + let mockCodeService: DeepMocked; + + beforeEach(async () => { + mockCodeService = createMock(); + + await TestBed.configureTestingModule({ + providers: [ + { + provide: CodeService, + useValue: mockCodeService, + }, + ], + declarations: [SubmissionDetailsComponent], + schemas: [NO_ERRORS_SCHEMA], + }).compileComponents(); + + fixture = TestBed.createComponent(SubmissionDetailsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/portal-frontend/src/app/features/public/notification/submission/submission-details.component.ts b/portal-frontend/src/app/features/public/notification/submission/submission-details.component.ts new file mode 100644 index 0000000000..d678595191 --- /dev/null +++ b/portal-frontend/src/app/features/public/notification/submission/submission-details.component.ts @@ -0,0 +1,59 @@ +import { Component, Input, OnDestroy, OnInit } from '@angular/core'; +import { Subject } from 'rxjs'; +import { ApplicationDocumentService } from '../../../../services/application-document/application-document.service'; +import { PARCEL_TYPE } from '../../../../services/application-parcel/application-parcel.dto'; +import { LocalGovernmentDto } from '../../../../services/code/code.dto'; +import { CodeService } from '../../../../services/code/code.service'; +import { PublicNotificationSubmissionDto } from '../../../../services/public/public-notification.dto'; +import { PublicDocumentDto, PublicOwnerDto, PublicParcelDto } from '../../../../services/public/public.dto'; +import { PublicService } from '../../../../services/public/public.service'; +import { OWNER_TYPE } from '../../../../shared/dto/owner.dto'; + +@Component({ + selector: 'app-public-app-submission-details', + templateUrl: './submission-details.component.html', + styleUrls: ['./submission-details.component.scss'], +}) +export class SubmissionDetailsComponent implements OnInit, OnDestroy { + $destroy = new Subject(); + + @Input() submission!: PublicNotificationSubmissionDto; + @Input() documents: PublicDocumentDto[] = []; + @Input() parcels: PublicParcelDto[] = []; + + parcelType = PARCEL_TYPE; + primaryContact: PublicOwnerDto | undefined; + localGovernment: LocalGovernmentDto | undefined; + OWNER_TYPE = OWNER_TYPE; + + private localGovernments: LocalGovernmentDto[] = []; + + constructor(private codeService: CodeService) {} + + ngOnInit(): void { + this.loadGovernments(); + if (this.submission) { + this.populateLocalGovernment(this.submission.localGovernmentUuid); + } + } + + ngOnDestroy(): void { + this.$destroy.next(); + this.$destroy.complete(); + } + + private async loadGovernments() { + const codes = await this.codeService.loadCodes(); + this.localGovernments = codes.localGovernments.sort((a, b) => (a.name > b.name ? 1 : -1)); + if (this.submission?.localGovernmentUuid) { + this.populateLocalGovernment(this.submission?.localGovernmentUuid); + } + } + + private populateLocalGovernment(governmentUuid: string) { + const lg = this.localGovernments.find((lg) => lg.uuid === governmentUuid); + if (lg) { + this.localGovernment = lg; + } + } +} diff --git a/portal-frontend/src/app/features/public/public.module.ts b/portal-frontend/src/app/features/public/public.module.ts index 7ed11a0b8e..c3e266a0db 100644 --- a/portal-frontend/src/app/features/public/public.module.ts +++ b/portal-frontend/src/app/features/public/public.module.ts @@ -27,6 +27,10 @@ const routes: Routes = [ loadChildren: () => import('./notice-of-intent/public-notice-of-intent.module').then((m) => m.PublicNoticeOfIntentModule), }, + { + path: 'notification', + loadChildren: () => import('./notification/public-notification.module').then((m) => m.PublicNotificationModule), + }, ]; @NgModule({ diff --git a/portal-frontend/src/app/services/public/public-notification.dto.ts b/portal-frontend/src/app/services/public/public-notification.dto.ts new file mode 100644 index 0000000000..0e94f58f6e --- /dev/null +++ b/portal-frontend/src/app/services/public/public-notification.dto.ts @@ -0,0 +1,30 @@ +import { ApplicationStatusDto } from '../application-submission/application-submission.dto'; +import { NoticeOfIntentPortalDecisionDto } from '../notice-of-intent-decision/notice-of-intent-decision.dto'; +import { ProposedStructure } from '../notice-of-intent-submission/notice-of-intent-submission.dto'; +import { PublicDocumentDto, PublicOwnerDto, PublicParcelDto } from './public.dto'; + +export interface GetPublicNotificationResponseDto { + submission: PublicNotificationSubmissionDto; + parcels: PublicParcelDto[]; + documents: PublicDocumentDto[]; +} +export interface PublicNotificationSubmissionDto { + fileNumber: string; + uuid: string; + createdAt: number; + updatedAt: number; + applicant: string; + contactFirstName: string | null; + contactLastName: string | null; + contactOrganization: string | null; + localGovernmentUuid: string; + type: string; + typeCode: string; + status: ApplicationStatusDto; + lastStatusUpdate: number; + transferees: PublicOwnerDto[]; + submittersFileNumber: string | null; + purpose: string | null; + totalArea: number | null; + hasSurveyPlan: boolean | null; +} diff --git a/portal-frontend/src/app/services/public/public.dto.ts b/portal-frontend/src/app/services/public/public.dto.ts index ead6fd8dbf..25f4aa3f8d 100644 --- a/portal-frontend/src/app/services/public/public.dto.ts +++ b/portal-frontend/src/app/services/public/public.dto.ts @@ -19,6 +19,7 @@ export interface PublicDocumentDto { fileName: string; fileSize?: number; mimeType: string; + source: string; uploadedAt: number; } diff --git a/portal-frontend/src/app/services/public/public.service.ts b/portal-frontend/src/app/services/public/public.service.ts index 2b8ffef848..c8de0e6cbc 100644 --- a/portal-frontend/src/app/services/public/public.service.ts +++ b/portal-frontend/src/app/services/public/public.service.ts @@ -5,6 +5,7 @@ import { environment } from '../../../environments/environment'; import { ToastService } from '../toast/toast.service'; import { GetPublicApplicationResponseDto } from './public-application.dto'; import { GetPublicNoticeOfIntentResponseDto } from './public-notice-of-intent.dto'; +import { GetPublicNotificationResponseDto } from './public-notification.dto'; @Injectable({ providedIn: 'root', @@ -61,4 +62,28 @@ export class PublicService { return undefined; } } + + async getNotification(fileId: string) { + try { + return await firstValueFrom( + this.httpClient.get(`${this.serviceUrl}/notification/${fileId}`) + ); + } catch (e) { + console.error(e); + this.toastService.showErrorToast('Failed to load Notification, please try again later'); + return undefined; + } + } + + async getNotificationOpenFileUrl(fileId: string, uuid: string) { + try { + return await firstValueFrom( + this.httpClient.get<{ url: string }>(`${this.serviceUrl}/notification/${fileId}/${uuid}/open`) + ); + } catch (e) { + console.error(e); + this.toastService.showErrorToast('Failed to load Notification, please try again later'); + return undefined; + } + } } diff --git a/services/apps/alcs/src/common/automapper/public.automapper.profile.ts b/services/apps/alcs/src/common/automapper/public.automapper.profile.ts index a6a7c62da6..0f38be46c2 100644 --- a/services/apps/alcs/src/common/automapper/public.automapper.profile.ts +++ b/services/apps/alcs/src/common/automapper/public.automapper.profile.ts @@ -3,6 +3,7 @@ import { AutomapperProfile, InjectMapper } from '@automapper/nestjs'; import { Injectable } from '@nestjs/common'; import { ApplicationDocument } from '../../alcs/application/application-document/application-document.entity'; import { NoticeOfIntentDocument } from '../../alcs/notice-of-intent/notice-of-intent-document/notice-of-intent-document.entity'; +import { NotificationDocument } from '../../alcs/notification/notification-document/notification-document.entity'; import { ApplicationSubmissionReview } from '../../portal/application-submission-review/application-submission-review.entity'; import { ApplicationOwner } from '../../portal/application-submission/application-owner/application-owner.entity'; import { ApplicationParcel } from '../../portal/application-submission/application-parcel/application-parcel.entity'; @@ -10,14 +11,20 @@ import { ApplicationSubmission } from '../../portal/application-submission/appli import { NoticeOfIntentOwner } from '../../portal/notice-of-intent-submission/notice-of-intent-owner/notice-of-intent-owner.entity'; import { NoticeOfIntentParcel } from '../../portal/notice-of-intent-submission/notice-of-intent-parcel/notice-of-intent-parcel.entity'; import { NoticeOfIntentSubmission } from '../../portal/notice-of-intent-submission/notice-of-intent-submission.entity'; +import { NotificationParcel } from '../../portal/notification-submission/notification-parcel/notification-parcel.entity'; +import { NotificationSubmission } from '../../portal/notification-submission/notification-submission.entity'; +import { NotificationTransferee } from '../../portal/notification-submission/notification-transferee/notification-transferee.entity'; import { PublicApplicationSubmissionDto, PublicApplicationSubmissionReviewDto, +} from '../../portal/public/application/public-application.dto'; +import { PublicNoticeOfIntentSubmissionDto } from '../../portal/public/notice-of-intent/public-notice-of-intent.dto'; +import { PublicNotificationSubmissionDto } from '../../portal/public/notification/public-notification.dto'; +import { PublicDocumentDto, PublicOwnerDto, PublicParcelDto, -} from '../../portal/public/application/public-application.dto'; -import { PublicNoticeOfIntentSubmissionDto } from '../../portal/public/notice-of-intent/public-notice-of-intent.dto'; +} from '../../portal/public/public.dto'; import { ParcelOwnershipType, ParcelOwnershipTypeDto, @@ -67,6 +74,24 @@ export class PublicAutomapperProfile extends AutomapperProfile { ), ); + createMap( + mapper, + NotificationSubmission, + PublicNotificationSubmissionDto, + forMember( + (a) => a.lastStatusUpdate, + mapFrom((ad) => { + return ad.status.effectiveDate?.getTime(); + }), + ), + forMember( + (a) => a.status, + mapFrom((ad) => { + return ad.status.statusType; + }), + ), + ); + createMap( mapper, ApplicationOwner, @@ -87,6 +112,16 @@ export class PublicAutomapperProfile extends AutomapperProfile { ), ); + createMap( + mapper, + NotificationTransferee, + PublicOwnerDto, + forMember( + (pd) => pd.displayName, + mapFrom((p) => `${p.firstName} ${p.lastName}`), + ), + ); + createMap( mapper, ApplicationParcel, @@ -171,6 +206,30 @@ export class PublicAutomapperProfile extends AutomapperProfile { ), ); + createMap( + mapper, + NotificationParcel, + PublicParcelDto, + forMember( + (pd) => pd.ownershipTypeCode, + mapFrom((p) => p.ownershipTypeCode), + ), + forMember( + (p) => p.ownershipType, + mapFrom((pd) => { + if (pd.ownershipType) { + return this.mapper.map( + pd.ownershipType, + ParcelOwnershipType, + ParcelOwnershipTypeDto, + ); + } else { + return undefined; + } + }), + ), + ); + createMap( mapper, ApplicationSubmissionReview, @@ -201,6 +260,10 @@ export class PublicAutomapperProfile extends AutomapperProfile { (a) => a.documentUuid, mapFrom((ad) => ad.document.uuid), ), + forMember( + (a) => a.source, + mapFrom((ad) => ad.document.source), + ), ); createMap( @@ -227,6 +290,40 @@ export class PublicAutomapperProfile extends AutomapperProfile { (a) => a.documentUuid, mapFrom((ad) => ad.document.uuid), ), + forMember( + (a) => a.source, + mapFrom((ad) => ad.document.source), + ), + ); + + createMap( + mapper, + NotificationDocument, + PublicDocumentDto, + forMember( + (a) => a.mimeType, + mapFrom((ad) => ad.document.mimeType), + ), + forMember( + (a) => a.fileName, + mapFrom((ad) => ad.document.fileName), + ), + forMember( + (a) => a.fileSize, + mapFrom((ad) => ad.document.fileSize), + ), + forMember( + (a) => a.uploadedAt, + mapFrom((ad) => ad.document.uploadedAt.getTime()), + ), + forMember( + (a) => a.documentUuid, + mapFrom((ad) => ad.document.uuid), + ), + forMember( + (a) => a.source, + mapFrom((ad) => ad.document.source), + ), ); }; } diff --git a/services/apps/alcs/src/portal/public/application/public-application.dto.ts b/services/apps/alcs/src/portal/public/application/public-application.dto.ts index 8badf0476e..f936453adb 100644 --- a/services/apps/alcs/src/portal/public/application/public-application.dto.ts +++ b/services/apps/alcs/src/portal/public/application/public-application.dto.ts @@ -1,29 +1,8 @@ import { AutoMap } from '@automapper/classes'; import { ApplicationStatusDto } from '../../../alcs/application/application-submission-status/submission-status.dto'; -import { OwnerTypeDto } from '../../../common/owner-type/owner-type.entity'; -import { DocumentTypeDto } from '../../../document/document.dto'; -import { ApplicationParcelOwnershipTypeDto } from '../../application-submission/application-parcel/application-parcel.dto'; import { NaruSubtypeDto } from '../../application-submission/application-submission.dto'; import { ProposedLot } from '../../application-submission/application-submission.entity'; - -export class PublicOwnerDto { - @AutoMap() - uuid: string; - - displayName: string; - - @AutoMap(() => String) - firstName?: string | null; - - @AutoMap(() => String) - lastName?: string | null; - - @AutoMap(() => String) - organizationName?: string | null; - - @AutoMap() - type: OwnerTypeDto; -} +import { PublicOwnerDto } from '../public.dto'; export class PublicApplicationSubmissionDto { @AutoMap() @@ -367,63 +346,3 @@ export class PublicApplicationSubmissionReviewDto { @AutoMap(() => Boolean) isAuthorized: boolean | null; } - -export class PublicParcelDto { - @AutoMap() - uuid: string; - - @AutoMap(() => String) - pid?: string | null; - - @AutoMap(() => String) - pin?: string | null; - - @AutoMap(() => String) - legalDescription?: string | null; - - @AutoMap(() => String) - civicAddress?: string | null; - - @AutoMap(() => Number) - mapAreaHectares?: number | null; - - @AutoMap(() => Number) - purchasedDate?: number | null; - - @AutoMap(() => Boolean) - isFarm?: boolean | null; - - @AutoMap(() => String) - ownershipTypeCode?: string | null; - - @AutoMap(() => String) - crownLandOwnerType?: string | null; - - ownershipType?: ApplicationParcelOwnershipTypeDto; - - @AutoMap(() => String) - parcelType: string; - - @AutoMap(() => Number) - alrArea: number | null; - - owners: PublicOwnerDto[]; -} - -export class PublicDocumentDto { - @AutoMap(() => String) - description?: string; - - @AutoMap() - uuid: string; - - @AutoMap(() => DocumentTypeDto) - type?: DocumentTypeDto; - - //Document Fields - documentUuid: string; - fileName: string; - fileSize?: number; - mimeType: string; - uploadedAt: number; -} diff --git a/services/apps/alcs/src/portal/public/application/public-application.service.ts b/services/apps/alcs/src/portal/public/application/public-application.service.ts index 049c98a940..115a572d49 100644 --- a/services/apps/alcs/src/portal/public/application/public-application.service.ts +++ b/services/apps/alcs/src/portal/public/application/public-application.service.ts @@ -10,6 +10,7 @@ import { } from '../../../alcs/application/application-document/application-document.entity'; import { ApplicationDocumentService } from '../../../alcs/application/application-document/application-document.service'; import { ApplicationService } from '../../../alcs/application/application.service'; +import { PublicDocumentDto, PublicParcelDto } from '../public.dto'; import { ApplicationPortalDecisionDto } from './application-decision.dto'; import { ApplicationSubmissionReview } from '../../application-submission-review/application-submission-review.entity'; import { ApplicationSubmissionReviewService } from '../../application-submission-review/application-submission-review.service'; @@ -18,10 +19,8 @@ import { ApplicationParcelService } from '../../application-submission/applicati import { ApplicationSubmission } from '../../application-submission/application-submission.entity'; import { ApplicationSubmissionService } from '../../application-submission/application-submission.service'; import { - PublicParcelDto, PublicApplicationSubmissionDto, PublicApplicationSubmissionReviewDto, - PublicDocumentDto, } from './public-application.dto'; @Injectable() diff --git a/services/apps/alcs/src/portal/public/notice-of-intent/public-notice-of-intent.dto.ts b/services/apps/alcs/src/portal/public/notice-of-intent/public-notice-of-intent.dto.ts index a266fb7694..abf7d29d13 100644 --- a/services/apps/alcs/src/portal/public/notice-of-intent/public-notice-of-intent.dto.ts +++ b/services/apps/alcs/src/portal/public/notice-of-intent/public-notice-of-intent.dto.ts @@ -1,7 +1,7 @@ import { AutoMap } from '@automapper/classes'; import { NoticeOfIntentStatusDto } from '../../../alcs/notice-of-intent/notice-of-intent-submission-status/notice-of-intent-status.dto'; import { ProposedStructure } from '../../notice-of-intent-submission/notice-of-intent-submission.entity'; -import { PublicOwnerDto } from '../application/public-application.dto'; +import { PublicOwnerDto } from '../public.dto'; export class PublicNoticeOfIntentSubmissionDto { @AutoMap() diff --git a/services/apps/alcs/src/portal/public/notice-of-intent/public-notice-of-intent.service.ts b/services/apps/alcs/src/portal/public/notice-of-intent/public-notice-of-intent.service.ts index ee28cfb968..da7617a8f3 100644 --- a/services/apps/alcs/src/portal/public/notice-of-intent/public-notice-of-intent.service.ts +++ b/services/apps/alcs/src/portal/public/notice-of-intent/public-notice-of-intent.service.ts @@ -8,15 +8,12 @@ import { NoticeOfIntentDecision } from '../../../alcs/notice-of-intent-decision/ import { NoticeOfIntentDocument } from '../../../alcs/notice-of-intent/notice-of-intent-document/notice-of-intent-document.entity'; import { NoticeOfIntentDocumentService } from '../../../alcs/notice-of-intent/notice-of-intent-document/notice-of-intent-document.service'; import { NoticeOfIntentService } from '../../../alcs/notice-of-intent/notice-of-intent.service'; +import { PublicDocumentDto, PublicParcelDto } from '../public.dto'; import { NoticeOfIntentPortalDecisionDto } from './notice-of-intent-decision.dto'; import { NoticeOfIntentParcel } from '../../notice-of-intent-submission/notice-of-intent-parcel/notice-of-intent-parcel.entity'; import { NoticeOfIntentParcelService } from '../../notice-of-intent-submission/notice-of-intent-parcel/notice-of-intent-parcel.service'; import { NoticeOfIntentSubmission } from '../../notice-of-intent-submission/notice-of-intent-submission.entity'; import { NoticeOfIntentSubmissionService } from '../../notice-of-intent-submission/notice-of-intent-submission.service'; -import { - PublicDocumentDto, - PublicParcelDto, -} from '../application/public-application.dto'; import { PublicNoticeOfIntentSubmissionDto } from './public-notice-of-intent.dto'; @Injectable() diff --git a/services/apps/alcs/src/portal/public/notification/public-notification.dto.ts b/services/apps/alcs/src/portal/public/notification/public-notification.dto.ts new file mode 100644 index 0000000000..f6d4508126 --- /dev/null +++ b/services/apps/alcs/src/portal/public/notification/public-notification.dto.ts @@ -0,0 +1,55 @@ +import { AutoMap } from '@automapper/classes'; +import { NotificationStatusDto } from '../../../alcs/notification/notification-submission-status/notification-status.dto'; +import { PublicOwnerDto } from '../public.dto'; + +export class PublicNotificationSubmissionDto { + @AutoMap() + fileNumber: string; + + @AutoMap() + uuid: string; + + @AutoMap() + createdAt: number; + + updatedAt: number; + + @AutoMap() + applicant: string; + + @AutoMap(() => String) + contactFirstName: string | null; + + @AutoMap(() => String) + contactLastName: string | null; + + @AutoMap(() => String) + contactOrganization: string | null; + + @AutoMap() + localGovernmentUuid: string; + + @AutoMap() + type: string; + + @AutoMap() + typeCode: string; + + status: NotificationStatusDto; + lastStatusUpdate: number; + + @AutoMap(() => [PublicOwnerDto]) + transferees: PublicOwnerDto[]; + + @AutoMap(() => String) + submittersFileNumber: string | null; + + @AutoMap(() => String) + purpose: string | null; + + @AutoMap(() => Number) + totalArea: number | null; + + @AutoMap(() => Boolean) + hasSurveyPlan: boolean | null; +} diff --git a/services/apps/alcs/src/portal/public/notification/public-notification.service.spec.ts b/services/apps/alcs/src/portal/public/notification/public-notification.service.spec.ts new file mode 100644 index 0000000000..2b02b76380 --- /dev/null +++ b/services/apps/alcs/src/portal/public/notification/public-notification.service.spec.ts @@ -0,0 +1,132 @@ +import { ServiceNotFoundException } from '@app/common/exceptions/base.exception'; +import { classes } from '@automapper/classes'; +import { AutomapperModule } from '@automapper/nestjs'; +import { createMock, DeepMocked } from '@golevelup/nestjs-testing'; +import { Test, TestingModule } from '@nestjs/testing'; +import { VISIBILITY_FLAG } from '../../../alcs/application/application-document/application-document.entity'; +import { NotificationDocument } from '../../../alcs/notification/notification-document/notification-document.entity'; +import { NotificationDocumentService } from '../../../alcs/notification/notification-document/notification-document.service'; +import { NotificationSubmissionStatusType } from '../../../alcs/notification/notification-submission-status/notification-status-type.entity'; +import { NOTIFICATION_STATUS } from '../../../alcs/notification/notification-submission-status/notification-status.dto'; +import { NotificationSubmissionToSubmissionStatus } from '../../../alcs/notification/notification-submission-status/notification-status.entity'; +import { NotificationType } from '../../../alcs/notification/notification-type/notification-type.entity'; +import { Notification } from '../../../alcs/notification/notification.entity'; +import { NotificationService } from '../../../alcs/notification/notification.service'; +import { PublicAutomapperProfile } from '../../../common/automapper/public.automapper.profile'; +import { NotificationParcelService } from '../../notification-submission/notification-parcel/notification-parcel.service'; +import { NotificationSubmission } from '../../notification-submission/notification-submission.entity'; +import { NotificationSubmissionService } from '../../notification-submission/notification-submission.service'; +import { PublicNotificationService } from './public-notification.service'; + +describe('PublicNotificationService', () => { + let service: PublicNotificationService; + let mockNOIService: DeepMocked; + let mockNOISubService: DeepMocked; + let mockNOIParcelService: DeepMocked; + let mockNOIDocService: DeepMocked; + + beforeEach(async () => { + mockNOIService = createMock(); + mockNOISubService = createMock(); + mockNOIParcelService = createMock(); + mockNOIDocService = createMock(); + + const module: TestingModule = await Test.createTestingModule({ + imports: [ + AutomapperModule.forRoot({ + strategyInitializer: classes(), + }), + ], + providers: [ + PublicNotificationService, + PublicAutomapperProfile, + { + provide: NotificationService, + useValue: mockNOIService, + }, + { + provide: NotificationSubmissionService, + useValue: mockNOISubService, + }, + { + provide: NotificationParcelService, + useValue: mockNOIParcelService, + }, + { + provide: NotificationDocumentService, + useValue: mockNOIDocService, + }, + ], + }).compile(); + + service = module.get(PublicNotificationService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); + + it('load a Notice of Intent and its related data for get NOI', async () => { + mockNOIService.getByFileNumber.mockResolvedValue( + new Notification({ + type: new NotificationType(), + }), + ); + mockNOISubService.getOrFailByFileNumber.mockResolvedValue( + new NotificationSubmission({ + get status() { + return new NotificationSubmissionToSubmissionStatus({ + statusType: new NotificationSubmissionStatusType({ + code: NOTIFICATION_STATUS.ALC_RESPONSE_SENT, + }), + }); + }, + }), + ); + mockNOIParcelService.fetchByFileId.mockResolvedValue([]); + mockNOIDocService.list.mockResolvedValue([]); + + const fileId = 'file-id'; + await service.getPublicData(fileId); + + expect(mockNOIService.getByFileNumber).toHaveBeenCalledTimes(1); + expect(mockNOISubService.getOrFailByFileNumber).toHaveBeenCalledTimes(1); + expect(mockNOIParcelService.fetchByFileId).toHaveBeenCalledTimes(1); + expect(mockNOIDocService.list).toHaveBeenCalledTimes(1); + expect(mockNOIDocService.list).toHaveBeenCalledWith(fileId, [ + VISIBILITY_FLAG.PUBLIC, + ]); + }); + + it('should call through to document service for getting files', async () => { + const mockDoc = new NotificationDocument({ + visibilityFlags: [VISIBILITY_FLAG.PUBLIC], + }); + mockNOIDocService.get.mockResolvedValue(mockDoc); + mockNOIDocService.getInlineUrl.mockResolvedValue(''); + + const documentUuid = 'document-uuid'; + await service.getInlineUrl(documentUuid); + + expect(mockNOIDocService.get).toHaveBeenCalledTimes(1); + expect(mockNOIDocService.getInlineUrl).toHaveBeenCalledTimes(1); + expect(mockNOIDocService.getInlineUrl).toHaveBeenCalledWith(mockDoc); + }); + + it('should throw an exception when the document is not public', async () => { + const mockDoc = new NotificationDocument({ + visibilityFlags: [VISIBILITY_FLAG.APPLICANT], + }); + mockNOIDocService.get.mockResolvedValue(mockDoc); + + const documentUuid = 'document-uuid'; + const promise = service.getInlineUrl(documentUuid); + + await expect(promise).rejects.toMatchObject( + new ServiceNotFoundException('Failed to find document'), + ); + + expect(mockNOIDocService.get).toHaveBeenCalledTimes(1); + expect(mockNOIDocService.getInlineUrl).toHaveBeenCalledTimes(0); + }); +}); diff --git a/services/apps/alcs/src/portal/public/notification/public-notification.service.ts b/services/apps/alcs/src/portal/public/notification/public-notification.service.ts new file mode 100644 index 0000000000..e261ca83a4 --- /dev/null +++ b/services/apps/alcs/src/portal/public/notification/public-notification.service.ts @@ -0,0 +1,108 @@ +import { ServiceNotFoundException } from '@app/common/exceptions/base.exception'; +import { Mapper } from '@automapper/core'; +import { InjectMapper } from '@automapper/nestjs'; +import { Injectable } from '@nestjs/common'; +import { VISIBILITY_FLAG } from '../../../alcs/application/application-document/application-document.entity'; +import { NotificationDocument } from '../../../alcs/notification/notification-document/notification-document.entity'; +import { NotificationDocumentService } from '../../../alcs/notification/notification-document/notification-document.service'; +import { NOTIFICATION_STATUS } from '../../../alcs/notification/notification-submission-status/notification-status.dto'; +import { NotificationService } from '../../../alcs/notification/notification.service'; +import { NotificationParcel } from '../../notification-submission/notification-parcel/notification-parcel.entity'; +import { NotificationParcelService } from '../../notification-submission/notification-parcel/notification-parcel.service'; +import { NotificationSubmission } from '../../notification-submission/notification-submission.entity'; +import { NotificationSubmissionService } from '../../notification-submission/notification-submission.service'; +import { PublicDocumentDto, PublicParcelDto } from '../public.dto'; +import { PublicNotificationSubmissionDto } from './public-notification.dto'; + +@Injectable() +export class PublicNotificationService { + constructor( + private notificationService: NotificationService, + private notificationSubmissionService: NotificationSubmissionService, + private notificationParcelService: NotificationParcelService, + private notificationDocumentService: NotificationDocumentService, + @InjectMapper() private mapper: Mapper, + ) {} + + async getPublicData(fileNumber: string) { + const notification = await this.notificationService.getByFileNumber( + fileNumber, + ); + + const submission = + await this.notificationSubmissionService.getOrFailByFileNumber( + fileNumber, + ); + + //Check if Response Sent Status + if ( + (submission.status.statusType.code as NOTIFICATION_STATUS) !== + NOTIFICATION_STATUS.ALC_RESPONSE_SENT + ) { + throw new ServiceNotFoundException( + `Failed to find notification with File ID ${fileNumber}`, + ); + } + + const parcels = await this.notificationParcelService.fetchByFileId( + fileNumber, + ); + + const mappedParcels = this.mapper.mapArray( + parcels, + NotificationParcel, + PublicParcelDto, + ); + + const mappedSubmission = this.mapper.map( + submission, + NotificationSubmission, + PublicNotificationSubmissionDto, + ); + mappedSubmission.type = notification.type.label; + + const documents = await this.notificationDocumentService.list(fileNumber, [ + VISIBILITY_FLAG.PUBLIC, + ]); + + const mappedDocuments = this.mapper.mapArray( + documents, + NotificationDocument, + PublicDocumentDto, + ); + + return { + submission: mappedSubmission, + parcels: mappedParcels, + documents: mappedDocuments, + }; + } + + async getDownloadUrl(documentUuid: string) { + const document = await this.notificationDocumentService.get(documentUuid); + + if (!document.visibilityFlags.includes(VISIBILITY_FLAG.PUBLIC)) { + throw new ServiceNotFoundException('Failed to find document'); + } + + const url = await this.notificationDocumentService.getDownloadUrl(document); + + return { + url, + }; + } + + async getInlineUrl(documentUuid: string) { + const document = await this.notificationDocumentService.get(documentUuid); + + if (!document.visibilityFlags.includes(VISIBILITY_FLAG.PUBLIC)) { + throw new ServiceNotFoundException('Failed to find document'); + } + + const url = await this.notificationDocumentService.getInlineUrl(document); + + return { + url, + }; + } +} diff --git a/services/apps/alcs/src/portal/public/public.controller.spec.ts b/services/apps/alcs/src/portal/public/public.controller.spec.ts index c087759ed6..15213deeb5 100644 --- a/services/apps/alcs/src/portal/public/public.controller.spec.ts +++ b/services/apps/alcs/src/portal/public/public.controller.spec.ts @@ -4,16 +4,19 @@ import { ClsService } from 'nestjs-cls'; import { mockKeyCloakProviders } from '../../../test/mocks/mockTypes'; import { PublicApplicationService } from './application/public-application.service'; import { PublicNoticeOfIntentService } from './notice-of-intent/public-notice-of-intent.service'; +import { PublicNotificationService } from './notification/public-notification.service'; import { PublicController } from './public.controller'; describe('PublicController', () => { let controller: PublicController; let mockAppService: DeepMocked; let mockNOIService: DeepMocked; + let mockNotificationService: DeepMocked; beforeEach(async () => { mockAppService = createMock(); mockNOIService = createMock(); + mockNotificationService = createMock(); const module: TestingModule = await Test.createTestingModule({ providers: [ @@ -25,6 +28,10 @@ describe('PublicController', () => { provide: PublicNoticeOfIntentService, useValue: mockNOIService, }, + { + provide: PublicNotificationService, + useValue: mockNotificationService, + }, { provide: ClsService, useValue: {}, @@ -49,4 +56,22 @@ describe('PublicController', () => { expect(mockAppService.getPublicData).toHaveBeenCalledTimes(1); }); + + it('should call through to service for loading a notice of intent', async () => { + mockNOIService.getPublicData.mockResolvedValue({} as any); + + const fileId = 'file-id'; + await controller.getNoticeOfIntent(fileId); + + expect(mockNOIService.getPublicData).toHaveBeenCalledTimes(1); + }); + + it('should call through to service for loading a notification', async () => { + mockNotificationService.getPublicData.mockResolvedValue({} as any); + + const fileId = 'file-id'; + await controller.getNotification(fileId); + + expect(mockNotificationService.getPublicData).toHaveBeenCalledTimes(1); + }); }); diff --git a/services/apps/alcs/src/portal/public/public.controller.ts b/services/apps/alcs/src/portal/public/public.controller.ts index 3d53cf077e..22d31c91d4 100644 --- a/services/apps/alcs/src/portal/public/public.controller.ts +++ b/services/apps/alcs/src/portal/public/public.controller.ts @@ -2,6 +2,7 @@ import { Controller, Get, Param } from '@nestjs/common'; import { Public } from 'nest-keycloak-connect'; import { PublicApplicationService } from './application/public-application.service'; import { PublicNoticeOfIntentService } from './notice-of-intent/public-notice-of-intent.service'; +import { PublicNotificationService } from './notification/public-notification.service'; @Public() @Controller('/public') @@ -9,6 +10,7 @@ export class PublicController { constructor( private publicAppService: PublicApplicationService, private publicNoticeOfIntentService: PublicNoticeOfIntentService, + private publicNotificationService: PublicNotificationService, ) {} @Get('/application/:fileId') @@ -72,4 +74,35 @@ export class PublicController { url, }; } + + @Get('/notification/:fileId') + async getNotification(@Param('fileId') fileNumber: string) { + return await this.publicNotificationService.getPublicData(fileNumber); + } + + @Get('/notification/:fileId/:uuid/download') + async getNotificationDocumentDownload( + @Param('fileId') fileId: string, + @Param('uuid') documentUuid: string, + ) { + const url = await this.publicNotificationService.getDownloadUrl( + documentUuid, + ); + + return { + url, + }; + } + + @Get('/notification/:fileId/:uuid/open') + async getNotificationDocumentOpen( + @Param('fileId') fileId: string, + @Param('uuid') documentUuid: string, + ) { + const url = await this.publicNotificationService.getInlineUrl(documentUuid); + + return { + url, + }; + } } diff --git a/services/apps/alcs/src/portal/public/public.dto.ts b/services/apps/alcs/src/portal/public/public.dto.ts new file mode 100644 index 0000000000..2ebe64d785 --- /dev/null +++ b/services/apps/alcs/src/portal/public/public.dto.ts @@ -0,0 +1,86 @@ +import { AutoMap } from '@automapper/classes'; +import { OwnerTypeDto } from '../../common/owner-type/owner-type.entity'; +import { DocumentTypeDto } from '../../document/document.dto'; +import { ApplicationParcelOwnershipTypeDto } from '../application-submission/application-parcel/application-parcel.dto'; + +export class PublicParcelDto { + @AutoMap() + uuid: string; + + @AutoMap(() => String) + pid?: string | null; + + @AutoMap(() => String) + pin?: string | null; + + @AutoMap(() => String) + legalDescription?: string | null; + + @AutoMap(() => String) + civicAddress?: string | null; + + @AutoMap(() => Number) + mapAreaHectares?: number | null; + + @AutoMap(() => Number) + purchasedDate?: number | null; + + @AutoMap(() => Boolean) + isFarm?: boolean | null; + + @AutoMap(() => String) + ownershipTypeCode?: string | null; + + @AutoMap(() => String) + crownLandOwnerType?: string | null; + + ownershipType?: ApplicationParcelOwnershipTypeDto; + + @AutoMap(() => String) + parcelType: string; + + @AutoMap(() => Number) + alrArea: number | null; + + owners: PublicOwnerDto[]; +} + +export class PublicDocumentDto { + @AutoMap(() => String) + description?: string; + + @AutoMap() + uuid: string; + + @AutoMap(() => DocumentTypeDto) + type?: DocumentTypeDto; + + @AutoMap() + source: string; + + //Document Fields + documentUuid: string; + fileName: string; + fileSize?: number; + mimeType: string; + uploadedAt: number; +} + +export class PublicOwnerDto { + @AutoMap() + uuid: string; + + displayName: string; + + @AutoMap(() => String) + firstName?: string | null; + + @AutoMap(() => String) + lastName?: string | null; + + @AutoMap(() => String) + organizationName?: string | null; + + @AutoMap() + type: OwnerTypeDto; +} diff --git a/services/apps/alcs/src/portal/public/public.module.ts b/services/apps/alcs/src/portal/public/public.module.ts index 5a3d31e493..8bb4e3fbce 100644 --- a/services/apps/alcs/src/portal/public/public.module.ts +++ b/services/apps/alcs/src/portal/public/public.module.ts @@ -6,14 +6,17 @@ import { ApplicationModule } from '../../alcs/application/application.module'; import { NoticeOfIntentDecisionModule } from '../../alcs/notice-of-intent-decision/notice-of-intent-decision.module'; import { NoticeOfIntentModule } from '../../alcs/notice-of-intent/notice-of-intent.module'; import { NotificationSubmissionStatusModule } from '../../alcs/notification/notification-submission-status/notification-submission-status.module'; +import { NotificationModule } from '../../alcs/notification/notification.module'; import { PublicAutomapperProfile } from '../../common/automapper/public.automapper.profile'; import { ApplicationSubmissionReviewModule } from '../application-submission-review/application-submission-review.module'; import { ApplicationSubmissionModule } from '../application-submission/application-submission.module'; import { NoticeOfIntentSubmissionModule } from '../notice-of-intent-submission/notice-of-intent-submission.module'; +import { NotificationSubmissionModule } from '../notification-submission/notification-submission.module'; import { ApplicationDecisionController } from './application/application-decision.controller'; import { PublicApplicationService } from './application/public-application.service'; import { NoticeOfIntentDecisionController } from './notice-of-intent/notice-of-intent-decision.controller'; import { PublicNoticeOfIntentService } from './notice-of-intent/public-notice-of-intent.service'; +import { PublicNotificationService } from './notification/public-notification.service'; import { PublicController } from './public.controller'; import { PublicSearchModule } from './search/public-search.module'; import { PublicStatusController } from './status/public-status.controller'; @@ -30,6 +33,8 @@ import { PublicStatusController } from './status/public-status.controller'; NoticeOfIntentModule, NoticeOfIntentSubmissionModule, NoticeOfIntentDecisionModule, + NotificationModule, + NotificationSubmissionModule, RouterModule.register([{ path: 'public', module: PublicSearchModule }]), ], controllers: [ @@ -42,6 +47,7 @@ import { PublicStatusController } from './status/public-status.controller'; PublicAutomapperProfile, PublicApplicationService, PublicNoticeOfIntentService, + PublicNotificationService, ], exports: [], }) From dc0c229e12039321839dd9b4d9f38c485226febc Mon Sep 17 00:00:00 2001 From: Daniel Haselhan Date: Tue, 10 Oct 2023 12:33:48 -0700 Subject: [PATCH 2/2] Code Review Feedback --- .../additional-information.component.spec.ts | 2 ++ .../public/notification/public-notification.service.spec.ts | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/portal-frontend/src/app/features/public/notice-of-intent/submission/additional-information/additional-information.component.spec.ts b/portal-frontend/src/app/features/public/notice-of-intent/submission/additional-information/additional-information.component.spec.ts index 417628f883..f747f2270f 100644 --- a/portal-frontend/src/app/features/public/notice-of-intent/submission/additional-information/additional-information.component.spec.ts +++ b/portal-frontend/src/app/features/public/notice-of-intent/submission/additional-information/additional-information.component.spec.ts @@ -1,3 +1,4 @@ +import { NO_ERRORS_SCHEMA } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { DeepMocked } from '@golevelup/ts-jest'; import { PublicService } from '../../../../../services/public/public.service'; @@ -18,6 +19,7 @@ describe('AdditionalInformationComponent', () => { useValue: mockPublicService, }, ], + schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); fixture = TestBed.createComponent(AdditionalInformationComponent); diff --git a/services/apps/alcs/src/portal/public/notification/public-notification.service.spec.ts b/services/apps/alcs/src/portal/public/notification/public-notification.service.spec.ts index 2b02b76380..95c226977b 100644 --- a/services/apps/alcs/src/portal/public/notification/public-notification.service.spec.ts +++ b/services/apps/alcs/src/portal/public/notification/public-notification.service.spec.ts @@ -66,7 +66,7 @@ describe('PublicNotificationService', () => { expect(service).toBeDefined(); }); - it('load a Notice of Intent and its related data for get NOI', async () => { + it('load a Notification and its related data for get notification', async () => { mockNOIService.getByFileNumber.mockResolvedValue( new Notification({ type: new NotificationType(),